9761b1d4be
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.
73 lines
3.4 KiB
C#
73 lines
3.4 KiB
C#
using ytLive.Services;
|
|
|
|
namespace ytLive.ViewModels;
|
|
|
|
public partial class MainViewModel
|
|
{
|
|
/// <summary>The branding credit as the preview pane shows it: a master-space
|
|
/// bitmap, or null while no credit is playing. It is the SAME frame the encoder
|
|
/// composites into the recording/stream (<see cref="BrandFlashFrame"/>), so what
|
|
/// the creator sees is what the viewers get — the old preview-only TextBlock lied
|
|
/// about this, and a 1080p credit that is only in the preview is not advertising
|
|
/// anyone. It cannot ride a per-source <c>VideoImageSource</c>: it is a global
|
|
/// overlay, like the social bar and the alert ticker.</summary>
|
|
private System.Windows.Media.Imaging.WriteableBitmap? _brandFlashBitmap;
|
|
|
|
public System.Windows.Media.Imaging.WriteableBitmap? BrandFlashImageSource
|
|
{
|
|
get => _brandFlashBitmap;
|
|
private set
|
|
{
|
|
_brandFlashBitmap = value;
|
|
OnPropertyChanged(nameof(BrandFlashImageSource));
|
|
OnPropertyChanged(nameof(BrandFlashVisible));
|
|
}
|
|
}
|
|
|
|
public bool BrandFlashVisible => _brandFlashBitmap != null;
|
|
|
|
/// <summary>The credit for the output compositor, or null when none is due. Read by
|
|
/// the frame pump every output frame; <c>null</c> for a premium user, always —
|
|
/// 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
|
|
/// overwrites its back buffer — a fresh 8MB bitmap per tick would churn the render
|
|
/// cache for nothing.</summary>
|
|
internal void PublishBrandFlashPreview(VideoFrame? frame)
|
|
{
|
|
if (frame == null)
|
|
{
|
|
if (_brandFlashBitmap != null) BrandFlashImageSource = null;
|
|
return;
|
|
}
|
|
|
|
if (_brandFlashBitmap == null
|
|
|| _brandFlashBitmap.PixelWidth != frame.Width
|
|
|| _brandFlashBitmap.PixelHeight != frame.Height)
|
|
{
|
|
BrandFlashImageSource = new System.Windows.Media.Imaging.WriteableBitmap(
|
|
frame.Width, frame.Height, 96, 96,
|
|
System.Windows.Media.PixelFormats.Bgra32, null);
|
|
}
|
|
|
|
var target = _brandFlashBitmap!;
|
|
target.WritePixels(
|
|
new System.Windows.Int32Rect(0, 0, frame.Width, frame.Height),
|
|
frame.BgraPixels, frame.Stride, 0);
|
|
// The bitmap instance is unchanged, so the pane's Source binding still points
|
|
// at it — nudge it so WPF re-reads the back buffer this tick.
|
|
OnPropertyChanged(nameof(BrandFlashImageSource));
|
|
}
|
|
}
|