KeyframeData is never written, so every HLS playlist re-reads the file #100

Closed
opened 2026-08-13 22:59:39 +02:00 by Claude · 1 comment
Collaborator

M5 named this and left it: upstream caches an extraction in the KeyframeData table and we read the container's index each time a playlist is asked for — three seeks into the mkv, per request, forever.

Two consequences, one of them already predicted in MILESTONES.md:

  • Cost. A remuxed HLS stream pays the read on every playlist request rather than once per file.
  • A database diff will report the table on any run where upstream remuxed an mkv over HLS. Nothing in the fixture reaches that today, so it has not fired yet.

Upstream's task is src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs, key KeyframeExtraction. It has two extractors — the cheap one that reads container metadata and the ffprobe one that decodes — and only the cheap one runs inline; the expensive one is exactly why the task exists. crates/jellyfin-matroska already does the cheap half for Matroska, so what is missing is the table and the mp4 side.

The playlist we build from the Cues element agrees with upstream's to the millisecond on the fixture's HEVC mkv, so this is a caching and coverage issue rather than a correctness one.


Where to start

M5 named this and left it, and the note there is the specification: upstream
caches the extraction in KeyframeData, ours reads the container index on every
playlist request — three seeks per request, forever.

  • Ours: crates/jellyfin-matroska reads the Cues element, and the playlist is
    built in crates/jellyfin-app/src/hls/.
  • Upstream: src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs
    (key KeyframeExtraction), and the two extractors behind it. Only the cheap
    one — the one that reads container metadata — runs inline; the ffmpeg one
    decodes the file and is exactly why the task exists. Keep that split.

The mp4 side is not written here at all; Matroska is.

How to know it is done

  • The playlist a route builds is unchanged: the existing agreement with
    upstream's — same nine segments to the millisecond on the fixture's HEVC mkv —
    still holds, and now costs one read per file rather than one per request.
  • diff-db reports no difference on KeyframeData after a run where upstream
    remuxed an mkv over HLS. That run does not exist yet; making it exist is part
    of this.
  • A rough before/after on repeated playlist requests, since the cost is the
    reason.

House rules

CLAUDE.md is the short version and these are the parts that bite: tests run
under cargo nextest; upstream's source is the checkout in upstream/ at the
pin
(v12.0-rc5, 6d501ba4) and not the clone beside the repository;
generated files are regenerated rather than edited; a comment says what the code
does, not how it got there; and Dn references stay out of code and comments.
ARCHITECTURE.md decides which crate a thing goes in and which dependency edges
are illegal, DECISIONS.md says why a rule exists.

The reference pair is cargo xtask reference up (and reference provision,
which is idempotent); a run is
cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR,
with diff-db for the two databases. crates/jellyfin-harness/README.md says
what a diff means and what an ignore entry has to carry — every entry needs a
reason and an expiry milestone, and an adds-only history is the smell the risk
table names.

M5 named this and left it: upstream caches an extraction in the `KeyframeData` table and we read the container's index each time a playlist is asked for — three seeks into the mkv, per request, forever. Two consequences, one of them already predicted in `MILESTONES.md`: - **Cost.** A remuxed HLS stream pays the read on every playlist request rather than once per file. - **A database diff will report the table** on any run where upstream remuxed an mkv over HLS. Nothing in the fixture reaches that today, so it has not fired yet. Upstream's task is `src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs`, key `KeyframeExtraction`. It has two extractors — the cheap one that reads container metadata and the ffprobe one that decodes — and only the cheap one runs inline; the expensive one is exactly why the task exists. `crates/jellyfin-matroska` already does the cheap half for Matroska, so what is missing is the table and the mp4 side. The playlist we build from the Cues element agrees with upstream's to the millisecond on the fixture's HEVC mkv, so this is a caching and coverage issue rather than a correctness one. --- ## Where to start M5 named this and left it, and the note there is the specification: upstream caches the extraction in `KeyframeData`, ours reads the container index on every playlist request — three seeks per request, forever. - Ours: `crates/jellyfin-matroska` reads the Cues element, and the playlist is built in `crates/jellyfin-app/src/hls/`. - Upstream: `src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs` (key `KeyframeExtraction`), and the two extractors behind it. Only the cheap one — the one that reads container metadata — runs inline; the ffmpeg one decodes the file and is exactly why the task exists. Keep that split. The mp4 side is not written here at all; Matroska is. ## How to know it is done - The playlist a route builds is unchanged: the existing agreement with upstream's — same nine segments to the millisecond on the fixture's HEVC mkv — still holds, and now costs one read per file rather than one per request. - `diff-db` reports no difference on `KeyframeData` after a run where upstream remuxed an mkv over HLS. That run does not exist yet; making it exist is part of this. - A rough before/after on repeated playlist requests, since the cost is the reason. ### House rules `CLAUDE.md` is the short version and these are the parts that bite: tests run under `cargo nextest`; upstream's source is the checkout in `upstream/` **at the pin** (`v12.0-rc5`, `6d501ba4`) and not the clone beside the repository; generated files are regenerated rather than edited; a comment says what the code does, not how it got there; and `Dn` references stay out of code and comments. `ARCHITECTURE.md` decides which crate a thing goes in and which dependency edges are illegal, `DECISIONS.md` says why a rule exists. The reference pair is `cargo xtask reference up` (and `reference provision`, which is idempotent); a run is `cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR`, with `diff-db` for the two databases. `crates/jellyfin-harness/README.md` says what a diff means and what an ignore entry has to carry — every entry needs a reason and an expiry milestone, and an adds-only history is the smell the risk table names.
Author
Collaborator

Done, on main — 434843f.

KeyframeData is read and written. A playlist request reads the row and reaches the container only on a miss, which is upstream's CacheDecorator; the ffprobe reader that walks every packet is reachable only from the task, because upstream's playlist generator keeps just the IsMetadataBased extractors. Two Cached values over one set of rows.

The mp4 index reader this issue expected does not exist upstream either. At the pin there are exactly two extractors — Matroska and ffprobe — so this was much smaller than it was billed as.

Measured against a pair built for it: nine rows each, diff-db clean on the table, ids and KeyframeTicks identical, and the variant playlist for the HEVC mkv byte-identical to upstream's before and after. Cost: 20 requests at 237 ms with the row against ~409 ms reaching the container each time.

Done, on `main` — `434843f`. `KeyframeData` is read and written. A playlist request reads the row and reaches the container only on a miss, which is upstream's `CacheDecorator`; the ffprobe reader that walks every packet is reachable **only** from the task, because upstream's playlist generator keeps just the `IsMetadataBased` extractors. Two `Cached` values over one set of rows. **The mp4 index reader this issue expected does not exist upstream either.** At the pin there are exactly two extractors — Matroska and ffprobe — so this was much smaller than it was billed as. Measured against a pair built for it: nine rows each, `diff-db` clean on the table, ids and `KeyframeTicks` identical, and the variant playlist for the HEVC mkv byte-identical to upstream's before and after. Cost: 20 requests at 237 ms with the row against ~409 ms reaching the container each time.
Sign in to join this conversation.
No description provided.