dokunfuk
This commit is contained in:
@@ -112,7 +112,7 @@ C# / WPF (.NET 8) following MVVM:
|
||||
### Current limitations / TODOs
|
||||
|
||||
- `Helpers/OAuthCredentials.cs` contains the real ClientId/ClientSecret. Auth is complete and the session **persists via Windows DPAPI** (`Helpers/TokenStore.cs` → `%APPDATA%\ytLlive\ytLlive.auth`, CurrentUser scope), reloaded best-effort at startup with a proactive refresh of a near-expiry access token. Sign-in/Change Account lives **inside the Start Stream dialog** (two-state flow — no separate Connect button). A **graceful End Livestream signs out**: `StopStream()` clears the session + token, so the next go-live needs a fresh sign-in; a crash never runs End, so the token survives and the creator stays signed in. `YouTubeAuthService` takes an optional `HttpClient` + `sessionChanged` callback (test seam + save hook; services are still constructed in `MainViewModel`)
|
||||
- Scene/source/asset layout + the social bar persist (SQLite, schema v8); the OAuth session persists (DPAPI); the paid-unlock state does not (yet — subscription entitlement verification pending, TASK 23)
|
||||
- Scene/source/asset layout + the social bar persist (SQLite, schema v8); the OAuth session persists (DPAPI); the paid-unlock state does not (yet — subscription entitlement verification pending, TASK 12)
|
||||
- `YouTubeStreamService` manages the **variable reusable stream** (shipped 2026-08-16): `GetOrCreateReusableStreamAsync` lists `liveStreams?mine=true` and reuses the existing `cdn.isReusable` stream, creating it only on first use (`resolution=variable`, `frameRate=variable`); the stream is cached via `LayoutStore` (`SaveReusableStream`/`LoadReusableStream`, Settings table) and bound at broadcast insert (`contentDetails.boundStreamId`). Health (shipped 2026-08-16): `GetStreamHealthAsync(streamId)` polls `liveStreams?part=status` for `healthStatus` + `configurationIssues[]` → `StreamHealth`; banner decision in pure `StreamHealthReporter`
|
||||
- Webcam capture is shipped (milestone 1); the live desktop/game backdrop is shipped (ship task #1); **the output compositor (TASK 4 ship step 1) is SHIPPED**, **the FFmpeg locator (TASK 4 ship step 2) is SHIPPED**, **the encoder + RTMP push (TASK 4 ship step 3) is SHIPPED**, **WASAPI audio capture (TASK 4 ship step 4) is SHIPPED** — full plan in `TASKS.md`; the frame-pipeline wiring follows (its own PR)
|
||||
- `StreamConfig` defaults (`TargetBitrate=6000`, `Resolution="1920x1080"`) are stale — the live dropdown drives `StreamHealth.CurrentBitrate`/`FPS` instead
|
||||
@@ -356,7 +356,7 @@ integration test fakes the whole subprocess (probe + encoder) with a Channel-bac
|
||||
`-re -f rawvideo -pix_fmt bgra -video_size WxH -framerate FPS -i pipe:0` + a **real audio input — the
|
||||
mixer writes IEEE-float stereo to a Windows named pipe** (`-f f32le -ar 48000 -ac 2
|
||||
-i \\.\pipe\ytllive_audio`, name via `EncoderOptions.AudioPipeName`; replaced the old `-f lavfi -i
|
||||
anullsrc` silence in the TASK 9 audio milestone) + explicit `-map 0:v -map 1:a` + `-c:v <enc> -b:v K
|
||||
anullsrc` silence in the TASK 8 audio milestone) + explicit `-map 0:v -map 1:a` + `-c:v <enc> -b:v K
|
||||
-maxrate K -bufsize 2K` + **`-g fps×4 -keyint_min fps×4 -sc_threshold 0 -bf 0 -pix_fmt yuv420p`**
|
||||
(≤4s keyframes, closed GOP, H.264 compliance) + `-c:a aac -ar 48000 -ac 2 -f flv <rtmpUrl>`.
|
||||
**Encoder choice is probed from the binary's `-encoders` listing** (`FfmpegEncoderPicker`, pure):
|
||||
@@ -365,7 +365,7 @@ forces one and skips the probe. `EncoderOptions` also carries W×H/FPS/bitrate f
|
||||
`GopSize` = FPS×4. Constructed by `MainViewModel` since ship step 5 (`new FfmpegEncoder(new FfmpegLocator())`),
|
||||
driven by the `FramePump` below.
|
||||
|
||||
### Live audio capture (TASK 4 ship step 4 — shipped 2026-08-12; game audio bar 2026-08-13; **TASK 9 audio milestone: real stream audio + filters + duck + TRAX, shipped 2026-08-14**, plan in TASKS.md)
|
||||
### Live audio capture (TASK 4 ship step 4 — shipped 2026-08-12; game audio bar 2026-08-13; **TASK 8 audio milestone: real stream audio + filters + duck + TRAX, shipped 2026-08-14**, plan in TASKS.md)
|
||||
|
||||
**Capture runs for the app's lifetime and is KISS by rule**: desktop/game audio is automatic (WASAPI
|
||||
loopback), the mic is the creator's only audio control (meter/mute/volume already shipped). The whole
|
||||
@@ -397,7 +397,7 @@ devices, no timers).
|
||||
`Failed`), and `RestartMic()` re-resolves + restarts just the mic (loopback keeps running). Failures
|
||||
log via `AppLog`; a mic failure zeroes the meter, a loopback failure never kills the mic. Meter `Push`
|
||||
is unconditional (a `?.` on the event would skip the meter update when nothing is subscribed yet).
|
||||
- **Go-live audio (TASK 9):** `BeginGoLive` → `StartLive(pipeName)` (idempotent) creates the named pipe
|
||||
- **Go-live audio (TASK 8):** `BeginGoLive` → `StartLive(pipeName)` (idempotent) creates the named pipe
|
||||
server (`NamedPipeAudioWriter`, behind the `IAudioPipeWriter` seam) and a live loop task; every
|
||||
`mixInterval` (default 10 ms) `FillAndMix` reads a chunk from each ring buffer (silence-fills
|
||||
underruns), computes the post-filter mic RMS → `AutoDucker` (threshold 0.02, duck ×0.25 / −12 dB,
|
||||
@@ -412,7 +412,7 @@ devices, no timers).
|
||||
`StopLive()` (closes the pipe → ffmpeg audio EOF) **BEFORE** stopping the frame pump (video EOF) —
|
||||
the reverse order stalls on pipe backpressure. `FfmpegEncoder` itself is untouched; the VM owns the
|
||||
pipe lifecycle.
|
||||
- **TRAX — free background music (TASK 9):** `MusicPlayer` = NAudio `MediaFoundationReader`
|
||||
- **TRAX — free background music (TASK 8):** `MusicPlayer` = NAudio `MediaFoundationReader`
|
||||
(mp3/wav/m4a) → `VolumeWaveProvider16` at the hardcoded **0.20** bed (no slider) → `WaveOutEvent` on
|
||||
the default device, **looping on any clean natural end** (`PlaybackStopped` with `e.Exception == null`
|
||||
→ rewind + replay; the earlier `Position >= Length` check was unreliable for `MediaFoundationReader`
|
||||
@@ -440,12 +440,12 @@ devices, no timers).
|
||||
chip, a mirror of the mic bar: meter + mute + volume slider). It's monitoring UI **in the preview
|
||||
grid** — the meter is display-only, but its **volume slider + mute are stream-honest** (via
|
||||
`AudioGainProvider.LoopbackGain`) **and drive TRAX locally** (`MusicPlayer.LocalGain`). Relabelled
|
||||
**"Desktop Audio"** (TASK 9) since TRAX rides the same channel.
|
||||
**"Desktop Audio"** (TASK 8) since TRAX rides the same channel.
|
||||
- **`WaveToFloat`** (pure, shared): WASAPI mix formats → interleaved float — IEEE float 32-bit direct,
|
||||
PCM 16-bit normalized to -1..1, `WaveFormatExtensible` with the IEEE-float subformat GUID
|
||||
(`NAudio.Dmo.AudioMediaSubtypes.MEDIASUBTYPE_IEEE_FLOAT`), trailing partial samples ignored.
|
||||
- Build **0 warnings**; **197 passing** (DSP/ring-buffer/ducker/resampler/pipe + mixer/hysteresis/
|
||||
game-detector/meter-scale unit tests + the TASK 9 integration test
|
||||
game-detector/meter-scale unit tests + the TASK 8 integration test
|
||||
`AudioPipelineTests.Mix_WithFiltersDuckAndGain_Lands_On_AudioPipe` + the polish-batch integration
|
||||
test `Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback` proving the provider gains reach the
|
||||
pipe and mute silences the loopback).
|
||||
@@ -462,7 +462,7 @@ seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<Compos
|
||||
- **`StartAsync` never throws** — the VM fires-and-forgets it from the sync command handler; failures log
|
||||
+ surface via the `Failed` event. `EncoderOptions == null` means "no RTMP URL": the pump logs and skips
|
||||
the encoder entirely. `MainViewModel._rtmpUrlProvider` is that seam — a `Func<string?>` that now yields
|
||||
the reusable stream's ingest URL (TASK 5, shipped 2026-08-16): loaded from the `LayoutStore` cache at
|
||||
the reusable stream's ingest URL (TASK 9, shipped 2026-08-16): loaded from the `LayoutStore` cache at
|
||||
startup and set fresh by `PrepareAndStartLiveAsync` before the pump starts. **The pump reads the URL
|
||||
once at startup**, which is why go-live ensures the stream BEFORE `StartAsync`.
|
||||
- **Stop ordering matters:** `StopAsync` stops the encoder (closes stdin → EOF → ffmpeg finalizes+exits)
|
||||
@@ -474,7 +474,7 @@ seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<Compos
|
||||
a background thread) and copies into `CurrentHealth` (the bottom bar's existing `CurrentHealth.*`
|
||||
bindings). `ResetHealth(status)` zeroes dropped/duration on go-live and on End so stats never linger
|
||||
from a previous session; bitrate/FPS stay on the tier's targets (`ApplyStreamQuality`). The bar shows
|
||||
real encoder values since TASK 5 (2026-08-16) wired the reusable stream URL into `_rtmpUrlProvider`.
|
||||
real encoder values since TASK 9 (2026-08-16) wired the reusable stream URL into `_rtmpUrlProvider`.
|
||||
- **`MainViewModel` owns the resolver** (`ResolveOutputFrame`): `WebcamSceneConfig` →
|
||||
`CameraManager.GetLatestFrame(WebcamId)`, `Source { IsLiveCapture, CaptureKey }` →
|
||||
`ScreenCaptureManager.GetLatestFrame(CaptureKey)` (the new accessor mirroring `CameraManager`), image/
|
||||
@@ -493,7 +493,7 @@ seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<Compos
|
||||
it; a concurrent-mutation exception is contained (logged + `Failed` + the pump stops) rather than
|
||||
crashing. The background thread + video pipeline is the new reality since this step.
|
||||
|
||||
### Social bar (TASK 14 — shipped 2026-08-12, plan in TASKS.md; bar bug-fix branch 2026-08-13)
|
||||
### Social bar (TASK 6 — shipped 2026-08-12, plan in TASKS.md; bar bug-fix branch 2026-08-13)
|
||||
|
||||
A **global bar layer** (never a Source, no sources-list row) that sits over the bottom or top of the
|
||||
output and carries the creator's social links — content-sized, centered, GREEN glow when ON, position is a
|
||||
@@ -569,7 +569,7 @@ guardrails below — written down so a future "quick fix" never reintroduces a G
|
||||
- **Forget the v1 license-texts gate** — `THIRD-PARTY-NOTICES.txt` links the canonical license texts; at
|
||||
**v1 (GA)** the full texts of every license it names MUST ship alongside it (TASK 4 requirement 9 is the
|
||||
release blocker). Queued early is wrong; the release pass owns it.
|
||||
- **Break the in-app About requirement** — since TASK 21 the About overlay is the app's in-app creator
|
||||
- **Break the in-app About requirement** — since TASK 11 the About overlay is the app's in-app creator
|
||||
hub: the real logo, the creator links (YouTube channel, Mastodon, Buy me a coffee, and the greyed
|
||||
"Unlock Premium" seam whose billing URL is tabled), and a **Licenses & legal** sub-panel that loads
|
||||
`THIRD-PARTY-NOTICES.txt` from the executable directory and renders it in a scrolling panel —
|
||||
@@ -610,8 +610,8 @@ annual subscription** (settled 2026-08-14 — replaces the earlier "one-time unl
|
||||
- **Billing:** NOT locked to itch.io (creator's call 2026-08-14). Hunted fact — **itch.io has no native
|
||||
subscription billing** (no annual/recurring product billing; only pay-what-you-want, pre-orders,
|
||||
early-access, keys, and a Patreon integration). Candidates for annual subs + license-key verification:
|
||||
**Gumroad** (its native affiliate program is a tiebreaker if the affiliate angle in TASK 23 matters)
|
||||
and **Lemon Squeezy**. Decision deferred to TASK 23.
|
||||
**Gumroad** (its native affiliate program is a tiebreaker if the affiliate angle in TASK 12 matters)
|
||||
and **Lemon Squeezy**. Decision deferred to TASK 12.
|
||||
- **Support (creator's model):** in-app bug-reporting mechanism → issues into git; most queries are
|
||||
how-tos / feature requests / manual-skimmers. Maintenance cadence = "when I get around to it" with
|
||||
emergency patches; not a 24/7 service promise.
|
||||
@@ -648,11 +648,11 @@ These are the hard facts behind every decision. Full list in `TASKS.md`.
|
||||
`enableAutoStart=true`, `enableAutoStop=true`, `enableMonitorStream=false`,
|
||||
`selfDeclaredMadeForKids=false`, `latencyPreference=low`. The encoder starting brings YouTube live.
|
||||
`enableMonitorStream=false` is what lets us skip the testing stage.
|
||||
- **Visibility picker (TASK 5 item 6)** — the "always Private" enforcement was temporary for testing
|
||||
- **Visibility picker (TASK 9 item 6)** — the "always Private" enforcement was temporary for testing
|
||||
during development ("always Private until v1"). For v1, the go-live dialog exposes Private/Unlisted/Public
|
||||
and `YouTubeStreamService.CreateBroadcast` uses the dialog's selection (not a hardcoded override).
|
||||
The PRIVATE badge in the top bar still shows when the stream is actually private.
|
||||
- **Full broadcast form (TASK 5 item 7)** — the go-live dialog exposes all YouTube API-supported fields.
|
||||
- **Full broadcast form (TASK 9 item 7)** — the go-live dialog exposes all YouTube API-supported fields.
|
||||
**Core tab** (always visible): title, description, visibility (Private/Unlisted/Public), made-for-kids,
|
||||
schedule (start + optional end datetime). **Advanced tab** (expandable, sane defaults): latency
|
||||
(Normal/Low/Ultra-Low, default Low), DVR (default on), embed (default on), record-from-start
|
||||
@@ -661,7 +661,7 @@ These are the hard facts behind every decision. Full list in `TASKS.md`.
|
||||
monitor stream (default off, for testing), region restrictions (country codes, optional).
|
||||
`categoryId` is removed from `CreateBroadcast` (not a `liveBroadcast` field, silently ignored).
|
||||
Monetization via `liveBroadcasts.update` (not settable on insert) — separate step after broadcast
|
||||
creation. Go-live order (TASK 5, shipped 2026-08-16):
|
||||
creation. Go-live order (TASK 9, shipped 2026-08-16):
|
||||
`BeginGoLive` → `PrepareAndStartLiveAsync` — ensure the reusable stream (`GetOrCreateReusableStreamAsync`,
|
||||
cache it), create the broadcast bound to it (`CreateBroadcast(..., stream.Id)` → `boundStreamId`),
|
||||
THEN start the pump (the URL must exist before `FramePump.StartAsync`, which reads it once). Failure →
|
||||
@@ -674,7 +674,7 @@ These are the hard facts behind every decision. Full list in `TASKS.md`.
|
||||
stream, and auto step-down is done by us dropping bitrate on the fly (zero API calls).
|
||||
- **Quality is greyed out while live** — resolution/frameRate/ingestionType are immutable after
|
||||
stream creation; editing title/description/privacy is fine at any time.
|
||||
- **Report-by-exception health (SHIPPED 2026-08-16, TASK 5 item 3)** — `YouTubeStreamService.GetStreamHealthAsync(streamId)`
|
||||
- **Report-by-exception health (SHIPPED 2026-08-16, TASK 9 item 3)** — `YouTubeStreamService.GetStreamHealthAsync(streamId)`
|
||||
polls `liveStreams.list?part=status`; render nothing on `good`/`ok`/`noData`, surface a banner only
|
||||
on `configurationIssues[]` with `warning`/`error` severity. The decision is the pure
|
||||
`Services/StreamHealthReporter.BannerFor` (null text = no banner; error beats warning). The VM polls
|
||||
|
||||
Reference in New Issue
Block a user