13 KiB
13 KiB
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 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) |
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/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 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. Not yet constructed by the app (ship step 5 wires it) |
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/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. Not yet constructed by the app (the encoder step wires it) |
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 → AudioLevelMeter → MicLevelChanged; 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.