Files
ytLlive/Services/index.md
T

13 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) + 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 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)
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/nodeinfosoftware.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 instead. Constructor takes optional HttpClient for tests
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. 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
Audio/IAudioSource.cs TASK 4 ship step 4 seam: live capture source — Start/Stop/SampleReady(AudioSample)/Failed(Exception), IDisposable; runs only while 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, zero UI
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 mic picked mid-session takes effect next go-live
Audio/AudioMixer.cs Owns both sources; Start/Stop follow go-live (MainViewModel.BeginGoLive/StopStream). Mic samples → AudioLevelMeterMicLevelChanged; loopback samples currently dropped (the future encoder AAC mix consumes them). Failures log via AppLog; mic failure zeroes the meter, loopback failure doesn't kill the mic
Audio/AudioLevelMeter.cs Pure smoothed RMS level (0..1): Push(AudioSample) + Reset — the unit-tested math behind AudioLevel
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

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