Skip to content
Breaking changes

v10.0.0-beta.27

Compare changes on GitHub

Five new places to play from, DRM on every HLS path, and a long stabilization pass over the API you write against. Beta.27 is the largest release of the beta so far, and the one with the most to update.

Breaking changes

There is a lot here. Read the list before you upgrade.

  • Source and engine options live on source (#1903, #2014). The untyped config bag is retired. Everything describing a source sits on source, and engine settings are namespaced under source.engine by engine. el.config = { preferPlayback: 'native', hlsJs: { maxBufferLength: 60 } } becomes el.source = { src, preferPlayback: 'native', engine: { hlsJs: { maxBufferLength: 60 } } }. The keys are hlsJs, nativeHls, dashJs, vimeo, and youtube. <dash-video> gains working engine configuration for the first time.
  • Mux derived URLs are read, not written (#1903). source.thumbnail is now source.poster, with a matching poster-time attribute on <mux-video>. The derived poster and storyboard URLs come from the read-only contentData record instead of el.thumbnail and el.storyboard.
  • SimpleHls* is now Hls* (#2096). <simple-hls-video> becomes <hls-video>, SimpleHlsVideo becomes HlsVideo, and SimpleHlsAudioOnly becomes HlsAudio. Update the tag, the identifier, and the subpath: @videojs/html/media/hls-video, @videojs/react/media/hls-audio, @videojs/spf/hls-video. There is no compatibility shim. The <hls-video> tag was freed by the beta.25 rename of the hls.js element to <hlsjs-video>, which is unchanged.
  • SPF background video is hls-background-video (#2097, #2062). The @videojs/spf/background-video entry point is gone; use @videojs/spf/hls-background-video. BackgroundVideoMediaElement is now HlsBackgroundVideoMediaElement, and its client-side maxResolution property is removed — narrow the rendition set with a delivery parameter on the URL (?max_resolution=720p) so the excluded renditions never reach the manifest. <mux-background-video> and MuxBackgroundVideo alias the new names. Neither background-video adapter declares loop, muted, autoplay, or preload any more; attach writes them onto the element instead.
  • SPF Medias moved into @videojs/spf (#2033). @videojs/media/dom/simple-hls and @videojs/media/dom/simple-hls-audio-only stop resolving. Import the Medias from @videojs/spf/hls-video and @videojs/spf/hls-audio, and createBackgroundVideoEngine from @videojs/spf/hls. Engines share one entry per format family; adapters get one each.
  • React createPlayer() returns Player (#2116). const { Provider } = createPlayer(…) becomes const { Player } = createPlayer(…), and ProviderProps becomes PlayerProps. Presets now export their player directly, so most apps can skip createPlayer entirely.
  • Containers are off the createPlayer() result (#2154). Import Container from @videojs/react and ContainerMixin from @videojs/html instead of destructuring them.
  • Menus lost their view machinery (#2029). Settings, i18n, value outlets, back-item types, and the viewport and view-stack abstractions are out of the base HTML and React menu APIs. Submenus are sibling nested menus opened by a trigger, back rows are ordinary items, and skins compose the captions, quality, playback rate, and audio track submenus themselves.
  • Radio groups expose options (#2047). The quality, captions, playback rate, and audio track cores return one ordered options list — normalized values, labels, disabled state, and availability — in place of tracks, rates, and renditions. Quality’s Auto and captions’ Off are entries in that list rather than special cases beside it.
  • Input indicators are separate components (#2046). The inputFeedback island and bubble styling contract is replaced by component-named ones: statusIndicator, volumeIndicator, seekIndicator, and the layout-only inputIndicatorOverlay. In authored CSS, .media-input-feedback becomes .media-input-indicator-overlay, .media-input-feedback-island--volume becomes .media-volume-indicator, and the bubble splits into .media-status-indicator--playback and .media-seek-indicator.
  • Skin custom properties settled (#2094). --media-color-primary is now --media-accent-color, with --media-accent-text-color beside it as an escape hatch. The skin root is named container rather than root, dark mode runs through light-dark() so a parent’s color-scheme steers it, and keyframes.css, properties.css, and tailwind.css are gone. Skin-local variables drop the --media- prefix; that prefix now marks the public customization API.
  • Time slider chapters need a template (#2204). Separate fallback children are gone. Supply an HTML <template> or a React renderChapter, and drop any duplicated fallback track. When there are no cues, core returns one cue-less range spanning the whole slider, so there is nothing left to fall back to.
  • Some low-level positioning exports were removed (#1904). getAnchorNameStyle, getPopoverCSSVars, PopoverCSSVarKey, and TooltipCSSVarKey are gone from @videojs/core/dom, and ManualOffsets is now PositioningOffsets.

More places to play from

YouTube, Cloudflare Stream, Spotify, TikTok, and Twitch all became media hosts this release (#1853, #2168, #2169, #2170, #2171). Each is a port of its muxinc/media-elements counterpart, reshaped into the same Media host contract Vimeo already used, so <youtube-video> or <TwitchVideo> composes with the player, the store, and the skins like any other element. Embed parameters go under source.engine, typed and spelled the way each provider spells them.

They are not identical underneath, and the hosts say so rather than pretending. YouTube takes watch, youtu.be, embed, shorts, and live URLs, bare video ids, playlists, t=1h30m15s start times, and the -nocookie host, and derives timeupdate from a poll because the iframe API emits none. Spotify is audio-only with no volume, mute, or playback rate at all, so its rate feature stays off instead of showing a dead control. TikTok has no SDK — the postMessage protocol is the API. Twitch is the only one that plays live channels, and needs the framing hostnames its embed demands.

Two fixes make every embed better behaved. Vimeo and YouTube used to build their embed during attach(), which for a custom element runs in the constructor, before Svelte, Vue, or Angular has set src; the build is now deferred until a source resolves (#2118). And clearing a source announces emptied on all six hosts, so the store stops reporting the previous video’s duration, buffered range, and stream type (#2217).

Start from the media sources concept for HTML and React.

DRM and DASH quality

Protected content plays on every HLS path. source.drm names the license servers once, keyed by EME key system id, and the same object works on NativeHlsVideo, HlsJsVideo, MuxVideo, and MuxAudio (#2014). hls.js hands it to its own drmSystems with EME switched on; for the browser’s native HLS, Video.js answers the element’s key requests itself. For Mux, a signed DRM token is the whole configuration — the FairPlay, Widevine, and PlayReady license servers are derived from the playback ID, and a token not scoped to DRM is ignored rather than sent (#1948). Widevine asks for HW_SECURE_ALL first and falls back to whatever robustness the browser offers, so L1-restricted content plays where it can. See the elements for HTML and React.

DASH picked up the standard video renditions API (#2060). videoRenditions was always empty for <dash-video>, which left the quality menu unavailable; representations are now mirrored onto it, selecting one pins that representation and turns dash.js bitrate switching off, and clearing the selection hands switching back. Read the quality feature for HTML and React, or the element for HTML and React.

The menu system got smaller (#2029). Base menus now do generic menu behavior and nothing else: no coordinator, no viewport, no view stack, no settings controller. Submenus are sibling nested menus, opened by a trigger and closed by Escape, and each skin composes its own captions, quality, playback rate, and audio track panels from menu and radio group primitives. Transitions reuse the popover lifecycle rather than a separate machine. It’s a breaking change; see Breaking changes.

Composing that way needs the primitives to exist in both frameworks, so React gained the three radio groups it was missing: audio track (#2124), captions (#2127), and quality (#2132). Each is hook-backed with a required renderItem, so the group owns selection, labels, and ARIA while you own the markup. The shipped skins use them (#2125, #2128), and all four are documented (#2126, #2129, #2136, #2139): audio track for HTML and React, captions for HTML and React, quality for HTML and React, and playback rate for HTML and React.

Controls with nothing to offer now get out of the way. A radio group whose options are unavailable — one audio track, no captions, a single rendition — reports hidden, sets the native hidden attribute in HTML, and returns null in React (#2069). The volume slider follows the same rule and refuses pointer, keyboard, and wheel input while volume control is unavailable (#2072). Menu roving focus skips hidden items while keeping disabled-but-visible ones reachable (#2071), and keyboard navigation no longer strands you when you back out of a submenu (#2214). The E2E suite runs axe against WCAG A and AA on the loaded player and on open menus, and the React issues it found are fixed (#2209). Details in the menu reference for HTML and React, and the accessibility concept for HTML and React.

Chapters on the time slider

The chaptered time slider is rebuilt on independently rendered HTML and CSS segments instead of an SVG mask (#2043), with normalized gaps, per-segment track, buffer and fill state, and chapter titles in the scrub preview. Default and Minimal render it in both the CSS and Tailwind sources. A follow-up removed the fallback concept entirely, so one template or renderChapter draws every range and a source with no cues gets a single full-width one (#2204) — see Breaking changes. The reference has the contract for HTML and React.

Skins, motion, and polish

The skins got a CSS clean-up that doubles as an API stabilization (#2094): --media-accent-color and --media-accent-text-color replace --media-color-primary, light-dark() replaces the dark-mode media query, and three files disappeared. Motion across controls, menus, sliders, and status indicators was reworked to avoid remounting and discrete display changes, with hover motion limited to fine pointers and reduced-motion respected (#2208). Submenu transitions (#2130, #2159) and SVG icon animation (#2216) were tuned alongside. Start from the skin customization guide for HTML and React.

A batch of fixes rounds out the UI work:

  • Controls stay visible while a menu is open or a slider is being dragged, and the idle timer restarts when the interaction ends (#1900).
  • The Settings button has its tooltip back, and it steps aside while the menu is open (#1915).
  • Popups are steadier: default-open React popups measure a live trigger (#1931), controls that share a menu and tooltip trigger keep the right CSS anchor (#1933), and popup coordination is scoped to the visual container so a playlist or transcript beside the player no longer joins the skin’s popup group (#2083).
  • Scrollable menus stop snapping back in Firefox when you move the pointer up an option (#2100).
  • The live-edge dot stays red instead of being grayed out with the rest of the disabled live button (#1921).
  • Ejected HTML players survive light DOM upgrading after the UI bundle runs, so volume and playback-rate popovers initialize without throwing (#2031).
  • Cast and speech icons are flat paths rather than mask: url(#a), which was risky with duplicate ids and is now also smaller (#2027); the quality icon reads HD (#2147).
  • Smaller passes on the default skin: a longer threshold before the time display hides (#2030) and pixels as the default scale unit (#2054).

Preset-first React

createPlayer() used to be the front door. Now each preset exports its player directly — VideoPlayer, AudioPlayer, LiveVideoPlayer, LiveAudioPlayer, BackgroundVideoPlayer — each with its own typed usePlayer, and Next.js Server Components can render them (#2116). createPlayer() is still there for custom feature compositions, and its result is now Player rather than Provider. Layout primitives came off that result too: import Container and ContainerMixin from the package root (#2154). The guides, references, demos, and generated snippets were rewritten preset-first (#2152). Both are breaking; see Breaking changes. The references cover it for HTML and React, with containers for HTML and React.

A player title can also come from two places now. Feature state resolves user-supplied values against media-supplied ones, so a content-title attribute or contentTitle prop overrides what the media reports, while defaultContentTitle yields to it (#1946, #1996).

Playback fixes

hls.js took most of the attention. Pressing play with preload="metadata" while the first init segment was in flight put it into an unbounded request-and-abort loop that never started playback; raising the buffer limits no longer restarts the load (#2103). A <track> child that loaded before hls.js got a source ended up selected and empty, because hls.js clears every text track’s cues on attach and no browser reparses a <track>; sideloaded cues and their mode now survive (#2119). And a quality change no longer snaps the viewer’s audio language back to the manifest default when the audio group switches (#2120).

Elsewhere: toggling captions on a source with several subtitle tracks turned all of them on at once, and now restores just the one you last had showing (#2102). Mux Data assumed media.engine was an hls.js instance, so DASH was monitored through the wrong integration and lost every engine-level signal; engines are matched by shape and routed accordingly (#2040). @videojs/store built an AbortController at module scope, which Cloudflare Workers and Deno Deploy disallow outside a request handler — it’s lazy now (#2099). And defaults() enumerated the object it was given, which for every HTML UI element was the element itself: 342 enumerable WebIDL accessors read per update, forcing layout and subtree serialization each time. It reads the declared keys instead (#2140).

Self-hosting

CDN bundles no longer reach out to a public CDN at load. The i18n registry was statically imported from a hard-coded jsDelivr URL, which broke any mirrored copy of cdn/; the registry keeps its layers on globalThis and the bundles import it relatively, so a build resolves entirely within itself (#2121). On top of that, every @videojs/html release now carries videojs-html-<version>.zip and .tar.gz with a SHA256SUMS file — roughly 700 KB of production bundles, no sourcemaps — which gives Composer-based projects like Drupal a browser-ready player again (#2122). The release run verifies the bundles are self-contained before publishing. See the self-hosting guide for HTML and React.

Under the hood: SPF

Live HLS re-landed on the presentation-timeline model (#1884). A per-source anchor frozen from EXT-X-PROGRAM-DATE-TIME settles segment placement at parse time, live windows re-place from PDT on every reload, and seekable is the intersection of the selected audio and video windows mirrored into MediaSource.setLiveSeekableRange. Playback starts at the live edge minus HOLD-BACK. Scope is sliding-window live consumed through the live skin; DVR and LL-HLS are not in it.

A source the engine can’t play now fails visibly instead of stalling in silence: producers append SVTA-coded conditions, the adapter decides which are fatal, and core maps the surfaced code to localized copy (#1936).

<mux-video> and <mux-audio> can run on SPF instead of hls.js, chosen by import path — @videojs/html/media/mux-video/spf sits beside the hls.js flavor, with the same element surface and the same React components (#2045). The SPF <mux-audio> fetches only audio renditions, where the hls.js flavor downloads video it never plays. The packaging behind all this moved: SPF Medias live in @videojs/spf rather than @videojs/media (#2033), <mux-background-video> landed over the SPF background-video engine (#2062), and the two identical background adapters collapsed into one named hls-background-video (#2097). All three are breaking; see Breaking changes.

Skins from source

Work started on generating the shipped skins from one canonical source. A private compiler package handles parsing, import rewriting, JSX transforms, and deterministic multi-entry output (#1981, #1990); core gained a constrained, target-neutral JSX runtime that rejects platform intrinsics (#1986, #1995); and @videojs/skins gained a canonical source tree beside its current build inputs (#1989). The canonical components landed piece by piece — play button, default video controls, volume controls, container, poster, and overlay (#1991, #1992, #1993, #2179) — along with Tailwind styles, HTML and React compiler plugins, a registry catalog adapter, and the pipeline that projects all of it into readable React, HTML, scoped CSS, and a shadcn-compatible registry (#2005, #2006, #2007, #2008, #2021). Nothing here changes what the packages ship yet — the generated output is staged until parity is done.

Docs and i18n

Lithuanian joined the built-in locale packs, hand-written against the English source rather than ported from v8, which had no Lithuanian at all (#1917). Translations elsewhere got a pass (#1914), the elapsed and remaining time labels read better (#2028), and the Hindi quality menu uses resolution wording (#2098). Read the i18n concept for HTML and React.

On the site, the API reference is grouped under one heading (#2023), the menu composition demos match the simplified model (#2131), and the time slider reference and HTML sub-part props were repaired (#2142, #2146). Draft migration guides for Plyr (#1740) and Media Chrome (#1706) landed in the repository, pending verification before they reach the site.

Thanks to @littlespex for their first contribution to Video.js.