Skip to content

Player — Advanced

The vanilla player uses a skin system to render its controls. A skin bundles structure descriptors, blueprint factories, icons, design tokens, and CSS. Skins can inherit from other skins, letting you override individual controls without rewriting everything.

import { registerSkin, resolveSkin, FwSkins, createPlayer } from "@livepeer-frameworks/player-core";
// Register a custom skin
registerSkin("mybrand", {
inherit: "default",
blueprints: {
// Replace the play button with a custom one
play: (ctx) => {
const btn = document.createElement("button");
btn.className = "my-play-btn";
btn.textContent = "Play";
ctx.subscribe.on("playing", (isPlaying) => {
btn.textContent = isPlaying ? "Pause" : "Play";
});
btn.onclick = () => ctx.api.togglePlay();
return btn;
},
},
tokens: {
"--fw-accent": "340 80% 55%",
"--fw-surface": "0 0% 8%",
},
css: {
skin: `.my-play-btn { padding: 8px 16px; border-radius: 4px; }`,
},
});
// Use it
const player = createPlayer({
target: "#player",
contentId: "pk_...",
skin: "mybrand",
});

Pass a SkinDefinition directly:

const player = createPlayer({
target: "#player",
contentId: "pk_...",
skin: {
inherit: "default",
blueprints: {
pip: () => null, // Hide PiP button
fullscreen: () => null, // Hide fullscreen button
},
tokens: {
"--fw-accent": "262 80% 60%",
},
},
});
interface SkinDefinition {
inherit?: string; // Parent skin name
structure?: { main: StructureDescriptor }; // Layout override
blueprints?: Record<string, BlueprintFactory>; // UI factory overrides
icons?: Record<string, { svg: string; size?: number }>; // Icon overrides
tokens?: Record<string, string>; // CSS custom properties
css?: { skin?: string }; // Additional CSS
}
// No controls at all (headless)
const player = createPlayer({ ..., skin: false });
// or equivalently
const player = createPlayer({ ..., controls: false });

The current vanilla createPlayer config treats controls as a boolean. Use skin for custom UI or controls: false for headless playback.

Blueprints are factory functions that receive a BlueprintContext and return an HTMLElement (or null to skip). They wire reactivity through ctx.subscribe.on().

import type { BlueprintFactory } from "@livepeer-frameworks/player-core";
const myVolumeKnob: BlueprintFactory = (ctx) => {
const el = document.createElement("div");
el.className = "my-volume-knob";
const slider = document.createElement("input");
slider.type = "range";
slider.min = "0";
slider.max = "1";
slider.step = "0.01";
// Reactive: subscribe fires immediately with current value, then on change
ctx.subscribe.on("volume", (v) => {
slider.value = String(v);
});
slider.oninput = () => (ctx.api.volume = parseFloat(slider.value));
el.appendChild(slider);
// Use i18n
const label = document.createElement("span");
label.textContent = ctx.translate("volume", "Volume");
el.appendChild(label);
return el;
};

Every blueprint factory receives this context object:

PropertyTypeDescription
videoHTMLVideoElement | nullUnderlying video element
subscribeReactiveStatePer-property reactive subscriptions
apiPlayerInstanceFull player API (Q/M/S)
fullscreenobject{ supported, active, toggle(), request(), exit() }
pipobject{ supported, active, toggle() }
infoStreamInfo | nullSource/track info from MistServer
optionsCreatePlayerConfigConfig passed to createPlayer
containerHTMLElementPlayer container element
translate(key, fallback?)functioni18n helper
buildIcon(name, size?)functionSVG icon builder
log(msg)functionDebug logger (no-op when debug off)
timersobjectsetTimeout/setInterval (auto-cleaned on destroy)

The layout is defined as a JSON tree. Each node’s type maps to a blueprint factory name:

import type { StructureDescriptor } from "@livepeer-frameworks/player-core";
const myStructure: StructureDescriptor = {
type: "container",
children: [
{ type: "videocontainer" },
{ type: "loading" },
{ type: "error" },
{
type: "controls",
children: [
{ type: "progress" },
{
type: "controlbar",
children: [
{ type: "play" },
{ type: "live" },
{ type: "currentTime" },
{ type: "spacer" },
{ type: "totalTime" },
{ type: "volume" },
{ type: "fullscreen" },
],
},
],
},
],
};

