Skip to content

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:

js
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:

ts
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.jsVideNotes
videojs(element, options)createPlayer(element)No config object — use plugins
player.src({ src, type })player.src = urlType auto-detected by source handlers
player.currentTime() / player.currentTime(30)player.currentTime / player.currentTime = 30Property, not method
player.duration()player.durationProperty, not method
player.volume(0.5)player.volume = 0.5Property, not method
player.muted(true)player.muted = trueProperty, not method
player.playbackRate(2)player.playbackRate = 2Property, not method
player.dispose()player.destroy()Renamed
player.el()player.elProperty, 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/Aplayer.stateNo 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 ↗
ts
player.on("statechange", ({ from, to }) => {
  console.log(`${from} → ${to}`);
});

// Read current state at any time
if (player.state === "playing") { /* ... */ }

Events

video.jsVideNotes
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:

js
const player = videojs("my-video", {
  autoplay: true,
  muted: true,
  loop: true,
  preload: "auto",
  poster: "poster.jpg",
});

Vide:

html
<video src="video.mp4" autoplay muted loop preload="auto" poster="poster.jpg"></video>
ts
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:

js
// 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:

ts
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); // auto

For DASH, replace hls with dash:

ts
import { dash } from "@videts/vide/dash";
player.use(dash());
player.src = "stream.mpd";

DRM

video.js + videojs-contrib-eme:

js
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:

ts
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:

js
player.addRemoteTextTrack({
  kind: "subtitles",
  src: "subs-en.vtt",
  srclang: "en",
  label: "English",
});
const tracks = player.textTracks();

Vide:

html
<video src="video.mp4">
  <track kind="subtitles" src="subs-en.vtt" srclang="en" label="English">
</video>
ts
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 track

See 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.

The VAST plugin parses and plays VAST XML directly — no external SDK, lighter bundle, full control over ad rendering and UI.

video.js + IMA:

js
import "videojs-contrib-ads";
import "videojs-ima";

const player = videojs("my-video");
player.ima({ adTagUrl: "https://example.com/vast.xml" });

Vide:

ts
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:

ts
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:

js
import "videojs-contrib-ads";
import "videojs-ima";

const player = videojs("my-video");
player.ima({ adTagUrl: "https://example.com/vast.xml" });

Vide:

ts
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:

js
player.on("error", () => {
  const err = player.error();
  console.error(err.code, err.message);
});

Vide:

ts
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:

js
const player = videojs("my-video", {
  controls: true,
  autoplay: false,
  preload: "auto",
});

Vide:

ts
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:

css
/* 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:

css
/* 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.jsVide
.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:

tsx
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:

tsx
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:

vue
<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:

vue
<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:

svelte
<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.jsVide
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.jsVideSize
VHS (built-in in 8.x) / videojs-http-streaming@videts/vide/hls1.4 KB (+ hls.js)
VHS (built-in in 8.x) / videojs-contrib-dash@videts/vide/dash1.4 KB (+ dashjs)
videojs-contrib-eme@videts/vide/drm2.6 KB
videojs-contrib-ads + videojs-ima@videts/vide/vast7.9 KB
N/A (ad SDK handles)@videts/vide/vmap8.8 KB
N/A (ad SDK handles)@videts/vide/ssai2.3 KB
N/A (ad SDK handles)@videts/vide/omid1.7 KB
N/A (ad SDK handles)@videts/vide/simid2.4 KB
N/A@videts/vide/vpaid2.1 KB
videojs-contrib-ads + videojs-ima@videts/vide/ima3.4 KB
Built-in@videts/vide/ui5.7 KB
Built-in@videts/vide/ui/theme.css4.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) with createPlayer(el)
  • Convert getter/setter methods to properties (currentTime()currentTime)
  • Move video config to native <video> HTML attributes
  • Add hls() or dash() plugin if streaming is needed
  • Move DRM from videojs-contrib-eme to drm() plugin
  • Replace IMA / videojs-contrib-ads with vast(), vmap(), or ima() if ad server requires IMA SDK
  • Update event methods (oneonce, triggeremit)
  • 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 + dispose with framework hooks