feat(stream): probe the ingest before going live — no more ghost broadcasts (TASK 32·5)
"I forgot the VPN" used to create a broadcast on YouTube that nothing ever pushed to: the app looked
live, YouTube held an empty broadcast, and the creator had to work out what happened. Prevention beats
recovery here, so go-live now asks one question first.
- `Services/IngestProbe.cs` — `IIngestProbe` + `TcpIngestProbe`: parse the RTMP URL, connect to the host
(rtmp → 1935, rtmps → 443) with a 2 s timeout. Any failure answers "not reachable"; the point is a
fast unambiguous answer, not a diagnosis.
- `MainViewModel.IngestPreflightAsync` runs **before the first API call** whenever a cached ingest URL
exists; on a first run it probes once the stream is resolved, still before any broadcast is created.
A false answer logs, tells the creator ("Nothing was created on YouTube…"), and rolls the session
back — nothing to clean up on YouTube.
- The probe is a test seam (`IngestProbeOverride`) like the camera and chroma ones.
Tests: `IngestPreflightTests` — unreachable aborts, reachable continues, URL parsing (rtmp/rtmps,
garbage), and the probe against **real sockets**: a listening port answers true, the same port closed
answers false and does so fast. FAST gate: 547 passed, 0 failed, 0 warnings.
This commit is contained in:
@@ -147,6 +147,22 @@ public partial class MainViewModel : ViewModelBase
|
||||
/// <summary>Creates the record file (auto-named <c>ty-…-0000.mp4</c> in the record
|
||||
/// folder) and returns its path, or null when the folder can't be prepared.</summary>
|
||||
|
||||
/// <summary>TASK 32·5 — is YouTube's ingest reachable right now? A false answer aborts go-live
|
||||
/// before anything is created on YouTube: "I forgot the VPN" used to produce a broadcast that
|
||||
/// nobody ever saw, and the app looked live while the push had nowhere to go.</summary>
|
||||
internal async Task<bool> IngestPreflightAsync(string rtmpUrl)
|
||||
{
|
||||
var probe = IngestProbeOverride ?? new TcpIngestProbe();
|
||||
if (await probe.CanReachAsync(rtmpUrl)) return true;
|
||||
|
||||
AppLog.Write("Ingest unreachable — go-live aborted before creating a broadcast",
|
||||
AppLogMessageType.Error, AppLogErrorClass.Class1);
|
||||
_notifications.Error("Can't reach YouTube's ingest",
|
||||
"Nothing was created on YouTube. Check your network or VPN, then try again.");
|
||||
StopStream(); // rollback: no ghost broadcast, no half-lit session
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>One-click go-live (TASK 5): ensure the reusable stream, create a
|
||||
/// broadcast bound to it, then start the frame pump. The pump reads the RTMP
|
||||
/// URL once at startup (FramePump.StartAsync), so the stream URL must be set
|
||||
@@ -156,6 +172,13 @@ public partial class MainViewModel : ViewModelBase
|
||||
{
|
||||
try
|
||||
{
|
||||
// TASK 32·5 — prevention. With a cached ingest URL we probe BEFORE the first API call, so
|
||||
// a dead network costs nothing and leaves nothing behind on YouTube. First run (no cached
|
||||
// URL yet) probes once the stream is resolved, still before any broadcast is created.
|
||||
var cachedIngest = _reusableStreamUrl;
|
||||
if (!string.IsNullOrWhiteSpace(cachedIngest) && !await IngestPreflightAsync(cachedIngest))
|
||||
return;
|
||||
|
||||
var stream = await _youtubeStream.GetOrCreateReusableStreamAsync();
|
||||
if (stream == null)
|
||||
{
|
||||
@@ -170,6 +193,9 @@ public partial class MainViewModel : ViewModelBase
|
||||
_reusableStreamUrl = stream.RtmpUrl;
|
||||
_layoutStore.SaveReusableStream(stream);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cachedIngest) && !await IngestPreflightAsync(stream.RtmpUrl))
|
||||
return;
|
||||
|
||||
_currentBroadcastId = await _youtubeStream.CreateBroadcast(
|
||||
StreamTitle, StreamDescription, DateTime.UtcNow);
|
||||
_broadcastSeenLive = false;
|
||||
|
||||
Reference in New Issue
Block a user