GuideMigrate
Migrate from Mux Player
Move a Mux Player embed to Video.js v10 using a player, media component, skin, and analytics extension
Mux Player combines HLS playback, UI, analytics, captions, remote playback, and keyboard shortcuts in one element. Video.js v10 composes those responsibilities from a player, media component, skin, and optional extensions.
This guide moves a working Mux Player embed to those pieces, then maps the settings and APIs you are most likely to need next.
AI Quickstart
Paste this prompt into your coding agent:
Migrate this project's Mux Player player to Video.js v10 for HTML. Use @videojs/html or its CDN bundles for a page without a build step.
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/html/guides/migrate-from-mux-player, 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/html/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 html --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/html/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.Before you migrate
Make a migration checklist before changing your embed:
- List the Mux Player attributes or React props you set. Compare them with the mapping tables below.
- Grep your codebase for
mux-playerselectors. After the swap, CSS rules andquerySelectorcalls that reach into the element stop matching, with no error. Look for patterns such asmux-player::part(…),mux-player [role="slider"], andplayer.shadowRoot. Restyle the UI with the skin’s custom properties or rebuild it with Video.js components. - Find your event listeners and imperative calls (
play(),currentTime,addChapters). Media APIs move to the media component; the rest is mapped under Drive playback. - Note your theme and CSS variables. Compare your Mux Player theme with the two Video.js skins, and map its CSS variables.
- Check Known gaps for features your app needs.
- Check your catalog for MPEG-TS, raw ADTS AAC, AES-128, low-latency HLS, or DVR streams. Check whether the SPF engine supports these requirements. If you use DRM, verify playback with whichever engine you choose. See Which Mux media should you use?.
Before loading Video.js, remove Mux Player imports and CDN scripts, plus any other imports that register legacy Mux Video or Media Chrome elements. Then reload the page. These libraries share custom-element names such as mux-video and media-play-button. A name can only have one definition per document, and Video.js keeps an existing definition. If both players must remain during migration, use separate documents, such as an iframe.
Your first player
The examples keep a playback ID, analytics metadata, and a poster from two seconds into the video. Replace YOUR_PLAYBACK_ID with your existing playback ID before running them.
From the CDN, load the video preset, the Mux media, and analytics:
With a bundler, install the HTML package, Mux media adapter, and Mux Data extension. Import the player, skin, media, and extension:
Preserve these behaviors when adapting the example:
- The skin has no size of its own, so the examples give it one with an inline
aspect-ratio. Any CSS that sizes the skin works, such as your own class or a Tailwind utility likeaspect-video. See Move your layout styles. - The Mux media supplies the poster. It builds the image URL from the playback ID, and the skin displays it automatically. The example keeps the frame from two seconds in by configuring the Mux media. Set
posteron the player only when you want to use your own URL. - Thumbnail previews come from the Mux media. The Mux media adds and maintains the thumbnail track itself, and removes it for live streams, where storyboards don’t exist.
- Analytics needs no environment key. Mux resolves the environment from the playback ID. See Mux Data.
- Chromecast is opt-in. To add Chromecast, follow Cast to AirPlay and Chromecast. Otherwise, leave the Google Cast extension out.
Move your layout styles
Mux Player was one visible, measurable element. Video.js separates player state from the visible surface.
If you use <video-skin>, put width, height, aspect ratio, positioning, and DOM measurements on <video-skin>. Importing the skin registers the styles that make the media, controls, poster, and overlays fill the skin and stack inside it. The skin also stops <video-player> from creating a layout box of its own.
If you leave out <video-skin> to build your own UI, add a <media-container>. Style and measure that element, and put overlays inside it:
<mux-video> and the other custom video elements do not create their own layout box. They fill their parent, so size and measure <media-container>, not the video element.
The examples leave out the player software name and version. If your app already sets them, copy them to the Mux Data extension. Use player-software-name and player-software-version in HTML, or playerSoftwareName and playerSoftwareVersion in React. Preserve your existing Mux Data metadata as well.
Three pieces instead of one
Use these three pieces when deciding where each Mux Player setting belongs:
The player is the outer custom element. It holds state, hands that state to everything inside it, and draws no UI itself. Which state it holds depends on the features it’s built from.
The media is the thing that plays the video. Use Mux media for Mux playback IDs and source settings. Choose another media component when changing providers, and check which capabilities it exposes.
The skin is the UI: the controls, the poster, the captions, the settings menu, the keyboard shortcuts. Skins are pre-built arrangements of smaller components, and you can use one as-is, restyle it, or add its source to your project and edit it.
Autoplay
Video.js uses native autoplay. Move Mux Player’s boolean autoplay setting to the media. For its string-valued modes:
muted: set bothautoplayandmutedon the media (autoPlayandmutedin React).any: onloadstart, callplay()on the media. If the returned promise rejects, mute the media and callplay()again. Implement this fallback in your app.
Keep a manual play control available if autoplay fails. See Handle autoplay.
Mux settings live in a source object
Mux Player exposes Mux stream parameters as attributes such as max-resolution, asset-start-time, and custom-domain.
Video.js collects them into a single source object that describes what to play and how.
The groups map to the three URLs Mux serves. playback modifies the video stream, poster modifies the still image, and storyboard modifies the hover-preview track. Video.js builds all three URLs and converts your camel-case keys to the snake_case query parameters Mux expects, so assetStartTime goes out as asset_start_time.
Set it as the element’s source property because an attribute cannot hold an object:
You can also skip the object entirely and set src to a full Mux URL. The element parses the URL, including its query parameters, into a source. Use this form in markup where you cannot run JavaScript:
There’s no playback-id attribute, so declarative markup uses the URL form.
Here’s where the Mux Player attributes land:
Signed playback behaves the way it does in Mux Player: a token replaces every other parameter in its group, so put resolution caps and clipping in the token itself.
Analytics moves to its own extension
Render the Mux Data extension inside the player, and move Mux Player’s analytics settings to its attributes or properties.
Set metadata as a property because it takes an object, while attributes take strings.
Your metadata keys don’t change. They’re the same snake_case names Mux Data has always taken, so the values port across untouched.
Titles and posters
Mux Player used the same title for analytics and on-screen display. Video.js separates them.
For analytics, use metadata.video_title on the Mux Data extension.
For display, use content-title on the player.
Mux media loads the asset’s title from its metadata. If you omit the display title on the player, the player uses the title set in the Mux dashboard.
The packaged video and live-video skins show the resolved title at the top of the player and fade it with the controls, so the embeds above already display it. The <media-title> element sets data-visible while the controls are visible, so your skin overrides can style the title differently when the controls show or hide. The audio skins have no title display.
In installed skin source or a custom layout, the title appears where you place the title component. Put the title component inside the sized container from Move your layout styles so it overlays the media:
<mux-video> supplies a poster from the playback ID, and source.poster controls the generated image. The skin displays it automatically. Pass poster to the player when you want to use your own URL.
To show a placeholder while the generated poster loads:
Replace /placeholder.webp with your existing placeholder image URL or data URL. The slotted image shows the placeholder while the full poster loads. The Mux media supplies that full poster using your source settings and any thumbnail token.
Mux serves poster images from its thumbnail endpoint. Configure the image URL with source.poster. Move Mux Player’s thumbnail-time value to poster-time on <mux-video> or to source.poster.time. Set width, height, fitMode, rotate, and ext on source.poster to control image size, cropping, rotation, and format. Video.js converts those camel-case keys to the snake_case query parameters Mux expects. See Add a poster and loading placeholder for more examples or the <media-poster> reference for the element itself.
Customize your player
Choose the smallest customization that meets your existing UI requirements: a packaged skin, its CSS variables, or editable skin source.
Level 1: pick a skin
Video.js has three packaged skins. The Default skin groups settings in a menu. The Neutral skin has a compact layout like a classic player control bar. The Compat skin uses plain surfaces and basic controls for broader browser support. All three bring controls, tooltips, captions, keyboard shortcuts, touch gestures, and a settings menu that appears when there’s something to put in it. See Skins.
Level 2: restyle it
Set custom properties on the skin. The common case is a brand color:
--media-accent-color reaches the sliders, the active buttons, and the accent surfaces. Video.js picks a readable text color for content on the accent color. To choose that text color yourself, set --media-accent-text-color. --media-border-radius rounds the player’s corners. --media-scale-unit scales the whole control bar. Customize skins has the full list.
The skins use a different visual design, so you may not need direct replacements for Mux Player’s primary and secondary colors. Start with --media-accent-color. Restyle individual surfaces, or add the skin source to your project, only when you need a closer palette match.
Level 3: edit skin source
For changes to controls, layout, or interactions, add the skin source to your project. Its TypeScript components and styles become local files that need a build step. A CDN-only page must add a build step before it can use this source. Customize skins covers the setup and available skins.
To migrate forward-seek-offset and backward-seek-offset, edit the installed skin’s hotkey and gesture declarations.
Replace the existing seek declarations, including the j and l shortcuts, with ones that use your offsets. These examples use 30 seconds forward and backward:
Neither packaged video skin includes skip buttons. To keep those controls from Mux Player, add a seek button, which takes seconds and defaults to 30.
To remove a control, remove it and its tooltip wrapper from the local controls layout.
Read and control the player
<mux-video> is a real media element, so every media event you already listen for still fires on it: play, timeupdate, ended, error, and the rest. Keep your Mux Player listeners. <mux-video> also fires streamtypechange, targetlivewindowchange, sourcechange, and contentdatachange.
For UI state rather than media state, use PlayerController inside a custom element. Give it a selector for the feature you care about and it keeps your element in sync:
Render it inside your existing <video-player> so it can read that player’s state:
Drive playback
Use the player store for state and actions:
Standard media properties update player state through media events. If the volume is 0, setMuted(false) also sets it to 0.25. Mux Player’s smart-unmute behavior carries over through setMuted(false). To unmute and keep a zero volume, set media.muted directly.
Call actions on the player element’s store only after player.store.target is set, which means the media has attached. For example, wire seeking to a button:
Store actions throw until the media attaches. Call fullscreen actions from a user interaction, such as a click handler.
Which Mux media should you use?
Choose and test the engine before migration, especially when your catalog uses DRM or TS-packaged media.
<mux-video> supports two playback engines: SPF and hls.js. Choose the engine by import path. The /spf import is smaller, and /hls-js is more compatible. The default import currently uses hls.js. Start with that default:
The SPF engine does not support MPEG-TS segments, raw ADTS AAC, or whole-segment AES-128 encryption. Keep the default for assets that need those formats. The SPF engine also lacks low-latency HLS support, and its DVR playback is experimental.
Both flavors support DRM through source.drm. Verify license exchange and playback with your protected assets on supported browsers before changing engines.
All three imports register the same <mux-video> tag and accept the same source, so changing the engine is an import change. Load only one registration for that tag on a page.
The default import currently uses hls.js, but its engine can change in a future release. Use /hls-js when your app depends on hls.js APIs. Test the chosen engine with your assets and supported browsers.
The Mux audio component has the same three import paths. Use it with the audio player and audio skins.
Live streams
Mux Player switches to live controls automatically. In Video.js, choose the live preset yourself.
Register the live preset and Mux media. From the CDN:
With a bundler, import the same pieces:
You get targetLiveWindow and liveEdgeStart in player state, plus a live button that jumps to the live edge. The live preset doesn’t include streamType state and doesn’t force a source to be live. The Mux media reads the stream type from the manifest: targetLiveWindow is NaN for an on-demand or unknown source, 0 for a sliding live window, and Infinity for a live event with playback history. See the live feature.
The live skins have no time slider. To preserve DVR scrubbing, add one to custom UI or installed skin source; follow the DVR setup.
The live composition is narrower than the video one on purpose: it leaves out quality, audio-track, and playback-rate state. A live skin’s settings menu therefore holds captions and nothing else. If you need one of the omitted features on a live player, build your own player from a feature list rather than taking the preset’s.
Use createPlayer to build a player element from that list.
Add your own jump-to-live button
The live skin already includes a live button. If you build your own controls, use that same component and style it to match your app. It keeps track of whether playback is live and returns viewers to the newest available point in the stream.
With a bundler, import @videojs/html/ui/live-button. Render the button inside your custom player layout.
Do not rebuild this behavior by reading duration or by setting currentTime to Infinity.
Troubleshoot live playback
An on-demand video shows live controls
Video.js does not replace the preset after reading the HLS playlist. Render the video preset for on-demand content and the live preset for live content. If the same part of your app handles both, choose the preset from application or content metadata before rendering. If only the manifest can tell you, use a custom player with the stream type feature and choose the controls from that state.
selectStreamType returns undefined
The live preset does not include the stream type feature. Add streamTypeFeature to a custom player when you need streamType in player state.
The HLS media object also reads 'live' or 'on-demand' from the playlist. Read streamType from <mux-video>.
A live stream has a finite duration
The browser may report Infinity for live playback, but the player’s time feature reports the end of the available video. That number stays finite and moves forward with the stream.
Do not use duration to decide whether a source is live. Read streamType, or check that targetLiveWindow is not NaN.
targetLiveWindow does not match the rewind time
Despite its name, targetLiveWindow does not report a number of seconds. It describes the kind of live stream:
To find the times a viewer can seek to, read buffer.seekable. It contains [start, end] pairs. The first start is the oldest available time, and the last end is the newest. Both move forward on a sliding live stream.
liveEdgeStart is the playback time where the player starts treating the viewer as live. The live button seeks to the last end in buffer.seekable, which may be later than liveEdgeStart.
The settings menu
The video skins include a settings menu with the available quality, audio-track, captions, and playback-rate controls:
When enabling captions without a previous selection in this player instance, toggleSubtitles() prefers a track matching navigator.language, then falls back to the first available caption or subtitle track. For an authored track that should show on load, add default. See Show captions and subtitles.
Video.js has no viewer-facing text-track settings dialog. You can style browser-rendered captions with ::cue.
Chapters and cue points
Add a chapters track with the default attribute inside your media. The skins then split the time slider into chapter segments and show the chapter title on hover:
To replace addChapters(), convert your chapter array to a WebVTT file and set its URL as the track’s src (#1268).
The time slider marks the segment containing the current playback time with data-active, so you can style the active segment. The player still has no active-chapter state, chapterchange event, or menu for jumping between chapters (#1873). If UI outside the time slider needs the current chapter, derive it from currentTime and the read-only chaptersCues array. Cue points aren’t implemented at all (#1442).
Signed playback and DRM
For signed playback, follow Mux’s secure video playback guide to create a signing key and generate tokens on your server. In the example below, replace YOUR_PLAYBACK_ID with the signed asset’s playback ID. Replace each token placeholder with the matching server-generated token. Each token goes in its own group.
Video.js checks the audience of poster, storyboard, and DRM tokens before building their URLs. It passes playback tokens through for Mux to validate. A misplaced playback token still produces a stream request that Mux rejects:
Signed playback needs its own poster token even when the Mux media would otherwise derive the poster from the playback ID. Put Mux’s thumbnail token at source.poster.token. If it is missing or has the wrong audience, the media cannot build a poster URL.
Neither player refreshes tokens. Mux Player detects an expired token and shows a message. Video.js does not report expired tokens yet (#1432).
For DRM on either Mux media flavor, supply a license token. Video.js derives the FairPlay, Widevine, and PlayReady license servers and the FairPlay application certificate:
DRM playback is always signed, so set both source.drm.token and source.playback.token. For content Mux does not license, add your own license servers to source.drm, keyed by key system. For each key system you configure, your server replaces the derived server.
The SPF flavor supports FairPlay, Widevine, and PlayReady through its EME license pipeline. DRM availability still depends on the browser, key system, and asset configuration.
Access the media and playback engine
source.preferPlayback is the closest mapping. To match prefer-playback="native", set it to 'native'. The Mux media then uses the browser’s built-in HLS support instead of building an MSE pipeline.
Video.js has no Program Date Time (PDT) helpers such as getStartDate() or currentPdt. The player exposes liveEdgeStart and targetLiveWindow. To read PDT, use the playback engine.
Known gaps
Check these limitations against your required features:
- The two packaged skins do not reproduce Mux Player’s themes, and there is no runtime theme switch. Plan skin customization when preserving a theme such as
classic. - Lazy loading (
loading="viewport|page") has no equivalent. - The controls auto-hide delay isn’t configurable (#1728). For always-visible controls, set
visibility="always"on the controls component in a custom layout or installed skin source. - Player preferences such as volume, captions language, speed, and quality are not persisted automatically (#944). Mux users can set
source.playback.defaultSubtitlesLangso the HLS playlist sets the default subtitle language. For application-managed persistence, follow Remember user preferences. - Debug mode has no equivalent yet (#1406).
See also
- Mux Video and Mux Audio
- Mux Data
- Skins and Customize skins