Files
ytLlive/Services/index.md
T
gramps 18a21010bb Scene compositor (TASK 4 ship step 1): encoder master-frame scaffolding
Software compositor that renders a scene into the encoder's master VideoFrame,
mirroring the XAML preview minus editing chrome: backdrop -> background ->
elements (UniformToFill cover-crop, round clip, mirror, opacity, border) ->
branding flash.

NOTE FOR USERS: this change shows NO difference in the app's UI — it is pure
backend scaffolding laying the groundwork for live video capture/streaming.
The preview you see is unchanged.

- Services/Compositor/: SceneCompositor (Render(scene, frameFor resolver,
  flashFrame, CompositorOptions)), CompositorOptions (source rect + output
  size; 16:9 full master, vertical 607x1080 -> 1080x1920), StretchMath (pure
  UniformToFill + bilinear), StaticPixelCache (asset bytes -> BGRA8 frame)
- Frame sources injected via Func<SceneElement, VideoFrame?> resolver, so the
  compositor is pure, WPF-free, and hermetic to test (D3D11 upgrade behind the
  same seam later)
- SceneElement.TryGetBorderColor public (shared hex parse), stale
  MainViewModel comment fixed, pre-existing CS1998 in YouTubeAuthServiceTests
  cleaned up
- Tests: SceneCompositorTests integration (full scene + vertical tier + flash)
  + StretchMath units, docs updated (72 tests passing, 0 warnings)
2026-08-10 10:15:58 -07:00

7.6 KiB
Raw Blame History

Services — index

External-facing logic: YouTube API, persistence. See schema.md for the memory-map conventions.

