Skip to content

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 to React components. Keep the playback sources and interactions your app needs. Rebuild each control’s visible contents and styles.

AI Quickstart

Paste this prompt into your coding agent:

Migrate this project's Media Chrome 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-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/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.

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.

npm install @videojs/react

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 components. The player owns state and renders nothing. The container holds the media and controls. Put the media directly inside the container.

<!-- Media Chrome -->
<media-controller>
  <video slot="media" src="video.mp4"></video>
  <media-control-bar>
    <media-play-button></media-play-button>
  </media-control-bar>
</media-controller>

Media Chrome’s React package wraps its custom elements. @videojs/react ships native components instead. VideoPlayer comes from the video preset, and Container holds the media and controls:

'use client';

import { Container, Controls, PlayButton } from '@videojs/react';
import { Video, VideoPlayer } from '@videojs/react/video';
import './player.css';

export function MyPlayer() {
  return (
    <VideoPlayer>
      <Container className="app-player-container">
        <Video className="app-player-media" src="video.mp4" playsInline />
        <Controls.Root>
          <Controls.Content className="app-player-controls">
            <PlayButton
              render={(props, state) => (
                <button {...props} className="player-play-button">
                  {state.paused ? 'Play' : 'Pause'}
                </button>
              )}
            />
          </Controls.Content>
        </Controls.Root>
      </Container>
    </VideoPlayer>
  );
}

Create player.css with the rules below for layout, button styling, and control visibility. If your framework does not allow global CSS imports in components, import player.css from the entry point your app uses for global styles. Alternatively, add these rules to your app’s global stylesheet and remove the ./player.css import:

.app-player-container {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
}

