Files
LlamaCasty/Services/index.md
T
gramps c01206fb8a feat: web sources frame-captured via composition (CoreWebView2CompositionController → Windows.Graphics.Capture); PNG poll + CaptureScheduler deleted
The ~30Hz CapturePreviewAsync PNG poll capped real cadence at ~20Hz
(35–165ms full-HD encode+decode), so a 60fps widget still juddered at ~1/6
speed. Replaced polling with frame-driven capture of the composition
controller's root visual — the mechanism WebView2CompositionControl and
Flutter's webview_windows use (graphics_context.cc captures the root
surface_ visual via CreateGraphicsCaptureItemFromVisual; reference:
github.com/microsoft/Windows.UI.Composition.WinUI / flutter-internal
webview_windows). Frames now arrive at the renderer's own pace; capture
memory is epoch'd ring reuse + one crop-sized shared WriteableBitmap.

New Services/WebCaptureFrameSource.cs owns GraphicsCaptureItem + free-
threaded Direct3D11CaptureFramePool + session (Straight alpha readback,
per-frame FindContentBounds → CropBounds). WebView2Manager reworked around
per-session composition controllers + one UI-thread Compositor created via
the CoreMessaging CreateDispatcherQueueController P/Invoke (the 19041
projection lacks CreateOnCurrentThread); internal seam ctor
(Dispatcher, Func<string,IScreenCaptureSource>?) for hermetic tests.
CaptureScheduler.cs deleted; the three SetCaptureInterval cadence hooks
removed; InitWebView2() moved from MainWindow ctor to Loaded (a parent
HWND must exist for the composition controller); the hidden WebViewHostPanel
overlay deleted. TransparentBackgroundScript unchanged.

Tests: WebView2ManagerTests reworked — 4 control-size + scheduler tests
dropped, FindContentBounds tests moved to WebCaptureFrameSource, ONE
integration test (Frames_PublishCroppedPreview_And_CoalesceToLatest_CarryingCropBounds)
drives the seam with a FakeWebSource + background-STA DispatcherPump.
Suite 293/293, 0 warnings.

NOTE: composition path NOT yet verified on a device — the take is the
next step. Web work committed locally only (no push per standing rule).

Docs same-commit: ai.md Slice 14 + supersede marker on Slice 11, HANDOFF,
MyMistakes (CoreMessaging DQ + namespace-landmine recipe), TASK 17,
Controls/ViewModels/Services indexes.
2026-09-14 16:14:00 -07:00

