Screen backdrop (schema v5/v6) + five-scene catalog + webcam polish: live desktop/game capture as a permanent non-deletable bottom layer (Source.IsBackdrop), auto full-screen game detection (Win32FullScreenDetector) else primary display, GraphicsCapturePicker re-designation, refcounted shared ScreenCaptureManager, Live-only backdrop by policy (HasBackdrop + v5-v6 backfill + EnforceBackdropPolicy, checkbox gone), SceneCatalog (Starting/Live/BRB/Chat/Ending) with + button re-adding missing scenes, webcam mid-session transparent-container fix (GetPreviewBitmap propagation), Chat half-screen-area cap, 'Add Webcam' always opens the picker (SwapWebcamIdentityAsync, no silent resurrect), WindowsRuntimeMarshal frame-read + 5s-throttled errors, docs updated, tests (65 passing)

This commit is contained in:
2026-08-07 14:15:50 -07:00
parent e037ba027b
commit ad9b3e1e48
27 changed files with 2232 additions and 116 deletions
+11
View File
@@ -11,6 +11,7 @@ public class Scene : INotifyPropertyChanged
private string _name = string.Empty;
private bool _isEditing;
private bool _isHidden;
private bool _hasBackdrop;
public string Name
{
@@ -30,6 +31,16 @@ public class Scene : INotifyPropertyChanged
set => Set(ref _isHidden, value);
}
/// <summary>
/// Whether the scene has a live desktop/game backdrop layer. Enforced by
/// policy: only the canonical Live scene ever has one (see SceneCatalog).
/// </summary>
public bool HasBackdrop
{
get => _hasBackdrop;
set => Set(ref _hasBackdrop, value);
}
/// <summary>
/// The scene's rendered elements in z-order (back to front): multi-instance
/// Sources plus this scene's webcam usage (<see cref="WebcamConfig"/>), if any.
+32
View File
@@ -0,0 +1,32 @@
using System;
using System.Linq;
namespace ytLive.Models;
/// <summary>
/// The app's five canonical scenes. Scenes are worked on by name; you can delete
/// them ("work with less"), but the app only ever adds back one of these five —
/// anything beyond them is OBS territory. The live desktop/game backdrop exists
/// only in the Live scene.
/// </summary>
public static class SceneCatalog
{
public const string Starting = "Starting";
public const string Live = "Live";
public const string Brb = "BRB";
public const string Chat = "Chat";
public const string Ending = "Ending";
public static readonly string[] All = { Starting, Live, Brb, Chat, Ending };
public static bool IsCanonical(string? name)
=> name != null && All.Contains(name.Trim(), StringComparer.OrdinalIgnoreCase);
public static bool Is(string? name, string canonical)
=> name != null && string.Equals(name.Trim(), canonical, StringComparison.OrdinalIgnoreCase);
public static bool IsChat(string? name) => Is(name, Chat);
/// <summary>Only the Live scene carries the live desktop/game backdrop.</summary>
public static bool HasBackdrop(string? name) => Is(name, Live);
}
+26 -2
View File
@@ -27,7 +27,22 @@ public enum ClipShape
public class Source : SceneElement
{
private SourceType _type;
public SourceType Type { get => _type; set => Set(ref _type, value); }
public SourceType Type
{
get => _type;
set
{
if (Set(ref _type, value))
{
Raise(nameof(IsLiveCapture));
Raise(nameof(DisplaySource));
}
}
}
/// <summary>The permanent scene backdrop (live desktop/game capture). Never removable/reorderable.</summary>
private bool _isBackdrop;
public bool IsBackdrop { get => _isBackdrop; set => Set(ref _isBackdrop, value); }
private bool _isEnabled = true;
public bool IsEnabled { get => _isEnabled; set => Set(ref _isEnabled, value); }
@@ -39,6 +54,15 @@ public class Source : SceneElement
private IntPtr? _windowHandle;
public IntPtr? WindowHandle { get => _windowHandle; set => Set(ref _windowHandle, value); }
/// <summary>
/// Identifies what this live-capture source points at: "monitor:&lt;index&gt;" or
/// "window:&lt;hwnd&gt;". Persisted so a re-designated backdrop survives a reload.
/// </summary>
private string? _captureKey;
public string? CaptureKey { get => _captureKey; set => Set(ref _captureKey, value); }
public bool IsLiveCapture => Type is SourceType.DisplayCapture or SourceType.WindowCapture;
// Image (asset stored in the layout database)
private string? _assetId;
private ImageSource? _imageSource;
@@ -59,7 +83,7 @@ public class Source : SceneElement
public ImageSource? ImageSource => _imageSource;
public override ImageSource? DisplaySource => _imageSource;
public override ImageSource? DisplaySource => IsLiveCapture ? VideoImageSource : _imageSource;
public override bool IsImageSource => Type == SourceType.Image;
}
+3 -2
View File
@@ -5,8 +5,9 @@ Plain data types. No logic beyond what a property can carry. See
| File | Purpose |
|------|---------|
| `Scene.cs` | A scene: `Name`, `IsHidden`, `IsChatScene`, `IsEditing`, `Elements` collection (images + webcam config), `WebcamConfig` accessor |
| `Source.cs` | An image source: `SourceType` enum (Image/Screen/Background/Text — **Webcam removed**), `X`/`Y`/`Width`/`Height`, `Opacity`, `IsEnabled`, `ImageSource` (static) / `VideoImageSource` (live frames), `DisplaySource`, `ClipShape` enum (Traditional/Round), `IsMirrored` + `MirrorScale`, `MirrorButtonText`/`ShapeButtonText`, asset identity, `IsImageSource` (XAML binds this, never `Type`) |
| `Scene.cs` | A scene: `Name`, `IsHidden`, `IsChatScene`, `IsEditing`, **`HasBackdrop`** (Live-only policy flag, default off — only the canonical Live scene has one, see `SceneCatalog`), `Elements` collection (images + webcam config), `WebcamConfig` accessor |
| `SceneCatalog.cs` | The five canonical scenes (Starting/Live/BRB/Chat/Ending) — the product, by name: `All`, `IsCanonical`, `Is(name, canonical)` (case-insensitive trim), `IsChat`, `HasBackdrop(name)` (true only for Live). Drives the empty-DB seed, the "+" missing-scenes menu, and `MainViewModel.EnforceBackdropPolicy` |
| `Source.cs` | A source: `SourceType` enum (DisplayCapture/WindowCapture/Background/Image/TextOverlay — **Webcam removed**), `X`/`Y`/`Width`/`Height`, `Opacity`, `IsEnabled`, `ImageSource` (static) / `VideoImageSource` (live frames), `DisplaySource` (`IsLiveCapture` → video, else static), `ClipShape` enum (Traditional/Round), `IsMirrored` + `MirrorScale`, `MirrorButtonText`/`ShapeButtonText`, asset identity, `IsImageSource` (XAML binds this, never `Type`), **backdrop surface**: `IsBackdrop` (permanent bottom layer, never removable), `CaptureKey` (persisted `monitor:<n>`/`window:<hwnd>`/`picker:<name>`), `IsLiveCapture` (`Type` is Display/WindowCapture) |
| `SceneElement.cs` | Base for anything placeable in a scene: shared layout/clip/mirror/border surface, `virtual IsWebcam`/`virtual IsImageSource`; `ToggleClipShape` + persisted `RectWidth`/`RectHeight` (pre-Round rect so round→rect restores after reload) |
| `Webcam.cs` | Singleton webcam identity: `Id`, `DeviceId`, `Name` (one row app-wide) |
| `WebcamSceneConfig.cs` | Per-scene webcam placement (subclass of `SceneElement`): geometry + `IsVisible` + border (`BorderColor`/`BorderOpacity`/`BorderWidth`/`BorderAnimation`) + `VideoImageSource`; `WebcamId` links to `Webcam` |