Skip to content

canPlay.dash / canPlay.hls return false when the manifest URL carries a #t= media fragment #2046

Description

@danihorvath

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 not

This 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 URIFragmentModel and uses t to compute the start time, so attachSource('movie.mpd#t=258') starts at 4:18.
  • Native HLS in Safari: hls-video-element assigns the URL straight to nativeEl.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 loads

Rendered 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.3 and 3.4.0dist/patterns.js is byte-identical between them, and src/patterns.ts on master is unchanged too
  • dash.js 5.2.0, dash-video-element 0.2.0, hls-video-element 1.5.11

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions