feat(9): real broadcast close-out — transition(complete) in StopStream after RTMP EOF
The specced 'End stream -> transition(complete)' call never existed: stopping relied entirely on enableAutoStop (viewers sat on a frozen stream-offline for ~a minute, VOD finalized late). Found during the 2026-09-01 recording-verification pass while the creator asked 'if there's proper close-out info yt needs, we'll provide it?' EndBroadcastAsync POSTs liveBroadcasts/transition?broadcastStatus=complete&id=..&part=status, called after the pump stops (RTMP EOF first) and only when a live session had a broadcast — record-only stops stay offline. invalidTransition/410 (autoStop already ended it) is logged and returned as an error string, never thrown: a stop must never fail over close-out. ONE integration test (URL shape + never-throws on 403). ai.md/TASKS.md design lines marked SHIPPED with the map-lie note. Ref: https://developers.google.com/youtube/v3/live/docs/liveBroadcasts/transition
This commit is contained in:
@@ -124,6 +124,28 @@ public class YouTubeStreamService
|
||||
return $"YouTube rejected the update ({(int)response.StatusCode})";
|
||||
}
|
||||
|
||||
/// <summary>Proper close-out (TASK 9 design decision 6 — implemented 2026-09-01;
|
||||
/// until then we relied entirely on enableAutoStop, leaving viewers on a frozen
|
||||
/// "stream offline" screen for ~a minute): POST liveBroadcasts.transition
|
||||
/// broadcastStatus=complete. MUST be called AFTER the encoder closed the RTMP
|
||||
/// push so no frames post-date the end. A broadcast YouTube already auto-completed
|
||||
/// answers 403 invalidTransition / 410 — logged and surfaced as an error string,
|
||||
/// never thrown: the stop path must not fail over a cosmetic close-out.</summary>
|
||||
public async Task<string?> EndBroadcastAsync(string broadcastId)
|
||||
{
|
||||
if (!await EnsureToken()) return "not signed in";
|
||||
|
||||
_http.DefaultRequestHeaders.Authorization = new("Bearer", _auth.CurrentChannel!.AccessToken);
|
||||
var response = await _http.PostAsync(
|
||||
$"{ApiBase}/liveBroadcasts/transition?broadcastStatus=complete&id={Uri.EscapeDataString(broadcastId)}&part=status",
|
||||
content: null);
|
||||
|
||||
if (response.IsSuccessStatusCode) return null;
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
AppLog.Write($"Broadcast transition(complete) failed ({(int)response.StatusCode}): {body}");
|
||||
return $"YouTube rejected the end transition ({(int)response.StatusCode})";
|
||||
}
|
||||
|
||||
/// <summary>Returns the channel's reusable stream (TASK 5 design decision 2):
|
||||
/// lists existing streams first and reuses the one with cdn.isReusable=true,
|
||||
/// creating it with variable resolution/frame rate on first use. Binding to a
|
||||
|
||||
Reference in New Issue
Block a user