20 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. CreateBroadcast always sends privacyStatus = "private" (ship step 7 — private-only until v1) and the one-click v3 flags (enableAutoStart/Stop, enableMonitorStream=false, latencyPreference=low); injectable HttpClient? http = null ctor seam so tests can fake the API. GetOrCreateReusableStreamAsync (TASK 9, shipped 2026-08-16) returns the channel's ReusableStream — lists liveStreams?mine=true and reuses the existing cdn.isReusable stream, inserting once only on first use (resolution=variable, frameRate=variable, isReusable=true); a stream id given to CreateBroadcast binds at insert via contentDetails.boundStreamId. RTMP URL = ingestionAddress + "/" + streamName. GetStreamHealthAsync(streamId) (TASK 9 item 3, shipped 2026-08-16) polls liveStreams?part=status → StreamHealth with parsed configurationIssues[]
StreamHealthReporter.cs Pure report-by-exception decision (TASK 9 item 3): BannerFor(issues) → HealthIssueReport(Text?, IsError) — null text on good/ok/noData/info-only, first warning/error issue produces its type text, error beats warning for color
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.NormalizeBackgrounds re-normalizes on every load — TASK 25: one locked "Background" per canonical screen (static art everywhere, DisplayCapture on Live), duplicates dropped, index 0, orphaned BackgroundUseDefault_*/BackgroundPath_* Settings keys purged at save). Settings key/value table holds app state: MicSourceName, ReusableStream (TASK 9 — the cached reusable stream's id/address/name so the pump has its RTMP URL at startup)
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) + SourceFailed(string) — seam for a running capture source (tests inject fakes)
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). Validated, not trusted: post-init checks (VideoDeviceId match, FrameSources non-empty, VideoDeviceController.GetAvailableMediaStreamProperties ≥ 1); reader.StartAsync() status read (throws on non-Success); capture.Failed + CameraStreamStateChanged subscribed → SourceFailed event. Fallback ladder: VideoPreview → VideoRecord retry. SharingMode.Exclusive + DeviceLost not in 19041 SDK projection; CameraStreamState.Failed compared by (int)2 (projection omits member names)
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. First-frame proof (4s timeout, configurable via ctor TimeSpan?): AcquireAsync returns true only after a real frame arrives — a reader that starts but never delivers (locked, suspended, dead) fails and surfaces CameraFailed with suspect-app names (CameraConflictProbe) instead of a silent empty box. SourceFailed forwarded from the frame source as async death signal
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 CreateDirect3D11DeviceFromDXGIDevice → MarshalInterface<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)
WebView2Manager.cs Web-source ownership (TASK 17, composition-capture since 2026-09-14): per registered Source { Type: WebSource } it owns a CoreWebView2CompositionController + its visual tree (RootVisualTarget child under a 1920×1080 root ContainerVisual on one Windows.UI.Composition.Compositor created on the UI thread via the CoreMessaging CreateDispatcherQueueController P/Invoke), a wrapped WebCaptureFrameSource, and the shared crop-sized WriteableBitmap preview (dispatcher-coalesced copy at Render priority, PreviewBitmapChanged raised only on recreation). Nav + TransparentBackgroundScript injection unchanged. Public ctor (Dispatcher); internal seam ctor (Dispatcher, Func<string, IScreenCaptureSource>?) for hermetic tests (no WinRT). Caller: MainViewModel.InitWebView2() (now invoked from MainWindow_Loaded — the parent HWND must exist). See ../ai.md "Slice 14"
WebCaptureFrameSource.cs The IScreenCaptureSource for a web session: owns the GraphicsCaptureItem (CreateFromVisual) + a free-threaded Direct3D11CaptureFramePool (2 buffers) + session; readback via SoftwareBitmap.CreateCopyFromSurfaceAsync (BGRA, BitmapAlphaMode.Straight — the compositor blends straight alpha); per-frame FindContentBounds alpha-bbox (internal static, unit-tested) stamps VideoFrame.CropBounds; ring up to 2 fresh frames + Epoch reuse; IsOpaque=false. Mirror of ScreenCaptureFrameSource
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 → social bar (optional socialBarFrame + socialBarTop in master space, blitted last so the bar sits above the flash; the old BlitFlash generalized to BlitOverlay with source offsets). 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)
Encoder/IFfmpegLocator.cs TASK 4 ship step 2 seam: resolves an absolute path to ffmpeg.exe (PATH first → cached %APPDATA%\ytLlive\tools\ffmpeg.exe → pinned BtbN LGPL-shared zip download). The encoder step and tests fake it
SocialValidator.cs ISocialValidator seam + HttpSocialValidator default: validates a social handle/URL by constructing the canonical profile URL and issuing an HTTP GET — the "act of validation" before adding to the bar. 200/redirect = exists; 404/connection failure = rejected. Fediverse @user@domain additionally does a best-effort nodeinfo lookup (/.well-known/nodeinfo → software.name) so the entry can show the instance's real logo; nodeinfo failure still validates (generic fediverse glyph). If the identity domain's nodeinfo is blocked (SSO) but the bare root 302s to the real instance (YunoHost default-app subdomains), the lookup follows the redirect and asks that host. Since the bar bug-fix branch the resolution probes well-known subdomains (mastodon., social., ... order in FediverseSubdomainCandidates) when both the identity domain and its redirect come up empty, bounded by a ~15s linked-CTS budget; ResolveFediverseSoftwareAsync(domain, ct) on the seam powers the load-time heal. Constructor takes optional HttpClient + Action<string> log for tests
Compositor/SocialBarRenderer.cs Social bar output strip (bar bug-fix branch): rasterizes the global social bar into a transparent straight-alpha BGRA8 VideoFrame strip (1920 wide, 40px content + 24px glow pad) via RenderTargetBitmap — one Path (logo) + one TextBlock (handle) per entry, white on transparent with the preview's green #2ecc71 glow baked in (Pbgra32 → straight-alpha unpremultiply). UI thread only; the frame is immutable afterwards so the FramePump reads it from its own thread
Encoder/EncoderOptions.cs One go-live's encoder config: full RtmpUrl (ingest + stream key), W×H/FPS/bitrate from the quality tier, VideoEncoder (null = probe), audio sample rate/channels, GopSize = FPS×4 (the ≤4s keyframe bound)
Encoder/IFfmpegEncoder.cs TASK 4 ship step 3 seam: start/feed/stop the live encoder; HealthUpdated (parsed StreamHealth), ProcessFailed on unexpected non-zero exit. Constructor-injected locator + process factory (tests fake both)
Encoder/FfmpegEncoder.cs The default encoder: spawn ffmpeg.exe (probe -encoders first → hardware NVENC/QSV/AMF, OpenH264 fallback — never libx264), feed raw BGRA frames into stdin, parse -stats lines into StreamHealth, graceful stop via stdin EOF + 10s kill watchdog. Constructed by MainViewModel since ship step 5 (new FfmpegEncoder(new FfmpegLocator())), driven by the FramePump
Encoder/IEncoderProcess.cs Seam around the spawned subprocess (probe + encoder): redirected stdin/stdout/stderr + exit control — the encoder never touches Process directly
Encoder/FfmpegEncoderProcess.cs The real process wrapper (Process with all three std streams redirected)
Encoder/FfmpegArgs.cs Pure FFmpeg command-line builder: rawvideo pipe:0 input, anullsrc silent audio (WASAPI replaces it), H.264+AAC, closed GOP (-g fps×4 -keyint_min -sc_threshold 0 -bf 0, yuv420p), FLV → RTMP
Encoder/FfmpegProgressParser.cs Pure parser for frame=/fps=/size=/time=/bitrate= stats lines → FfmpegProgress
Encoder/FfmpegEncoderPicker.cs Pure H.264 encoder picker from -encoders output: NVENC → QSV → AMF → OpenH264; never returns libx264 (GPL)
Encoder/FramePump.cs TASK 4 ship step 5: the live frame producer — while live it snapshots the active scene each tick, resolves every element to its latest frame (Func<SceneElement, VideoFrame?> resolver), composites it into the tier's output frame, and paces frames into the encoder at the tier's FPS. All collaborators constructor-injected seams; free of WPF and the capture managers. StartAsync never throws (failures log + Failed); no RTMP URL = encoder skipped; StopAsync stops the encoder before awaiting the loop (backpressure deadlock); ProcessFailed self-stops. Since the bar bug-fix branch it takes an optional socialBar: Func<(VideoFrame? Frame, SocialBarPosition Position)>? seam — a pre-rendered bar strip composited last (above the flash) at the top edge or SourceRectHeight − bar height. See ai.md "Live frame pipeline"
Encoder/FfmpegLocator.cs The default locator: PATH probe wins, then the cache, then pull+extract (ffmpeg.exe + the libav*.dll family via a staging dir, failures via AppLog). Pinned to BtbN autobuild-2026-08-09-13-03 lgpl-shared (dynamic linking = LGPL compliance without static-relink §6 material; no GPL libx264; NVENC/QSV/AMF + libopenh264 + native AAC — see ai.md → Licensing). Search dirs / tools dir / downloader constructor-injected for hermetic tests. Wired into the encoder since ship step 5
INotificationService.cs Toast seam (TASK 24): Show(title, message, AppNotificationSeverity) + AppNotificationSeverity { Info, Success, Warning, Error } + Info/Success/Warning/Error extension methods — non-blocking user-facing messages; VMs/services raise through this so failures surface without MessageBox blocking the UI thread
NotificationService.cs The default seam impl (Notification.Wpf 11.0.0, MIT): wraps a library NotificationManager; routes to the in-window NotificationArea named ToastArea (MainWindow bottom-right above the footer); lifetimes via the pure BuildRequest mapping + Cards tint table — Info/Success ~4s, Warning ~8s auto-dismiss, Error sticky (NeverExpires() = TimeSpan.MaxValue, never null); dark card tints matching the health-banner language; all Show calls marshal to the captured Dispatcher. See ../ai.md "Toast notifications"
Audio/IAudioSource.cs TASK 4 ship step 4 seam: live capture source — Start/Stop/Started/SampleReady(AudioSample)/Failed(Exception), IDisposable. Capture now runs for the app's lifetime (started at startup) so the footer meters preview live. The app consumes this seam; tests inject hermetic fakes
Audio/AudioSample.cs One captured chunk: interleaved PCM float (-1..1) + sample rate + channels
Audio/WasapiLoopbackAudioSource.cs Desktop/game capture: NAudio WasapiLoopbackCapture on the default render device — automatic at unity; the game bar's meter consumes it
Audio/WasapiMicAudioSource.cs Mic capture: NAudio WasapiCapture with the device resolved by FriendlyName matching MicSourceName (DisplayName only), default capture endpoint fallback; name provider Func<string?> re-read at each Start so a device picked mid-session takes effect immediately (the mixer restarts the mic on pick)
Audio/AudioMixer.cs Owns both sources; capture starts once at startup (MainViewModel.StartMicCaptureAsync) and stops on Shutdown — NOT go-live (preview monitoring). Mic samples → AudioLevelMeter → MicLevelChanged; loopback samples → the game bar's meter via LoopbackLevelChanged. Surfaces mic connection state: MicConnected/MicFailed (drives the status dot) + RestartMic() (device swap mid-session, keeps loopback). Failures log via AppLog; mic failure zeroes the meter, loopback failure doesn't kill the mic. Note: the meter Push is unconditional (the ?. on the event would otherwise skip the argument when nothing is subscribed)
Audio/AudioLevelMeter.cs Pure smoothed RMS level (0..1): Push(AudioSample) + Reset — the unit-tested math behind AudioLevel and GameAudioLevel. ToDisplay(float) maps the raw linear RMS onto the meter's display scale (−60..0 dBFS spread across 0..1, with +10 dB input amplification so real speech peaks hit the red zone at maxed volume) — real speech/game RMS (~0.01..0.1) would otherwise leave a flat scale looking dead; ≤0.001 linear reads as zero (never idles on background noise)
Audio/WaveToFloat.cs Pure WASAPI buffer → float conversion: IEEE float 32-bit direct, PCM 16-bit normalized, WaveFormatExtensible IEEE-float subformat GUID, trailing partial samples ignored
Audio/MasterLimiter.cs Master output limiter: a −1 dBFS ceiling (Ceiling = 0.891) applied after the live mix so the honest gains can never sum past 0 dBFS and clip the AAC encode. Instant attack per frame (a hot frame is scaled exactly to the ceiling — no overshoot), smoothed release toward unity so loud passages don't pump. Pure — unit-tested

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