From 4ef11ef5ac90b3237499d7a4000414fc9067cc88 Mon Sep 17 00:00:00 2001 From: gramps Date: Thu, 6 Aug 2026 09:04:20 -0700 Subject: [PATCH] Branding flash monetization: full-frame 'made with ytLlive!' every 300s replaces the always-on watermark --- MainWindow.xaml | 35 ++++++++++++++++++- TASKS.md | 9 +++-- ViewModels/MainViewModel.cs | 70 +++++++++++++++++++++++++++++++++++++ ai.md | 22 +++++++----- 4 files changed, 125 insertions(+), 11 deletions(-) diff --git a/MainWindow.xaml b/MainWindow.xaml index 5a57213..8a2100d 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -376,6 +376,39 @@ + + + + + + + + + @@ -622,7 +655,7 @@ - $"Version {typeof(MainViewModel).Assembly.GetName().Version?.ToString(3)}"; @@ -135,6 +143,33 @@ public class MainViewModel : ViewModelBase public bool ShowPreviewPlaceholder => !ShowEmptySceneHint && ActiveBackgroundImage == null; public bool ShowSourcesEmptyHint => ActiveScene == null || ActiveScene.Sources.Count == 0; + // Paid unlock flips this off (see ai.md "Monetization"). When disabled the + // cadence timer is stopped and any active flash is hidden immediately. + public bool BrandFlashEnabled + { + get => _brandFlashEnabled; + set + { + if (!SetProperty(ref _brandFlashEnabled, value)) return; + if (value) + { + if (IsLive) StartBrandFlashTimer(); + } + else + { + _brandFlashTimer.Stop(); + _brandFlashOffTimer.Stop(); + BrandFlashActive = false; + } + } + } + + public bool BrandFlashActive + { + get => _brandFlashActive; + private set => SetProperty(ref _brandFlashActive, value); + } + private void UpdateActiveBackground() { var background = ActiveScene?.Sources.FirstOrDefault(s => s.Type == SourceType.Background); @@ -356,6 +391,12 @@ public class MainViewModel : ViewModelBase _saveDebounce = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1500) }; _saveDebounce.Tick += (_, _) => SaveLayoutNow(); + + _brandFlashTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(300) }; + _brandFlashTimer.Tick += OnBrandFlashTimerTick; + _brandFlashOffTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(750) }; + _brandFlashOffTimer.Tick += (_, _) => { _brandFlashOffTimer.Stop(); BrandFlashActive = false; }; + Scenes.CollectionChanged += OnScenesChanged; AddSceneCommand = new RelayCommand(_ => AddScene()); @@ -852,15 +893,44 @@ public class MainViewModel : ViewModelBase LiveElapsedText = "00:00:00"; LivePulseOpacity = 1.0; _liveTimer.Start(); + if (BrandFlashEnabled) StartBrandFlashTimer(); } else { _liveTimer.Stop(); + _brandFlashTimer.Stop(); + _brandFlashOffTimer.Stop(); + BrandFlashActive = false; LiveElapsedText = "00:00:00"; LivePulseOpacity = 1.0; } } + // First flash shortly after go-live, then every 300s (the cadence resets on + // the first tick so the interval is 5s only for that one shot). + private void StartBrandFlashTimer() + { + _brandFlashTimer.Stop(); + _brandFlashTimer.Interval = TimeSpan.FromSeconds(5); + _brandFlashTimer.Start(); + } + + private void OnBrandFlashTimerTick(object? sender, EventArgs e) + { + _brandFlashTimer.Stop(); + _brandFlashTimer.Interval = TimeSpan.FromSeconds(300); + _brandFlashTimer.Start(); + ShowBrandFlash(); + } + + private void ShowBrandFlash() + { + if (!IsLive || !BrandFlashEnabled) return; + BrandFlashActive = true; + _brandFlashOffTimer.Stop(); + _brandFlashOffTimer.Start(); + } + private void OnLiveTimerTick(object? sender, EventArgs e) { _liveElapsed = _liveElapsed.Add(TimeSpan.FromSeconds(1)); diff --git a/ai.md b/ai.md index c66ab5b..f33ec90 100644 --- a/ai.md +++ b/ai.md @@ -81,20 +81,26 @@ Apply this to every UI decision: - Visual/drag-and-drop scene building over property panels - Every action produces a visible outcome — no dead ends -## Monetization (design decision — the watermark is the sword) +## Monetization (design decision — the branding flash is the sword) Free forever: all streams unlimited, no time caps, no subscription, no per-feature paywalls. The **one paid line is a one-time unlock** (delivered via itch.io — they handle hosting, payment, and key delivery; we never own a server or a key shop): -- **Free:** a small "made with ytLlive" watermark is always on, every frame, every stream — the sword - of Damocles. Standard practice; only Streamlabs runs watermark-nagging to a capitalist extreme. -- **Paid (one-time):** watermark removed + **Alerts** (Super Chat / membership / subscribe pop-ins). +- **Free:** a periodic full-frame branding flash — "made with ytLlive!" rendered big and centered at + ~25% opacity for about one second (soft 250ms fade in/out), repeated every 300s, on the live output + (and on v0.2 local recordings). Implemented as `BrandFlashLayer` in the preview compositor + (`MainWindow.xaml` CanvasGrid) + `BrandFlashTimer` in `MainViewModel` — cadence 300s, first flash + ~5s after go-live, only while live or recording. An always-on watermark can be cropped or covered; + an intermittent full-frame flash can't be cropped and is impractical to edit around on a live feed. +- **Paid (one-time):** branding flash removed (flips `BrandFlashEnabled` off) + **Alerts** + (Super Chat / membership / subscribe pop-ins). -Deliberately rejected: hard stream-time cutoffs (the worst dead end — a stream dying mid-broadcast -reads as broken, and YouTube streams routinely run 2-4 hours), soft-limit nagging, freemium tiers, -and donation-only (relies on the kindness of strangers). Resolution/quality ceilings are **deferred** — -that decision belongs to the resolution & streaming-constraints conversation, not monetization. +Deliberately rejected: always-on watermark (obscurable — replaced by the flash), hard stream-time +cutoffs (the worst dead end — a stream dying mid-broadcast reads as broken, and YouTube streams +routinely run 2-4 hours), soft-limit nagging, freemium tiers, and donation-only (relies on the +kindness of strangers). Resolution/quality ceilings are **deferred** — that decision belongs to the +resolution & streaming-constraints conversation, not monetization. ## Auth gates Go Live, but not exploration