From bitmovin-player-web
Integrate the Bitmovin Web Player SDK into a web app. Use when the user asks to add video playback, embed a player, play HLS/DASH/MP4/WHEP/MOQ, set up DRM (Widevine/PlayReady/FairPlay), integrate ads, customize the Player UI, or work with the Bitmovin Player in any way. Covers both Player v8 (stable) and Player Web X / PWX (next-gen).
How this skill is triggered — by the user, by Claude, or both
Slash command
/bitmovin-player-web:bitmovin-player-webThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are an expert at integrating the Bitmovin Web Player SDK. When the user asks you to add video playback, embed a player, integrate streaming, customize the Player UI, or work with the Bitmovin Player — use this skill.
You are an expert at integrating the Bitmovin Web Player SDK. When the user asks you to add video playback, embed a player, integrate streaming, customize the Player UI, or work with the Bitmovin Player — use this skill.
bitmovin-player, or streaming integrationDefault to Player Web v8 unless the user explicitly asks for PWX, is evaluating PWX, or the architecture tradeoff matters.
Only ask which player they want when the answer changes the implementation, for example:
Recommended options:
Player Web v8 (bitmovin-player) — the stable production default. Supports HLS, DASH, Smooth, WHEP, DRM (Widevine/PlayReady/FairPlay), ads (VAST/VMAP/IMA), analytics, subtitles, Chromecast, AirPlay, and is the primary choice for Web Player and Player UI integrations. It also supports a range of 3rd-party integrations such as Yospace SSAI and Conviva Analytics. Mature API, full documentation.
Player Web X / PWX (@bitmovin/player-web-x) — the next-generation modular player. Still evolving and feature-incomplete. Use when the user explicitly wants PWX, wants the package architecture, or is validating PWX-specific behavior. MOQ playback is currently in this version rather than v8.
For anything this skill doesn't cover — obscure APIs, recent SDK releases, specific code samples — query the Bitmovin Docs MCP instead of guessing:
https://agentic.bitmovin.com/documentation/mcp
It indexes developer.bitmovin.com documentation and the official GitHub sample repositories (bitmovin-player-web-samples, bitmovin-player-ios-samples, bitmovin-player-android-samples, bitmovin-player-roku-samples, bitmovin-api-sdk-examples).
When to use it:
Add it as an MCP connector in the chat client, or fetch URLs from developer.bitmovin.com directly. Prefer the MCP when it's available — it's faster than fetching individual doc pages.
npm install bitmovin-player
For explicit UI customization or version pinning, also install the dedicated UI package:
npm install bitmovin-player-ui
The main package includes the player runtime and TypeScript types. The dedicated bitmovin-player-ui package ships the UI assets and type declarations for explicit UI work.
<script src="https://cdn.jsdelivr.net/npm/bitmovin-player@8/bitmovinplayer.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bitmovin-player-ui@4/dist/js/bitmovinplayer-ui.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bitmovin-player-ui@4/dist/css/bitmovinplayer-ui.css" />
These are UMD bundles that attach to global namespaces — NOT ES modules. Do not import from the CDN URL:
window.bitmovin.player.Playerwindow.bitmovin.playerui.UIFactoryFor ES module import syntax, install via npm and use a bundler.
Every Bitmovin Player instance needs a license key. localhost is auto-allowed; deployed domains must be added to the license allowlist.
Get one of these ways:
bitmovin player licenses list — lists all licenses with their keys and allowed domains. bitmovin player licenses get <id> --json for details.import { Player } from 'bitmovin-player';
const container = document.getElementById('player');
const player = new Player(container, {
key: 'YOUR_LICENSE_KEY',
playback: {
autoplay: false,
muted: false,
},
});
// Load a source
await player.load({
hls: 'https://example.com/stream.m3u8',
title: 'My Video',
poster: 'https://example.com/poster.jpg',
});
Last verified: 2026-04-15 against Bitmovin Web release notes and UI v4 docs.
For new Web SDK integrations on current v8 releases, do not assume you must wire UIFactory manually. Since Web 8.245.0 (released 2026-02-09), Bitmovin's default Web UI integration migrated to UI v4 and standard setups can use the default UI without extra wiring.
Use the explicit UI package only when you need one of these:
For explicit UI wiring, prefer the dedicated bitmovin-player-ui package because it ships type declarations.
import { Player } from 'bitmovin-player';
import { UIFactory } from 'bitmovin-player-ui';
import 'bitmovin-player-ui/dist/css/bitmovinplayer-ui.css';
const player = new Player(document.getElementById('player'), {
key: 'YOUR_LICENSE_KEY',
ui: false,
});
UIFactory.buildUI(player);
When you want to pin the hosted UI assets explicitly, configure them through PlayerConfig.location:
const player = new Player(document.getElementById('player'), {
key: 'YOUR_LICENSE_KEY',
location: {
ui: 'https://cdn.jsdelivr.net/npm/bitmovin-player-ui@4/dist/js/bitmovinplayer-ui.js',
ui_css: 'https://cdn.jsdelivr.net/npm/bitmovin-player-ui@4/dist/css/bitmovinplayer-ui.css',
},
});
If you set ui: false, you are responsible for attaching a UI yourself.
The source object tells the player what to play. At least one of hls, dash, smooth, or progressive is required.
API reference: https://developer.bitmovin.com/playback/reference/web-sdk-source-config.md
await player.load({
// Stream URL — pick one:
hls: 'https://cdn.example.com/stream.m3u8', // HLS
dash: 'https://cdn.example.com/stream.mpd', // DASH
smooth: 'https://cdn.example.com/stream.ism', // Smooth Streaming
progressive: 'https://cdn.example.com/video.mp4', // Progressive MP4/WebM
// Metadata
title: 'Video Title',
description: 'Video description',
poster: 'https://cdn.example.com/poster.jpg',
// Subtitles
subtitleTracks: [
{ url: 'https://cdn.example.com/subs_en.vtt', lang: 'en', label: 'English' },
{ url: 'https://cdn.example.com/subs_de.vtt', lang: 'de', label: 'Deutsch' },
],
// Thumbnails (for seek preview)
thumbnailTrack: { url: 'https://cdn.example.com/thumbs.vtt' },
});
The config object passed to new Player(container, config).
API reference: https://developer.bitmovin.com/playback/reference/web-sdk-player-config.md
| Field | Type | Description |
|---|---|---|
key | string | Required. License key from Bitmovin Dashboard. |
playback.autoplay | boolean | Start playback automatically. Default: false. |
playback.muted | boolean | Start muted (required for autoplay in most browsers). |
ui | boolean | Set to false when using UIFactory. |
adaptation.startupBitrate | string | Initial quality (e.g. "2000kbps"). |
network.preprocessHttpRequest | function | Modify requests before they're sent (add tokens, headers). |
network.sendHttpRequest | function | Completely replace the HTTP fetch mechanism. |
buffer.video.forwardduration | number | Forward buffer target in seconds. Default: 30. |
style.width | string | Player width. Default: '100%'. |
style.aspectratio | string | Aspect ratio. Default: '16:9'. |
API reference: https://developer.bitmovin.com/playback/reference/web-sdk-player-api.md
// Playback control
await player.load(sourceConfig);
player.play();
player.pause();
player.seek(120); // seek to 2:00
player.setVolume(50); // 0-100
// State queries
player.getCurrentTime(); // seconds
player.getDuration(); // seconds (Infinity for live)
player.isPlaying();
player.isPaused();
player.getVolume();
// Quality
player.getAvailableVideoQualities();
player.setVideoQuality('auto');
// Events
player.on('play', () => { ... });
player.on('pause', () => { ... });
player.on('timechanged', () => { ... });
player.on('error', (e) => { console.error(e.code, e.message); });
player.on('ready', () => { ... });
player.on('sourceloaded', () => { ... });
// Cleanup
player.destroy();
Add drm to the source config. The player handles license acquisition automatically.
await player.load({
dash: 'https://cdn.example.com/encrypted.mpd',
drm: {
widevine: {
LA_URL: 'https://license.example.com/widevine',
headers: { 'X-Auth': 'token123' }, // optional
},
playready: {
LA_URL: 'https://license.example.com/playready',
},
fairplay: {
LA_URL: 'https://license.example.com/fairplay',
certificateURL: 'https://license.example.com/fairplay/cert',
},
},
});
Key points:
hls + drm.fairplay; DASH with Widevine uses dash + drm.widevineSupports VAST, VMAP, and IMA SDK integration.
At minimum, configure advertising: {} if you want ad support enabled up front but plan to schedule ads dynamically later via player.ads.schedule(...).
By default, the Google IMA SDK is used for ad playback. That is often the best choice when Google Ad Manager / Google Ad Server is involved because it unlocks capabilities such as programmatic demand and ActiveView measurement. If you integrate with non-Google ad servers, consider switching to the Bitmovin Advertising Module (BAM) by importing bitmovin-player/modules/bitmovinplayer-advertising-bitmovin.js and registering that module with the player. BAM can be preferable when you want Bitmovin's full ads UI and ads UI customization options.
const player = new Player(container, {
key: 'YOUR_KEY',
advertising: {
adBreaks: [
{
tag: { url: 'https://example.com/vast-preroll.xml', type: 'vast' },
position: 'pre',
},
{
tag: { url: 'https://example.com/vast-midroll.xml', type: 'vast' },
position: '50%', // midroll at 50%; other supported formats include time-based values like '90'
},
{
tag: { url: 'https://example.com/vast-postroll.xml', type: 'vast' },
position: 'post',
},
],
},
});
For VMAP (server-defined ad schedule):
advertising: {
adBreaks: [
{ tag: { url: 'https://example.com/vmap.xml', type: 'vmap' }, position: 'pre' },
],
}
For mid-rolls, position supports multiple string formats, not just percentages. Common examples include percentage-based values such as '50%', time-based values such as '90' for 1m30s, and other formats documented here: https://developer.bitmovin.com/playback/reference/web-sdk-advertising#position
Bitmovin Analytics is bundled with the player. Enable by providing an analytics license key:
const player = new Player(container, {
key: 'YOUR_PLAYER_KEY',
analytics: {
key: 'YOUR_ANALYTICS_KEY',
title: 'My Video',
videoId: 'video-123',
customData1: 'category-sports',
},
});
Set analytics: false to disable.
const player = new Player(container, {
key: 'YOUR_KEY',
network: {
preprocessHttpRequest: (type, request) => {
if (type.startsWith('media/') || type.startsWith('manifest/')) {
request.headers['Authorization'] = 'Bearer ' + getToken();
}
return Promise.resolve(request);
},
},
});
Use sendHttpRequest to intercept ALL HTTP requests. Useful for proxying, offline playback, or running in restricted environments (like MCP App sandboxes).
network: {
sendHttpRequest: (type, request) => {
const isText = type.startsWith('manifest/');
return {
getResponse: async () => {
const resp = await myCustomFetch(request.url);
return {
request,
url: request.url,
headers: {},
status: 200,
statusText: 'OK',
body: isText ? await resp.text() : await resp.arrayBuffer(),
};
},
setProgressListener: () => {},
cancel: () => {},
};
},
}
import { useEffect, useRef } from 'react';
import { Player } from 'bitmovin-player';
function BitmovinPlayer({ source, licenseKey }) {
const containerRef = useRef<HTMLDivElement | null>(null);
const playerRef = useRef<Player | null>(null);
useEffect(() => {
if (!containerRef.current) return;
const player = new Player(containerRef.current, {
key: licenseKey,
});
playerRef.current = player;
return () => {
playerRef.current?.destroy();
playerRef.current = null;
};
}, [licenseKey]);
useEffect(() => {
const player = playerRef.current;
if (!player) return;
let cancelled = false;
(async () => {
try {
await player.load(source);
} catch (error) {
if (!cancelled) {
console.error('Bitmovin load failed', error);
}
}
})();
return () => {
cancelled = true;
};
}, [source]);
return <div ref={containerRef} />;
}
Important: Create the player once, then load new sources separately. Do not key the effect off source.hls || source.dash || source.progressive because that misses other source changes such as DRM, subtitles, poster, start offset, or analytics metadata.
The player touches window/document on import — it cannot SSR. Use dynamic import:
import dynamic from 'next/dynamic';
const BitmovinPlayer = dynamic(() => import('./BitmovinPlayer'), { ssr: false });
<template>
<div ref="container" />
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { Player } from 'bitmovin-player';
const container = ref(null);
let player = null;
onMounted(async () => {
player = new Player(container.value, { key: 'YOUR_KEY' });
await player.load({ hls: 'https://example.com/stream.m3u8' });
});
onUnmounted(() => player?.destroy());
</script>
Assuming manual UIFactory.buildUI() is required on every v8 integration — On current Web SDK releases, standard setups can use the default UI integration without manual wiring.
Using UIFactory or UIManager directly without disabling built-in UI handling — If you wire the UI yourself, set ui: false in the player config so the player does not also try to manage the default UI.
Disabling the UI without attaching one — If you set ui: false, the player will not create a default UI for you.
Not destroying on unmount — The player creates DOM elements and event listeners. Always call player.destroy() when removing the player.
Autoplay without muted — Browsers block unmuted autoplay. Set playback: { autoplay: true, muted: true } or handle the play() promise rejection.
SSR importing — The player SDK accesses window and document at import time. Always use dynamic imports with ssr: false in Next.js/Nuxt.
Missing explicit UI assets when pinning or customizing the UI — If you override location.ui / location.ui_css or wire the UI package manually, load the matching JS and CSS assets together.
Loading multiple sources without awaiting — player.load() returns a promise. Await it before calling load() again or querying state.
Hardcoding the license key — Use environment variables. The key is exposed to the browser (client-side SDK) but shouldn't be in source control.
Reference: https://developer.bitmovin.com/playback/reference/web-sdk-modules.md
For smaller bundles, import only the modules you need:
import { Player } from 'bitmovin-player/modules/bitmovinplayer-core';
import EngineBitmovinModule from 'bitmovin-player/modules/bitmovinplayer-engine-bitmovin';
import MseRendererModule from 'bitmovin-player/modules/bitmovinplayer-mserenderer';
import HlsModule from 'bitmovin-player/modules/bitmovinplayer-hls';
import AbrModule from 'bitmovin-player/modules/bitmovinplayer-abr';
import ContainerTSModule from 'bitmovin-player/modules/bitmovinplayer-container-ts';
import ContainerMP4Module from 'bitmovin-player/modules/bitmovinplayer-container-mp4';
import PolyfillModule from 'bitmovin-player/modules/bitmovinplayer-polyfill';
[EngineBitmovinModule, MseRendererModule, HlsModule, AbrModule,
ContainerTSModule, ContainerMP4Module, PolyfillModule]
.forEach(m => Player.addModule(m));
This gives you HLS playback at ~1.2MB instead of 2.2MB. Add DashModule, DrmModule, etc. as needed.
If you use a modular build at all — meaning you import at least Player from bitmovin-player/modules/bitmovinplayer-core — then do not import anything from bitmovin-player directly anywhere in that bundle. Keep all Bitmovin player imports on the bitmovin-player/modules/* path. Mixing modular imports with bitmovin-player can cause both the modular modules and the full player build to end up in the application bundle, defeating the bundle-size benefit.
When targeting TV or console platforms, check whether Bitmovin provides platform-specific modules and add them explicitly where needed. Some platform integrations are not part of the generic full player build and are expected to be included separately for the relevant target platform. Examples include Samsung Tizen TVs, LG webOS TVs, and PlayStation 5. If the user is building for one of these environments, verify the required platform module set before proposing the final integration.
Use these public streams for development and testing:
| Stream | URL | Type |
|---|---|---|
| Sintel (HLS) | https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8 | HLS |
| Sintel (DASH) | https://bitmovin-a.akamaihd.net/content/sintel/sintel.mpd | DASH |
| Art of Motion (DASH) | https://bitmovin-a.akamaihd.net/content/MI201109210084_1/mpds/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd | DASH |
| Big Buck Bunny (MP4) | https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 | Progressive |
| Tears of Steel (HLS) | https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8 | HLS |
Player Web X is Bitmovin's next-generation web player built on the Phoenix Framework — a from-scratch architecture with structured concurrency, an effect system, and a package-first design. It is modular, extensible, and produces smaller bundles than v8.
Last verified: 2026-04-15 against the PWX getting started guide, v8 compatibility guide, support matrix, and package docs.
Status: PWX is in active development and its capability surface is volatile. Use v8 for production by default unless the user explicitly needs PWX's package architecture or is validating PWX itself.
Next in the official support matrixnpm install @bitmovin/player-web-x
<!-- HLS bundle (most common) -->
<script src="https://cdn.jsdelivr.net/npm/@bitmovin/player-web-x@10/bundles/playerx-hls.js"></script>
<!-- DASH bundle (preliminary) -->
<script src="https://cdn.jsdelivr.net/npm/@bitmovin/player-web-x@10/bundles/playerx-dash.js"></script>
<!-- v8 compatibility bundle (use v8 API with PWX engine) -->
<script src="https://cdn.jsdelivr.net/npm/@bitmovin/player-web-x@10/bundles/playerx-bitmovin-v8.js"></script>
These are UMD bundles — NOT ES modules. Load via <script src>, not import. Global namespace attachments:
| Bundle | Global |
|---|---|
playerx-hls.js, playerx-dash.js, playerx-core.js | window.bitmovin.playerx.Player |
playerx-bitmovin-v8.js, playerx-bitmovin-v8-core.js | window.bitmovin.player.Player (⚠️ overwrites v8!) |
For ES module import syntax, install via npm (@bitmovin/player-web-x) and use a bundler.
The PWX API is fundamentally different from v8. Do not mix them. Minimum working example:
import { Player } from '@bitmovin/player-web-x/bundles/playerx-hls';
// CDN: const { Player } = window.bitmovin.playerx;
const player = Player({
key: 'YOUR_LICENSE_KEY',
defaultContainer: document.getElementById('player'),
});
// sources.add() returns a source HANDLE synchronously — it is NOT a promise.
// Do not `await` it. The handle is where playback control lives.
const source = player.sources.add({
resources: [{ url: 'https://example.com/stream.m3u8' }],
});
// Trigger playback via the source, not the player. Autoplay config is unreliable;
// call source.play() explicitly (muted so browsers allow it).
source.play({ isMuted: true });
// Listen for first-frame / state changes on the SOURCE, not the player.
source.events.on('playing', () => console.log('first frame rendered'));
// Teardown
player.dispose(); // NOT player.destroy() — that's a v8 name
| Concern | v8 | PWX (native) |
|---|---|---|
| Construct player | new Player(container, config) | Player({ defaultContainer, ...config }) (no new) |
| Load a stream | player.load({ hls: url }) returns a Promise | player.sources.add({ resources: [{ url }] }) returns a source handle synchronously |
| Play / pause | player.play(), player.pause() | source.play({ isMuted: true }), source.pause() — lives on the source handle, NOT the player |
| Subscribe to events | player.on('playing', fn) | source.events.on('playing', fn) OR player.events.on(...) — the player instance has no .on() |
| Dispatch commands | N/A | player.events.dispatch({ type: 'play', ... }) (event-driven architecture) |
| Autoplay config | playback: { autoplay: true, muted: true } works | Config does not autoplay — must call source.play() |
| Teardown | player.destroy() | player.dispose() |
| Player instance surface | rich API: play, pause, seek, on, load, destroy, ... | minimal: { packages, events, sources, dispose } |
DO NOT call play() on the underlying <video> element to work around missing playback. That races the player's internal start flow and produces "play() request was interrupted by a new load request." Always use source.play().
key — license key (same as v8)defaultContainer — DOM element to mount into (v8 passes this as a constructor arg; PWX puts it in config)playback.muted, playback.autoplay — present in config, but unreliable in current PWX. Prefer explicit source.play({ isMuted: true }).| Bundle | Description | Use when |
|---|---|---|
playerx-hls.js | HLS playback (TS + fMP4) | Most common use case |
playerx-dash.js | DASH playback (fMP4, preliminary) | DASH-only content |
playerx-core.js | Core only, add packages manually | Custom/minimal builds |
playerx-bitmovin-v8.js | HLS + v8 API compatibility layer | Migrating from v8 |
playerx-bitmovin-v8-core.js | Core + v8 API base compatibility | Custom v8-compat builds |
If you want the familiar v8 API (new Player(), player.load(), etc.) with the PWX engine:
<script src="https://cdn.jsdelivr.net/npm/@bitmovin/player-web-x@10/bundles/playerx-bitmovin-v8.js"></script>
<script>
// Same API as v8!
const player = new bitmovin.player.Player(
document.getElementById('player'),
{
key: 'YOUR_KEY',
playback: { autoplay: true, muted: true },
location: {
ui: 'https://cdn.jsdelivr.net/npm/bitmovin-player-ui@4/dist/js/bitmovinplayer-ui.js',
ui_css: 'https://cdn.jsdelivr.net/npm/bitmovin-player-ui@4/dist/css/bitmovinplayer-ui.css',
},
}
);
player.load({ hls: 'https://example.com/stream.m3u8' });
</script>
This mirrors the current official v8-compat documentation more closely than manually calling UIFactory.buildUI(). Prefer the documented location.ui / location.ui_css path unless you have a specific reason to own the UI bootstrap yourself.
Caveat: Not all v8 APIs are implemented. Some methods are NOPs pending PWX feature completion. Check the live support matrix before relying on specific features.
Both bundles attach to window.bitmovin.player.Player — whichever loads second wins. To run both side-by-side (e.g. an A/B demo), capture each reference between script loads:
<!-- 1) Load v8 -->
<script src="https://cdn.jsdelivr.net/npm/bitmovin-player@8/bitmovinplayer.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bitmovin-player-ui@4/dist/js/bitmovinplayer-ui.js"></script>
<script>
window.V8Player = window.bitmovin.player.Player; // capture v8
</script>
<!-- 2) Load PWX v8-compat (this OVERWRITES window.bitmovin.player.Player) -->
<script src="https://cdn.jsdelivr.net/npm/@bitmovin/player-web-x@10/bundles/playerx-bitmovin-v8.js"></script>
<script>
window.PwxPlayer = window.bitmovin.player.Player; // capture PWX v8-compat
</script>
<script>
const v8 = new window.V8Player(v8Container, { key, ui: false });
const pwx = new window.PwxPlayer(pwxContainer, { key, ui: false });
bitmovin.playerui.UIFactory.buildUI(v8);
bitmovin.playerui.UIFactory.buildUI(pwx); // same UI on both
v8.load({ hls: url });
pwx.load({ hls: url });
</script>
window.bitmovin.playerui is a separate namespace from a separate script — it is not affected by the collision.
Both v8 and the PWX v8-compat bundle emit a 'playing' event when the first frame renders. Measure time-to-first-frame like this:
async function measureStartup(player, source) {
const firstFrame = new Promise(resolve => player.on('playing', () => resolve(performance.now())));
const t0 = performance.now();
await player.load(source);
const t1 = await firstFrame;
return t1 - t0; // startup in ms
}
Run both players in parallel (so network contention is equal) or sequentially (for isolated numbers) depending on what you're benchmarking. Do not hardcode an expected delta into your guidance: startup results vary significantly by browser, device, stream packaging, cache state, and SDK version.
PWX's killer feature is its package system. You can extend, replace, or add functionality:
import { Player } from '@bitmovin/player-web-x';
const player = Player({
key: 'YOUR_KEY',
defaultContainer: document.getElementById('player'),
});
// Add packages for the features you need
player.packages.add(hlsPackage);
player.packages.add(adaptationPackage);
player.packages.add(myCustomPackage);
See Creating packages for the package authoring guide.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub bitmovin/skills --plugin bitmovin-encoding-live