From ba64d950014aa8c0d962129b5ab4e240d39fc8e1 Mon Sep 17 00:00:00 2001 From: Wesley Luyten Date: Thu, 1 Oct 2026 17:06:06 -0700 Subject: [PATCH 1/2] feat!: detect players with Video.js resolveAdapterType 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/ and vimeo/ 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. --- src/patterns.ts | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/patterns.ts b/src/patterns.ts index 395bcdc3..da09a8e9 100644 --- a/src/patterns.ts +++ b/src/patterns.ts @@ -1,16 +1,7 @@ +import { resolveAdapterType, resolveMimeType, type AdapterType } from '@videojs/react'; + 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)(#t=[,\d+]+)?($|\?)/i; -export const HLS_EXTENSIONS = /\.(m3u8)($|\?)/i; -export const DASH_EXTENSIONS = /\.(mpd)($|\?)/i; -// Match Mux m3u8 URLs without the extension so users can use hls.js with Mux by adding the `.m3u8` extension. https://regexr.com/7um5f -export const MATCH_URL_MUX = /stream\.mux\.com\/(?!\w+\.m3u8)(\w+)/; -export const MATCH_URL_YOUTUBE = - /(?:youtu\.be\/|youtube(?:-nocookie|education)?\.com\/(?:embed\/|v\/|watch\/|watch\?v=|watch\?.+&v=|shorts\/|live\/))((\w|-){11})|youtube\.com\/playlist\?list=|youtube\.com\/user\//; -export const MATCH_URL_VIMEO = /vimeo\.com\/(?!progressive_redirect).+/; -export const MATCH_URL_WISTIA = /(?:wistia\.(?:com|net)|wi\.st)\/(?:medias|embed)\/(?:iframe\/)?([^?]+)/; -export const MATCH_URL_SPOTIFY = /open\.spotify\.com\/(\w+)\/(\w+)/i; -export const MATCH_URL_TWITCH = /(?:www\.|go\.)?twitch\.tv\/([a-zA-Z0-9_]+|(videos?\/|\?video=)\d+)($|\?)/; -export const MATCH_URL_TIKTOK = /tiktok\.com\/(?:player\/v1\/|share\/video\/|@[^/]+\/video\/)([0-9]+)/; const canPlayFile = (url: string, test: (u: string) => boolean) => { if (Array.isArray(url)) { @@ -27,15 +18,28 @@ const canPlayFile = (url: string, test: (u: string) => boolean) => { return test(url); }; +const adapterType = (url: string) => (typeof url === 'string' ? resolveAdapterType(url) : null); + +const isAdapter = (type: AdapterType) => (url: string) => adapterType(url) === type; + +// Mux stream URLs with the `.m3u8` extension resolve to `mux`, but are played with hls.js so +// users can opt into it by adding the extension. +const isMuxHls = (url: string) => adapterType(url) === 'mux' && resolveMimeType(url) === 'application/x-mpegurl'; + export const canPlay = { - html: (url: string) => canPlayFile(url, (u: string) => AUDIO_EXTENSIONS.test(u) || VIDEO_EXTENSIONS.test(u)), - hls: (url: string) => canPlayFile(url, (u: string) => HLS_EXTENSIONS.test(u)), - dash: (url: string) => canPlayFile(url, (u: string) => DASH_EXTENSIONS.test(u)), - mux: (url: string) => MATCH_URL_MUX.test(url), - youtube: (url: string) => MATCH_URL_YOUTUBE.test(url), - vimeo: (url: string) => MATCH_URL_VIMEO.test(url) && !VIDEO_EXTENSIONS.test(url) && !HLS_EXTENSIONS.test(url), - wistia: (url: string) => MATCH_URL_WISTIA.test(url), - spotify: (url: string) => MATCH_URL_SPOTIFY.test(url), - twitch: (url: string) => MATCH_URL_TWITCH.test(url), - tiktok: (url: string) => MATCH_URL_TIKTOK.test(url), + html: (url: string) => + canPlayFile(url, (u: string) => { + const type = adapterType(u); + // Fall back to the extension patterns for formats Video.js doesn't recognize (e.g. m4v, weba, oga). + return type === 'video' || type === 'audio' || AUDIO_EXTENSIONS.test(u) || VIDEO_EXTENSIONS.test(u); + }), + hls: (url: string) => canPlayFile(url, (u: string) => isAdapter('hls')(u) || isMuxHls(u)), + dash: (url: string) => canPlayFile(url, isAdapter('dash')), + mux: (url: string) => isAdapter('mux')(url) && !isMuxHls(url), + youtube: isAdapter('youtube'), + vimeo: isAdapter('vimeo'), + wistia: isAdapter('wistia'), + spotify: isAdapter('spotify'), + twitch: isAdapter('twitch'), + tiktok: isAdapter('tiktok'), }; From ef45ca49036f92b24854ac09c5c1404fd125469e Mon Sep 17 00:00:00 2001 From: Wesley Luyten Date: Thu, 1 Oct 2026 17:55:44 -0700 Subject: [PATCH 2/2] refactor!: derive canPlay from a single adapter-type lookup 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). --- src/patterns.ts | 51 +++++++++++++------------------------------------ src/players.ts | 20 +++++++++---------- 2 files changed, 23 insertions(+), 48 deletions(-) diff --git a/src/patterns.ts b/src/patterns.ts index da09a8e9..8e7502c9 100644 --- a/src/patterns.ts +++ b/src/patterns.ts @@ -3,43 +3,18 @@ import { resolveAdapterType, resolveMimeType, type AdapterType } from '@videojs/ 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)(#t=[,\d+]+)?($|\?)/i; -const canPlayFile = (url: string, test: (u: string) => boolean) => { - if (Array.isArray(url)) { - for (const item of url) { - if (typeof item === 'string' && canPlayFile(item, test)) { - return true; - } - if (canPlayFile(item.src, test)) { - return true; - } - } - return false; - } - return test(url); -}; - -const adapterType = (url: string) => (typeof url === 'string' ? resolveAdapterType(url) : null); - -const isAdapter = (type: AdapterType) => (url: string) => adapterType(url) === type; +export type PlayerKey = Exclude | 'html'; -// Mux stream URLs with the `.m3u8` extension resolve to `mux`, but are played with hls.js so -// users can opt into it by adding the extension. -const isMuxHls = (url: string) => adapterType(url) === 'mux' && resolveMimeType(url) === 'application/x-mpegurl'; - -export const canPlay = { - html: (url: string) => - canPlayFile(url, (u: string) => { - const type = adapterType(u); - // Fall back to the extension patterns for formats Video.js doesn't recognize (e.g. m4v, weba, oga). - return type === 'video' || type === 'audio' || AUDIO_EXTENSIONS.test(u) || VIDEO_EXTENSIONS.test(u); - }), - hls: (url: string) => canPlayFile(url, (u: string) => isAdapter('hls')(u) || isMuxHls(u)), - dash: (url: string) => canPlayFile(url, isAdapter('dash')), - mux: (url: string) => isAdapter('mux')(url) && !isMuxHls(url), - youtube: isAdapter('youtube'), - vimeo: isAdapter('vimeo'), - wistia: isAdapter('wistia'), - spotify: isAdapter('spotify'), - twitch: isAdapter('twitch'), - tiktok: isAdapter('tiktok'), +/** The key of the built-in player that plays `url`, from its Video.js adapter type. */ +const resolvePlayerKey = (url: string): PlayerKey | null => { + const type = resolveAdapterType(url); + // Mux stream URLs with the `.m3u8` extension resolve to `mux`, but are played with hls.js so + // users can opt into it by adding the extension. + if (type === 'mux' && resolveMimeType(url) === 'application/x-mpegurl') return 'hls'; + if (type === 'video' || type === 'audio') return 'html'; + // The extension patterns cover formats Video.js doesn't recognize (e.g. m4v, weba, oga). + if (!type && (AUDIO_EXTENSIONS.test(url) || VIDEO_EXTENSIONS.test(url))) return 'html'; + return type; }; + +export const canPlay = (key: PlayerKey) => (url: string) => resolvePlayerKey(url) === key; diff --git a/src/players.ts b/src/players.ts index af0c196b..25d9883e 100644 --- a/src/players.ts +++ b/src/players.ts @@ -24,7 +24,7 @@ const lazyMedia =

( const Players: PlayerEntry[] = [ { key: 'hls', - canPlay: canPlay.hls, + canPlay: canPlay('hls'), canEnablePIP: () => true, player: lazyMedia(() => import(/* webpackChunkName: 'reactPlayerHls' */ '@videojs/react/media/hlsjs-video').then((m) => m.HlsJsVideo) @@ -32,7 +32,7 @@ const Players: PlayerEntry[] = [ }, { key: 'dash', - canPlay: canPlay.dash, + canPlay: canPlay('dash'), canEnablePIP: () => true, player: lazyMedia(() => import(/* webpackChunkName: 'reactPlayerDash' */ '@videojs/react/media/dash-video').then((m) => m.DashVideo) @@ -40,7 +40,7 @@ const Players: PlayerEntry[] = [ }, { key: 'mux', - canPlay: canPlay.mux, + canPlay: canPlay('mux'), canEnablePIP: () => true, player: lazyMedia( () => import(/* webpackChunkName: 'reactPlayerMux' */ '@videojs/react/media/mux-video').then((m) => m.MuxVideo), @@ -49,7 +49,7 @@ const Players: PlayerEntry[] = [ }, { key: 'youtube', - canPlay: canPlay.youtube, + canPlay: canPlay('youtube'), player: lazyMedia(() => import(/* webpackChunkName: 'reactPlayerYouTube' */ '@videojs/react/media/youtube-video').then( (m) => m.YouTubeVideo @@ -58,14 +58,14 @@ const Players: PlayerEntry[] = [ }, { key: 'vimeo', - canPlay: canPlay.vimeo, + canPlay: canPlay('vimeo'), player: lazyMedia(() => import(/* webpackChunkName: 'reactPlayerVimeo' */ '@videojs/react/media/vimeo-video').then((m) => m.VimeoVideo) ), }, { key: 'wistia', - canPlay: canPlay.wistia, + canPlay: canPlay('wistia'), canEnablePIP: () => true, player: lazyMedia( () => @@ -77,7 +77,7 @@ const Players: PlayerEntry[] = [ }, { key: 'spotify', - canPlay: canPlay.spotify, + canPlay: canPlay('spotify'), canEnablePIP: () => false, player: lazyMedia(() => import(/* webpackChunkName: 'reactPlayerSpotify' */ '@videojs/react/media/spotify-audio').then( @@ -87,7 +87,7 @@ const Players: PlayerEntry[] = [ }, { key: 'twitch', - canPlay: canPlay.twitch, + canPlay: canPlay('twitch'), canEnablePIP: () => false, player: lazyMedia(() => import(/* webpackChunkName: 'reactPlayerTwitch' */ '@videojs/react/media/twitch-video').then((m) => m.TwitchVideo) @@ -95,7 +95,7 @@ const Players: PlayerEntry[] = [ }, { key: 'tiktok', - canPlay: canPlay.tiktok, + canPlay: canPlay('tiktok'), canEnablePIP: () => false, player: lazyMedia(() => import(/* webpackChunkName: 'reactPlayerTiktok' */ '@videojs/react/media/tiktok-video').then((m) => m.TikTokVideo) @@ -103,7 +103,7 @@ const Players: PlayerEntry[] = [ }, { key: 'html', - canPlay: canPlay.html, + canPlay: canPlay('html'), canEnablePIP: () => true, player: HtmlPlayer, },