82 lines
4.1 KiB
Markdown
82 lines
4.1 KiB
Markdown
# TASK 65 — Replay clips: buffer the last 15 seconds, show it, save it
|
||
|
||
> Catalog: [`TASKS.md`](../TASKS.md) — status and requirements live here.
|
||
> Spec settled with the creator 2026-11-09. **Not a Twitch-clip clone:** the platform owns viewer
|
||
> clips; this is the creator-side replay buffer YouTube does not give you.
|
||
|
||
## The rule
|
||
|
||
**15 seconds is a clip.** Options 10 / 15 / 20, default **15**, no slider, no 30. A clip is a moment,
|
||
not a segment — 3-minute Shorts are the platform's problem, not ours.
|
||
|
||
## Why the ring holds encoded bytes, not frames
|
||
|
||
A 1080p BGRA composite frame is 8.3 MB; fifteen seconds at 60 fps is **~7.4 GB** of raw frames. The
|
||
15 MB figure is *after* compression. So the ring keeps **fragmented MP4** (`frag_keyframe+empty_moov`)
|
||
that ffmpeg writes to a pipe, in a **byte ring in RAM** — never on disk, never raw.
|
||
|
||
- Fragments are **1 s** (not 2): the ring can only hand back whole fragments, so a longer fragment
|
||
means the save can miss the punchline.
|
||
- The buffer is capped **by bytes**, not seconds — bitrate moves. When 15 s would exceed the cap, the
|
||
clip is simply shorter.
|
||
- The ring is a member of the process: it dies with the app, so a crash leaves no debris and needs no
|
||
sweep. **Nothing touches the disk until F10.**
|
||
|
||
## Trigger
|
||
|
||
**F10** saves the clip (F1–F9 are taken; F11 is the fallback if a game steals F10). No focus switch, no
|
||
mouse. Deferred deliberately: **voice triggering** (always-listening in a streaming app is a privacy
|
||
liability, and the game's own audio produces false positives) and **`!clip`** (only for viewers/mods,
|
||
never the creator, who cannot reach a keyboard mid-moment).
|
||
|
||
## The replay layer
|
||
|
||
**Scope settled 2026-11-10:** the immediate replay of a fresh clip happens **only in the Live scene**.
|
||
While that replay is playing, **F10 is inert** ("replay in progress") — that is what replaces the
|
||
hall-of-mirrors exclusion, which is impossible with one encoder and two muxes: the clip *is* what the
|
||
stream got.
|
||
|
||
One layer type, added by the creator to the scenes they want; behaviour comes from the scene:
|
||
|
||
| Scene | Behaviour |
|
||
|---|---|
|
||
| **Live** | plays the new clip **once**, then **hides** — you're there to see it or you're not |
|
||
| Every other scene | loops the last **1–3** saved clips **in order** (highlight reel), configurable |
|
||
|
||
- **Audio off by default**, toggleable per layer. The show's sound design is the creator's; a replay
|
||
should not re-mix it uninvited.
|
||
- **No exclusion, no second encode.** The clip is a second mux of the frames the stream already got, so
|
||
it contains whatever viewers saw. Double-clipping is prevented instead: F10 does nothing while the
|
||
Live replay is playing.
|
||
|
||
## Storage and retention
|
||
|
||
- Saved to `clips\` beside recordings, as a normal `.mp4`.
|
||
- **The three most recent are kept**; the fourth deletes the oldest.
|
||
- **Auto-clip on monetization events** (off by default): a Super Chat / membership / raid saves the
|
||
preceding 15 s hands-free. Same retention, so it cannot accumulate.
|
||
- **Disk discipline:** no steady writes while live (the ring is RAM); the free-space guard that already
|
||
protects REC also disables the ring when space is low — **the recording always wins**.
|
||
|
||
## Privacy
|
||
|
||
Named in `PRIVACY.md`: clips are local only, the three most recent are kept, older ones are deleted
|
||
automatically, and nothing is uploaded. The first save says the same in one line, so the policy is not
|
||
only a document.
|
||
|
||
## Shipped 2026-11-10
|
||
|
||
Six commits: ring (`b93744e`), clip mux (`5066d81`), library (`f7d3d13`), F10 + settings (`2865791`),
|
||
layer model (`a5c2061`), layer playback (`5fcfa54`). 608 tests.
|
||
|
||
Length **10/15 (default 10)**, keep **3/4/5 (default 3)**, off by default, guard while replaying.
|
||
|
||
**Known limits, stated on purpose:** the clip is a second mux of the program, so it contains whatever
|
||
viewers saw; there is no clean feed and no editor. The feature is a capture.
|
||
|
||
## Tests
|
||
|
||
- Ring: fragment boundaries respected, byte cap enforced, save emits a playable MP4, oldest pruned.
|
||
- Layer: once-then-hide in Live, in-order loop elsewhere, silent by default, layer excluded from the clip.
|
||
- Failure: save with an empty ring (just started) says so instead of writing a 0-byte file.
|