Default blueprint types: container, videocontainer, controls, controlbar, play, progress, currentTime, totalTime, speaker, volume, fullscreen, pip, settings, loading, error, seekForward, seekBackward, live, spacer.

Structure nodes support:

FieldTypeDescription
typestringBlueprint name to invoke
classesstring[]Extra CSS classes
styleRecord<string, string>Inline styles
childrenStructureDescriptor[]Child nodes
if(ctx) => booleanConditional render
thenStructureDescriptorRender when if true
elseStructureDescriptorRender when if false

Register custom playback engines with registerPlayer():

import { registerPlayer } from "@livepeer-frameworks/player-core";
registerPlayer("myproto", {
name: "My Protocol Player",
priority: 5,
mimeTypes: ["application/x-myproto"],
isBrowserSupported: () => typeof RTCPeerConnection !== "undefined",
async build(source, video, container) {
// source.url contains the stream URL
// video is a pre-created <video> element
// Set up your custom playback here
video.src = source.url;
await video.play();
},
destroy() {
// Clean up custom resources
},
});

Once registered, the scoring algorithm considers your player alongside built-in engines. Matching is based on mimeTypes — when a source’s MIME type matches, your player competes by priority (lower = higher priority).

interface SimplePlayerDefinition {
name: string;
priority?: number; // Default: 10
mimeTypes: string[];
isBrowserSupported?: () => boolean;
build(
source: StreamSource,
video: HTMLVideoElement,
container: HTMLElement
): void | Promise<void>;
destroy?(): void;
}

The player.subscribe object provides per-property reactive subscriptions. Callbacks fire immediately with the current value, then on every change (with shallow equality deduplication).

// Subscribe to a property
const unsub = player.subscribe.on("currentTime", (t) => {
myTimeLabel.textContent = `${t.toFixed(1)}s`;
});
// Read current value synchronously
const vol = player.subscribe.get("volume");
// Unsubscribe
unsub();
// Unsubscribe all listeners for a property
player.subscribe.off("volume");
// Unsubscribe everything
player.subscribe.off();

Available properties: paused, playing, currentTime, duration, volume, muted, playbackRate, loop, buffering, fullscreen, pip, tracks, streamState, error, loading, ended, seeking.

playbackMode controls the protocol preference order:

ModePreferenceUse Case
low-latencyWebCodecs/WS → WHEP/WebRTC → MP4/WS → HLSReal-time interaction
qualityMP4/WS and WebCodecs/WS → HLS/MP4 → WebRTCStable, high quality
vodMP4/HLS/DASH → MP4/WS → WHEP/WebRTCPre-recorded content
autoBalanced score-basedDefault

LL-HLS and DASH are served from MistServer’s CMAF output (/play/{id}/cmaf/index.m3u8 and /play/{id}/cmaf/index.mpd) on every helmsman-managed edge.

const player = createPlayer({
...,
playbackMode: "low-latency",
});

Force a specific source type (bypasses scoring):

const player = createPlayer({
...,
forceType: "whep",
});

The player includes a built-in ABR controller with four modes:

ModeDescription
autoUses viewport resize ABR first, then bitrate monitoring if resize is unavailable
resizeSelects quality from viewport/container size changes
bitrateMonitors playback quality and steps down/up based on performance
manualUser-selected quality level; automatic switching is disabled
// Check current mode
player.abrMode; // "auto"
// Switch to manual
player.abrMode = "manual";
player.selectQuality("720p");
// Switch back to auto
player.abrMode = "auto";
// List available qualities
const qualities = player.getQualities();
// [{ id: "1080p", label: "1080p", width: 1920, height: 1080, bitrate: 4000000 }, ...]
// Lock to specific quality
player.selectQuality("720p");
StateDescription
bootingInstance created, initializing
gateway_loadingQuerying gateway for endpoints
gateway_readyEndpoints received
gateway_errorGateway query failed
no_endpointNo compatible endpoints
selecting_playerScoring player/source combinations
connectingEstablishing media connection
bufferingConnected, waiting for data
playingActive playback
pausedPaused by user
endedPlayback ended
errorUnrecoverable error
destroyedInstance torn down

The player wraps multiple playback engines and selects the best one for each browser/protocol combination:

EngineProtocolsNotes
hls.jsHLS (TS, CMAF)Primary HLS engine for non-Safari
Video.jsHLSFallback HLS engine
Dash.jsDASHMPEG-DASH adaptive streaming
MewsWsPlayerWebSocket MP4/WebMCustom MSE-based low-latency
Native WHEPWebRTCBrowser-native WebRTC
WebCodecsWebSocketFrame-accurate, background-safe
Native HLSHLSSafari native playback

