feat(ui): show YouTube-confirmed live state in top bar

Normal ON-AIR stays red until YouTube reports lifeCycleStatus=live, then turns green; TEST stays gold and stop restores the idle color.

Status-color reference: https://www.carbondesignsystem.com/building-blocks/core/patterns/status-indicators
This commit is contained in:
2026-11-04 11:19:40 -08:00
parent f921c0174b
commit 4b7e798757
5 changed files with 77 additions and 33 deletions
@@ -234,6 +234,12 @@ public partial class MainViewModel : ViewModelBase
EditingActive = false;
await _framePump.StartAsync(); // never throws; failures log + surface via Failed
// Check promptly until YouTube confirms the broadcast is live. Health
// polling returns to its normal interval after that confirmation.
_healthPollTimer.Interval = TimeSpan.FromSeconds(2);
_healthPollTimer.Start();
_ = PollHealthAsync();
string? liveChatId = null;
try
{
@@ -264,10 +270,6 @@ public partial class MainViewModel : ViewModelBase
AppLog.Write($"Test session dock open for {_currentBroadcastId}");
}
// Health polling (TASK 5 item 3): poll immediately, then every 30s while
// live. Report-by-exception — a banner appears only on warning/error.
_healthPollTimer.Start();
_ = PollHealthAsync();
}
private void OnHealthPollTick(object? sender, EventArgs e) => _ = PollHealthAsync();
@@ -293,19 +295,21 @@ public partial class MainViewModel : ViewModelBase
{
AppLog.Write("Stream health poll failed; check the OAuth session",
AppLogMessageType.Error, AppLogErrorClass.Class2);
return;
}
CurrentHealth.HealthStatus = health.HealthStatus;
CurrentHealth.HealthMessage = health.HealthStatus;
CurrentHealth.ConfigurationIssues.Clear();
CurrentHealth.ConfigurationIssues.AddRange(health.ConfigurationIssues);
OnPropertyChanged(nameof(CurrentHealth));
ApplyHealthIssue(health.ConfigurationIssues);
else
{
CurrentHealth.HealthStatus = health.HealthStatus;
CurrentHealth.HealthMessage = health.HealthStatus;
CurrentHealth.ConfigurationIssues.Clear();
CurrentHealth.ConfigurationIssues.AddRange(health.ConfigurationIssues);
OnPropertyChanged(nameof(CurrentHealth));
ApplyHealthIssue(health.ConfigurationIssues);
}
if (!string.IsNullOrWhiteSpace(_currentBroadcastId) && !_broadcastSeenLive)
{
var lifeCycle = await _youtubeStream.LogBroadcastDiagnosticsAsync(_currentBroadcastId, _reusableStream.Id);
if (lifeCycle is "live" or "liveStarting") _broadcastSeenLive = true;
ApplyBroadcastLifecycleStatus(lifeCycle);
}
}
catch (Exception ex)
@@ -505,17 +509,23 @@ public partial class MainViewModel : ViewModelBase
private void UpdateLiveVisuals()
{
var live = IsLive;
// A live TEST broadcast wears gold, not the live red — the TEST badge +
// gold are the "this is a rehearsal" cue (TASK 41).
// TEST keeps its gold identity. For a normal ON-AIR session, green means
// YouTube has confirmed live; before then it keeps the existing red color.
TopBarBackground = live
? ThemeBrush.Hex(IsTestStream ? "Brush.TestContainer" : "Brush.Primary")
? IsTestStream
? ThemeBrush.Hex("Brush.TestContainer")
: ThemeBrush.Hex(_youtubeBroadcastLive ? "Brush.Positive" : "Brush.Primary")
: ThemeBrush.Hex("Brush.SurfaceContainer");
PreviewGlowBrush = live
? ThemeBrush.Hex(IsTestStream ? "Brush.TestContainer" : "Brush.Primary")
: "Transparent";
PreviewGlowThickness = live ? new Thickness(3) : new Thickness(0);
if (!live) _healthPollTimer.Stop();
if (!live)
{
_healthPollTimer.Stop();
_healthPollTimer.Interval = TimeSpan.FromSeconds(30);
}
// NOTE: the branding credit is deliberately NOT started/stopped here.
// CREATOR RULING 2026-09-26 — it must appear in EVERY scene and in
+18
View File
@@ -24,6 +24,7 @@ public partial class MainViewModel : ViewModelBase
/// <summary>Latches once the broadcast reaches live so the per-poll broadcast
/// diagnostic stops logging (2026-10-04). Reset at broadcast creation.</summary>
private bool _broadcastSeenLive;
private bool _youtubeBroadcastLive;
private string? _reusableStreamUrl;
private ReusableStream? _reusableStream;
private string? _healthIssueBanner;
@@ -63,6 +64,7 @@ public partial class MainViewModel : ViewModelBase
{
if (SetProperty(ref _streamStatus, value))
{
_youtubeBroadcastLive = false;
OnPropertyChanged(nameof(IsOffline));
OnPropertyChanged(nameof(IsLive));
OnPropertyChanged(nameof(IsEditMode));
@@ -373,6 +375,22 @@ public partial class MainViewModel : ViewModelBase
set => SetProperty(ref _topBarBackground, value);
}
internal void ApplyBroadcastLifecycleStatus(string? lifeCycleStatus)
{
if (!IsLive) return;
if (!string.Equals(lifeCycleStatus, "live", StringComparison.OrdinalIgnoreCase))
{
if (!_broadcastSeenLive)
_healthPollTimer.Interval = TimeSpan.FromSeconds(2);
return;
}
_broadcastSeenLive = true;
_youtubeBroadcastLive = true;
_healthPollTimer.Interval = TimeSpan.FromSeconds(30);
UpdateLiveVisuals();
}
public string PreviewGlowBrush
{
get => _previewGlowBrush;