feat(topbar): the recording lives in the pill — green pulse, REC clock, Stop Recording
Build 730 (creator): - Left pill while recording: solid #2ECC71 dot pulsing 1.0 <-> 0.2 on the existing one-second timer, and "REC: hh:mm:ss" in Consolas so the digits cannot jitter the pill's width. Idle face unchanged. - Right pill while recording: a stop square and "Stop Recording"; clicking it runs the normal stop, which halts and resets the clock and returns the pair to "Local MP4" / "Start Recording". - The separate reality line (dot + REC/LIVE + elapsed + End) is now LIVE-ONLY: a recording would otherwise print the same three facts twice. RunningVisible is IsLive, and ShowOutputStage no longer hides itself for a recording (it must be on screen to carry the clock and the stop). Two rules collided and are now separated rather than merged: a creator's Stop on a recording restores the Local MP4 pair (build 730), while a pipeline FAILURE still clears both pills (the 2026-09-01 zombie rule). StopStream(afterFailure:) carries the difference, and the pump-failure path passes it. RealAppHost.Run was rethrowing with `throw failure;`, which destroyed the original stack — a failing assertion inside the action reported the wrapper's line instead of its own. ExceptionDispatchInfo now preserves it; that is how the two failures above were located in one pass instead of five. 680 pass (one audio-pipe instance flake under full-suite load, passes alone), 0 warnings. Scope justification: Assets/starting-background.jpg is outside this unit — the creator's replacement art, still awaiting their visual check.
This commit is contained in:
@@ -404,12 +404,15 @@ public partial class MainViewModel : ViewModelBase
|
||||
HealthIssueBanner = report.Text;
|
||||
}
|
||||
|
||||
private async void StopStream()
|
||||
/// <param name="afterFailure">True when the session is being torn down because something broke,
|
||||
/// not because the creator pressed Stop. A failure clears both outputs (there is no session and no
|
||||
/// choice worth keeping); a creator's Stop of a recording restores the Local MP4 pair.</param>
|
||||
private async void StopStream(bool afterFailure = false)
|
||||
{
|
||||
var dispatcher = Application.Current?.Dispatcher;
|
||||
if (dispatcher != null && !dispatcher.CheckAccess())
|
||||
{
|
||||
_ = dispatcher.BeginInvoke(new Action(StopStream));
|
||||
_ = dispatcher.BeginInvoke(new Action(() => StopStream()));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -419,6 +422,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
RemoveAttentionAlert("stream-recovery");
|
||||
var wasLive = IsLive;
|
||||
var wasRecording = IsRecording;
|
||||
var wasRecordingOnly = IsRecording && !IsLive && !afterFailure;
|
||||
|
||||
StreamStatus = StreamStatus.Offline;
|
||||
_sessionStartedAtUtc = null;
|
||||
@@ -429,11 +433,20 @@ public partial class MainViewModel : ViewModelBase
|
||||
// and drops the TEST face — a test never outlives its run.
|
||||
TestSession.EndTestSilently();
|
||||
IsTestStream = false;
|
||||
// Stop ends EVERYTHING (creator ruling 2026-09-01): a lit pill with no session
|
||||
// behind it is a lie — the stuck REC pill after the failed 2026-09-01 recording
|
||||
// attempt was the report. Intent resets with reality; re-arming is one click.
|
||||
RecordPillOn = false;
|
||||
OnAirPillOn = false;
|
||||
// Stop ends the session (creator ruling 2026-09-01): a lit pill with no session behind it is a
|
||||
// lie. Since build 730 the pills are the chosen OUTPUT, not an armed intent, so a finished
|
||||
// local recording returns to its idle pair — Local MP4 chosen, Start Recording beside it
|
||||
// (creator's words) — while a finished stream clears both and asks again.
|
||||
if (wasRecordingOnly)
|
||||
{
|
||||
RecordPillOn = true;
|
||||
OnAirPillOn = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
RecordPillOn = false;
|
||||
OnAirPillOn = false;
|
||||
}
|
||||
WindowTitle = "LlamaCasty";
|
||||
// Deactivate zoom when stopping
|
||||
if (_zoomState.IsActive || _zoomState.IsMoving)
|
||||
@@ -544,7 +557,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
{
|
||||
var live = IsLive;
|
||||
_notifications.Error(live ? "The stream pipeline stopped" : "Recording stopped", message);
|
||||
StopStream();
|
||||
StopStream(afterFailure: true);
|
||||
}
|
||||
var dispatcher = System.Windows.Application.Current?.Dispatcher;
|
||||
if (dispatcher != null && !dispatcher.CheckAccess())
|
||||
@@ -643,6 +656,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
}
|
||||
OnPropertyChanged(nameof(RecDotOpacity));
|
||||
OnPropertyChanged(nameof(LiveElapsedText));
|
||||
OnPropertyChanged(nameof(RecordChipText));
|
||||
}
|
||||
|
||||
private void OnLiveTimerTick(object? sender, EventArgs e)
|
||||
@@ -650,8 +664,9 @@ public partial class MainViewModel : ViewModelBase
|
||||
_liveElapsed = _liveElapsed.Add(TimeSpan.FromSeconds(1));
|
||||
LiveElapsedText = _liveElapsed.ToString(@"hh\:mm\:ss");
|
||||
_recordLength = _liveElapsed;
|
||||
_recDotPulse = _recDotPulse > 0.5 ? 0.35 : 1.0;
|
||||
_recDotPulse = _recDotPulse > 0.5 ? 0.2 : 1.0;
|
||||
OnPropertyChanged(nameof(RecDotOpacity));
|
||||
OnPropertyChanged(nameof(RecordChipText));
|
||||
|
||||
// TASK 55·3: the last useful warning before the encoder fails for a reason it won't explain.
|
||||
if (IsRecording && !_lowDiskWarned && _recordFolder is { } folder
|
||||
|
||||
Reference in New Issue
Block a user