Protocol selection uses a scoring algorithm ported from MistServer’s MistMetaPlayer, evaluating codec support, browser compatibility, and source priority.

thumbnailUrl sets a static image shown on the click-to-play overlay before playback starts. Typically the stream’s .jpg endpoint (latest keyframe) or a custom image.

// React
<Player
contentId="pk_..."
contentType="live"
thumbnailUrl="https://your-cdn.com/poster.jpg"
options={{ autoplay: false }}
/>
// Vanilla
const player = createPlayer({
...,
poster: "https://your-cdn.com/poster.jpg",
autoplay: false,
});

When the stream has thumbnail processing enabled, the player auto-detects the thumbvtt track from MistServer metadata and shows sprite thumbnails on the seek bar during hover. No configuration needed — this works automatically for both live and VOD content.

When paused, use , and . keyboard shortcuts to step backward/forward one frame:

  • WebCodecs — true frame stepping via decoded frame buffer
  • Other engines — step within the already-buffered range only (no network seek)
if (player.capabilities.pip) {
player.togglePiP();
// or
await player.requestPiP();
}
player.on("pipChange", ({ isPiP }) => {
console.log("PiP:", isPiP);
});
if (player.capabilities.fullscreen) {
player.toggleFullscreen();
// or
await player.requestFullscreen();
}
player.on("fullscreenChange", ({ isFullscreen }) => {
console.log("Fullscreen:", isFullscreen);
});
player.on("error", (error) => {
console.error("Playback error:", error.message);
// Retry current source
player.retry();
// Or try next endpoint
player.retryWithFallback();
// Or full reload
player.reload();
});
// Dismiss error state
player.clearError();
const audioTracks = player.getAudioTracks();
player.selectAudioTrack(audioTracks[1].id);
const textTracks = player.getTextTracks();
player.selectTextTrack(textTracks[0].id);
const stats = await player.getStats();
TypeBehavior
liveLive stream — shows live badge, penalizes seeking
dvrLive with DVR buffer — enables seeking within buffer
clipShort clip — seekable, may loop
vodVideo on demand — full seeking, duration display
<Player contentType="dvr" contentId="pk_..." />
<Player contentType="clip" contentId="pk_..." />
<Player contentType="vod" contentId="pk_..." />

The player resolves playback sources through one of three modes, evaluated in priority order. The first one set wins. If you do not pass any resolution option, the player uses the official FrameWorks Gateway.

PriorityOptionResolutionWhen to Use
1endpointsNone — uses the endpoints as-isYou already have resolved edge node URLs from your own orchestration layer
2mistUrlFetches Mist JSON metadata directly from MistServerStandalone setups pointing at a known MistServer node
3gatewayUrlQueries the FrameWorks Gateway GraphQL APIFully self-hosted control planes or local Gateway previews

The official FrameWorks Gateway resolves the best edge node for the viewer, returns structured endpoints, and handles failover across clusters. Resolve through Gateway from the viewer’s browser or playback device. If your backend calls Gateway, Foghorn routes using the backend or proxy IP, not the final viewer’s location.

Override gatewayUrl only when you run a fully self-hosted FrameWorks control plane or a local Gateway preview:

createPlayer({
target: "#player",
contentId: "pk_abc123",
gatewayUrl: "https://bridge.selfhost.example/graphql",
});

Connects directly to a MistServer node without Gateway involvement. The player fetches Mist JSON metadata to get the full source list and codec metadata, then runs the scoring algorithm locally. MistServer is the authority for available protocols and codecs — the player preserves the raw source types (including ws/video/raw for WebCodecs).

createPlayer({
target: "#player",
contentId: "my-stream",
mistUrl: "https://mist.example.com:8080",
});

Bypasses all resolution. You provide the endpoint structure directly. The player builds a synthetic source list from the outputs map.

createPlayer({
target: "#player",
contentId: "my-stream",
contentType: "live",
endpoints: {
primary: {
nodeId: "edge-1",
baseUrl: "https://edge1.example.com",
outputs: {
HLS: { url: "https://edge1.example.com/hls/stream/index.m3u8" },
WHEP: { url: "https://edge1.example.com/webrtc/stream" },
},
},
fallbacks: [],
},
});