docs: reconcile Day 2 release policy and output tasks
This commit is contained in:
+20
-14
@@ -13,7 +13,8 @@ storefront, support or promo work. Cause of death for every retired path:
|
||||
- **Pro is the paid Steam app; the free Demo is associated with it.** The paid app is **$79 during
|
||||
the announced launch period, then $99** on the announced date. No subscription or third-party
|
||||
checkout.
|
||||
- **The only delta is output quality.** Features are never gated; a payment buys pixels.
|
||||
- **The only delta is frame rate.** Features are never gated; every supported resolution is free at
|
||||
30 fps, and 60 fps is paid. Resolution does not decide entitlement.
|
||||
- **No watermark, ever**, in either edition. The branding flash is retired as a paid delta and
|
||||
stays off (`ShowBrandFlash`, default `false`).
|
||||
- **Clean numbers, no `.99`**, no discount codes, no countdowns. **The one dated exception** is the
|
||||
@@ -25,33 +26,38 @@ storefront, support or promo work. Cause of death for every retired path:
|
||||
|
||||
## Output profile — the one entitlement rule
|
||||
|
||||
The cap is a pixel budget plus a frame rate, not the word "720p":
|
||||
The product rule is frame rate, not pixel count or a resolution label:
|
||||
|
||||
| | Free | Pro |
|
||||
|---|---|---|
|
||||
| Pixels | ≤ **921,600** | any shipped tier |
|
||||
| Frame rate | ≤ **30 fps** | any shipped tier |
|
||||
| Landscape | 1280×720 @30 | 1280×720 @60 · 1920×1080 @30 · @60 |
|
||||
| Vertical 9:16 | 720×1280 @30 | 1080×1920 @60 |
|
||||
| Pixels | any supported tier | any supported tier |
|
||||
| Frame rate | ≤ **30 fps** | up to **60 fps** |
|
||||
| Landscape | 1280×720 @30 · 1920×1080 @30 | all Free tiers + 1280×720 @60 · 1920×1080 @60 |
|
||||
| Vertical 9:16 | 1080×1920 @30 | all Free tiers + 1080×1920 @60 |
|
||||
|
||||
- **Records too.** The cap governs the file the creator keeps as well as the stream; otherwise
|
||||
free 1080p recording is a re-upload hole. **There is one ffmpeg process with two output blocks**
|
||||
- **Implementation gap (2026-11-04):** `Models/OutputProfile.cs` still gates by pixel budget plus
|
||||
frame rate. Update that rule and its tests to use frame rate alone; this docs unit records the
|
||||
product decision but does not change code.
|
||||
|
||||
- **Records too.** The frame-rate rule governs the file the creator keeps as well as the stream;
|
||||
otherwise free 60-fps recording is a re-upload hole. **There is one ffmpeg process with two output blocks**
|
||||
(FLV to RTMP, MP4 to disk) fed by one `EncoderOptions`, and the profile is resolved once when
|
||||
those options are built (`MainViewModel.Streaming.Operations.cs` → `Models/OutputProfile.cs`).
|
||||
There is no second encoder to keep in step and therefore no second check to forget.
|
||||
- **Fail closed.** Unresolved, unreadable or unvalidated entitlement = free tier. Never Pro.
|
||||
- **The development override exists and never ships.** `MainViewModel.DeveloperMode` bypasses the
|
||||
budget in a Debug build only; the Settings row that toggles it is **excluded from the Release
|
||||
entitlement gate in a Debug build only; the Settings row that toggles it is **excluded from the Release
|
||||
compile** (`ytLive.csproj` removes its Page and code-behind), and the setter refuses the value
|
||||
when `Helpers/DevBuild.IsDevelopment` is false. Not a hidden row — an absent one.
|
||||
- **Never downgrade a running stream.** The gate applies to the next start; pulling quality out
|
||||
of a live broadcast is a worse bug than one free 1080p session.
|
||||
- **The greyed menu entry is the sales surface.** It stays clickable and is labelled ("720p30 ·
|
||||
free tier"). Selecting a locked tier opens the in-app About panel; the browser opens only if the
|
||||
- **Never downgrade a running stream.** The gate applies to the next start; pulling 60 fps out of a
|
||||
live broadcast is a worse bug than one session starting at an unentitled frame rate.
|
||||
- **The greyed menu entry is the sales surface.** It stays clickable; locked 60-fps tiers are marked
|
||||
as paid. Selecting a locked tier opens the in-app About panel; the browser opens only if the
|
||||
creator then clicks its llamacasty.com link.
|
||||
- **Ownership is checked at app startup** and cached in `IsAppOwned` for that run. Purchases and
|
||||
refunds are reflected on the next app start; there is no subscription lapse or sign-out path.
|
||||
- **Bitrate follows the profile** — a capped user is not offered a 1080p bitrate.
|
||||
- **Bitrate follows the selected profile** — free users may choose any supported 30-fps tier; paid
|
||||
users may choose supported 60-fps tiers.
|
||||
|
||||
## Entitlement — one source
|
||||
|
||||
|
||||
@@ -28,9 +28,10 @@ paid app's Steam App ID. The website links the store page; it never takes paymen
|
||||
nothing" proposal was rejected: Steamworks requires in-app purchases to use the Steam Wallet, so
|
||||
it is a listing-level risk to save ~$24 a sale.
|
||||
|
||||
**Kept:** the free tier is the whole app at a 720p30 output profile, the paid delta is output
|
||||
quality only, and the app never sells anything itself — the greyed tier opens the Steam store
|
||||
overlay.
|
||||
**Kept at the time; superseded 2026-11-04:** the free tier was described as a 720p30 profile. The
|
||||
current policy is the whole app free at any supported resolution up to 30 fps; Pro unlocks 60 fps,
|
||||
regardless of resolution. The app never sells anything itself — the greyed tier opens the Steam
|
||||
store overlay.
|
||||
|
||||
## Commercial mechanics — died 2026-10-31 (same week they were written)
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ Read the file whose row matches the task. There is no required reading order.
|
||||
| [`environment.md`](environment.md) | LAN hosts/IPs/SSH and the Gitea push recipe, the Windows `dotnet.exe` build + run commands, the test suite map, and the hermetic real-`MainWindow` test rule | Before building, running, adding a test, or reaching any box on the LAN |
|
||||
| [`product.md`](product.md) | Brand/naming split, AI transparency, the licensing prohibitions, the freemium posture, auth/account assumptions, and packaging behaviour | Branding, naming, packaging, or a legal/third-party obligation |
|
||||
| [`commercial.md`](commercial.md) | The live commercial policy: model and prices, the output-profile cap (stream **and** recording), paid-app ownership (`BIsSubscribedApp`), **Steam as the only channel**, signing (moot), costs, **support policy**, anti-piracy posture | Any question about money or support: pricing, what free gets, how it ships, what a sale costs |
|
||||
| [`youtube-api.md`](youtube-api.md) | Go-live ordering + the broadcast-binding fix, the private-only lock, health parsing, end-stream pre-flight, Test mode, and chat-insert shape | Touching go-live, broadcast lifecycle, health polling, streaming close-out, or the chat service |
|
||||
| [`youtube-api.md`](youtube-api.md) | Go-live ordering + broadcast binding, the development/test Private safeguard and release visibility flow, health parsing, end-stream pre-flight, Test mode, and chat-insert shape | Touching go-live, broadcast lifecycle, health polling, streaming close-out, or the chat service |
|
||||
| [`overlays.md`](overlays.md) | The social bar, the native alert box (six animations, alert clip, ticker) and the chat box conventions — everything composited over the frame | Adding or changing anything drawn on top of the output |
|
||||
| [`decisions.md`](decisions.md) | Retired paths with cause of death: the paid-tier history and pricing, the free-forever era, Store IAP, the Store ordering, rejected monetization and anti-piracy ideas, and the permanent out-of-product list | You are about to re-open a decision that was already settled |
|
||||
| [`ui-surface.md`](ui-surface.md) | The control surface, scene strip, preview rail, hotkeys, broadcast drawer, toasts, transitions, brand logo, launch geometry, and the theme/properties-panel conventions | Changing anything the creator sees: layout, controls, theming, properties panel |
|
||||
|
||||
+26
-78
@@ -1,7 +1,7 @@
|
||||
# YouTube Live API — design constraints (do not violate)
|
||||
|
||||
Go-live ordering and binding, the deliberate private-only lock, health parsing, end-stream
|
||||
pre-flight, Test mode and chat-insert rules. Read before touching go-live, broadcasts, health,
|
||||
Go-live ordering and binding, development/test privacy safeguards and release visibility, health
|
||||
parsing, end-stream pre-flight, Test mode and chat-insert rules. Read before touching go-live, broadcasts, health,
|
||||
or the chat service. The native alert box built on these events lives in
|
||||
[`overlays.md`](overlays.md).
|
||||
|
||||
@@ -107,77 +107,23 @@ These are the hard facts behind every decision. Full list in `TASKS.md`.
|
||||
register as active server-side, which is a genuine step forward — combined with the 99MB muxed
|
||||
in the earlier run, the stream half of the system is now verified healthy. The broadcast half is
|
||||
still unobserved pending a re-run.
|
||||
- **Visibility picker (TASK 9 item 6) — DELIBERATE TEST-PHASE LOCK, not drift (2026-09-01).**
|
||||
The "always Private" enforcement in `YouTubeStreamService.CreateBroadcast` stays while the creator
|
||||
runs multi-month real-world testing: non-private test streams would clutter the channel with VODs
|
||||
highlighting where the product breaks. `recordFromStart`/DVR stay ON — Private VODs are invisible
|
||||
to subscribers and serve as post-mortem review tapes; bulk-delete pre-GA. The unlock is a
|
||||
deliberate final-pass item in **TASK 36 (gold pass)**, wired with the dialog selection — never
|
||||
opportunistic. The PRIVATE badge keeps showing when the stream is actually private.
|
||||
- **⛔⛔ STOP. The always-Private lock is a DECISION, not a bug, not drift, not an open question.
|
||||
(Creator ruling, restated 2026-10-04 after being asked about it AGAIN.)**
|
||||
`privacyStatus = "private"` hardcoded in `YouTubeStreamService.CreateBroadcast` is
|
||||
**exactly right as written. Do not change it, do not "unlock" it, and — this is the part I keep
|
||||
getting wrong — do not raise it as a question, an observation, a suggestion, or a
|
||||
"heads-up".** The creator has now answered this more than once.
|
||||
- **What it becomes:** the hardcoded lock is replaced by a real, user-selectable
|
||||
visibility option in the Go-Live dialog — **at v1, not before.** Not at 1.0-candidate, not
|
||||
at GA prep, not opportunistically during any other unit.
|
||||
- **Why it exists:** pre-v1 test streams would otherwise publish VODs of a product still
|
||||
breaking, in public, to subscribers. Private keeps those tapes usable for post-mortem review
|
||||
while staying invisible to the channel's audience.
|
||||
- **Do not let these look like symptoms of the one they resemble.** All are working as designed:
|
||||
`privacy=private` in the broadcast diag line; the PRIVATE badge; `recordFromStart`/DVR ON;
|
||||
and the absence of any delete call in the codebase (there is no delete feature, by design at
|
||||
this stage — "can we delete the test VODs" is a *retired* question, answered 2026-10-04:
|
||||
`videos.delete` accepts `youtube.force-ssl` which we already hold, private is a visibility
|
||||
setting not a deletion lock, and anyway a broadcast that never reaches `live` never becomes a
|
||||
VOD at all).
|
||||
- **`UpdateBroadcast` is NOT the unlock — and it is also currently DEAD CODE. (Fact corrected
|
||||
2026-10-04: an earlier version of this note said `CreateBroadcast:112` /
|
||||
`UpdateBroadcastMetadataAsync`, both wrong.)** The method that can change a live broadcast's
|
||||
privacy is `YouTubeStreamService.UpdateBroadcast(broadcastId, meta)`, which sends
|
||||
`privacyStatus = meta.Visibility ?? "private"`. **It has no production caller** — the only
|
||||
caller in the repo is `BroadcastPullOutTests.cs:120`. So `CreateBroadcast` hardcoding private
|
||||
is not merely backed up by convention, it is the *only* thing enforcing it.
|
||||
- **⇒ Is a Go Live video private? YES — verified 2026-10-04, and this answers a standing
|
||||
creator doubt.** Three independent hardcodes (`CreateBroadcast` → `"private"`;
|
||||
`GoLiveViewModel.Visibility => "Private"`; `BeginTestStream` → `StreamVisibility = "Private"`)
|
||||
plus one unreachable method. There is no path by which a broadcast this app creates can become
|
||||
non-private today. The creator's actual concern — "do not populate my channel with videos
|
||||
showing the app at its worst" — is fully met.
|
||||
- **⛔ THE LANDMINE, and the reason the above matters:** `LiveBroadcastFormViewModel` still
|
||||
exposes `VisibilityOptions = {private, unlisted, public}` and `UpdateBroadcast` would happily
|
||||
honour it. **The v1 visibility option is the first thing that will ever wire `UpdateBroadcast`
|
||||
up — so v1 is exactly when this guarantee ends, by the creator's own choice.** When that
|
||||
happens, private stops being guaranteed and becomes a default the user can change. That is
|
||||
intended at v1; it is a disaster before v1. Do not wire `UpdateBroadcast` into any
|
||||
production path before v1, and do not "fix" the dead-code smell by calling it.
|
||||
- **⛔ The TEST button is HIDDEN at v1 — it exists only because of the lock above (creator
|
||||
ruling 2026-10-04).** This was **absent from the map entirely** until that date, which is how the
|
||||
dry-run framing below came to be the only recorded one. Both are true, and the second is the
|
||||
load-bearing one:
|
||||
- **Its real job is being the guaranteed-private path.** `BeginTestStream` hard-sets
|
||||
`StreamVisibility = "Private"` (`MainViewModel.Streaming.Operations.cs:105`), so Test can never
|
||||
publish. That is *why* it exists for the whole pre-v1 phase — a dry run you cannot leak.
|
||||
- **At v1 it is redundant and gets hidden**, because once visibility is a real dialog option the
|
||||
user picks Private themselves and a separate "safe" button no longer earns its place. Hiding it
|
||||
is part of the v1 visibility work, not a separate cleanup.
|
||||
- **The pre-existing "dry run" framing is now known to be incomplete** (`ShowTestButton`'s own
|
||||
comment calls it "a dry-run of the livestream"). Do not read that comment as the reason the
|
||||
button must persist, and do not re-derive the button's purpose from it. If a dry run is still
|
||||
wanted at v1 it must be re-justified on its own merits — that question is **open, and is for
|
||||
v1, not now.**
|
||||
- **It is a computed predicate, not a setting** — `ShowTestButton => ShowStreamActions &&
|
||||
IsConnected` (`MainViewModel.Streaming.cs:182`), feeding both `CanStartTest` and the XAML
|
||||
visibility. So the v1 hide is a one-line predicate change, not a settings migration.
|
||||
- **Not a v1 blocker:** the Test route is the *same* pipeline as Go Live —
|
||||
`BeginTestStream` → `StartStreamingSession(alsoRecord: false)` → `PrepareAndStartLiveAsync` —
|
||||
so Test and Go Live create identical broadcasts (`boundStreamId`, `enableAutoStart`). Test
|
||||
differs only in never recording (`alsoRecord: false`) and forcing Private. **This is why Test
|
||||
is the correct way to run any diagnostic stream**, and why a Test result is evidence about Go
|
||||
Live.
|
||||
- **Full broadcast form (TASK 9 item 7) — RESCOPED 2026-09-01** — the core editable fields ship (since 2026-08-24) as the always-visible **Text drawer**: title, description, tags, visibility, made-for-kids, live-editable; scheduling ships as **TASK 34** (drawer ☑ Scheduled + adoption at Start). The old **Advanced tab is permanently out of product** (the 10% margin — see TASKS.md → "Out of product"): latency locked `low`, DVR/record-from-start locked on, embed/projection/CC/region fixed at sane defaults, invisible — every exposed field is a support ticket. `categoryId` is removed from `CreateBroadcast` (not a `liveBroadcast` field, silently ignored). Monetization enablement (if ever needed) rides the reward-events chain via `liveBroadcasts.update`, not a form field. Go-live order (TASK 9, shipped 2026-08-16):
|
||||
- **Visibility policy (creator clarification 2026-11-04):** force Private during development and
|
||||
test runs so unfinished captures cannot accidentally publish. This is a safeguard for those
|
||||
environments, not a permanent product lock. The released creator flow offers Private, Unlisted,
|
||||
or Public, defaults to Private, and supports changing visibility while live. A PRIVATE badge is
|
||||
shown whenever the current broadcast is private.
|
||||
- **Release visibility flow (TASK 9, Day 2):** persist a default visibility (Private initially);
|
||||
ON-AIR opens a preflight with that default selected and an explicit “Start Broadcast — [scope]”
|
||||
action. Apply the selected scope when creating a broadcast and through
|
||||
`YouTubeStreamService.UpdateBroadcast` for live changes. Development/test runs stay forced Private.
|
||||
- **Implementation status:** current code still forces Private for all broadcasts. Do not mistake
|
||||
that description of today's code for the target Release behavior; the selectable flow is queued,
|
||||
not implemented. `UpdateBroadcast` currently has no production caller; Day 2 wires it for live
|
||||
visibility changes, while development/test runs remain Private.
|
||||
- **Test mode remains Private.** `BeginTestStream` forces `StreamVisibility = "Private"`, so retain
|
||||
that safeguard after normal Release go-live offers a choice. The existing v1 decision to hide the
|
||||
separate TEST button remains tracked with the release flow.
|
||||
- **Full broadcast form (TASK 9 item 7) — RESCOPED 2026-09-01** — the core editable fields ship (since 2026-08-24) as the always-visible **Text drawer**: title, description, tags, and made-for-kids; release visibility is the Day 2 flow above. Scheduling ships as **TASK 34** (drawer ☑ Scheduled + adoption at Start). The old **Advanced tab is permanently out of product**: latency locked `low`, DVR/record-from-start locked on, embed/projection/captions fixed at sane defaults, invisible. `categoryId` is video metadata, not a `liveBroadcast` field; category selection, tag updates through the video API, and thumbnail upload remain follow-ups. Monetization enablement (if ever needed) rides the reward-events chain via `liveBroadcasts.update`, not a form field. 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 →
|
||||
@@ -196,8 +142,10 @@ These are the hard facts behind every decision. Full list in `TASKS.md`.
|
||||
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. **Parsing (fix 2026-09-22):** the real API
|
||||
every **2s until YouTube confirms `lifeCycleStatus=live`**, then every **30s** (`DispatcherTimer`
|
||||
`_healthPollTimer`, first poll immediately after the frame pump starts); the top bar turns green only
|
||||
on that confirmed `live` state. It clears on End via `ResetHealth`; poll failures log only.
|
||||
**Parsing (fix 2026-09-22):** the real API
|
||||
nests `status.healthStatus = {status, lastUpdateTimeSeconds, configurationIssues[]}` — reading it as
|
||||
a string crashed every poll (`requires an element of type 'String'`, seen 2026-09-22 on both test
|
||||
sessions); the parse now reads the nested `.status`/`configurationIssues` and tolerates the old
|
||||
@@ -213,9 +161,9 @@ These are the hard facts behind every decision. Full list in `TASKS.md`.
|
||||
mono/stereo only. YouTube flags violations via health status.
|
||||
- **Broadcast ID == Video ID** — one ID tracks status, health, and the auto-created VOD
|
||||
(`recordFromStart` + `enableDvr` default true).
|
||||
- **Test Stream mode (TASK 41, SHIPPED 2026-09-22)** — every broadcast is private-only anyway
|
||||
(see the visibility lock above), so "Test" is a **session variant over the real go-live
|
||||
pipeline**, not a new mode: `BeginTestStream` → `StartStreamingSession(alsoRecord:false)`
|
||||
- **Test Stream mode (TASK 41, SHIPPED 2026-09-22)** — Test remains forced Private even when
|
||||
normal Release go-live offers a visibility choice, so "Test" is a **session variant over the real
|
||||
go-live pipeline**, not a new mode: `BeginTestStream` → `StartStreamingSession(alsoRecord:false)`
|
||||
(the dialog path was extracted into this shared boot — no Go Live dialog for a Test, no
|
||||
`BroadcastForm.CaptureGoLive` "last live" stamp, no recording). `IsTestStream` flips the face:
|
||||
gold top bar + glow (`#9c6f1c`), a **TEST** badge, and the End button reads **"End Test"**;
|
||||
|
||||
Reference in New Issue
Block a user