feat(stream): TASK 41 Test Stream mode — private test broadcast + TEST drawer

Test button next to Start runs the real private-only go-live pipeline as a
session variant (IsTestStream): BeginTestStream -> extracted
StartStreamingSession(alsoRecord:false) shared with the dialog path — no Go
Live dialog, no 'last live' stamp, no recording. Gold top bar + TEST badge +
'End Test' button face; TEST drawer (third right-rail pull-out, three-way
exclusivity) auto-opens once the liveChatId resolves.

Chat tooling: Mock Chat Input sends a REAL liveChat/messages.insert
(YouTubeStreamService.InsertChatMessageAsync) that round-trips the real ~2s
poll and renders through the live overlay; simulated Subscriber/New
Member/Super Chat events inject through the poller's MessageReceived seam
(YouTubeChatService.InjectSimulatedMessage, ChatMessage.IsSimulated) — the
insert API only creates textMessageEvent, so non-text events are local-only
by design (ref: developers.google.com/youtube/v3/live/docs/liveChatMessages/insert).
Scopes youtube + youtube.force-ssl already cover insert; no re-consent.

Good Dog: ytLive.Tests/TestStreamTests.cs — coordinator protocol with fake
HTTP + real-window drawer exclusivity/gate. 0 warnings; 314/314 pass (clean
run; intermittent host abort is the pre-existing WinRT-webcam flake).

Docs ride in the same change: TASKS.md row 41, TASKS/task-41-test-stream-mode.md,
ai.md (YouTube Live API constraints), HANDOFF rewrite.
This commit is contained in:
2026-09-22 09:31:00 -07:00
parent 57841d39ac
commit bb5dcb4ba2
16 changed files with 893 additions and 85 deletions
+39 -2
View File
@@ -41,6 +41,13 @@ public partial class MainViewModel : ViewModelBase
private bool _onAirPillOn;
private bool _isRecording;
/// <summary>TASK 41: the current session is a private TEST broadcast — the real
/// go-live pipeline runs (broadcast create, reusable stream, RTMP push, chat
/// poll), but the visuals say TEST (top-bar gold + badge), no "last live" stamp
/// is written, and the Test drawer is open. One flag flips the whole face of the
/// session; cleared on stop / rollback.</summary>
private bool _isTestStream;
/// <summary>Resolved record folder — the configured choice, or Downloads (0th case).</summary>
@@ -64,6 +71,7 @@ public partial class MainViewModel : ViewModelBase
OnPropertyChanged(nameof(ShowPrimaryStartButton));
OnPropertyChanged(nameof(ShowEndStreamButton));
OnPropertyChanged(nameof(EndButtonText));
OnPropertyChanged(nameof(ShowTestButton));
OnPropertyChanged(nameof(AccountStatusLightToolTip));
OnPropertyChanged(nameof(ShowChatInactiveMessage));
OnPropertyChanged(nameof(ShowPreviewPlaceholder));
@@ -126,6 +134,7 @@ public partial class MainViewModel : ViewModelBase
OnPropertyChanged(nameof(ShowEndStreamButton));
OnPropertyChanged(nameof(ShowPrimaryStartButton));
OnPropertyChanged(nameof(EndButtonText));
OnPropertyChanged(nameof(ShowTestButton));
UpdateSessionTimer();
}
}
@@ -154,8 +163,35 @@ public partial class MainViewModel : ViewModelBase
public bool ShowEndStreamButton => IsLive || IsRecording;
/// <summary>The one button whose label changes: while recording it reads
/// "Stop Recording", while live "End Stream".</summary>
public string EndButtonText => IsRecording && !IsLive ? "Stop Recording" : "End Stream";
/// "Stop Recording", while live "End Stream" — and while a test broadcast runs,
/// "End Test" so the creator knows exactly what they're stopping.</summary>
public string EndButtonText =>
IsRecording && !IsLive ? "Stop Recording"
: IsTestStream ? "End Test"
: "End Stream";
/// <summary>TASK 41 — the Test button (top bar, left of Start): a one-click
/// private test broadcast. Only meaningful signed-in and idle — it disappears
/// the moment a session (live OR recording) starts.</summary>
public bool ShowTestButton => IsOffline && !IsRecording && IsConnected;
public bool CanStartTest => ShowTestButton;
/// <summary>TASK 41 — the current session is a test broadcast (drives the TEST
/// badge + gold top bar + "End Test" face). Internal setter is a test seam like
/// IsRecording: <see cref="BeginTestStream"/> flips it before <see cref="StreamStatus"/>
/// goes Streaming so the visuals initialize amber, and the stop/rollback path
/// clears it.</summary>
public bool IsTestStream
{
get => _isTestStream;
internal set
{
if (!SetProperty(ref _isTestStream, value)) return;
OnPropertyChanged(nameof(EndButtonText));
UpdateLiveVisuals();
}
}
public StreamHealth CurrentHealth
{
@@ -330,6 +366,7 @@ public partial class MainViewModel : ViewModelBase
}
public ICommand StartStreamCommand { get; }
public ICommand StartTestCommand { get; }
public ICommand EndStreamCommand { get; }
public ICommand SignInCommand { get; }
public ICommand ChooseRecordFolderCommand { get; }