Skip to content

A file is what its bytes are; Firefox's own player keeps the page's headers - #607

Merged
Nawid3333 merged 4 commits into
mainfrom
fix/bytes-decide-direct-headers
Oct 28, 2026
Merged

Nawid3333 merged 4 commits into
mainfrom
fix/bytes-decide-direct-headers

Conversation

@Nawid3333

@Nawid3333 Nawid3333 commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

What was wrong

A hoster served a Matroska file as video/mp4, from a URL ending in .mp4. The page's own <video> played it: Firefox reads a file by its bytes. FastStream took it for an MP4 by its type, mp4box stopped at the first bytes (Invalid data found while parsing box of type '£B' at position 0), and FastStream's player failed where the page's had played.

The same server answers a request without the page's Referer with a 302 to a decoy. Firefox's own player in FastStream (DIRECT) got the page's headers from a background rule that lived 5 s; the element asks for ranges whenever it needs them, so its first request after 5 s (a Matroska file's index at the end, a seek) went out without a Referer, and the client's recovery reloaded the source.

The fix (no site rule)

  • MP4Player looks at the first range's first 8 bytes (ContainerSniff.startsLikeMp4: a box's size, then four printable characters) and hands a file that cannot be an MP4 to Firefox's own player (playDirectly, the existing hand-over), one range in. Every MP4/QuickTime file starts with a box, so none leaves MP4Player.
  • DIRECT's header rule lives 60 s and is renewed every 10 s while the player has the source (SET_HEADERS gets an optional lifetimeMs, capped at 60 s in RuleEntry): Firefox may hold a hidden tab's timers back, and a renewal up to 50 s late still comes in time. A request's rule stays 5 s. After the player is gone the rule lasts at most 60 s, limited to the extension's own requests to that URL in that tab.

Tests

  • ext-specs/mislabelled-file.e2e.mjs (new): a Matroska film served as video/mp4 from an .mp4 URL, by a server that sends a request without the page's Referer to a decoy, at 400 KB/s. The player plays it in Firefox's own player, plays on after a seek 12 s in, and no request lacks the Referer.
    • main: fails (MP4 mode, the mp4box error).
    • the hand-over alone: fails (the request 12 s in had no Referer, the decoy got it, the source reloaded).
    • this branch: passes.
  • Unit: ContainerSniff.test.mjs (MP4/QuickTime starts vs. Matroska, WebM, Ogg, FLAC, ID3, WAV, FLV, MPEG-TS), NetRequestRuleManager.test.mjs (lifetime, cap, floor), DirectVideoPlayer.test.mjs (renewal, stop on destroy and on a new source, none without headers). The renewal and lifetime tests fail without the change.
  • Local: lint, tsc, all 3318 unit tests; ext specs header-rules, redirected-source, source-length, same-video-file, fresh-link, framed-player, mislabelled-file; web specs mp4-without-ranges, mp4-files, mp4-loading, player-recovery, seek-downloads.

glm reviewed both halves. The 30 s → 60 s change came from it. Its other finding (the seek preview's MP4Player handing over) is harmless: the preview has its own event context, stops after one range, and the main player's hand-over replaces it (resetPlayer).

Notes in docs/notes/player.md ("A file is what its bytes are…"), including that the e2e web build is cross-origin isolated (COOP/COEP), so DIRECT there loads only files with Cross-Origin-Resource-Policy.

🤖 Generated with Claude Code

…eaders

A hoster served a Matroska file as video/mp4 from an .mp4 URL. The page's own <video>
played it; FastStream took it for an MP4 by its type, mp4box stopped at the first bytes
("Invalid data found while parsing box of type '£B'"), and the player failed.

- MP4Player looks at the first range's first 8 bytes (ContainerSniff.startsLikeMp4: a
  box's size, then four printable characters) and hands a file that cannot be an MP4 to
  Firefox's own player (playDirectly), which reads files by their bytes.
- That server sends a request without the page's Referer to a decoy. DirectVideoPlayer set
  its header rule once, and rules lived 5 s: the element's first request after that (the
  Matroska index at the end, a seek) had no Referer, and the source was reloaded. Its rule
  now lives 30 s and is renewed every 10 s while it has the source (SET_HEADERS'
  lifetimeMs, capped at 60 s). A request's rule stays 5 s.

mislabelled-file.e2e.mjs fails on main (the mp4box error) and with the hand-over alone
(the request 12 s in had no Referer); it passes with both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nawid3333 and others added 3 commits October 28, 2026 21:17
…jection)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Firefox may hold a hidden tab's timers back: with a 30 s rule renewed every 10 s, a
renewal more than 20 s late left the element's next request without the page's
headers. At 60 s a renewal may come 50 s late.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ct-headers

# Conflicts:
#	tests/unit/typeChecked.test.mjs
@Nawid3333
Nawid3333 merged commit cfa4aac into main Oct 28, 2026
23 checks passed
@Nawid3333
Nawid3333 deleted the fix/bytes-decide-direct-headers branch October 28, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants