Migrating from video.js
video.js has been the standard open-source video player for over a decade, powering countless production deployments. Vide takes a different approach — not better or worse, but optimized for a different set of priorities.
Instead of bundling everything into a monolithic core, Vide wraps the native <video> element with a typed event bus and state machine, and leaves everything else to explicit plugins. The result is a smaller, tree-shakeable player where you only load what you use.
If you're coming from video.js 7.x or 8.x, this guide maps the APIs you already know to their Vide equivalents.
At a Glance
video.js 8.x — monolithic, config-driven:
import videojs from "video.js";
import "video.js/dist/video-js.css";
const player = videojs("my-video", {
controls: true,
autoplay: false,
preload: "auto",
sources: [{ src: "stream.m3u8", type: "application/x-mpegURL" }],
});Vide — modular, plugin-driven:
import { createPlayer } from "@videts/vide";
import { hls } from "@videts/vide/hls";
import { ui } from "@videts/vide/ui";
import "@videts/vide/ui/theme.css";
const player = createPlayer(document.querySelector("video")!);
player.use(hls());
player.use(ui({ container: document.getElementById("player-container")! }));
player.src = "stream.m3u8";video.js uses a config object to declare behavior. Vide uses native <video> attributes for behavior and player.use() for capabilities.
Core Player
video.js uses dual-purpose methods — player.currentTime() returns the value, player.currentTime(30) sets it. Vide uses native JS properties.
| video.js | Vide | Notes |
|---|---|---|
videojs(element, options) | createPlayer(element) | No config object — use plugins |
player.src({ src, type }) | player.src = url | Type auto-detected by source handlers |
player.currentTime() / player.currentTime(30) | player.currentTime / player.currentTime = 30 | Property, not method |
player.duration() | player.duration | Property, not method |
player.volume(0.5) | player.volume = 0.5 | Property, not method |
player.muted(true) | player.muted = true | Property, not method |
player.playbackRate(2) | player.playbackRate = 2 | Property, not method |
player.dispose() | player.destroy() | Renamed |
player.el() | player.el | Property, not method |
player.on("timeupdate", fn) | player.on("timeupdate", fn) | Same API |
player.ready(fn) | player.on("statechange", fn) | Check player.state for current state |
| N/A | player.state | No video.js equivalent — use the state machine |
State Machine
video.js tracks state implicitly through CSS classes and events. Vide uses an explicit state machine:
idle → loading → ready → playing ⇄ paused → ended
↘ ad:loading → ad:playing ⇄ ad:paused ↗player.on("statechange", ({ from, to }) => {
console.log(`${from} → ${to}`);
});
// Read current state at any time
if (player.state === "playing") { /* ... */ }Events
| video.js | Vide | Notes |
|---|---|---|
player.on("event", fn) | player.on("event", fn) | Same for both custom and native events |
player.one("event", fn) | player.once("event", fn) | Renamed |
player.off("event", fn) | player.off("event", fn) | Same |
player.trigger("custom") | player.emit("custom", data) | Renamed, requires data argument |
component.on("statechanged", fn) | player.on("statechange", fn) | video.js uses per-event (play, pause, etc.); Vide unifies via state machine |
player.on("loadedmetadata", fn) | player.on("loadedmetadata", fn) | Native events work with on() |
Vide's on() handles both custom events (statechange, error, ad events) and native <video> events (loadedmetadata, volumechange, etc.). You can also use player.addEventListener() for native events — both work.
HTML Attributes
video.js requires a config object for basic video behavior. Vide uses native HTML attributes directly.
video.js:
const player = videojs("my-video", {
autoplay: true,
muted: true,
loop: true,
preload: "auto",
poster: "poster.jpg",
});Vide:
<video src="video.mp4" autoplay muted loop preload="auto" poster="poster.jpg"></video>const player = createPlayer(document.querySelector("video")!);
// Attributes are already applied by the browser. No translation layer.The <video> element is the config.
Streaming: HLS & DASH
video.js 7+ includes VHS (Video.js HTTP Streaming) in core, which handles both HLS and DASH automatically. Vide uses separate plugins that wrap hls.js and dashjs.
video.js 8.x:
// VHS is built-in — just set the source
const player = videojs("my-video");
player.src({ src: "stream.m3u8", type: "application/x-mpegURL" });
// Quality levels (built-in since 8.x via videojs-contrib-quality-levels)
const levels = player.qualityLevels();
levels.on("addqualitylevel", (event) => {
console.log(event.qualityLevel);
});Vide:
import { createPlayer } from "@videts/vide";
import { hls } from "@videts/vide/hls";
const player = createPlayer(document.querySelector("video")!);
player.use(hls());
player.src = "stream.m3u8"; // type auto-detected from .m3u8 extension
// Quality levels are built-in
player.on("qualitiesavailable", ({ qualities }) => {
console.log(qualities); // { id, width, height, bitrate, label }[]
});
player.setQuality(2); // select specific level
player.setQuality(-1); // autoFor DASH, replace hls with dash:
import { dash } from "@videts/vide/dash";
player.use(dash());
player.src = "stream.mpd";DRM
video.js + videojs-contrib-eme:
import "videojs-contrib-eme";
const player = videojs("my-video");
player.eme();
player.src({
src: "stream.m3u8",
type: "application/x-mpegURL",
keySystems: {
"com.widevine.alpha": {
url: "https://license.example.com/widevine",
},
},
});Vide:
import { createPlayer } from "@videts/vide";
import { hls } from "@videts/vide/hls";
import { drm } from "@videts/vide/drm";
const player = createPlayer(document.querySelector("video")!);
player.use(hls());
player.use(drm({
widevine: { licenseUrl: "https://license.example.com/widevine" },
fairplay: {
licenseUrl: "https://license.example.com/fairplay",
certificateUrl: "https://certificate.example.com/fairplay.cer",
},
}));
player.src = "stream.m3u8";See DRM plugin docs for the full options reference.
Text Tracks
video.js:
player.addRemoteTextTrack({
kind: "subtitles",
src: "subs-en.vtt",
srclang: "en",
label: "English",
});
const tracks = player.textTracks();Vide:
<video src="video.mp4">
<track kind="subtitles" src="subs-en.vtt" srclang="en" label="English">
</video>const player = createPlayer(document.querySelector("video")!);
// Or add programmatically
player.addTextTrack({ kind: "subtitles", src: "subs-en.vtt", srclang: "en", label: "English" });
player.on("texttracksavailable", ({ tracks }) => console.log(tracks));
player.setTextTrack(tracks[0]); // activate a trackSee Text Tracks guide for the full API.
Ads
video.js ad integration typically requires videojs-contrib-ads + videojs-ima (Google IMA SDK wrapper, ~200 KB). Vide offers two approaches: a native VAST parser with no external SDK dependency, or an IMA SDK bridge for ad servers that require it.
VAST plugin (recommended)
The VAST plugin parses and plays VAST XML directly — no external SDK, lighter bundle, full control over ad rendering and UI.
video.js + IMA:
import "videojs-contrib-ads";
import "videojs-ima";
const player = videojs("my-video");
player.ima({ adTagUrl: "https://example.com/vast.xml" });Vide:
import { createPlayer } from "@videts/vide";
import { vast } from "@videts/vide/vast";
const player = createPlayer(document.querySelector("video")!);
player.use(vast({ tagUrl: "https://example.com/vast.xml" }));
player.on("ad:start", ({ adId }) => console.log("Ad started:", adId));
player.on("ad:end", ({ adId }) => console.log("Ad ended:", adId));For scheduled ad breaks (pre-roll, mid-roll, post-roll), use VMAP:
import { vmap } from "@videts/vide/vmap";
player.use(vmap({ url: "https://example.com/vmap.xml" }));VMAP handles VAST resolution internally — no separate VAST import needed.
IMA plugin
If your ad server or SSP requires IMA SDK integration (e.g., Google Ad Manager, AdSense for Video), use the IMA plugin instead. IMA controls its own ad UI, rendering, and tracking — vide acts as a thin bridge.
video.js + IMA:
import "videojs-contrib-ads";
import "videojs-ima";
const player = videojs("my-video");
player.ima({ adTagUrl: "https://example.com/vast.xml" });Vide:
import { createPlayer } from "@videts/vide";
import { ima } from "@videts/vide/ima";
const video = document.querySelector("video")!;
const container = video.parentElement!; // position: relative wrapper
const player = createPlayer(video);
player.use(ima({
adTagUrl: "https://example.com/vast.xml",
adContainer: container,
}));The IMA SDK is loaded automatically — no <script> tag needed. See IMA plugin docs for VMAP/Ad Rules, layout structure, and framework integration.
See Ads Setup guide for ad pods, companions, SSAI, OMID, and SIMID.
Error Handling
video.js:
player.on("error", () => {
const err = player.error();
console.error(err.code, err.message);
});Vide:
player.on("error", ({ code, message, source, recoverable, retryCount }) => {
if (recoverable) {
console.log(`Auto-retrying (attempt ${retryCount})...`);
} else {
console.error(`[${source}] ${code}: ${message}`);
}
});Vide errors are typed with code, message, source (which plugin produced the error), and optional recoverable / retryCount fields. HLS and DASH plugins handle automatic recovery for transient network errors.
UI
video.js bundles UI into core. Vide keeps UI as an optional plugin.
video.js:
const player = videojs("my-video", {
controls: true,
autoplay: false,
preload: "auto",
});Vide:
import { createPlayer } from "@videts/vide";
import { ui } from "@videts/vide/ui";
import "@videts/vide/ui/theme.css";
const player = createPlayer(document.querySelector("video")!);
player.use(ui({ container: document.getElementById("player-container")! }));Custom Skins
video.js uses a monolithic CSS override approach. Vide uses BEM classes and CSS custom properties:
/* video.js */
.video-js .vjs-play-progress { background: red; }
/* Vide */
.vide-progress__bar { background: red; }A single design token controls all accent uses across the player:
/* video.js — override individual nested selectors */
.video-js .vjs-play-progress { background: #3b82f6; }
.video-js .vjs-volume-level { background: #3b82f6; }
.video-js .vjs-big-play-button { border-color: #3b82f6; }
/* Vide — single token */
:root { --vide-accent: #3b82f6; }State Classes
| video.js | Vide |
|---|---|
.vjs-playing | .vide-ui--playing |
.vjs-paused | .vide-ui--paused |
.vjs-ended | .vide-ui--ended |
.vjs-waiting | .vide-ui--buffering |
.vjs-error | .vide-ui--error |
.vjs-ad-playing (contrib-ads) | .vide-ui--ad-playing |
See UI Design Tokens for the full reference.
Framework Migration
React
video.js in React — manual lifecycle management:
import { useRef, useEffect } from "react";
import videojs from "video.js";
import "video.js/dist/video-js.css";
function Player({ src }: { src: string }) {
const videoRef = useRef<HTMLVideoElement>(null);
const playerRef = useRef<ReturnType<typeof videojs>>(null);
useEffect(() => {
playerRef.current = videojs(videoRef.current!, { controls: true });
playerRef.current.src({ src, type: "application/x-mpegURL" });
return () => playerRef.current?.dispose();
}, []);
return <video ref={videoRef} className="video-js" />;
}Vide in React — hooks handle lifecycle:
import { useVidePlayer, useHls, Vide } from "@videts/vide/react";
import "@videts/vide/ui/theme.css";
function Player({ src }: { src: string }) {
const player = useVidePlayer();
useHls(player);
return (
<Vide.Root player={player}>
<Vide.UI>
<Vide.Video src={src} />
<Vide.Controls>
<Vide.PlayButton />
<Vide.Progress />
<Vide.TimeDisplay />
<Vide.Volume />
<Vide.FullscreenButton />
</Vide.Controls>
</Vide.UI>
</Vide.Root>
);
}See the full React guide.
Vue
video.js in Vue — manual initialization in onMounted:
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import videojs from "video.js";
const videoEl = ref(null);
let player;
onMounted(() => {
player = videojs(videoEl.value, { controls: true });
player.src({ src: "stream.m3u8", type: "application/x-mpegURL" });
});
onUnmounted(() => player?.dispose());
</script>
<template>
<video ref="videoEl" class="video-js" />
</template>Vide in Vue — composables handle lifecycle:
<script setup lang="ts">
import {
useVidePlayer, useHls,
VideUI, VideVideo, VideControls, VidePlayButton, VideProgress,
} from "@videts/vide/vue";
import "@videts/vide/ui/theme.css";
const player = useVidePlayer();
useHls(player);
</script>
<template>
<VideUI>
<VideVideo src="stream.m3u8" />
<VideControls>
<VidePlayButton />
<VideProgress />
</VideControls>
</VideUI>
</template>See the full Vue guide.
Svelte
Vide in Svelte — functions and components:
<script lang="ts">
import {
createVidePlayer, useHls,
VideUI, VideVideo, VideControls, PlayButton, Progress,
} from "@videts/vide/svelte";
import "@videts/vide/ui/theme.css";
const player = createVidePlayer();
useHls(player);
</script>
<VideUI>
<VideVideo src="stream.m3u8" />
<VideControls>
<PlayButton />
<Progress />
</VideControls>
</VideUI>See the full Svelte guide.
Cleanup
| video.js | Vide |
|---|---|
player.dispose() | player.destroy() |
player.destroy() calls all plugin cleanup functions, removes event listeners, and clears internal state.
In React, Vue, and Svelte, the framework hooks (useVidePlayer / createVidePlayer) handle destroy() automatically on component unmount.
Plugins
| video.js | Vide | Size |
|---|---|---|
VHS (built-in in 8.x) / videojs-http-streaming | @videts/vide/hls | 1.4 KB (+ hls.js) |
VHS (built-in in 8.x) / videojs-contrib-dash | @videts/vide/dash | 1.4 KB (+ dashjs) |
videojs-contrib-eme | @videts/vide/drm | 2.6 KB |
videojs-contrib-ads + videojs-ima | @videts/vide/vast | 7.9 KB |
| N/A (ad SDK handles) | @videts/vide/vmap | 8.8 KB |
| N/A (ad SDK handles) | @videts/vide/ssai | 2.3 KB |
| N/A (ad SDK handles) | @videts/vide/omid | 1.7 KB |
| N/A (ad SDK handles) | @videts/vide/simid | 2.4 KB |
| N/A | @videts/vide/vpaid | 2.1 KB |
videojs-contrib-ads + videojs-ima | @videts/vide/ima | 3.4 KB |
| Built-in | @videts/vide/ui | 5.7 KB |
| Built-in | @videts/vide/ui/theme.css | 4.6 KB |
video.js bundles streaming, UI, and core into one package. Vide ships each as a separate entry point — import only what you use.
Migration Checklist
- Replace
videojs(el, options)withcreatePlayer(el) - Convert getter/setter methods to properties (
currentTime()→currentTime) - Move video config to native
<video>HTML attributes - Add
hls()ordash()plugin if streaming is needed - Move DRM from
videojs-contrib-emetodrm()plugin - Replace IMA /
videojs-contrib-adswithvast(),vmap(), orima()if ad server requires IMA SDK - Update event methods (
one→once,trigger→emit) - Replace
.dispose()with.destroy() - Add
ui()plugin if you need player controls (not bundled in core) - Update CSS selectors (
.vjs-*→.vide-*, or use design tokens) - For React / Vue / Svelte: replace manual
useEffect+disposewith framework hooks