4e0915a36e
Click-to-pin an open window as the full-bleed Live backdrop, via the existing
window:<hwnd> capture path (same render layer as game/desktop). Session-scoped:
window:/picker: keys heal to auto on reload (HWND-recycle hazard); pin wins
while the window enumerates, then auto-fallbacks (game → desktop → static); a
dead window's capture session heals to auto, no dead ends.
Fix: ScreenCaptureSourceFactory.Resolve hex-parsed "window:0x<Hwnd>" WITHOUT
stripping the 0x prefix — NumberStyles.HexNumber rejects it, so every pin
resolved null ("No capture target for this key") and the heal silently rolled
back. Same flaw in IsAliveWindowPin (pin-wins guard was dead). Both fixed.
Commit also carries an out-of-scope prerequisite: PillRadioTests.cs:105 had a
committed stray token ("...PrimaryStartButtonLabel soil;", CS1003) blocking the
whole test-project compile — token removed.
Plus the previous session's uncommitted pricing docs (one-time $29/$49) that
share TASKS.md/ai.md/HANDOFF.md.
79 lines
3.0 KiB
C#
79 lines
3.0 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Threading.Tasks;
|
|
using Windows.Graphics.Capture;
|
|
|
|
namespace ytLive.Services;
|
|
|
|
/// <summary>
|
|
/// Turns a capture key ("monitor:<n>", "window:<hwnd>", "picker:<...>")
|
|
/// into a live <see cref="IScreenCaptureSource"/>, and hosts the OS
|
|
/// GraphicsCapturePicker used by "Change Capture…". Picker picks are transient:
|
|
/// the picked GraphicsCaptureItem has no HWND/monitor identity, so a
|
|
/// "picker:" key resolves only within the current session and re-detection
|
|
/// takes over after a reload (documented v1 limit).
|
|
/// </summary>
|
|
public sealed class ScreenCaptureSourceFactory
|
|
{
|
|
private readonly Func<IntPtr> _ownerHwndProvider;
|
|
private readonly Dictionary<string, GraphicsCaptureItem> _picked = new();
|
|
private readonly object _gate = new();
|
|
|
|
public ScreenCaptureSourceFactory(Func<IntPtr> ownerHwndProvider)
|
|
{
|
|
_ownerHwndProvider = ownerHwndProvider;
|
|
}
|
|
|
|
/// <summary>Creates the source for a persisted key, or null when it can't be resolved.</summary>
|
|
public IScreenCaptureSource? Resolve(string key)
|
|
{
|
|
if (key.StartsWith("monitor:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var indexText = key.AsSpan("monitor:".Length);
|
|
if (int.TryParse(indexText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index))
|
|
return ScreenCaptureFrameSource.CreateForMonitor(index);
|
|
return null;
|
|
}
|
|
|
|
if (key.StartsWith("window:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Keys are written as "window:0x<Hwnd:X>" (CaptureInterop/WindowInfo),
|
|
// but NumberStyles.HexNumber does not accept the "0x" prefix — strip it.
|
|
var hexText = key.AsSpan("window:".Length).TrimStart();
|
|
if (hexText.Length >= 2 && hexText[0] == '0' && (hexText[1] == 'x' || hexText[1] == 'X'))
|
|
hexText = hexText[2..];
|
|
if (long.TryParse(hexText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hwnd))
|
|
return ScreenCaptureFrameSource.CreateForWindow(new IntPtr(hwnd));
|
|
return null;
|
|
}
|
|
|
|
if (key.StartsWith("picker:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
lock (_gate)
|
|
return _picked.TryGetValue(key, out var item)
|
|
? ScreenCaptureFrameSource.CreateForPicker(key, item)
|
|
: null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows the OS capture picker (must run on the UI thread, owner window set
|
|
/// via IInitializeWithWindow). Returns the session key, or null if cancelled.
|
|
/// </summary>
|
|
public async Task<string?> PickAsync()
|
|
{
|
|
var picker = new GraphicsCapturePicker();
|
|
CaptureInterop.SetWindowOwner(picker, _ownerHwndProvider());
|
|
var item = await picker.PickSingleItemAsync();
|
|
if (item == null) return null;
|
|
|
|
var key = $"picker:{item.DisplayName}";
|
|
lock (_gate)
|
|
_picked[key] = item;
|
|
return key;
|
|
}
|
|
}
|