GuideMigrate
Migrate from Vidstack
Move a Vidstack Player integration to Video.js 10, mapping the player, providers, layouts, and media store onto composed components
Vidstack Player gives you one <media-player> that holds state, picks a provider from src, and hosts a layout you configure with props, slots, and CSS variables.
Video.js 10 grew out of the same ideas: composed, accessible components, one store per player, and state you can style against. The teams behind Vidstack, Plyr, Media Chrome, and Video.js now focus their work on Video.js. The code is new, though, and the jobs <media-player> did are split across smaller pieces. Most of this guide is about where each Vidstack concept moved.
AI Quickstart
The Video.js skill teaches coding agents how Video.js 10 is composed and points them at documentation that matches your installed version. This prompt installs it and starts the migration.
Paste this prompt into your coding agent:
Migrate this project's Vidstack player to Video.js v10 for React. Use @videojs/react.
Run `npx @videojs/cli agents skills` to get the Video.js skill installation instructions. Follow the printed steps for your agent. If they require a session reload, reload the session and continue with this prompt. If you can't run commands, use the installation instructions at https://github.com/videojs/skills. Use the skill throughout this migration.
Read the migration guide at https://videojs.org/docs/framework/react/guides/migrate-from-vidstack, including its Known gaps section. Inspect the existing player and list its media URLs, source formats, tracks, options, custom controls, event handlers, and integrations. Compare that list with the known gaps and report unsupported requirements before changing code. Keep the existing media URLs and required behavior throughout the migration.
For installation guidance, read https://videojs.org/docs/framework/react/llms.txt and follow its link to the installation guide. Keep using that online index for a CDN installation.
From the application root, run `npx @videojs/cli agents init` without flags to list installation options and their accepted values.
Choose values for these options:
- --media and --source-url: the existing content and playback requirements.
- --method: the installation method.
- --preset, --skin, and --extensions: the player UI and integrations.
Add those options to `npx @videojs/cli agents init --framework react --project existing` and run the command to print the installation plan. Follow any version-mismatch notice. Both CLI commands print instructions without changing project files. Use the printed plan to install Video.js and configure the player.
After installing a package, switch to node_modules/@videojs/react/docs/llms.txt for documentation matching its version.
Implement the migration. Then verify playback, captions, controls, and any analytics integration on every browser the project supports.Once Video.js is installed, the agent switches to the docs that ship with the package, which match your version, so it doesn’t rely on training data that mostly describes older players. See Build with AI to install the skill yourself or give your tool the docs another way.
Before you migrate
Two checks save the most time.
Compare your features with Known gaps. Saved preferences, the chapters menu, caption style settings, audio gain, clipping, and non-VTT captions have no Video.js equivalent yet. If your player depends on one of them, that shapes your timeline more than anything else in this guide.
Migrate one player at a time if you need to. Neither @vidstack/react nor @videojs/react registers custom elements, so both can render in the same app while you move players across. Remove @vidstack/react and its stylesheets once nothing imports them. While Vidstack’s player styles are loaded, they also give every <video> without a width or height a 16:9 box, Video.js media included.
Three pieces instead of one
In Vidstack, <media-player> does most of the work. It holds state, selects and loads a provider, acts as the box you size and take fullscreen, carries the state attributes you style against, and listens for keyboard shortcuts.
Video.js 10 splits those jobs:
The player holds state and hands it to everything inside it. It draws nothing and takes no layout. Which state it holds depends on the features it’s built from.
The media plays the video. This is where Vidstack’s provider went, except you choose it yourself: a plain <video> for progressive files, or a media component for HLS, DASH, YouTube, Vimeo, and Mux. There’s no <media-provider>; the media component sits inside the skin.
The skin is the UI, and it’s where Vidstack’s layout went. Every skin renders a container, the box that sizes the player, goes fullscreen, and receives gestures and hotkeys.
Note the nesting. A Vidstack layout sits next to <media-provider>; a Video.js skin wraps the media.
Terminology
“Remote” also changes meaning. Vidstack’s remote control dispatched requests; in Video.js, remote playback means AirPlay and Cast.
Your first player
Here’s a typical Vidstack player with the Default Layout, captions, thumbnails, and a poster:
Install @videojs/react:
The video preset gives you a player, a skin, and a media component that already fit together:
What changed:
- No
MediaProvider.Videois the media, and it sits inside the skin. srcandplaysInlinemoved to the media.titleandposterstay on the player, and the skin renders them.Trackbecame a native<track>. UsesrcLanginstead oflang.- Thumbnails are a track. The
thumbnailslayout prop becomes a<track kind="metadata" label="thumbnails" default>on the media. - No
iconsprop and no theme stylesheet. The skin ships its icons, and one stylesheet covers the whole skin. - Set the aspect ratio yourself. Vidstack’s base styles gave video a 16:9 box by default. Video.js skins don’t, so size the skin with CSS.
The preset entry already marks itself as client code, so this file doesn’t need 'use client' yet. Keep it in Next.js and other React Server Components setups once the file uses hooks such as usePlayer, or passes functions such as renderPoster. VideoPlayer takes only its config props and children. It renders no element, so it takes no className, style, or ref; style the skin instead.
If you used the Plyr Layout, start from the Neutral skin instead. It’s the closer match to a classic control bar, and Migrate from Plyr maps the Plyr options that layout mirrored.
Where your player props went
<media-player> accepted about fifty props. In Video.js they scatter in four directions:
- Media attributes move to the media component, where the browser already understands them.
- Player metadata, the title and poster, stays on the player so the skin can render it.
- Playback values such as volume and current time become actions you call.
- Behavior such as live UI, shortcuts, and casting becomes a choice of preset, component, or extension.
Vidstack queued paused, volume, and the other playback props until the media could play. Video.js doesn’t queue: most actions fail until the player has attached to its media, seek() included, which only waits for metadata once attached. Call them from event handlers.
Don’t call them from a mount effect inside the player. React runs a child’s effects before the player’s, so the player hasn’t attached yet.
Providers become media components
Vidstack’s <media-provider> read src, guessed a type from the extension or a HEAD request, and loaded the matching provider. Video.js doesn’t guess. You pick the media component for your source, and that choice is the playback engine choice. Swapping one for another is a component change, and the rest of the player keeps working.
Import each media component from @videojs/react/media/<name>, for example @videojs/react/media/hlsjs-video.
HLS and DASH
Vidstack loaded hls.js and dash.js from jsDelivr at runtime unless you passed a library. Video.js media packages bundle their engine, so there’s no library option, no hls-lib-* or dash-lib-* events, and no CDN host to allow in your Content Security Policy. dash.js also moves from version 4 to version 5; check your dash.js settings against its migration notes.
Engine configuration moves from the provider-change event to the media’s source:
source is replaced, not merged. Changing hls.js options rebuilds the engine; dash.js and Shaka apply new settings to the running one. See Media sources for the options every engine shares, such as preferPlayback, rendition caps, and DRM.
Where you used provider.instance or onInstance, read the engine from the media object that useMedia returns. useMedia doesn’t know which media component you rendered, so check the media with instanceof HlsJsAdapter first; that types engine as the hls.js instance. HlsJsAdapter and Hls come from @videojs/hlsjs-video, the package you installed for HlsJsVideo, which exports them for this kind of low-level access. Outside the player, pass mediaRef to HlsJsVideo instead: it receives the same HlsJsAdapter, so mediaRef.current?.engine is already typed as the hls.js instance. Vidstack re-dispatched hls.js events as onHls* callbacks; Video.js doesn’t forward engine events, so subscribe on the engine itself:
engine is null while the browser’s own HLS is playing. The media builds a new engine when its engine options, DRM, preferPlayback, or content type change; a new URL of the same type keeps the current one.
Treat engine access as an escape hatch. It couples your app to one engine, so prefer player state and media events when they cover what you need.
YouTube and Vimeo
The embeds take a URL, a bare ID, or one of Vidstack’s shorthands as src:
youtube/IDplays from the privacy-enhanced host,youtube-nocookie.com, as it did in Vidstack.vimeo/ID?hash=HASHstill plays an unlisted video. In a full Vimeo URL, give the hash as?h=HASHor as the last path segment.
A ref on YouTubeVideo or VimeoVideo is the <iframe>, which can’t play or seek. Where you called methods on Vidstack’s YouTube or Vimeo provider, pass mediaRef to get the adapter that drives the embed:
Vidstack fetched a poster for embeds automatically. Video.js doesn’t (#3049); set poster on the player if you want your own image before playback.
Choose the media from the URL
We’re working on a media component that picks and loads the right engine from its source, the way <media-provider> did. Its API isn’t settled yet; follow #2160 for progress.
Until then, resolveAdapterType tells you which media component plays a URL, and you render that component with the same src. If your app plays sources from a catalog or from user input, a switch on its result covers the common cases:
Each embed and streaming component loads with React.lazy, so the page only downloads the engine its source needs, the way Vidstack’s provider loaders did. Video plays through the browser with no engine, so it’s imported directly. When the media changes, the player detaches from the old one, resets its state, and attaches to the new one; the skin follows.
resolveAdapterType recognizes the URLs and shorthands Vidstack’s providers did, plus Wistia, Mux, Cloudflare Stream, Spotify, TikTok, and Twitch, and returns null for anything else. Vidstack sent a HEAD request when a URL had no file extension; pass the MIME type as the second argument instead. The result names a kind of source, not an engine: hls covers every HLS media, so choose one yourself.
For a single source, change the media’s src or source instead. The same media swaps the URL without being replaced.
Loading
Vidstack waited until the player was visible before it loaded the provider (load="visible"), and embeds loaded lazily with preconnect hints. Video.js media starts loading as soon as it attaches, following its preload attribute, and embeds create their iframe right away.
To defer network work, set preload="none" on streaming media; hls.js-backed media then waits for playback to fetch segments. For players far down a page, render or import the media when it scrolls into view with your own IntersectionObserver. There’s no startLoading(); set src when you’re ready. Follow #3043 for built-in deferred loading and #1433 for preconnect hints.
Layouts become skins
Vidstack rendered the audio and video layouts side by side and matched one at runtime, then switched live controls on by stream type. Video.js gives each case its own preset and skin, chosen when you import it.
The video skins cover the Default Layout’s core controls: play, volume, time, the time slider with thumbnails and chapter segments, captions, fullscreen, picture-in-picture, AirPlay, Cast, and a settings menu with quality, audio track, playback rate, and captions. They add an error dialog and on-screen feedback for hotkeys and gestures. The Default Layout’s chapters menu (#1873), accessibility menu, audio boost (#1135), and download button (#3041) have no equivalent yet.
Map layout props
Packaged skins take almost no props. Most layout props either move somewhere else or need you to edit the skin source.
Slots become skin source
Vidstack’s slots prop inserted, replaced, or removed controls at more than forty named slots, each with before and after positions. Video.js has no slot API for controls. Children of a packaged skin render with the media, not in the control bar. The two content overrides are renderPoster and renderThumbnail.
To add, move, or remove a control, add the skin source to your project and edit it. slots={{ afterCaptionButton: <MyButton /> }} becomes one line in the copied layout file; slots={{ pipButton: null }} becomes a deleted line.
Customize your player
Vidstack customization climbed from layout props, through CSS variables and slots, to composing your own layout. Video.js has three levels. Try them in order.
Level 1: pick a skin
Choose Default, Neutral, or Compat for video, audio, live video, or live audio. See Skins.
Level 2: restyle it
Packaged skins expose eight public custom properties: --media-accent-color, --media-accent-text-color, --media-border-color, --media-border-radius, --media-font-family, --media-object-fit, --media-object-position, and --media-scale-unit. That’s far fewer than Vidstack’s layout variables, so expect to reach Level 3 sooner. See Map theme variables by meaning and Customize skins.
Level 3: edit skin source
For changes to controls, layout, or interactions, add the skin source to your project with the Shadcn registry. The components and styles become local files you own. This is where Vidstack’s slots, icon overrides, and switches such as noGestures end up. See Customize skins.
To build a layout from scratch, the way you might have composed Vidstack components with the Default Theme, compose the UI components inside a container yourself.
Rewrite your styles
State attributes
Vidstack reflected about forty state attributes onto the player’s element, so any descendant could style against [data-media-player][data-paused]. Video.js puts state on the component it belongs to. VideoPlayer renders no element, and Container only reflects data-controls-visible.
For your own components, you usually don’t need an attribute at all. Read the value with usePlayer, or pass a function to className on a Video.js component: className={(state) => …}.
Some names mean different things now. data-active meant a Vidstack slider was being dragged, pointed at, or focused; in Video.js it marks active captions or the chapter that’s playing. data-orientation was the screen orientation; now it’s a slider’s axis.
Map theme variables by meaning
The shared --media- prefix is a naming convention, not a compatibility layer. Only --media-font-family keeps its name and meaning.
The slider variables are values the component publishes for your styles to read, not inputs you set, the same as in Vidstack. They only gained the --media- prefix.
Tailwind
Vidstack’s Tailwind plugin added state variants like media-paused: and media-can-play:. Video.js has no plugin; use Tailwind’s built-in data-* variants on the component that carries the state. The bare data-paused: form needs Tailwind 4; on Tailwind 3, write data-[paused]:.
Skin source in Tailwind defines its own media-sm:, media-lg:, and similar variants. Those are container-width breakpoints, not state. The Tailwind skin source needs Tailwind 4.3 or later, and it’s available for React only; HTML skin source uses CSS (#3055). Vidstack’s plugin targeted Tailwind 3.
Responsive layouts
Vidstack switched to a small layout when smallWhen matched the player’s width or height. Video.js skins restyle one layout with container queries against a container named media-root, using width only. The breakpoints are fixed in packaged skins; edit them in skin source. For your own layout around the player, write your own @container rules.
Color scheme
Vidstack’s colorScheme toggled light and dark classes on the layout. Video.js skins read the inherited CSS color-scheme, so :root { color-scheme: light dark; } follows the system setting. The audio skins change their palette with the scheme. The video skins stay dark over video; only their border follows it (#3054).
Icons
@vidstack/react/icons becomes @videojs/react/icons, with a smaller set of 25 icons in Default, Neutral, and Compat families. Icons take standard SVG props; there’s no size prop. Several names changed:
Icons such as download, chapters, and accessibility have no Video.js counterpart. Bring your own SVGs for those.
Map the components
Most Vidstack components have a Video.js counterpart with the same job. What changes is how they compose.
asChild becomes render. Pass an element to merge props into it, the way asChild did, or a function that receives props and state:
Several roots render no element of their own: Controls.Root, Tooltip.Root, Menu.Root, and Gesture among them. Put your classes on Controls.Content, Tooltip.Popup, and Menu.Popup.
A few behaviors changed across the board:
- Unsupported controls hide themselves. Vidstack kept them in the DOM without
data-supported, and you hid them with CSS. Video.js buttons exposedata-availabilityand hide when the feature is unsupported. - Sliders take focus on the thumb. Keyboard control moved from the slider root to its thumb, so a slider without a thumb can’t be used from the keyboard.
- Toggle buttons change their label instead of setting
aria-pressed. A play button announces “Play” or “Pause” depending on state. - Placement is two props.
placement="top center"becomesside="top"andalign="center", and the rendered placement is reflected asdata-sideanddata-align. - No request events. Controls call player actions directly, so there’s nothing to intercept with
preventDefault().
Read player state
Vidstack’s media store becomes the player store. Two differences catch most migrations:
- State keys come from features. Every Vidstack key always existed. In Video.js, a key exists only when its feature is part of the player, and some live in no preset.
- Subscriptions don’t track what you read. Vidstack’s
subscribere-ran only when the keys you read changed. The Video.js store notifies on any change; selectors give you change-only updates.
The player store reference lists every state field and action, grouped by the feature that adds it.
usePlayer() with no selector returns the store without subscribing, so const { paused } = usePlayer() never re-renders. Always pass a selector for values you render. The preset’s usePlayer from @videojs/react/video is typed to the preset’s features; createPlayer returns a typed pair for a custom feature list.
VideoPlayer renders no DOM, so wrapping more of your page in it is free. That replaces the ref you passed to useMediaState to read state from outside the player.
Drive playback
Vidstack’s remote control dispatched request events that the player satisfied. Video.js actions call the media directly. They aren’t queued, so most fail until the player has attached to its media, seek() included; once attached, seek() waits for metadata. Their promises reject where Vidstack fired play-fail or fullscreen-error.
The store has no toggles apart from toggleSubtitles() and toggleControls(), so call the pair you need, as in paused ? play() : pause(). Hotkeys and gestures keep the toggle names, so togglePaused still works as their action.
Vidstack methods took an optional trigger event as their last argument. Video.js actions don’t, so wrap them in a handler, as in onClick={() => seek(30)}. Passing an action straight to onClick would hand it the click event.
Where you called play() or set currentTime through a MediaPlayerInstance ref, pass mediaRef to the media component instead. It receives the element for Video and Audio, and the playback adapter for HlsJsVideo, embeds, and other adapter-backed media. That’s the same object useMedia() returns:
State keys follow the same pattern. The common renames:
Two keys behave differently. started can turn off again after the media is reset or sits paused at the start, and currentTime follows native timeupdate, about four times a second, where Vidstack updated it every animation frame.
Events
Vidstack fired a normalized set of events on <media-player>, including state-change events such as fullscreen-change and controls-change. Video.js fires no player events of its own.
Put standard media event props on the media component: onPlay, onCanPlay, onTimeUpdate, onEnded, and so on. They receive React’s synthetic event on media that renders a <video>, and a plain Event on the embeds, where Vidstack’s callbacks received a detail and the event. The embeds route the same prop names. For anything that isn’t a media event, read the state:
Event triggers, originEvent, and isOriginTrusted have no equivalent. Check event.isTrusted in your own handler before calling an action if you need to know a person started it.
Captions, chapters, and thumbnails
Captions
Caption <track> elements move from <media-provider> to the media. Video.js reads captions and subtitles from those tracks and from streaming manifests.
The browser parses and renders the cues, so:
- Only WebVTT works. Vidstack parsed SRT, SSA/ASS, and JSON with
type(#3037). Convert them to WebVTT at build time or on your server. - Style captions with
::cue.<media-captions>, itsdata-partselectors, and the--media-cue-*variables have no equivalent (#3038). The skins keep native captions clear of the controls in Chromium and WebKit browsers. - There’s no caption style menu for viewers to pick fonts, colors, and backgrounds (#1437). Viewers can still set caption preferences in their operating system or browser.
- Adding tracks from code is native.
textTracks.add()becomes a<track>element, oraddTextTrack()andVTTCueon a plain<video>.
The captions toggle picks a track differently. Vidstack restored the last track shown, then the default track, then the first one. Video.js restores the last track shown, then one that matches the browser’s language, then the first track. See Captions.
Chapters
A <track kind="chapters" default> still segments the time slider, and the chapter under the pointer shows in the preview. Vidstack’s chapters menu, ChapterTitle outside the slider, and useChapterOptions have no equivalent (#1873). To add chapters from code, use the native track APIs described under Captions; there’s no dedicated chapters API yet (#1268). For a current-chapter label elsewhere, find the cue in the chaptersCues state that contains currentTime.
Thumbnails
The thumbnails layout prop and Thumbnail’s src become a track on the media, and the skin picks it up:
default is required, or the cues never load. Set crossOrigin on the media when the storyboard comes from another origin. A cross-origin track only loads in CORS mode, and the thumbnail images load in the same mode, so their host needs CORS headers too. Vidstack also accepted JSON and Mux storyboard.json URLs (#3044); fetch and convert those yourself and pass the result to the thumbnails prop on Thumbnail.Root, or use MuxVideo, which adds the storyboard track for you. See Thumbnails.
Keyboard shortcuts and gestures
Keyboard shortcuts
The packaged video skins ship a default set close to Vidstack’s: Space and k to play, m to mute, f for fullscreen, c for captions, i for picture-in-picture, arrow keys and j/l to seek and change volume, 0–9 to jump, and </> for speed. They also add Home and End. The live skins leave out seeking, jumping, Home and End, and speed, and the audio skins leave out f, c, and i.
Two defaults differ. Seeking moves 10 seconds with or without Shift, where Vidstack’s Default Layout moved 10 seconds, or 20 with Shift. And the speed keys step through the fixed rate list and wrap around instead of moving by 0.25.
The keyShortcuts prop becomes one hotkey per shortcut in a layout you compose or in skin source. Each takes a single key pattern, so togglePaused: 'k Space' becomes two hotkeys:
Shortcuts with custom callbacks become the useHotkey hook: useHotkey({ keys: 'n', onActivate: playNext }).
keyTarget="document" becomes target="document" on each hotkey. With several players on a page, the first one registered handles the key; Vidstack sent it to the last player you used. keyDisabled has no player-wide switch, and packaged skins always include their hotkeys, so remove them in skin source. A tooltip’s Shortcut part shows the key bound to its button, and buttons set aria-keyshortcuts from the registered hotkeys. Writing aria-keyshortcuts yourself no longer creates a shortcut. See Keyboard shortcuts.
Gestures
Vidstack’s gestures listened for any DOM event and took their hit area from their own box. Video.js gestures recognize taps and double taps, divide the player into regions, and can be limited to one pointer type:
The packaged video skins already bind these defaults. Vidstack’s swipe-to-scrub gesture has no equivalent (#3046).
Fullscreen and orientation
Fullscreen targets the skin’s container, falling back to the video element on iPhone. There’s no target option; to take only the video fullscreen, call the native method on it yourself.
Vidstack locked the screen to landscape in fullscreen by default. Video.js presets don’t. Add orientationLockFeature to a player built with createPlayer to lock it again. Turning Vidstack’s fullscreen orientation off just means leaving the feature out. Vidstack’s screen orientation state and lockScreenOrientation() have no equivalent; use screen.orientation.
Live streams
Vidstack switched the Default Layout to live controls by streamType. Video.js has separate live presets, whose skins show a live button and no time slider:
- DVR comes from the stream.
live:dvrbecomestargetLiveWindow === Infinity, which the media reports for event playlists. There’s no way to force DVR on a sliding window. Add a time slider in skin source to let viewers seek. - The live edge moved to the live button.
liveEdge,userBehindLiveEdge, andseekToLiveEdge()become the live button’s state and behavior. It counts playback as live within 5 seconds of the stream’s live edge start, which already sits behind the seekable end by the playlist’s hold-back. Vidstack counted the lastliveEdgeToleranceseconds before the seekable end, 10 by default and configurable (#1730). streamTypeneeds a feature. No preset includesstreamTypeFeature; add it to a custom player when one player switches between live and on-demand UI.- Live presets are narrower. They leave out playback rate, quality, and audio-track state. Build a player from a feature list to add them back.
See Live streams.
AirPlay and Google Cast
The video skins include AirPlay and Cast buttons. Both call one action, promptRemotePlayback(), and each button shows only on the platform that supports it. Without the Google Cast extension, the Cast button uses the browser’s Remote Playback API.
Vidstack’s googleCast prop becomes the GoogleCast extension, placed anywhere inside the player:
Only the receiver ID carries over from Vidstack’s Cast options, and the Cast prompt events have no equivalent (#3048). There’s no device name or remotePlaybackType either (#3047). See Cast to AirPlay and Chromecast.
Languages
Vidstack layouts took a translations object keyed by English strings, and you supplied every language yourself. Video.js ships locale packs for around 50 languages. Wrap the player in the i18n provider and it loads the pack for the nearest lang attribute; without one, the controls use English. Override strings with namespaced keys:
Strings for the chapters menu, caption styles, audio boost, and download button have no keys, because those features don’t exist yet. See Internationalize the player for the full key list.
Preferences and autoplay
Saved preferences
Vidstack’s storage saved volume, mute, captions, language, playback rate, quality, audio gain, and playback position. Video.js doesn’t save anything between visits yet (#944). Remember user preferences shows how to restore and save volume and caption choices yourself.
Autoplay
Move autoPlay from MediaPlayer to the media component, along with muted and playsInline. There’s no auto-play-fail event or autoPlayError state; a blocked autoplay looks like media that never started. To react to it, leave out autoPlay, call play() once the player attaches, and catch the rejection. See Autoplay.
Behavior changes to check
These changes don’t throw errors, so test for them:
- Loading. Media loads as soon as it attaches, not when the player scrolls into view, and embeds don’t preconnect.
- Size. There’s no default 16:9 box; set
aspect-ratioon the skin. - Fullscreen. Presets don’t lock the screen to landscape, and media that isn’t set to play inline no longer enters fullscreen when it starts on Android and iPad. iPhone Safari still plays it fullscreen natively.
- Controls. They hide as soon as the pointer leaves the player during playback, where Vidstack waited for its 2-second idle delay.
- Hotkeys. Shift no longer doubles the 10-second seek step, and the speed keys wrap around a fixed list.
- Captions. The captions toggle prefers the browser’s language over the
defaulttrack. - Embeds. YouTube URLs and Vimeo no longer use their privacy-enhanced modes by default;
youtube/IDshorthands still do. - Time.
currentTimeupdates about four times a second instead of every frame. - Live. Playback counts as live within 5 seconds of the stream’s live edge start, not 10 seconds of the seekable end, and live skins have no time slider, quality, audio track, or rate menu.
- Volume.
setVolume()above 0 also unmutes, and unmuting at 0 restores 25%. Vidstack’s controls did the same, but setting itsvolumeormutedproperties didn’t. - System integration. Nothing is saved between visits, and Video.js doesn’t set Media Session metadata for lock screens and hardware keys.
- Tooltips. They open after 600 ms instead of 700 ms.
Known gaps
Ordered roughly by how likely each is to block a Vidstack migration. Follow Vidstack Parity for progress.
- No automatic provider selection.
resolveAdapterTypetells you which media component plays a URL, but you render it yourself (#2160). - Nothing persists between visits: volume, captions, language, rate, quality, or position (#944).
- No caption style settings (#1437), no custom caption renderer (#3038), and WebVTT only: no SRT, SSA/ASS, or JSON captions (#3037). Audio players don’t show captions (#3039).
- No chapters menu or chapter title outside the time slider (#1873), and no dedicated API for chapters from code (#1268). Cue points and markers aren’t implemented (#1442).
- Fixed playback rates:
0.2,0.5,0.7,1,1.2,1.5,1.7,2(#1404). No speed or quality sliders (#3052). - No audio gain or boost (#1135).
- No load strategies. Vidstack’s media and poster loading strategies and
startLoading()have no equivalent (#3043), and embeds don’t preconnect (#1433). - No clipping. Vidstack’s clip start and end times have no equivalent (#3040).
- No download button (#3041).
- No Remotion provider or Remotion components (#3053).
- No Media Session metadata from
title,artist, andartwork(#3042). - Fixed timing. The controls hide delay (#1728) and the live edge tolerance (#1730) aren’t configurable, and there’s no duration override (#1729).
- No debug logging to replace Vidstack’s log level (#1406).
- Fewer embed conveniences. No automatic embed posters (#3049), Vimeo chapters (#3050), or Vimeo quality selection (#3051).
- Less remote playback detail. No route type or device name (#3047), and no Cast prompt events or Cast options beyond the receiver ID (#3048).
- Fewer time slider and gesture extras. No video preview in the time slider (#3045), JSON thumbnail storyboards (#3044), or swipe to scrub (#3046).
- No request events, event triggers, or ways to cancel a request, by design.
- No light theme for video skins (#3054), no Plyr-style skin (#181), and no Tailwind skin source for HTML (#3055).
- No ads (#3056), which are on the roadmap for late 2026.