TASK 5 items 1-3: reusable stream, health banner, docs refresh
- Reusable stream: GetOrCreateReusableStreamAsync, cached via LayoutStore, bound at broadcast insert - Health monitoring: GetStreamHealthAsync polls liveStreams.status, report-by-exception banner - Go-live order: ensure stream → create broadcast → then start pump (URL must exist first) - Full broadcast form scoped (TASK 5 items 6-7) in TASKS.md + ai.md - TASK 23 trimmed to technical scope, TASK 26 added (marcom/launch kit) - Confidential files: MONETIZATION.md + MARCOM.md (gitignored, not committed) - .gitignore updated for confidential business files - HANDOFF.md updated with session state and next steps
This commit is contained in:
@@ -113,7 +113,7 @@ C# / WPF (.NET 8) following MVVM:
|
||||
|
||||
- `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)
|
||||
- `YouTubeStreamService` uses hardcoded `1080p`/`60fps` and per-broadcast streams — must switch to the v3 `variable` reusable stream
|
||||
- `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
|
||||
|
||||
@@ -461,8 +461,10 @@ 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?>` returning null
|
||||
until TASK 5 supplies the reusable stream's ingest URL, so go-live runs the current visual flow.
|
||||
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
|
||||
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)
|
||||
**before** awaiting the loop, because closing stdin unblocks a write stuck on pipe backpressure — the
|
||||
reverse order would deadlock. `ProcessFailed` self-stops the pump. `Failed` while live flips
|
||||
@@ -472,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 once TASK 5 fills `_rtmpUrlProvider`; until then the pump skips the encoder.
|
||||
real encoder values since TASK 5 (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/
|
||||
@@ -646,25 +648,41 @@ 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.
|
||||
- **Private-only go live (ship step 7, shipped 2026-08-14)** — until v1, go-live is locked to private
|
||||
streams so a software error can never publish something public. Double-enforced: the Go Live dialog
|
||||
is locked to Private (`GoLiveViewModel.Visibility` is a get-only `"Private"`, no dropdown — the
|
||||
settings "Default Visibility" dropdown + `MainViewModel.Visibilities`/`DefaultStreamVisibility` were
|
||||
removed as dead-ends) AND `YouTubeStreamService.CreateBroadcast` always sends `privacyStatus="private"`
|
||||
(injectable `HttpClient? http = null` seam; the broadcast-insert integration test asserts the body
|
||||
carries `"privacyStatus":"private"`). `BeginGoLive` fires `CreateBroadcastAsync` and remembers
|
||||
`_currentBroadcastId` for TASK 5's bind/transition (failure → `StreamStatus.Error`, never a crash);
|
||||
`StopStream` clears it. The REC sign shows a dark-red **PRIVATE badge** (next to REC, `IsLivePrivate`)
|
||||
whenever the live stream is private.
|
||||
- **Variable reusable stream** — `liveStreams.insert` once per channel with
|
||||
`cdn.resolution=variable`, `cdn.frameRate=variable`, `isReusable=true`; cache the ingestion URL +
|
||||
stream name and reuse for every broadcast. Any quality tier works without recreating the stream,
|
||||
and auto step-down is done by us dropping bitrate on the fly (zero API calls).
|
||||
- **Visibility picker (TASK 5 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.
|
||||
**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
|
||||
(default on), projection (rectangular/360°, default rectangular), closed captions
|
||||
(disabled/embedded/HTTP, default disabled), auto-start (default on), auto-stop (default on),
|
||||
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):
|
||||
`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 →
|
||||
`StreamStatus.Error`, never a crash; `StopStream` clears `_currentBroadcastId`.
|
||||
- **Variable reusable stream** (shipped 2026-08-16) — `GetOrCreateReusableStreamAsync` lists
|
||||
`liveStreams?mine=true` and reuses the existing `cdn.isReusable` stream, inserting once per channel
|
||||
(`cdn.resolution=variable`, `cdn.frameRate=variable`, `isReusable=true`) only on first use; the
|
||||
ingestion URL is cached via `LayoutStore` Settings (`SaveReusableStream`/`LoadReusableStream`) and
|
||||
bound to each broadcast at insert (`boundStreamId`). Any quality tier works without recreating the
|
||||
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** — poll `liveStreams.list`; render nothing on `good`/`ok`, surface a
|
||||
banner only on `configurationIssues[]` with `warning`/`error` severity. Bottom strip = YouTube logo
|
||||
+ green/red connection dot (clickable → opens the dialog).
|
||||
- **Report-by-exception health (SHIPPED 2026-08-16, TASK 5 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
|
||||
every **30s while live** (`DispatcherTimer` `_healthPollTimer`, first poll right after go-live) and
|
||||
clears on End via `ResetHealth`; poll failures log only. UI = a full-width banner strip under the
|
||||
top bar, `HealthIssueBanner` text + `HealthIssueBackground` (amber `#b8860b` warning / dark red
|
||||
`#8f1f1f` error), hidden by `NotNullToVis`. The design's bottom-strip YouTube logo + green/red dot
|
||||
(clickable → dialog) is still queued.
|
||||
- **One dialog, three states** — not connected / connected-offline (all editable) / live
|
||||
(title + description + visibility editable; quality + account greyed out). Both entry points
|
||||
(Start Stream button + bottom strip) open it; prefilled from saved session profile.
|
||||
|
||||
Reference in New Issue
Block a user