branding credit: run in every scene and in recordings, not only while live
Creator 2026-09-26: "the made with llamacasty flash should appear in all scenes,
not just live" and "should also appear in recordings". The presenter was
Start()/Stop()-ed from UpdateLiveVisuals()'s IsLive branch, so a recording made
WITHOUT ever going live carried no credit at all -- the exact case the creator hit
while judging compositing from local recordings.
It is now Start()ed once in the MainViewModel ctor and never stopped on live-state
churn. One start covers every scene, the preview, the stream and the recording,
because go-live and local recording are the SAME FramePump: both
Streaming.Operations.cs:68 and :199 call StartAsync with the same brandFlash:
delegate. Verified by reading both call sites, not assumed -- there is no second
encoder path that needed a "redirect". The licence gate needs no live branch at
all: IsPremium's setter already pushes BrandFlashPresenter.Enabled from anywhere.
Fixed a trap that app-lifetime exposed: Enabled = false stops the presenter's
DispatcherTimer to cut the advertisement mid-credit. When Start() was per-go-live
the next go-live restarted it; with a single app-lifetime Start() nothing would,
so a key entered mid-session would leave the credit dead until the process was
restarted. The setter now restarts the timer when re-enabling while _running.
Tests (12 facts in BrandFlashOutputTests, +2):
Credit_IsComposited_WithNoLiveSession_AndSoARecordingCarriesIt -- drives a real
never-live VM past the 5s first-flash delay and asks the frame the pump would
composite. Uses a new internal AdvanceBrandFlash seam; because Advance only
advances the cadence while the presenter is running, a credit coming out
proves Start() happened at construction.
ADowngradeMidSession_RestartsTheCadenceTimer -- asserts the premium/downgrade
edge decision directly (IsCadenceTimerEnabled) instead of sleeping through a
30-60s interval, which a synchronous test body cannot observe.
Full suite 364/364.
This commit is contained in:
@@ -31,6 +31,14 @@ public partial class MainViewModel
|
||||
/// the licence check lives inside the presenter so no caller can bypass it.</summary>
|
||||
public VideoFrame? BrandFlashFrame() => _brandFlash.Frame;
|
||||
|
||||
/// <summary>Test-only seam: advances the credit's clock deterministically, exactly
|
||||
/// like <see cref="Services.BrandFlashPresenter.Advance"/>. It proves the wiring
|
||||
/// (the presenter is started, and NOT gated on <c>IsLive</c>) without waiting out
|
||||
/// the real 5s DispatcherTimer — and because <c>Advance</c> only advances the
|
||||
/// cadence while the presenter is running, a credit coming out of here is proof
|
||||
/// that <c>Start()</c> happened at construction. Never call this from product code.</summary>
|
||||
internal void AdvanceBrandFlash(double seconds) => _brandFlash.Advance(seconds);
|
||||
|
||||
/// <summary>Publish one credit frame to the preview pane, UI thread only (the
|
||||
/// presenter fires this from its own dispatcher tick, so the bitmap write is legal).
|
||||
/// Reuses one <see cref="System.Windows.Media.Imaging.WriteableBitmap"/> and
|
||||
|
||||
@@ -429,18 +429,15 @@ public partial class MainViewModel : ViewModelBase
|
||||
PreviewGlowBrush = live ? IsTestStream ? "#9c6f1c" : "#e94560" : "Transparent";
|
||||
PreviewGlowThickness = live ? new Thickness(3) : new Thickness(0);
|
||||
|
||||
if (live)
|
||||
{
|
||||
// Cadence lives in the presenter: first credit 5s after go-live, then
|
||||
// every rand(30s)+30s. It refuses to emit while premium.
|
||||
_brandFlash.Enabled = !IsPremium;
|
||||
_brandFlash.Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
_healthPollTimer.Stop();
|
||||
_brandFlash.Stop();
|
||||
}
|
||||
if (!live) _healthPollTimer.Stop();
|
||||
|
||||
// NOTE: the branding credit is deliberately NOT started/stopped here.
|
||||
// CREATOR RULING 2026-09-26 — it must appear in EVERY scene and in
|
||||
// recordings, not only while streaming. It used to be gated on IsLive, which
|
||||
// meant a recording made without ever going live carried no credit at all.
|
||||
// The presenter is started once for the life of the app (MainViewModel ctor);
|
||||
// the licence gate rides on BrandFlashPresenter.Enabled via IsPremium, so it
|
||||
// needs no live branch — IsPremium's setter pushes it from any thread.
|
||||
UpdateSessionTimer();
|
||||
}
|
||||
|
||||
|
||||
@@ -152,6 +152,15 @@ public partial class MainViewModel : ViewModelBase
|
||||
|
||||
_brandFlash = new BrandFlashPresenter();
|
||||
_brandFlash.OnFrame = PublishBrandFlashPreview;
|
||||
// Start the cadence ONCE for the life of the app, here. CREATOR RULING
|
||||
// 2026-09-26: the credit must show in every scene and in recordings, so it
|
||||
// cannot be tied to IsLive (that gate meant a recording made without going
|
||||
// live carried no credit). The pump reads BrandFlashFrame() whenever it runs
|
||||
// — go-live and local recording are the same FramePump, so one start covers
|
||||
// output, recording and the preview pane alike. Enabled is seeded from the
|
||||
// current licence state; ValidateLicenseAtStartupAsync re-pushes it later.
|
||||
_brandFlash.Enabled = !IsPremium;
|
||||
_brandFlash.Start();
|
||||
|
||||
_healthPollTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) };
|
||||
_healthPollTimer.Tick += OnHealthPollTick;
|
||||
|
||||
Reference in New Issue
Block a user