117 lines
6.4 KiB
Markdown
117 lines
6.4 KiB
Markdown
# TASK 50 — One-button "Publish this video" (resumable upload of the local recording)
|
||
|
||
> Catalog: [`TASKS.md`](../TASKS.md) — status and requirements live here.
|
||
> Carved 2026-09-28 from a creator ruling: *"For me, as a creator, I'd love a one-button
|
||
> publish this video feature. Who wouldn't?"*
|
||
|
||
**Status:** ☐ Queued — creator-ruled, not yet scoped in detail
|
||
|
||
**Goal:** turn the local REC-side MP4 into a channel video with one button, and stop relying on
|
||
YouTube's silent automatic archiving to get a recording onto the channel at all.
|
||
|
||
**Size:** S–M. Media already exists; the work is a resumable-transfer lifecycle. **Not a quarter.**
|
||
|
||
---
|
||
|
||
## Why
|
||
|
||
Two reasons, and the second is the one that actually justifies it.
|
||
|
||
**1. It fixes a defect the creator already reported as broken.** On 2026-09-26 the creator could
|
||
not get private test recordings saved to YouTube, and used the local recording as the proof of
|
||
concept instead. The mechanism is now understood and CONFIRMED empirically (creator inspected the
|
||
channel 2026-09-28: 20+ private videos, all content-less placeholders). A deliberate upload is
|
||
the actual fix; the current behaviour is a side-effect nobody designed.
|
||
|
||
**2. It is not the heavy feature it looks like.** The instinct is to call upload a big risky
|
||
addition because it moves bytes. It doesn't: ffmpeg already moves the media, and the recording
|
||
already exists as a finished local MP4. The genuine new work is a **discrete long-running job
|
||
with a lifecycle** — resumable, with progress, retry, cancel and quota. RTMP has none of that
|
||
because the connection belongs to the encoder process and simply reconnects. Different protocol,
|
||
different failure model. That is the whole cost.
|
||
|
||
**Creator ruling, verbatim:** one button. Not a media manager, not a playlist tool.
|
||
|
||
---
|
||
|
||
## Mechanism — CORRECTED 2026-09-28 (creator's channel inspection disproved the earlier model)
|
||
|
||
Verified against the code AND against the creator's channel:
|
||
|
||
- `liveBroadcasts.insert` creates a **placeholder video** on the channel for every broadcast the
|
||
app makes — that is the source of the 20+ private videos. The creator confirmed 2026-09-28:
|
||
a couple dozen+ private videos, all **scheduled placeholders with NO content**.
|
||
- Content arrives only if the stream is actually pushed and archived: broadcast reaches **LIVE**
|
||
(`liveBroadcasts/transition`), ffmpeg pushes RTMP to the bound stream key, and the broadcast
|
||
ends long enough to be transcoded. Content-less placeholders mean **the stream never lands** —
|
||
one of those three steps fails in practice.
|
||
- `enableAutomaticArchiving` is **never set** by the app, so YouTube's `true` default applies.
|
||
- Broadcasts are inserted with `privacyStatus = "private"`
|
||
(`Services/YouTubeStreamService.cs:49`), so those orphans are not public. That is correct and
|
||
must stay.
|
||
- The app has **no delete path** — nothing in `Services/` deletes a broadcast or a video. The
|
||
creator clears each orphan by hand in Studio.
|
||
|
||
**Consequence for the original 2026-09-26 complaint:** it was correct. The private recordings
|
||
never saved to YouTube because **no video content was ever produced on the channel** — not
|
||
because the app couldn't reach a VOD that existed. The upload pipeline (broadcast → LIVE → RTMP
|
||
push → archive) fails; only broadcast creation succeeds.
|
||
|
||
### The one diagnostic question to answer before this task is scoped
|
||
|
||
During a real test stream, does ffmpeg actually reach `a.rtmp.youtube.com`, and does the
|
||
broadcast reach the LIVE state? If the encoder never starts or the transition never fires, that
|
||
is the observed bug — and it is worth a fix BEFORE any publish feature, because this task's
|
||
premise ("the recording can get to YouTube") depends on it.
|
||
|
||
### Sequencing constraint — do not flip the flag early
|
||
|
||
`enableAutomaticArchiving: false` is the correct end state: the app uploads the local file
|
||
deliberately, on demand, instead of YouTube archiving silently. **But automatic archiving is
|
||
currently the only route a recording takes to YouTube.** Setting it to `false` before publish
|
||
ships would leave test streams with nothing on the channel at all and re-create the 2026-09-26
|
||
complaint in a worse form.
|
||
|
||
**Therefore: ship the upload first, flip the flag as part of the same task, never before.**
|
||
|
||
---
|
||
|
||
## Design notes
|
||
|
||
- **Source is the local MP4**, not the YouTube archive. The app already has it (REC side,
|
||
`EncoderOptions.RecordPath`); the archived VOD is a duplicate and an orphan.
|
||
- **Resumable upload is mandatory, not optional.** Stream-length recordings are large (hours of
|
||
4–8 Mbps video); a plain single POST is not viable. YouTube's `videos.insert` supports
|
||
resumable sessions, and the failure mode that matters is a drop at 80%.
|
||
- **Default visibility must be `private` or `unlisted`, never `public`.** A bad take going public
|
||
is unrecoverable, and this app is used for test streams by definition. The creator picks, and
|
||
the default is the safe one.
|
||
- **A title still has to come from somewhere.** One button, not zero decisions: default to the
|
||
recording's filename, allow an edit, do not block on it.
|
||
- **Quota:** `videos.insert` costs 1600 units against a 10,000/day project allowance — roughly
|
||
six uploads per day. Fine for a creator, but it is a real ceiling and the app should surface a
|
||
quota error rather than retrying into it.
|
||
- Scope already permits it: the single `auth/youtube` scope covers upload. No scope change needed.
|
||
|
||
---
|
||
|
||
## Open question to settle before coding
|
||
|
||
**Microsoft Store 11.12 (User-Generated Content).** The standing answer is that the app hosts no
|
||
user content and does not interact with third-party UGC — chat is read-only and never persisted,
|
||
which is load-bearing for that answer. A publish feature makes the app *push* creator content to
|
||
a third-party platform, which touches the same area. It does not obviously break the answer (the
|
||
destination is YouTube, which has its own mature moderation, and the app stores nothing), but it
|
||
**must be re-checked against
|
||
[`research-store-certification.md`](research-store-certification.md) §11.12 and the IARC answers
|
||
in [`task-48`](task-48-distribution-msix.md) before submission** — do not let the IARC answers go
|
||
to certification describing an app that cannot publish.
|
||
|
||
---
|
||
|
||
## Precedence
|
||
|
||
Belongs to **TASK 48** (MSIX/Store), but does **not** block the Store submission: the app is
|
||
fully shippable without it. Recommend MSIX submission first, publish second — unless the creator
|
||
reverses that, in which case it moves ahead of the remaining TASK 48 items.
|