File Purpose
YouTubeAuthService.cs OAuth2 via Google: loopback callback (http://localhost:8765/oauth2/callback), token exchange, refresh, channel fetch. Constructor takes optional HttpClient + sessionChanged callback (test seam + save hook); session persists via Helpers/TokenStore (DPAPI); ClearSession() signs out (called by MainViewModel.StopStream on End Livestream)
YouTubeStreamService.cs Broadcast/stream management via the v3 API (enableAutoStart/Stop). Not yet switched to the variable reusable stream
YouTubeChatService.cs Polls liveChat/messages, raises MessageReceived; IDisposable
LayoutStore.cs SQLite persistence (Microsoft.Data.Sqlite) at %APPDATA%\ytLlive\ytLlive.db; assets stored as BLOBs keyed by SHA-256 content hash; save/open layout files; schema user_version 6 (Source.ClipShape/IsMirrored via ALTER TABLE for pre-v2 DBs; v3 = singleton Webcam + per-scene WebcamSceneConfig, migrated idempotently without backfill — the stale Source.DeviceId column remains but is no longer read/written; v4 = WebcamSceneConfig.RectWidth/RectHeight, the pre-Round rect for the round-to-rect restore; v5 = Source.IsBackdrop + Source.CaptureKey, the live-capture backdrop; v6 = Scene.HasBackdrop — Live-only policy, one-time backfill turns Starting/BRB/Chat/Ending off + drops their backdrop sources; MainViewModel.EnforceBackdropPolicy re-normalizes on every load)
VideoFrame.cs Normalized CPU frame seam (Width/Height/tightly-packed BGRA byte[]) — the only pixel type the rest of the app knows about; future capture sources (screen, background-removed webcam) feed the same seam
CameraDeviceInfo.cs (Id, DisplayName) for a physical capture device
ICameraEnumerator.cs GetCamerasAsync() — seam so the picker/CameraManager never touch WinRT (tests inject fakes)
ICameraFrameSource.cs StartAsync/StopAsync/FrameAvailable(VideoFrame) — seam for a running capture source
MediaCaptureCameraEnumerator.cs WinRT enumeration via DeviceInformation.FindAllAsync(DeviceClass.VideoCapture)
MicrophoneDeviceInfo.cs (Id, DisplayName) for an audio capture (mic) device
IMicrophoneEnumerator.cs GetMicrophonesAsync() — seam so the mic picker never touches WinRT (tests inject fakes)
WinRtMicrophoneEnumerator.cs WinRT mic enumeration via DeviceInformation.FindAllAsync(DeviceClass.AudioCapture) (no NAudio needed — capture libs stay deferred to the audio pipeline milestone)
MediaCaptureFrameSource.cs CPU-first MediaCapture source (MemoryPreference = Cpu, BGRA8 via CreateFrameReaderAsync); frames arrive on a worker thread
CameraManager.cs Webcam capture ownership: refcounted by DeviceId, one shared WriteableBitmap per camera, dispatcher-coalesced ~render-rate UI updates (latest-frame drop); PreviewBitmapChanged/CameraFailed events. ReleaseAsync decrements a ref (stops at zero); ReleaseAllAsync force-drops every ref + stops the source (webcam swap / layout reload). GetPreviewBitmap(deviceId) returns the current shared bitmap so a WebcamSceneConfig added mid-session (after the first frame already created the bitmap) still receives the live frames
IFullScreenDetector.cs Seam for the win32 full-screen detector: int? GetForegroundFullScreenMonitorIndex(), int PrimaryMonitorIndex(), IReadOnlyList<DisplayInfo> GetDisplays()
Win32FullScreenDetector.cs GetForegroundWindow + DwmGetWindowAttribute(DWMWA_EXTENDED_FRAME_BOUNDS) + MonitorFromWindow + GetMonitorInfo; monitor covers all four edges → full-screen; own process excluded; monitor order = EnumDisplayMonitors order (static GetMonitorHandle(int) maps index→HMONITOR for the capture factory). GetDisplays() returns DisplayInfo (index/name/resolution/bounds/IsPrimary, friendly name via EnumDisplayDevices) for the in-app "Capture Display" picker; PrimaryMonitorIndex() is the auto-key fallback when no full-screen game is detected
IScreenCaptureSource.cs Key + StartAsync/StopAsync/FrameAvailable(VideoFrame) — seam so ScreenCaptureManager never touches WinRT (tests inject fakes)
Direct3D11Helper.cs The one shared IDirect3DDevice per process: P/Invoke d3d11.dll!D3D11CreateDevice (hardware, BGRA_SUPPORT, explicit 11.1-first feature array) → QI IDXGIDevice → the WinRT interop export CreateDirect3D11DeviceFromDXGIDeviceMarshalInterface<IDirect3DDevice>.FromAbi. CsWinRT does not project Windows.Graphics.Direct3D11.Direct3D11Helper, so this hand-rolls it. Do not revert to QI-for-IDirect3DDxgiInterfaceAccess — the device no longer exposes it on newer Windows (E_NOINTERFACE on build 26200)
CaptureInterop.cs ComImport bridges the WinRT-projection gap: IGraphicsCaptureItemInterop (CreateForMonitor/CreateForWindow), IInitializeWithWindow (GraphicsCapturePicker owner window so the OS picker shows)
ScreenCaptureFrameSource.cs One Direct3D11CaptureFramePool (free-threaded, 2 buffers) + session per target; frames → SoftwareBitmap.CreateCopyFromSurfaceAsync (BGRA, alpha ignored) → VideoFrame, bytes read via WindowsRuntimeMarshal.TryGetDataUnsafe (CsWinRT-safe — the IMemoryBufferByteAccess ComImport cast fails on every frame and is gone). Surfaces > 1920×1080 downscaled bilinearly to the master; conversion failures logged ≤ once/5 s. DRM content = black frames (OS limit). CreateForMonitor/CreateForWindow/CreateForPicker
ScreenCaptureManager.cs Screen-capture ownership mirroring CameraManager: refcounted by target key, one shared WriteableBitmap, dispatcher-coalesced latest-frame copies; PreviewBitmapChanged/CaptureFailed events; ReleaseAllAsync used on re-designation
ScreenCaptureSourceFactory.cs Resolve(key) parses monitor:<n> / window:<hwnd> / picker:<name> into a source; PickAsync() shows the OS GraphicsCapturePicker and returns the picker: key (transient — a reload falls back to auto-detection)
Compositor/SceneCompositor.cs The output compositor (TASK 4 ship step 1): renders a scene into the encoder's master VideoFrame (tightly-packed BGRA8), mirroring the XAML preview minus editing chrome — backdrop → background → elements (UniformToFill cover-crop, round clip, mirror, opacity, border) → branding flash. Pure and WPF-free: frames injected via a Func<SceneElement, VideoFrame?> resolver (webcam → DeviceId, image → AssetId, backdrop → CaptureKey); output sized by CompositorOptions (16:9 = full master 1:1; vertical 9:16 = 607×1080 crop → 1080×1920 bilinear). Preview stays XAML (editing view); this is the output view — see ai.md "Scene compositor"
Compositor/CompositorOptions.cs The active tier's output rect (source space over the 1920×1080 master, integer-aligned — MainViewModel.OutputRectX can be 656.5) + target W×H
Compositor/StretchMath.cs Pure pixel math: the WPF UniformToFill cover-crop, clamped bilinear sample/scale (unit-tested half of the compositor)
Compositor/StaticPixelCache.cs Asset bytes → cached BGRA8 VideoFrame, decoded once per content hash — the output path's raw-pixel source (the preview uses ImageCache's BitmapImage)

Related: constructed in ViewModels/MainViewModel.cs (no DI container yet). Models in Models/index.md. OAuth credentials live in Helpers/OAuthCredentials.cs.