Skip to content

feat!: use Video.js v10 media components - #2049

Open
luwes wants to merge 16 commits into
masterfrom
feat/videojs-v10-media
Open

luwes wants to merge 16 commits into
masterfrom
feat/videojs-v10-media

Conversation

@luwes

@luwes luwes commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Moves ReactPlayer's media from the standalone *-video-element packages and @mux/mux-player-react onto the Video.js v10 React media components (@videojs/react/media/*). The public API stays the same apart from the breaking changes below: the same props, callbacks, static methods and lazy loading per player. Players are picked with v10's resolveAdapterType, so ReactPlayer recognizes the same sources as the media it renders. It also migrates the repo's tooling to pnpm and Vite+. (Includes #2050.)

Breaking change: this releases as a new major version (4.0.0). See Breaking changes below and the new v3 → v4 migration guide.

Requires @videojs/*@10.0.1, which fixes autoPlay on embeds and a React 18 dev warning (videojs/v10#3136), a crash when switching away from YouTube (videojs/v10#3153), and hands the adapter to mediaRef for streams (videojs/v10#3141).

How it works

Every v10 media component behaves like a <video> tag: the same attributes and on* callbacks, a ref that receives the rendered element, and a mediaRef that receives the playable media. That's the <video> itself for a file, or the playback adapter for a stream or an embed. ReactPlayer now exposes exactly the same ref and mediaRef, so the glue here is small:

  • src/MediaPlayer.tsx: createMediaPlayer(Component, toMediaProps) maps src + config onto the v10 source prop. Everything else, ref and mediaRef included, passes through unchanged. v10 compares sources structurally, so nothing is memoized.
  • src/sources.ts: the src/config → source mappings:
    • engineSource (the default): config is keyed by engine name exactly like v10's source.engine, so every player gets the same { src, engine: config } and reads only its own key. One prop still configures every player.
    • muxSource: ReactPlayer matches extension-less Mux URLs (stream.mux.com/<id>?…), which become { playbackId, playback }, with config.mux layered on top. Mux plays through hls.js, so it also reads config.hlsJs/config.nativeHls.
    • wistiaSource: src plus config.wistia as Wistia's own options.
  • src/patterns.ts: resolves a URL to its player key once with resolveAdapterType (and resolveMimeType) from @videojs/react. Every built-in player key is a v10 adapter type, plus html, so each player's canPlay is canPlay('<key>'). Two rules stay on top: Mux URLs ending in .m3u8 still play with hls.js, and the HTML player keeps AUDIO_EXTENSIONS/VIDEO_EXTENSIONS as a fallback for extensions v10 doesn't recognize (m4v, m4b, weba, oga, spx, …).
  • src/players.ts: each entry lazy-loads its @videojs/react/media/* component, so code splitting per player is unchanged.
  • src/Player.tsx: drives playing, volume, playbackRate and pip through the media, composing its own ref with mediaRef via v10's useComposedRefs.
  • src/HtmlPlayer.tsx: a native <video>/<audio> both renders and plays the media, so it is both ref and mediaRef.
  • src/types.ts: Config is composed from the v10 *EngineConfig types, plus mux and wistia.

mediaRef.current is an HTMLMediaElement-like object for every player, so playing, volume, playbackRate, pip and mediaRef.current.currentTime = … work the same everywhere. ref.current is the DOM element: the <video>/<audio>, the embed's <iframe>, or <wistia-player>. mediaRef.current.engine is the engine for every adapter: hls.js, dash.js, or the embed's SDK.

Breaking changes

  • React 17 is no longer supported. The peer range is now ^18 || ^19, because @videojs/react requires React 18 or later.

  • config is keyed by v10 engine name and holds that engine's own options: config.hls is now config.hlsJs (hls.js config), config.dash is now config.dashJs (dash.js settings), plus nativeHls, youtube, vimeo, spotify, twitch and tiktok. config.mux takes MuxSource options (playback, poster, storyboard, drm); Mux reads config.hlsJs for its engine. The README table is updated.

    // before
    <ReactPlayer src={src} config={{ hls: { maxBufferLength: 60 }, youtube: { color: 'white' } }} />
    // after
    <ReactPlayer src={src} config={{ hlsJs: { maxBufferLength: 60 }, youtube: { color: 'white' } }} />
  • Custom players receive the whole config rather than only their own key, and PlayerEntry no longer has a name field.

  • ref points to the DOM element; the media API moves to the new mediaRef prop. For files both are the <video>/<audio> element. For HLS, DASH and Mux, ref is the <video> and mediaRef is the playback adapter that drives it. For embeds, ref is the <iframe> (<wistia-player> for Wistia) and mediaRef is the playback adapter, where ref used to be a media custom element.

    // before
    <ReactPlayer ref={playerRef} src={src} />
    playerRef.current.currentTime = 30;
    // after
    <ReactPlayer mediaRef={mediaRef} src={src} />
    mediaRef.current.currentTime = 30;
  • Custom players must forward ref to the element they render and hand the media to the mediaRef prop, like the v10 media components.

  • Engines moved from ref.current.api to mediaRef.current.engine, for every player except Wistia: hls.js for HLS and Mux, dash.js for DASH, and the embed's SDK for YouTube, Vimeo, Spotify, Twitch and TikTok.

  • Mux plays with MuxVideo instead of Mux Player: no built-in UI (use controls, or a v10 skin or UI components), no automatic poster, and no built-in Mux Data. Mux Data and Google Cast now come from v10 extensions: wrap ReactPlayer in v10's VideoPlayer and add <MuxData />, <GoogleCast /> and <CastButton /> next to it (see New).

  • Media Chrome, which v3's README suggested for custom controls, can no longer control embeds, because slot="media" now lands on the embed's <iframe>. File, HLS, DASH and Mux sources still render a <video>. v4 recommends Video.js v10 skins or UI components instead (see New), which work with every source.

  • Some embed options changed shape to match the providers' own parameters: Spotify startAt → t and theme: 'dark' → theme: 0, TikTok booleans → 0 | 1, Twitch time number → '1h30m10s'. The unused config.html is removed.

  • react-player/patterns no longer exports the URL regexes (HLS_EXTENSIONS, DASH_EXTENSIONS, MATCH_URL_MUX, MATCH_URL_YOUTUBE, MATCH_URL_VIMEO, MATCH_URL_WISTIA, MATCH_URL_SPOTIFY, MATCH_URL_TWITCH, MATCH_URL_TIKTOK), and canPlay is a function of the player key: canPlay.youtube(url) becomes canPlay('youtube')(url). Use resolveAdapterType from @videojs/react to classify URLs:

    import { resolveAdapterType } from '@videojs/react';
    
    resolveAdapterType('https://youtu.be/oUFJJNQGwhk'); // 'youtube'

New

  • URL matching loses nothing that played in v3. v3's regexes also matched youtube.com/user/…, vimeo.com/channels/…, vimeo.com/showcase/… and Twitch's own player.twitch.tv/?video=v…&parent=… embed URLs, but the v3 player elements couldn't parse those either. The two that did play, music.youtube.com/watch?v=… and player.twitch.tv/?video=<id>, are tracked upstream in Bug: resolveAdapterType Rejects music.youtube.com and player.twitch.tv URLs That the Embeds Can Play videojs/v10#3139 and need that fix before release (see the checklist).

  • Custom controls with Video.js v10 skins or UI components. Wrap ReactPlayer in v10's VideoPlayer and add a skin or individual controls; they drive files, streams and embeds alike. The README's Media Chrome example is replaced with these:

    <VideoPlayer>
      <VideoSkin style={{ aspectRatio: '16 / 9' }}>
        <ReactPlayer src={src} width="100%" height="100%" />
      </VideoSkin>
    </VideoPlayer>
  • Mux Data and Google Cast via v10 extensions. ReactPlayer's media attaches to a surrounding v10 player, so v10's extensions follow it whatever the source:

    <VideoPlayer>
      <ReactPlayer src={src} controls />
      <MuxData />
      <GoogleCast />
      <CastButton />
    </VideoPlayer>

    The v10 media components did this already; HtmlPlayer now registers its native <video>/<audio> too (useMediaAttach, like v10's Video). The README and migration guide document it.

  • Newly recognized sources: localized Spotify URLs (open.spotify.com/intl-de/track/...), spotify: URIs, youtube/<id> and vimeo/<id> shorthands, and .flac files.

Other changes

  • MIGRATING.md has a v3 → v4 section covering every breaking change, plus how to add Mux Data and Google Cast, and the README links to it.
  • ReactPlayer finds the active player with Array#find/#some, and VideoElementProps extends React's own VideoHTMLAttributes instead of redeclaring it.
  • The demo targets es2020, because @wistia/wistia-player contains BigInt literals.
  • Removed the unused cloudflare-video-element dependency.
  • Tooling migrated to pnpm and Vite+ (a424322), replacing npm, the custom scripts/builder/scripts/tester esbuild wrappers, Biome, c8 and zora/sinon:
    • build: vp pack (unbundled ESM, es2019) with tsgo declarations
    • demo: a Vite app in examples/react that resolves react-player to src
    • test: vp test (Vitest); tests are named *.test.* and run on Node 22 without flags
    • lint/format: Oxlint and Oxfmt via vp check, with a vp staged pre-commit hook; the codebase is formatted with Oxfmt
    • typecheck: tsgo --noEmit
    • CI/CD: pnpm, Node from .node-version, plus a typecheck step

Testing

  • pnpm lint, pnpm typecheck, pnpm test, pnpm build and pnpm build:demo pass, on each commit.
  • New test/MediaPlayer.test.tsx (source mapping, prop/callback pass-through, ref as the element and mediaRef as the media via a fake adapter, playback props driven through the media) and test/sources.test.tsx (the mappings), plus tests that config never reaches a native <video>, that a native player hands the same element to ref and mediaRef, that ReactPlayer's media attaches to a surrounding v10 player, and that a v10 PlayButton plays it (both fail without the HtmlPlayer change), and that mediaRef is the same hls.js adapter as useMedia(), with engine, for a stream.
  • Checked in headless Chrome against @videojs/*@10.0.1 and live sources:
  • Checked end to end in jsdom with the real YouTubeVideo inside ReactPlayer's Player: the media is the YouTubeAdapter with target set to the <iframe>, volume/playbackRate are applied, config.youtube reaches the embed URL, and onReady/onStart/onPlay fire. (Checked before config was re-keyed and before the ref/mediaRef split, when that adapter was on ref.)
  • Compared player selection before and after resolveAdapterType across ~50 URLs; the differences are the URLs described under New, each checked against the v3 player elements' own parsers. The canPlay(key) refactor picks the same player as the previous commit for every URL.
  • Wistia, Spotify, Twitch and TikTok were only checked for switching away to an MP4, not for playback; see the checklist below.

Before merging

Replace the *-video-element and @mux/mux-player-react dependencies with the @videojs/react media
components (@videojs/react/media/*). Each player is wrapped by createMediaPlayer, which maps src
and the player's config onto the v10 source prop and passes ReactPlayer's ref as mediaRef, so
ref.current is the playable media for every player (the embed iframe is ref.current.target).

Breaking changes:
- React 17 is no longer supported; the react peer range is now ^18 || ^19.
- config keys are now the v10 engine options (config.hls -> hls.js config, config.dash -> dash.js
  settings, config.mux -> MuxSource options, etc.).
- The ref for embed players (YouTube, Vimeo, Wistia, Spotify, Twitch, TikTok) is the playback
  adapter rather than a custom element. The media API is unchanged; use ref.current.target for
  the DOM node.

BREAKING CHANGE: requires React 18+, config keys are now the Video.js v10 engine options, and the
ref for embed players is the playback adapter (DOM node at ref.current.target).
@luwes
luwes force-pushed the feat/videojs-v10-media branch from 1ab7c58 to 4ab26b9 Compare October 2, 2026 00:09
@luwes luwes changed the title feat: use Video.js v10 media components feat!: use Video.js v10 media components Oct 2, 2026
@luwes
luwes marked this pull request as ready for review October 2, 2026 00:09
@luwes
luwes added this pull request to stack #2051 October 2, 2026 00:09
@luwes
luwes force-pushed the feat/videojs-v10-media branch from c61bb46 to 4ab26b9 Compare October 2, 2026 00:33
config now mirrors v10's source.engine: engine options are keyed by engine name (hlsJs, dashJs, nativeHls, youtube, vimeo, spotify, twitch, tiktok) and every player gets the same object as source.engine, reading only its own key. This keeps one prop for configuring every player while dropping the per-player config lookup and engineSource(key). mux and wistia keep their own keys for their non-engine source options; Mux also reads hlsJs and nativeHls.

Also simplifies the surrounding code:
- MediaPlayer no longer memoizes source, since v10 compares sources structurally.
- ReactPlayer finds the active player with Array#find/#some.
- PlayerEntry drops the unused name field.
- VideoElementProps extends React's VideoHTMLAttributes instead of redeclaring it.
- HtmlPlayer no longer passes config to the native element.

Breaking changes:
- config.hls is now config.hlsJs and config.dash is now config.dashJs.
- config.mux no longer takes engine; use config.hlsJs / config.nativeHls, which Mux reads too.
- Custom players receive the whole config object instead of their own key.
- PlayerEntry no longer has a name field.

BREAKING CHANGE: config is keyed by Video.js v10 engine name (config.hls -> config.hlsJs, config.dash -> config.dashJs), custom players receive the whole config, and PlayerEntry drops name.
luwes added 2 commits October 1, 2026 18:41
Replace npm, the custom scripts/builder and scripts/tester esbuild wrappers, Biome, c8 and zora/sinon with pnpm and Vite+ (vp):

- build: vp pack (unbundled ESM, target es2019) with tsgo declarations
- demo: Vite app in examples/react, resolving react-player to src
- test: vp test (Vitest); tests converted from zora/sinon and named *.test.*
- lint/format: Oxlint and Oxfmt via vp check, with a vp staged pre-commit hook
- typecheck: tsgo --noEmit
- CI/CD: pnpm, Node from .node-version, plus a typecheck step

Also formats the codebase with Oxfmt, and indexes Player's props by `keyof typeof props` so it typechecks under tsgo.
Follow the Video.js v10 media contract: ReactPlayer's `ref` receives the rendered element (the `<video>`/`<audio>`, the embed's `<iframe>`, or `<wistia-player>`), and the new `mediaRef` prop receives the HTMLMediaElement-compatible object that plays the media (the element itself for files and streams, the playback adapter for embeds).

- Player drives playing/volume/playbackRate/pip through the media, composing its own ref with `mediaRef` via v10's useComposedRefs. This also stops calling useCallback after an early return.
- MediaPlayer passes `ref` and `mediaRef` straight through to the v10 component.
- HtmlPlayer hands the native element to both refs.
- The demo and README use `mediaRef` for instance methods.

Breaking changes:
- `ref` is the DOM element. Use `mediaRef` for the media API (`play()`, `currentTime`, ...), which for embeds was previously on `ref`.
- Custom players must forward `ref` to their element and hand the media to the `mediaRef` prop.

BREAKING CHANGE: ReactPlayer's `ref` points to the DOM element; the media API moves to the new `mediaRef` prop, and custom players must accept `mediaRef`.
luwes added 2 commits October 1, 2026 18:45
Replace the URL regexes in patterns.ts with resolveAdapterType and resolveMimeType from @videojs/react, so ReactPlayer recognizes the same sources as the Video.js v10 media components it renders.

- YouTube, Vimeo, Wistia, Mux, Spotify, Twitch, TikTok and DASH sources are matched by adapter type.
- Mux stream URLs ending in .m3u8 still play with hls.js.
- File playback keeps AUDIO_EXTENSIONS and VIDEO_EXTENSIONS as a fallback for extensions Video.js does not recognize (m4v, m4b, weba, oga, spx, ...).

Newly recognized sources:
- localized Spotify URLs (open.spotify.com/intl-de/track/...) and spotify: URIs
- youtube/<id> and vimeo/<id> shorthands
- .flac files

Breaking changes:
- react-player/patterns no longer exports HLS_EXTENSIONS, DASH_EXTENSIONS, MATCH_URL_MUX, MATCH_URL_YOUTUBE, MATCH_URL_VIMEO, MATCH_URL_WISTIA, MATCH_URL_SPOTIFY, MATCH_URL_TWITCH or MATCH_URL_TIKTOK. Use resolveAdapterType from @videojs/react instead.
- These URLs are no longer matched to a service player and fall back to the HTML player: youtube.com/user/..., music.youtube.com, vimeo.com/channels/..., vimeo.com/showcase/... and player.twitch.tv/?video=...

BREAKING CHANGE: URL pattern exports were removed from react-player/patterns in favor of resolveAdapterType from @videojs/react, and some URLs now resolve to a different player.
Every built-in player key is a Video.js adapter type (plus html), so patterns.ts resolves a URL to its player key once and canPlay(key) compares against it, instead of a table of per-player predicates. The Mux .m3u8 -> hls.js rule and the extension fallback for the HTML player are unchanged; player selection is identical for every URL compared. Also removes canPlayFile, which handled arrays of sources that src no longer accepts.

Breaking changes:
- react-player/patterns exports canPlay as a function of the player key: canPlay.youtube(url) becomes canPlay('youtube')(url).

BREAKING CHANGE: canPlay in react-player/patterns is now canPlay(key)(url) instead of canPlay[key](url).
Covers React 18, ref vs mediaRef (and api -> engine for embed SDKs), the engine-keyed config and its changed option shapes, Mux moving from Mux Player to MuxVideo, Media Chrome with embeds, URL matching via resolveAdapterType, the react-player/patterns changes, the custom player contract and the dropped *-video-element dependencies. The README links to it and drops the Mux Player-only --controls variable from the Media Chrome example.
luwes added 2 commits October 1, 2026 19:15
TikTok options are 0 | 1 in Video.js v10 instead of booleans.
Of the URLs v3 matched and resolveAdapterType does not, only music.youtube.com/watch?v= and player.twitch.tv/?video= actually played in v3; the YouTube user, Vimeo channel/showcase and player.twitch.tv URLs with Twitch's own params were matched but failed to parse in the v3 elements too. The two that did play are tracked upstream in videojs/v10#3139, to be picked up with the @videojs bump before release.
luwes added 2 commits October 1, 2026 19:27
The v10 media components register with a surrounding v10 Player on their own; HtmlPlayer's plain <video>/<audio> did not, so v10 extensions such as Mux Data and Google Cast never saw file sources. Register it with useMediaAttach, like v10's own Video component.
Mux Player sent Mux Data automatically. In v4, analytics and casting come from v10 extensions: wrap ReactPlayer in VideoPlayer and add MuxData, GoogleCast and CastButton. Covers the envKey rules, casting embeds, controlling playback while casting, and sharing one @videojs/react copy.
luwes added 2 commits October 1, 2026 23:10
A v10 PlayButton inside VideoPlayer plays ReactPlayer's native media. Fails without HtmlPlayer attaching to the surrounding player.
Replace the Media Chrome example with VideoPlayer + VideoSkin and VideoPlayer + UI components examples, which work with every source, embeds included. The migration guide points Mux and Media Chrome users to them and keeps a note that Media Chrome can't control embeds in v4.
The migration guide said the hls.js and dash.js instances were no longer exposed. They are, as `.engine` on the Video.js adapter, but for HLS, DASH and Mux `mediaRef` is the <video> element, so the adapter is reached with useMedia() inside a v10 player. Document that, and add a test that useMedia() returns the hls.js adapter (with `engine`) for a stream ReactPlayer renders.
A jscodeshift transform in codemods/v4.ts that renames ref to mediaRef, migrates config keys and values, rewrites react-player/patterns usage and removes name from custom player entries. Changes that need a decision are left as TODO(react-player v4) comments. Run it with `pnpm codemod:v4 <dir>`, or from its URL as described in MIGRATING.md.

@decepulis decepulis left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huge. Excited for how this sets up react-player to take advantage of everything Video.js has to offer!

10.0.1 brings the fixes ReactPlayer was waiting on:

- youtube-video: unmounting YouTube no longer crashes React with removeChild, e.g. when src changes from YouTube to another player (videojs/v10#3153, #3154)
- react: autoPlay reaches the embed players, and the React 18 callback-ref warning is gone (videojs/v10#3136)
- react: mediaRef is the playback adapter for HLS, DASH and Mux too, so the engine is mediaRef.current.engine for every player (videojs/v10#3141)
- mux-video: extension-less stream.mux.com URLs parse to a playback ID (videojs/v10#3143)

Updates the hls.js adapter test, the migration guide, README and codemod for the mediaRef change: the codemod no longer flags .engine, and the guide drops the useMedia() workaround for streams.

This branch was successfully deployed

1 active deployment
github-preview — edadf50b Deployed Oct 3, 2026 by luwes via deploy-preview #287
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