canPlay.dash / canPlay.hls return false when the manifest URL carries a #t= media fragment
Summary
DASH_EXTENSIONS and HLS_EXTENSIONS only allow .mpd/.m3u8 to be followed by end-of-string or ?, so a manifest URL carrying a media fragment (#t=) fails detection. VIDEO_EXTENSIONS explicitly allows #t=, so behaviour is inconsistent between progressive files and manifests:
// src/patterns.ts
export const VIDEO_EXTENSIONS = /\.(mp4|og[gv]|webm|mov|m4v)(#t=[,\d+]+)?($|\?)/i; // handles #t=
export const HLS_EXTENSIONS = /\.(m3u8)($|\?)/i; // does not
export const DASH_EXTENSIONS = /\.(mpd)($|\?)/i; // does notThis matters because #t= on a manifest is a legitimate, supported way to start a stream at an offset:
- dash.js parses the URL fragment via
URIFragmentModeland usestto compute the start time, soattachSource('movie.mpd#t=258')starts at 4:18. - Native HLS in Safari:
hls-video-elementassigns the URL straight tonativeEl.src, where the browser honours the media fragment.
Impact
Because index.ts passes the html entry as playerFallback, a non-matching URL does not render nothing — it silently falls through to HtmlPlayer and produces a plain <video> pointed at the manifest. In any browser without native DASH support that is an unplayable source, so the symptom looks like a broken stream or a broken CDN rather than a URL-detection problem.
Reproduction
<ReactPlayer src="https://example.com/movie.mpd" /> // -> <dash-video src="..."> OK
<ReactPlayer src="https://example.com/movie.mpd#t=258" /> // -> <video src="..."> dash.js never loadsRendered output, confirmed in jsdom on 3.3.3 and 3.4.0:
PLAIN .mpd -> <dash-video src="https://example.com/movie.mpd">
WITH .mpd#t=258 -> <video src="https://example.com/movie.mpd#t=258">Related: #t= with a decimal never matches either
(#t=[,\d+]+)? has no . in its character class, so fractional times fail even for progressive files. (The + inside the class is a literal, which also looks unintended.)
src |
matched player |
|---|---|
a.mpd |
dash |
a.mpd#t=258 |
none |
a.m3u8 |
hls |
a.m3u8#t=258 |
none |
a.mp4#t=258 |
html |
a.mp4#t=10,20 |
html |
a.mp4#t=10.5 |
none |
a.mp3#t=5 |
none |
Fractional start positions are common when resuming playback from a stored position, and AUDIO_EXTENSIONS has no fragment handling at all.
Suggested fix
Treat # like ? — a fragment simply terminates the extension — instead of special-casing one fragment shape:
export const AUDIO_EXTENSIONS = /\.(m4a|m4b|mp4a|mpga|mp2|mp2a|mp3|m2a|m3a|wav|weba|aac|oga|spx)($|[?#])/i;
export const VIDEO_EXTENSIONS = /\.(mp4|og[gv]|webm|mov|m4v)($|[?#])/i;
export const HLS_EXTENSIONS = /\.(m3u8)($|[?#])/i;
export const DASH_EXTENSIONS = /\.(mpd)($|[?#])/i;This collapses the special-cased #t= group into a general terminator and fixes decimals, commas, posix: times and any other fragment form in one go.
Workaround
Re-registering the built-in entries with a fragment-tolerant canPlay, since custom players are consulted first:
import ReactPlayer from 'react-player'
import Players from 'react-player/players'
for (const [key, pattern] of Object.entries({ hls: /\.m3u8($|[?#])/i, dash: /\.mpd($|[?#])/i })) {
const player = Players.find((entry) => entry.key === key)
if (player) ReactPlayer.addCustomPlayer({ ...player, canPlay: (src) => pattern.test(src) })
}Environment
- react-player
3.3.3and3.4.0—dist/patterns.jsis byte-identical between them, andsrc/patterns.tsonmasteris unchanged too - dash.js 5.2.0, dash-video-element 0.2.0, hls-video-element 1.5.11
Source: cookpete/react-player