Porting a Video.js 8 player to v10: the parts that actually break
TL;DR Video.js 10 went GA on October 1, 2026. There is no plugin system, VHS is gone, and videojs() does not exist. We port a real v8 player step by step: packages, markup, HLS engine, options, events, CSS, and the plu
TL;DR
Video.js 10 went GA on October 1, 2026. There is no plugin system, VHS is gone, and
videojs()does not exist. We port a real v8 player step by step: packages, markup, HLS engine, options, events, CSS, and the plugins that have no v10 path yet.
The player we are porting is the one most of us have: a <video class="video-js"> element, an HLS source, a couple of custom control bar buttons, some brand CSS, and videojs-contrib-eme for DRM. Everything below is from the official v8 migration guide, which opens by saying "This is a rebuild, not a drop-in update." Believe it.
1. Install the right package π¦
v10 does not ship as video.js. It ships as scoped packages, and you pick one based on your framework.
# vanilla / web components
npm install @videojs/html
# React
npm install @videojs/react
# plus one media adapter per source type you support
npm install @videojs/hlsjs-video
added 14 packages in 3s
β οΈ Note:
npm install video.jsstill gives you v8. Thevideo.jspackage publishes v10 under npm'snexttag, and unversioned CDN URLs also still resolve to v8. That is deliberate, and it means nothing breaks until you opt in.
Other adapters exist for other sources: @videojs/dash-video, @videojs/shaka-video (DASH with DRM), @videojs/mux-video, @videojs/youtube-video, @videojs/vimeo-video, @videojs/wistia-video.
For CDN use:
<script type="module" src="https://cdn.jsdelivr.net/npm/@videojs/[email protected]/video.js"></script>
2. Delete the videojs() call
This is the step that reframes everything else. v10 "does not scan the page for players," so there is no class="video-js", no data-setup, and no constructor.
Before:
<!-- index.html (v8) -->
<video id="player" class="video-js" controls data-setup='{}' poster="/poster.jpg">
<source src="/vod/stream.m3u8" type="application/x-mpegURL">
<track kind="chapters" src="/vod/chapters.vtt">
</video>
<script>
const player = videojs('player', {
fluid: true,
playbackRates: [1, 1.5, 2],
});
</script>
After:
<!-- index.html (v10) -->
<video-player content-title="Episode 4" poster="/poster.jpg" style="aspect-ratio: 16 / 9">
<video-skin>
<hlsjs-video src="/vod/stream.m3u8" default-playback-rates="1,1.5,2">
<track kind="chapters" src="/vod/chapters.vtt" default>
</hlsjs-video>
</video-skin>
</video-player>
// main.js (v10)
import '@videojs/html/video/player';
import '@videojs/html/video/skin';
import '@videojs/hlsjs-video';
Three things to notice. There is no controls attribute, because the skin draws the controls. The fluid: true option is gone, replaced by plain CSS (aspect-ratio, or width: 100% on the skin). And the chapters track needs default, otherwise the browser never loads the cues. Thumbnail tracks need it too.
3. Pick an HLS engine π₯
In v8, HLS "just worked" because videojs-http-streaming shipped inside the bundle. VHS is not carried over, so you now choose an engine per source. This is the single biggest decision in the port.
| Engine | Element | Good for | Does not do |
|---|---|---|---|
| hls.js | <hlsjs-video> |
Drop-in replacement for VHS behaviour, widest format coverage | hls.js tuning is its own API, not Video.js options |
| SPF | <hls-video> |
Smallest bundle, fMP4 / CMAF | MPEG-TS segments, raw ADTS AAC, whole-segment AES-128, DVR, LL-HLS |
| Native | native HLS video | Safari and anywhere the platform plays HLS itself | No control over the engine internals |
SPF is the "streaming engine composition framework" that the GA post credits for much of the bundle saving, and the project is explicit that it does not yet support ads or true low-latency live. So:
// pick-engine.js
// Still shipping MPEG-TS segments? Use hls.js, not SPF.
// Already on fMP4/CMAF and want the smallest bundle? Try SPF.
π‘ Tip: run
curl -s https://your.cdn/stream.m3u8 | head -20and look at your segment extensions before you choose. If you see.ts, SPF is not an option for that stream today.
Your VHS tuning does not come with you either. The guide states that html5.vhs.* options have no direct equivalents, and that rendition capping and bandwidth hints are engine-specific now. One mapping does exist:
<!-- v8: html5: { vhs: { withCredentials: true } } -->
<hlsjs-video src="/vod/stream.m3u8" crossorigin="use-credentials"></hlsjs-video>
4. Port your options
Most v8 options have a v10 equivalent; they just moved to attributes or CSS.
| v8 option | v10 |
|---|---|
sources: [{src, type}] |
src attribute, or <source> children |
posterImage: false |
poster="" on the player |
title (TitleBar) |
content-title on <video-player>
|
fluid: true |
width: 100% on the skin |
aspectRatio: '16:9' |
aspect-ratio: 16 / 9 |
playbackRates |
defaultPlaybackRates |
language, videojs.addLanguage
|
<media-i18n lang="..."> and the i18n registry |
inactivityTimeout: 0 |
visibility="always" |
children, controlBar
|
No equivalent. Use a skin, or edit the skin source |
Two with sharp edges. inactivityTimeout as a duration is not configurable at all right now (issue #1728), only the always-visible case maps. And autoplay is native-only: autoplay: 'muted' becomes both autoplay and muted attributes, while autoplay: 'any' needs app code that calls play() on loadstart and handles the rejection yourself.
5. Port your events and methods
The player object is no longer the event hub. Events come off the media element, and UI state comes from a store.
// v8
player.on('timeupdate', () => report(player.currentTime()));
player.on('useractive', showBranding);
player.currentTime(10);
player.src({ src: next, type: 'application/x-mpegURL' });
player.dispose();
// v10
const media = document.querySelector('hlsjs-video');
media.addEventListener('timeupdate', () => report(media.currentTime));
// useractive / userinactive -> userActive and controlsVisible state
media.currentTime = 10; // or seek(10) through the store
media.src = next; // set src on the media
videoPlayerEl.remove(); // dispose = remove the element
A few more worth writing on a sticky note:
-
player.tech()is now the media'sengineproperty. -
videojs.getPlayer(id)is gone. Hold an element reference. -
playerresizehas no equivalent. Use aResizeObserver. -
texttrackchangebecomestextTrackListstate. -
setVolume(value)now also unmutes when the value is above zero. - Fullscreen must be called from a user gesture.
6. Port your CSS to custom properties
If your brand styling was selector surgery, this part is a relief.
/* v8 */
.video-js .vjs-play-progress { background-color: #c2410c; }
.video-js .vjs-big-play-button { border-radius: 8px; }
/* v10 */
video-player {
--media-accent-color: #c2410c;
--media-accent-text-color: #fff;
--media-border-radius: 8px;
--media-scale-unit: 1.1;
}
For anything deeper than tokens, v10's answer is that the skin is yours: the Shadcn CLI can add the skin source to your project, so the layout, controls and styles become editable files. Four skins ship today (Default, Neutral, Compat and Scaffold), the Compat skin is what extends browser compatibility back to Safari 16, and runtime theme switching is not supported.
7. The plugins you cannot port yet β οΈ
Run this inventory before you commit to a migration date:
- [ ] Does your player load an ads / IMA plugin? There is no v10 equivalent.
- [ ] Do you ship a playlist? Not supported.
- [ ] A chapters menu? Not implemented. (HLS JSON chapters did arrive in 10.1.0.)
- [ ] Cue points? Not implemented (#1442).
- [ ] Persisted user preferences such as quality or captions choice? Not persisted automatically (#944).
- [ ] Live with
streamTypeorliveTrackertuning, or a time slider on live? Not there yet (#1730). - [ ]
textTrackSettingsorspatialNavigation? No equivalents. - [ ] Custom DRM
getLicensecallbacks?keySystemsbecomessource.drmkeyed by EME key system ID, and the guide says custom license auth does not transfer automatically.
Anything you wrote yourself is a rewrite as a component, because v10 has no videojs.registerPlugin, no player.myPlugin(), and no plugin lifecycle. That is called out as deliberate: "No plugin system, by design."
8. Keep v8 around
There is no codemod and no shim, so plan for both versions coexisting for a while.
# pin the old player while you port
npm install video.js@8
v8 is on a security-only maintenance policy, lives at github.com/videojs/videojs-v8, and keeps its docs at legacy.videojs.org. For the port itself, v10 ships agent tooling instead of a codemod:
npx @videojs/cli agents skills
That installs a skill so a coding assistant writes v10 instead of confidently writing v8 at you, which given the size of the API change is more useful than it sounds.
What's next
Two follow-ups worth doing before you ship. First, benchmark your own bundle rather than trusting the headline: the 60 percent reduction quoted in the GA post is against v8's default bundle including adaptive streaming, and your number depends entirely on which adapters you import. Second, read the v8 migration guide end to end, including the gaps list, before you give anyone a date.
If you are on Media Chrome, Vidstack, Plyr or Mux Player, the same rebuild underlies all of them, and each has its own migration guide linked from the v10 announcement. Worth a look if you were considering a switch anyway. #webdev #javascript
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.