GuideMigrate
Migrate from Media Chrome
Move a Media Chrome player to Video.js v10, mapping its controller, controls, styles, and imperative calls
Replace Media Chrome’s controller with a Video.js player and container, then migrate your controls. The libraries share media-* tag names, but their attributes, child markup, icons, and styles differ.
AI Quickstart
Paste this prompt into your coding agent:
Migrate this project's Media Chrome 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-media-chrome, 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 begin
Check Known gaps for controller options and preferences your app relies on. List your media sources, custom controls, event handlers, and theme rules.
Install Video.js and choose a preset (see Installation). If the preset’s ready-made skin has the controls you need, start with it. To keep a custom control bar, build it from the individual Video.js UI components instead.
Before loading Video.js HTML, remove every Media Chrome import and CDN script from the page, including any that another component loads. Then reload the page. Both libraries register custom elements with names such as media-play-button. A document can hold only one definition per name, and Video.js keeps any definition that already exists. If you need both libraries during migration, load them in separate documents, such as an iframe.
For a page without a bundler, load the CDN video preset. It registers the player, the UI elements and icons used in the controller example below, and the <media-i18n> element:
Some elements in the mapping table below need separate bundles. If you use any of these elements, also load its bundle:
media-seek-button:https://cdn.jsdelivr.net/npm/@videojs/cdn@10.0.1/ui/seek-button.jsmedia-playback-rate-button:https://cdn.jsdelivr.net/npm/@videojs/cdn@10.0.1/ui/playback-rate-button.jsmedia-thumbnail:https://cdn.jsdelivr.net/npm/@videojs/cdn@10.0.1/ui/thumbnail.js
Load each with <script type="module" src="…"></script>. See CDN for individual component bundles.
For a bundler installation:
Map the controller
The examples use the same MP4 source before and after migration. For HLS, DASH, or an embed, keep your existing source and choose its media component.
Media Chrome wraps a slotted <video slot="media"> in a single <media-controller>. Video.js splits that into two elements. The player owns state and renders no UI. The <media-container> holds the media and controls. Put the media directly inside the container.
Use the HTML and CSS tabs for both installation methods. With a bundler, also add the JavaScript imports. With the CDN video preset, skip those imports because the preset already registers these elements.
Map controller attributes
Media Chrome configures behavior through <media-controller> attributes. Video.js v10 has no single controller element, so these settings move to three places:
- Player features — playback behavior that comes from the player’s feature set. A preset includes a fixed feature set. With
createPlayer, you choose the feature set. <media-container>and<media-controls>— layout, focus, and autohide.- Dedicated elements —
<media-hotkey>,<media-gesture>, and similar elements, declared in markup.
Hotkeys
Media Chrome toggles keyboard shortcuts with nohotkeys and hotkeys. Video.js declares each shortcut as its own element, so you opt in to exactly the keys you want and set seek offsets inline:
In a custom layout, put these elements inside the player’s <media-container>. With installed skin source, edit its existing hotkey declarations.
To drop a shortcut, remove its element or add disabled. There’s no global “all hotkeys off” switch; omit the elements you don’t need.
Gestures
Declare each tap or double-tap behavior with a <media-gesture> element. For example, declare a tap that toggles playback, or a double-tap that seeks or enters fullscreen:
In a custom layout, put these elements inside the player’s <media-container>. With installed skin source, edit its existing gesture declarations. A packaged skin already declares its own gestures, so do not add duplicates.
Remove an element or set disabled to turn a gesture off.
Map the elements
Most names match. Start with these name changes:
Unchanged names: media-play-button, media-mute-button, media-fullscreen-button, media-pip-button, media-airplay-button, media-cast-button, media-captions-button, media-playback-rate-button, media-tooltip, and media-thumbnail.
Video.js sliders use media-slider-track, media-slider-fill, and media-slider-thumb. See media-time-slider and media-volume-slider for copy-paste markup and CSS.
Media Chrome’s named icon slots do not carry over. Remove attributes such as slot="play" and put the icon directly inside the Video.js button. With a bundler, import @videojs/html/icons/element to use <media-icon>; the CDN video preset already registers it. See the example in Map the controller. Set family="neutral" or family="compat" to use the Neutral or Compat skin’s icon designs. You can also use your own text or SVG.
media-poster also works differently. Where media-poster-image took its own src, media-poster wraps the image you bring:
Rewrite your styles
Media Chrome reflects state in attributes such as mediapaused. Video.js uses data-* attributes such as data-paused.
Values that change continuously, such as slider position, use CSS custom properties: sliders expose --media-slider-fill and --media-slider-pointer.
Map theme variables by meaning
Media Chrome and Video.js both use names beginning with --media-, but the shared prefix does not make their variables compatible. Keep a variable only when its documented meaning matches.
The packaged Video.js skins expose eight custom properties:
- Colors:
--media-accent-color,--media-accent-text-color, and--media-border-color. - Corners:
--media-border-radius. - Interface font:
--media-font-family. - Media and poster fit and position:
--media-object-fitand--media-object-position. - Control sizing:
--media-scale-unit.
See Customize skins for their meanings and defaults. Do not assume another Media Chrome variable will work because its name starts with --media-.
Themes become skins
Media Chrome’s <template>-based themes (media-theme) become Video.js skins and presets. Start from a preset, then add its skin source to your project.
Ready-made HTML skins keep their controls inside Shadow DOM. Code outside the skin sees a click as coming from the skin, not the button inside it. Some control events also stay inside the skin. Do not use an outside listener to identify which built-in button was clicked.
Keep playback event handlers on your <video> or <audio>. When you build a custom control bar, attach click and input handlers directly to those controls. With a ready-made skin, read media or player state instead of listening for clicks inside the skin.
Keep the skin when its CSS variables and content slots cover your changes. Build a custom control bar when you want to start from scratch. Add the skin source to your project when you want its existing layout and styles but need to change the controls inside it.
Control the player imperatively
Use store actions such as play, pause, seek, setVolume, requestFullscreen, and exitFullscreen to control playback and UI state.
Call actions on the player element’s store only after player.store.target is set, which means the media has attached. Fullscreen requests must run from a user interaction:
Keep native media calls such as video.play() and assignments such as video.currentTime = 30. Video.js updates player state from media events. Set src on the media component to change content, and replace the component when the source needs a different playback engine.
Known gaps
These Media Chrome features have no direct equivalent yet. Several can be approximated; see Workarounds.
- No configurable
autohidedelay and noautohideovercontrols(#1728). To disable autohide (autohide="-1"), setvisibility="always"on the controls component; see Workarounds. - No
defaultdurationplaceholder before the media loads (#1729) - Volume and muted preferences are not persisted across sessions, so Media Chrome’s
novolumeprefandnomutedprefopt-outs have nothing to opt out of (#944). Video.js can choose an initial subtitle track from the locale, but it does not yet remember the viewer’s later language choice (#1786). - No
breakpointsor container-breakpoint attributes; use CSS container queries instead - No
seektoliveoffsetornoautoseektolivecontrols, and live-edge offset and tolerance aren’t configurable (#1730) - Chapters render and each time-slider segment reflects
data-active, but there’s no player-level active chapter value,chapterchangeevent, or menu for jumping between chapters (#1873) - Cue points are not supported (#1442)
Chapters still work. Add a <track kind="chapters"> with the default attribute. The packaged skins then split the time slider into chapter segments and show the chapter title on hover, via media-time-slider-chapters and media-time-slider-chapter-title.
Workarounds
Disable autohide (autohide="-1")
Set visibility="always" on media-controls to keep the controls visible regardless of activity. Packaged skins do not expose this attribute. To keep a packaged skin’s look, add its source to your project and set visibility="always" on its controls element.
Breakpoints
For custom UI, add a class to your container and another to the controls you want to change. Define and query your own CSS container:
Packaged HTML skin controls are inside Shadow DOM; external selectors cannot reach them. React skin selectors are private. To change a packaged skin’s responsive layout, add its source to your project and edit the container queries in its local styles.
Default duration
There is no player input for a duration placeholder. Set preload="metadata" to request the duration before playback. The duration is available only after the network request and engine loading finish. If you defer loading, render a static placeholder in your own UI.
Preference persistence
Media Chrome remembers volume, muted state, and language across sessions. Video.js does not persist these preferences. Follow Remember user preferences to restore and save them in your app.
Actions throw before the media attaches. Wait until store.target is set, restore the saved values, and then start saving changes. For captions or quality, also wait for the relevant track or rendition list before restoring a selection.
Live edge offsets
seektoliveoffset and noautoseektolive are governed by the playback engine rather than the UI layer, so there’s no attribute to tune them today. As a workaround, build a custom live control that reads player state through selectLive and selectTime.