.app-player-media {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.app-player-controls {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  display: flex;
  padding: 0.75rem;
  background: rgb(0 0 0 / 70%);
}

.app-player-controls:not([data-visible]) {
  visibility: hidden;
  pointer-events: none;
}

.player-play-button {
  padding: 0.5rem;
  color: white;
  cursor: pointer;
  background: #222;
  border: 1px solid white;
  border-radius: 0.25rem;
}

.player-play-button:focus-visible {
  outline: 2px solid white;
  outline-offset: 2px;
}

A preset player has a fixed set of features. For a custom set, call createPlayer once outside your React component. Pass it the feature list, then use the Player component and typed usePlayer hook it returns.

import { createPlayer } from '@videojs/react';
import { videoFeatures } from '@videojs/react/video';

const { Player } = createPlayer({ features: videoFeatures });

Read player state with usePlayer, useMedia, and useStore. Player hooks must run in a descendant component rendered inside VideoPlayer or the Player returned by createPlayer. A hook called in the component that renders VideoPlayer or Player is outside that provider. Call the hook from a child component.

Map controller attributes

Media Chrome configures behavior through <media-controller> attributes. Video.js v10 has no single controller component, 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.
  • Container and Controls — layout, focus, and autohide.
  • Dedicated components — Hotkey, Gesture, and similar components, rendered inside the player.
Media Chrome attribute Video.js v10 How
audio the audio preset Choose an audio player and audio skin rather than toggling at runtime.
autohide Controls (built in) Controls hide after inactivity by default. For autohide="-1", set visibility="always" on Controls.Root. The delay is not configurable yet; see Known gaps.
fullscreenelement Container Fullscreen targets the container element.
gesturesdisabled disabled on each Gesture, or omit it Media Chrome disables all gestures at once. In Video.js, set disabled on each Gesture or omit it.
nohotkeys disabled on each Hotkey, or omit it Media Chrome disables all hotkeys at once. In Video.js, set disabled on each Hotkey or omit it.
hotkeys="noarrowleft …" individual Hotkey Each shortcut is its own component, so remove the ones you don’t want.
keyboardforwardseekoffset, keyboardbackwardseekoffset value on Hotkey Set the signed seek amount per component.
defaultsubtitles <track default> for authored tracks Add default to an authored captions or subtitles track. For tracks supplied by a provider or manifest, Video.js has no player-level setting that picks the default.
defaultstreamtype streamType feature Partial support. Video.js reads the stream type from the media. You cannot set a default stream type for use before the media loads.
liveedgeoffset, seektoliveoffset live media engine Live-edge behavior lives in the playback engine, not a UI attribute.
lang I18nProvider Wrap the player in I18nProvider and pass locale. See Internationalize the player.

Hotkeys

Media Chrome toggles keyboard shortcuts with nohotkeys and hotkeys. Video.js declares each shortcut as its own component, so you opt in to exactly the keys you want and set seek offsets inline:

import { Hotkey } from '@videojs/react';

<>
  <Hotkey keys="Space" action="togglePaused" />
  <Hotkey keys="m" action="toggleMuted" />
  <Hotkey keys="ArrowRight" action="seekStep" value={5} />
  <Hotkey keys="ArrowLeft" action="seekStep" value={-5} />
</>

In a custom layout, render these components inside the player’s Container. With installed skin source, edit its existing hotkey declarations.

To drop a shortcut, remove its component or pass disabled. There’s no global “all hotkeys off” switch; omit the components you don’t need.

Gestures

Declare each tap or double-tap behavior with a Gesture component. For example, declare a tap that toggles playback, or a double-tap that seeks or enters fullscreen:

import { Gesture } from '@videojs/react';

<>
  <Gesture type="tap" action="togglePaused" pointer="mouse" region="center" />
  <Gesture type="doubletap" action="seekStep" value={-10} region="left" />
  <Gesture type="doubletap" action="seekStep" value={10} region="right" />
</>

In a custom layout, render these components inside the player’s Container. With installed skin source, edit its existing gesture declarations. A packaged skin already declares its own gestures, so do not add duplicates.

Remove a component or pass disabled to turn a gesture off.

Map the elements

Most names carry over as PascalCase components without the media- prefix. Start with these name changes:

Media Chrome Video.js v10 Note
media-controller a player plus Container one element becomes two: state and layout
media-control-bar Controls.Root, Controls.Group grouping
media-time-range TimeSlider “range” becomes “slider”
media-volume-range VolumeSlider “range” becomes “slider”
media-time-display, media-duration-display Time.Value one component; set type to choose the value
media-loading-indicator BufferingIndicator rename
media-poster-image Poster rename, and the image source moves — see below
media-seek-backward-button, media-seek-forward-button SeekButton one component, direction via signed seconds
media-rendition-menu QualityRadioGroup inside Menu
media-captions-menu CaptionsRadioGroup inside Menu
media-playback-rate-menu PlaybackRateRadioGroup inside Menu
media-audio-track-menu AudioTrackRadioGroup inside Menu

Unchanged names, in PascalCase: PlayButton, MuteButton, FullscreenButton, PiPButton, AirPlayButton, CastButton, CaptionsButton, PlaybackRateButton, Tooltip, and Thumbnail.

React buttons do not include visible content either. Use a button’s render prop to add it, as shown in Map the controller. Build sliders from parts such as TimeSlider.Track, TimeSlider.Fill, and TimeSlider.Thumb, then add your own CSS.

Compound parts are namespaced under their component, such as TimeSlider.Track. See the UI components concept for the full set.

Poster also works differently. Put the poster URL on VideoPlayer; Poster.Root owns its state and Poster.Image accepts image attributes such as alt. If you set srcSet on Poster.Image, it overrides the player poster URL. Pair srcSet with sizes for responsive images.

Inside your custom player’s container, render the poster beside its media and controls:

import { Poster } from '@videojs/react';

<Poster.Root>
  <Poster.Image alt="" />
</Poster.Root>

Rewrite your styles

Media Chrome reflects state in attributes such as mediapaused. Video.js uses data-* attributes such as data-paused.

/* Media Chrome */
media-play-button[mediapaused] .play-icon { display: inline; }
/* Video.js React: target the className on your rendered button */
.player-play-button[data-paused] { /* your paused-button styles */ }

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.

Media Chrome theme variable Packaged Video.js skin Migration
--media-accent-color or --media-range-bar-color --media-accent-color Closest match for slider fills and accented controls.
--media-primary-color no direct equivalent Often colors every control icon in Media Chrome; Video.js’s accent variable colors fewer parts of the skin.
--media-secondary-color no direct equivalent Add the skin source and write CSS for this color.
--media-control-background no direct equivalent Add the skin source and author the CSS.
--media-control-hover-background no direct equivalent Add the skin source and author the CSS.
--media-range-track-height no direct equivalent Add the skin source and author the slider CSS.

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-fit and --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.

React skins do not use Shadow DOM. Use render props when you build individual controls. Add a ready-made skin’s source to your project when you need to change its controls or layout.

Control the player imperatively

Use store actions such as play, pause, seek, setVolume, requestFullscreen, and exitFullscreen to control playback and UI state.

Select an action with the same usePlayer hook you read state with:

import { usePlayer } from '@videojs/react/video';

function FullscreenShortcut() {
  const isFullscreen = usePlayer((state) => state.isFullscreen);
  const requestFullscreen = usePlayer((state) => state.requestFullscreen);
  const exitFullscreen = usePlayer((state) => state.exitFullscreen);

  return (
    <button type="button" onClick={() => isFullscreen ? exitFullscreen() : requestFullscreen()}>
      Toggle fullscreen
    </button>
  );
}

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.

For video-backed media, a ref points to the rendered HTMLVideoElement. YouTube and Vimeo refs point to an HTMLIFrameElement. To call media methods on those embeds, pass a mediaRef, which receives the playback adapter, or call useMedia() from a component inside the player.

useMedia() returns the Video.js media object. If that object exposes an engine property, use it to access the playback engine.

Known gaps

These Media Chrome features have no direct equivalent yet. Several can be approximated; see Workarounds.

  • No configurable autohide delay and no autohideovercontrols (#1728). To disable autohide (autohide="-1"), set visibility="always" on the controls component; see Workarounds.
  • No defaultduration placeholder before the media loads (#1729)
  • Volume and muted preferences are not persisted across sessions, so Media Chrome’s novolumepref and nomutedpref opt-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 breakpoints or container-breakpoint attributes; use CSS container queries instead
  • No seektoliveoffset or noautoseektolive controls, 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, chapterchange event, 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 TimeSlider.Chapters and TimeSlider.ChapterTitle.

Workarounds

Disable autohide (autohide="-1")

Set visibility="always" on Controls.Root to keep the controls visible regardless of activity. Packaged skins do not expose this prop. To keep a packaged skin’s look, add its source to your project and set visibility="always" on its controls component.

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:

.app-player-container {
  container-type: inline-size;
  container-name: app-player;
}

@container app-player (width > 40rem) {
  .app-player-controls { /* your wide layout */ }
}

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.

See also