Repository navigation
feat(detection): find manifests by their Content-Type, and play the longest stream - #50
Merged
Merged
Conversation
client-setup.e2e.mjs waited for optionsApplied with a continuation line that ESLint's indent rule rejects. CI's first step failed on it, so no test ran on 699dc5d and the release was skipped. The same check, written as the wait above it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ongest stream - A URL that names no stream type is HLS or DASH when its Content-Type says so (application/vnd.apple.mpegurl, application/x-mpegurl, audio/mpegurl, audio/x-mpegurl, application/dash+xml). anikage.cc's episode is such a playlist. - Of a page's streams, the longest plays: an ad or an intro runs for seconds, the video for minutes or hours. The background reads each detected stream's length - an HLS playlist (the master's first variant), a DASH manifest, or the header of an MP4 or WebM file, in ranged reads - with the page's Referer, Origin and cookies, which a declarativeNetRequest rule gives its own requests (tabIds: TAB_ID_NONE). A stream of unknown length counts as 10 minutes, and streams within 10% of the longest are left to the old rule, so a page with one stream plays what it did before. With two or more, the player waits up to 2.5 s for lengths still being read; the reads start at detection. - MPV: the toolbar and the on-play fallback send the newest of the longest, not the newest. - The background's own reads are not detected as streams of tab -1. Tests: StreamLength (30), StreamLengths (17) and ManifestTypes (4) unit tests, 40 mutations each caught; source-length (3 cases, all fail on main) and a new mpv-shortcut case (an MSE player's film, not the ad it fetched after it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 29, 2026
Nawid3333
added a commit
that referenced
this pull request
Sep 29, 2026
… two subtitle bugs (#52) * fix(headers): match a header rule's URL as it is, with nothing escaped declarativeNetRequest's urlFilter has no escape character: Firefox's CompiledUrlFilter splits it at `*` and matches every other character, a backslash included, literally. So escaping a URL's `*`, `^` and `|` (7d9053d) made the header rule require a backslash no URL has, and a stream whose URL contains a `*` - an Akamai `acl=/*` token, say - lost its Referer and Origin. Unescaped, those characters can only widen the match, never exclude the URL itself. header-rules.e2e.mjs checks, on the installed extension, that the server sees the Referer for a plain URL, one with a `*` and one with `^` and `|`; the unit test pins the filter to the URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(archive): keep a dropped archive's URL until it has been read httpGetLarge fetches the file in 250 MB chunks as parseFSA reads it, but the object URL was revoked (d4d533a) once the first two chunks had been requested. Every later chunk failed, so any .fsa archive over 500 MB could not be opened. FastStreamArchiveUtils.parseFSAFile now owns the URL and revokes it after parsing. An unreadable file now ends in "archive failed" too, instead of an uncaught error. The chunk size is FastStreamArchiveUtils.fileChunkSize, which archive-roundtrip.e2e.mjs lowers to 64 KB so a small archive spans many chunks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(archive): keep manifests in the store; drop one only for a reload To keep live streams moving, #49 removed every HLS playlist and DASH MPD from the download store right after loading it. But "dump buffer" writes the store into the .fsa archive, so archives had no manifest, and a player opened from one had to fetch it again - impossible offline or once the URL expired. The seek preview, which shares the store, also downloaded every playlist a second time. Now a player's first load of a manifest may come from the store and leaves it there; each player remembers which it has loaded (loadedManifests), and only a second load by the same player - hls.js or dash.js refreshing a live stream - drops the stored copy first. Unit tests cover both sides (another player gets the stored copy; the same player's reload downloads it again). archive-roundtrip.e2e.mjs plays HLS and DASH, writes the archive in memory, drops it on the player, and checks it plays again with no manifest from the network. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mp4): end a fragmented file's stream only after its last range checkEndOfStream ended the MediaSource once every sample mp4box knew of was appended. A fragmented file's samples become known one moof at a time, so that also held mid-file whenever a 1 MB range ended between two fragments (measured with mp4box 2.4.1: true after a cut at or a few bytes into any moof). The MediaSource then cut the duration to what was buffered and the video stopped early. For a fragmented file the range holding its last byte must now be in as well. mp4-fragmented-end.e2e.mjs builds that case - a `free` box moves the fixture's second moof to 8 bytes before the first range ends - and holds the second range back: the stream must stay open, the duration whole, and the video play to its end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): fix the indent ESLint rejects in client-setup 4dc5ab7 left main's lint red. The same line as PR #50's, so the two merge cleanly in either order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(subtitles): keep a SubRip cue's text below a line of spaces c4d765d split cues at a line of spaces or tabs as if it were empty, so in a cue like "Top / (spaces) / Bottom" the text below that line became a block of its own with no timestamp and was dropped. ffmpeg and VLC keep it as part of the cue, as upstream did. Cues are now split only at empty lines. A line of spaces between two cues still ends the first: d3fec9b starts a cue at its timestamp line, and the trailing whitespace line is trimmed there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(subtitles): show nothing for a cue with no text, rather than throw A SubRip cue with nothing under its timestamp becomes a cue with empty text, and vtt.js's convertCueToDOMTree returns null for it. renderSubtitles appended that null, and appendChild(null) throws. While such a cue was on screen, every timeUpdated() stopped there, so a subtitle track after it showed nothing and the skip segments were not updated. The cue is now skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mp4): read a file's length from Content-Range before parsing it MP4Player set the file's length from the first answer's Content-Range only after handing the data to mp4box, and parsing the moov runs onMetadataParsed, which then works the length out from the samples known so far. For a fragmented file those are only the fragments in the first range, so the ranges after it were never made. The end-of-stream check 066d962 added took the file to be complete there, and the stream was ended after the first range. The length is now read before the data is parsed. A server that gives no total (no Content-Range, or `bytes a-b/*`) still gets the estimate from the sample table, as before. The fragmented-end spec had asked for the whole duration while the last range was held. An empty_moov file, as ffmpeg writes it, has no mehd box, so its duration is what has been parsed so far and grows with each range. The spec now checks what the bug did: the stream is not ended early, and the video plays to its end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): open the dumped archive on a fresh player page Dropped on the player still showing the same video, the archive's source is taken for the one already in the sources list, which is not marked as loaded from an archive. Opening the archive later, as it is used, means a fresh page, so the spec now carries the archive over to an empty player page and drops it there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): a long fragmented MP4 plays, and seeks far into it The 160 s fixture cut into fragments looked 10 s long before the length came from Content-Range (the previous commit), and the rest was never loaded. The note it replaces said such a file stopped after about 3 s. The limit that is left - past its first 30 ranges, 30 MB, MP4Player cannot pick a range to load in a fragmented file - is measured and noted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): let the empty-cue test's cues span the whole video The test sought to 1 s and then paused, with both cues on 0-5 s. In the full verify the video had played on to 8.33 s before the pause took, so no cue was on and the second track showed nothing. Both cues now span the whole 10 s video, and the test only pauses: whatever the time is, the cues are on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Nawid3333
added a commit
that referenced
this pull request
Sep 29, 2026
…#61) Since 1.3.82.38 (#50), a short HLS or DASH title whose segments are named .mp4 opened as one lone fragment. Init and media segments now measure as PIECE_LENGTH and rank below every stream; the player also waits for the lengths of sources detected while it waits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
FastStream now finds HLS and DASH manifests whose URL has no extension, by their Content-Type. Of a page's streams, the player now plays the longest, not the newest. This fixes anikage.cc (upstream issue 554), and the ad that played instead of the film on some cineby servers.
Fixes you would notice
anikage.cc plays. The episode is an HLS master playlist at
og.bakayaro.live/m3u8/<token>. The URL has no extension; only theContent-Type: application/vnd.apple.mpegurlsays what it is, so FastStream never detected it. Instead it tookog.bakayaro.live/stream/<token>for an MP4: a media request, labelledimage/jpeg, with MPEG-TS inside. That showed "Failed to load video!".application/vnd.apple.mpegurl,application/x-mpegurl,audio/mpegurl,audio/x-mpegurlorapplication/dash+xml(chrome/background/ManifestTypes.mjs). Only manifests count: segments sayvideo/mp4orvideo/mp2t.The film plays, not the ad. When a page runs an ad or an intro first, the ad is among the detected streams. The player picked by kind and age alone: the first HLS or DASH stream, or else the newest MP4. That was often the ad. Now the background reads how long each detected stream runs, and the player plays the longest.
What is read (
chrome/player/utils/StreamLength.mjs, no browser APIs):mediaPresentationDuration, or the sum of its periods;A live stream counts as the longest.
With the page's own Referer, Origin and cookies (
chrome/background/StreamLengths.mjs). Afetch()can't set those, so a declarativeNetRequest session rule adds them for the stream's URL, as for the player's own requests. The rule is limited to requests in no tab (tabIds: [TAB_ID_NONE]), so only the background's reads get them. A server that answers only the site's pages answers these reads too.When lengths are unknown or close: a stream of unknown length counts as 10 minutes. It beats a short ad and loses to a known full episode. Streams within 10% of the longest tie, and the old rule decides between those. So a page with one stream, or with streams of unknown length, plays what it did before.
The Sources menu still lists every stream, to pick one by hand.
MPV: the toolbar button and Ctrl+Shift+U's on-play fallback (for a
blob:player) send the newest of the longest streams, not the newest one.Behaviour changes
Tests
StreamLength(30),StreamLengths(17) andManifestTypes(4).source-length, a new spec intest:ext(3 cases), on the installed extension with the site on the auto-enable list:mpv-shortcut: a new case. An MSE player's film goes to mpv, not the ad it fetched after it.Checked
pnpm run verifyandverify:linux.main's code: all 3source-lengthcases fail there.TAB_ID_NONEapplies to the background'sfetch(). The CDN case above depends on it.http-on-examine-response, whichonHeadersReceivedis built on), and a page'sfetch()resolves, only when the body starts, not when the server sends the headers. So a slow manifest is detected late. The spec slows the variant playlist, not the master, for that reason.Not in this PR
cineby, all 41 servers on this build, in the test browser:
Two ideas for later, not built: waiting out pages that have only short streams, and falling back to the next stream when one fails to load.
The branch also carries fd5f874, the ESLint fix for the error that stopped
main's CI (client-setup.e2e.mjs). #51 carries the same commit, so merging both is harmless.🤖 Generated with Claude Code