Branding flash monetization: full-frame 'made with ytLlive!' every 300s replaces the always-on watermark
This commit is contained in:
+34
-1
@@ -376,6 +376,39 @@
|
|||||||
<Ellipse Width="26" Height="26" Fill="#e94560" Stroke="White" StrokeThickness="2"
|
<Ellipse Width="26" Height="26" Fill="#e94560" Stroke="White" StrokeThickness="2"
|
||||||
HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,-13,-13"/>
|
HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,-13,-13"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<Grid x:Name="BrandFlashLayer" Width="1920" Height="1080" IsHitTestVisible="False" Opacity="0">
|
||||||
|
<Grid.Style>
|
||||||
|
<Style TargetType="Grid">
|
||||||
|
<Setter Property="Opacity" Value="0"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding BrandFlashActive}" Value="True">
|
||||||
|
<DataTrigger.EnterActions>
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<DoubleAnimation Storyboard.TargetProperty="Opacity"
|
||||||
|
From="0" To="0.25" Duration="0:0:0.25"/>
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</DataTrigger.EnterActions>
|
||||||
|
<DataTrigger.ExitActions>
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<DoubleAnimation Storyboard.TargetProperty="Opacity"
|
||||||
|
From="0.25" To="0" Duration="0:0:0.25"/>
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</DataTrigger.ExitActions>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Grid.Style>
|
||||||
|
<Viewbox Stretch="Uniform" MaxWidth="1600" MaxHeight="800"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="made with ytLlive!" FontWeight="Bold" Foreground="White"
|
||||||
|
FontSize="180" TextAlignment="Center"/>
|
||||||
|
</Viewbox>
|
||||||
|
</Grid>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
@@ -622,7 +655,7 @@
|
|||||||
<TextBlock Text="Live streaming for YouTube — simple enough that even the most right-brained person can intuit it."
|
<TextBlock Text="Live streaming for YouTube — simple enough that even the most right-brained person can intuit it."
|
||||||
Foreground="#a0a0b0" FontSize="13" TextWrapping="Wrap" MaxWidth="380"
|
Foreground="#a0a0b0" FontSize="13" TextWrapping="Wrap" MaxWidth="380"
|
||||||
TextAlignment="Center" Margin="0,16,0,0"/>
|
TextAlignment="Center" Margin="0,16,0,0"/>
|
||||||
<TextBlock Text="Free forever with a watermark. A one-time unlock removes the watermark and adds alerts."
|
<TextBlock Text="Free forever — live streams carry a brief "made with ytLlive!" flash. A one-time unlock removes it and adds alerts."
|
||||||
Foreground="#888" FontSize="12" TextWrapping="Wrap" MaxWidth="380"
|
Foreground="#888" FontSize="12" TextWrapping="Wrap" MaxWidth="380"
|
||||||
TextAlignment="Center" Margin="0,12,0,0"/>
|
TextAlignment="Center" Margin="0,12,0,0"/>
|
||||||
<TextBlock Text="Made by gramps · llama chile shop" Foreground="#a0a0b0" FontSize="12"
|
<TextBlock Text="Made by gramps · llama chile shop" Foreground="#a0a0b0" FontSize="12"
|
||||||
|
|||||||
@@ -139,7 +139,12 @@ Preview shows the transition too (WYSIWYG). No wipes/slides/LUTs beyond the four
|
|||||||
4. **Chat box** — rendered from the live chat poll (right panel is the same feed, raw)
|
4. **Chat box** — rendered from the live chat poll (right panel is the same feed, raw)
|
||||||
5. **Scene compositing** — per-scene source layering (z-order = sources list order, top-to-bottom
|
5. **Scene compositing** — per-scene source layering (z-order = sources list order, top-to-bottom
|
||||||
back-to-front), preview rendered via D3DImage or MediaElement
|
back-to-front), preview rendered via D3DImage or MediaElement
|
||||||
6. **Drag/drop placement & reorder** — intuitive, visual (per design principle):
|
6. **Branding flash** — the topmost full-frame "made with ytLlive!" layer at ~25% opacity, ~1s on /
|
||||||
|
300s off (see Monetization in `ai.md`), gated on `BrandFlashEnabled` + live/recording. Lives in the
|
||||||
|
preview compositor now (`BrandFlashLayer` in `MainWindow.xaml` CanvasGrid, driven by
|
||||||
|
`BrandFlashActive`/`BrandFlashTimer` in `MainViewModel`); the encoder output renders the same layer,
|
||||||
|
and v0.2 local recordings carry it too
|
||||||
|
7. **Drag/drop placement & reorder** — intuitive, visual (per design principle):
|
||||||
- **Preview:** click-drag a source in the center panel to reposition it; resize via handles
|
- **Preview:** click-drag a source in the center panel to reposition it; resize via handles
|
||||||
- **Scenes list:** drag rows to reorder scenes
|
- **Scenes list:** drag rows to reorder scenes
|
||||||
- **Sources list:** drag rows to reorder sources (this *is* the z-order) — implemented
|
- **Sources list:** drag rows to reorder sources (this *is* the z-order) — implemented
|
||||||
@@ -231,6 +236,6 @@ Preview shows the transition too (WYSIWYG). No wipes/slides/LUTs beyond the four
|
|||||||
|
|
||||||
## Backlog (future versions)
|
## Backlog (future versions)
|
||||||
|
|
||||||
- v0.2 — Recording to local file
|
- v0.2 — Recording to local file (recordings carry the branding flash — see TASK 3 / `ai.md` Monetization)
|
||||||
- v0.3 — Stream scheduling
|
- v0.3 — Stream scheduling
|
||||||
- v0.4 — Multi-destination restreaming
|
- v0.4 — Multi-destination restreaming
|
||||||
|
|||||||
@@ -56,6 +56,14 @@ public class MainViewModel : ViewModelBase
|
|||||||
private DispatcherTimer? _saveDebounce;
|
private DispatcherTimer? _saveDebounce;
|
||||||
private bool _isLoading;
|
private bool _isLoading;
|
||||||
|
|
||||||
|
// Branding flash (monetization): a full-frame "made with ytLlive!" shown
|
||||||
|
// for ~1s every 300s on the live output, ~25% opacity. Paid unlock sets
|
||||||
|
// BrandFlashEnabled = false. See ai.md "Monetization".
|
||||||
|
private readonly DispatcherTimer _brandFlashTimer;
|
||||||
|
private readonly DispatcherTimer _brandFlashOffTimer;
|
||||||
|
private bool _brandFlashEnabled = true;
|
||||||
|
private bool _brandFlashActive;
|
||||||
|
|
||||||
private const string SupportEmail = "gramps@llamachile.shop";
|
private const string SupportEmail = "gramps@llamachile.shop";
|
||||||
private const string ChannelUrl = "https://youtube.com/@llamachileshop";
|
private const string ChannelUrl = "https://youtube.com/@llamachileshop";
|
||||||
public static string AppVersionLabel => $"Version {typeof(MainViewModel).Assembly.GetName().Version?.ToString(3)}";
|
public static string AppVersionLabel => $"Version {typeof(MainViewModel).Assembly.GetName().Version?.ToString(3)}";
|
||||||
@@ -135,6 +143,33 @@ public class MainViewModel : ViewModelBase
|
|||||||
public bool ShowPreviewPlaceholder => !ShowEmptySceneHint && ActiveBackgroundImage == null;
|
public bool ShowPreviewPlaceholder => !ShowEmptySceneHint && ActiveBackgroundImage == null;
|
||||||
public bool ShowSourcesEmptyHint => ActiveScene == null || ActiveScene.Sources.Count == 0;
|
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()
|
private void UpdateActiveBackground()
|
||||||
{
|
{
|
||||||
var background = ActiveScene?.Sources.FirstOrDefault(s => s.Type == SourceType.Background);
|
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 = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1500) };
|
||||||
_saveDebounce.Tick += (_, _) => SaveLayoutNow();
|
_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;
|
Scenes.CollectionChanged += OnScenesChanged;
|
||||||
|
|
||||||
AddSceneCommand = new RelayCommand(_ => AddScene());
|
AddSceneCommand = new RelayCommand(_ => AddScene());
|
||||||
@@ -852,15 +893,44 @@ public class MainViewModel : ViewModelBase
|
|||||||
LiveElapsedText = "00:00:00";
|
LiveElapsedText = "00:00:00";
|
||||||
LivePulseOpacity = 1.0;
|
LivePulseOpacity = 1.0;
|
||||||
_liveTimer.Start();
|
_liveTimer.Start();
|
||||||
|
if (BrandFlashEnabled) StartBrandFlashTimer();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_liveTimer.Stop();
|
_liveTimer.Stop();
|
||||||
|
_brandFlashTimer.Stop();
|
||||||
|
_brandFlashOffTimer.Stop();
|
||||||
|
BrandFlashActive = false;
|
||||||
LiveElapsedText = "00:00:00";
|
LiveElapsedText = "00:00:00";
|
||||||
LivePulseOpacity = 1.0;
|
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)
|
private void OnLiveTimerTick(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
_liveElapsed = _liveElapsed.Add(TimeSpan.FromSeconds(1));
|
_liveElapsed = _liveElapsed.Add(TimeSpan.FromSeconds(1));
|
||||||
|
|||||||
@@ -81,20 +81,26 @@ Apply this to every UI decision:
|
|||||||
- Visual/drag-and-drop scene building over property panels
|
- Visual/drag-and-drop scene building over property panels
|
||||||
- Every action produces a visible outcome — no dead ends
|
- 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
|
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
|
**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):
|
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
|
- **Free:** a periodic full-frame branding flash — "made with ytLlive!" rendered big and centered at
|
||||||
of Damocles. Standard practice; only Streamlabs runs watermark-nagging to a capitalist extreme.
|
~25% opacity for about one second (soft 250ms fade in/out), repeated every 300s, on the live output
|
||||||
- **Paid (one-time):** watermark removed + **Alerts** (Super Chat / membership / subscribe pop-ins).
|
(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
|
Deliberately rejected: always-on watermark (obscurable — replaced by the flash), hard stream-time
|
||||||
reads as broken, and YouTube streams routinely run 2-4 hours), soft-limit nagging, freemium tiers,
|
cutoffs (the worst dead end — a stream dying mid-broadcast reads as broken, and YouTube streams
|
||||||
and donation-only (relies on the kindness of strangers). Resolution/quality ceilings are **deferred** —
|
routinely run 2-4 hours), soft-limit nagging, freemium tiers, and donation-only (relies on the
|
||||||
that decision belongs to the resolution & streaming-constraints conversation, not monetization.
|
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
|
## Auth gates Go Live, but not exploration
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user