Player — Advanced
Custom Skins (Whitelabeling)
Section titled “Custom Skins (Whitelabeling)”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.
Skin Registry
Section titled “Skin Registry”import { registerSkin, resolveSkin, FwSkins, createPlayer } from "@livepeer-frameworks/player-core";
// Register a custom skinregisterSkin("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 itconst player = createPlayer({ target: "#player", contentId: "pk_...", skin: "mybrand",});Inline Skin (No Registration)
Section titled “Inline Skin (No Registration)”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%", }, },});Skin Definition
Section titled “Skin Definition”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}Headless / No UI
Section titled “Headless / No UI”// No controls at all (headless)const player = createPlayer({ ..., skin: false });// or equivalentlyconst 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.
Blueprint System
Section titled “Blueprint System”Blueprints are factory functions that receive a BlueprintContext and return an HTMLElement (or null to skip). They wire reactivity through ctx.subscribe.on().
Writing a Blueprint
Section titled “Writing a Blueprint”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;};BlueprintContext
Section titled “BlueprintContext”Every blueprint factory receives this context object:
| Property | Type | Description |
|---|---|---|
video | HTMLVideoElement | null | Underlying video element |
subscribe | ReactiveState | Per-property reactive subscriptions |
api | PlayerInstance | Full player API (Q/M/S) |
fullscreen | object | { supported, active, toggle(), request(), exit() } |
pip | object | { supported, active, toggle() } |
info | StreamInfo | null | Source/track info from MistServer |
options | CreatePlayerConfig | Config passed to createPlayer |
container | HTMLElement | Player container element |
translate(key, fallback?) | function | i18n helper |
buildIcon(name, size?) | function | SVG icon builder |
log(msg) | function | Debug logger (no-op when debug off) |
timers | object | setTimeout/setInterval (auto-cleaned on destroy) |
Structure Descriptors
Section titled “Structure Descriptors”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:
| Field | Type | Description |
|---|---|---|
type | string | Blueprint name to invoke |
classes | string[] | Extra CSS classes |
style | Record<string, string> | Inline styles |
children | StructureDescriptor[] | Child nodes |
if | (ctx) => boolean | Conditional render |
then | StructureDescriptor | Render when if true |
else | StructureDescriptor | Render when if false |
Custom Protocol Players
Section titled “Custom Protocol Players”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;}Reactive State API
Section titled “Reactive State API”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 propertyconst unsub = player.subscribe.on("currentTime", (t) => { myTimeLabel.textContent = `${t.toFixed(1)}s`;});
// Read current value synchronouslyconst vol = player.subscribe.get("volume");
// Unsubscribeunsub();
// Unsubscribe all listeners for a propertyplayer.subscribe.off("volume");
// Unsubscribe everythingplayer.subscribe.off();Available properties: paused, playing, currentTime, duration, volume, muted, playbackRate, loop, buffering, fullscreen, pip, tracks, streamState, error, loading, ended, seeking.
Playback Modes
Section titled “Playback Modes”playbackMode controls the protocol preference order:
| Mode | Preference | Use Case |
|---|---|---|
low-latency | WebCodecs/WS → WHEP/WebRTC → MP4/WS → HLS | Real-time interaction |
quality | MP4/WS and WebCodecs/WS → HLS/MP4 → WebRTC | Stable, high quality |
vod | MP4/HLS/DASH → MP4/WS → WHEP/WebRTC | Pre-recorded content |
auto | Balanced score-based | Default |
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 Protocol
Section titled “Force Protocol”Force a specific source type (bypasses scoring):
const player = createPlayer({ ..., forceType: "whep",});Adaptive Bitrate (ABR)
Section titled “Adaptive Bitrate (ABR)”The player includes a built-in ABR controller with four modes:
| Mode | Description |
|---|---|
auto | Uses viewport resize ABR first, then bitrate monitoring if resize is unavailable |
resize | Selects quality from viewport/container size changes |
bitrate | Monitors playback quality and steps down/up based on performance |
manual | User-selected quality level; automatic switching is disabled |
// Check current modeplayer.abrMode; // "auto"
// Switch to manualplayer.abrMode = "manual";player.selectQuality("720p");
// Switch back to autoplayer.abrMode = "auto";Quality Selection
Section titled “Quality Selection”// List available qualitiesconst qualities = player.getQualities();// [{ id: "1080p", label: "1080p", width: 1920, height: 1080, bitrate: 4000000 }, ...]
// Lock to specific qualityplayer.selectQuality("720p");Player States
Section titled “Player States”| State | Description |
|---|---|
booting | Instance created, initializing |
gateway_loading | Querying gateway for endpoints |
gateway_ready | Endpoints received |
gateway_error | Gateway query failed |
no_endpoint | No compatible endpoints |
selecting_player | Scoring player/source combinations |
connecting | Establishing media connection |
buffering | Connected, waiting for data |
playing | Active playback |
paused | Paused by user |
ended | Playback ended |
error | Unrecoverable error |
destroyed | Instance torn down |
Multi-Engine Architecture
Section titled “Multi-Engine Architecture”The player wraps multiple playback engines and selects the best one for each browser/protocol combination:
| Engine | Protocols | Notes |
|---|---|---|
| hls.js | HLS (TS, CMAF) | Primary HLS engine for non-Safari |
| Video.js | HLS | Fallback HLS engine |
| Dash.js | DASH | MPEG-DASH adaptive streaming |
| MewsWsPlayer | WebSocket MP4/WebM | Custom MSE-based low-latency |
| Native WHEP | WebRTC | Browser-native WebRTC |
| WebCodecs | WebSocket | Frame-accurate, background-safe |
| Native HLS | HLS | Safari native playback |
Protocol selection uses a scoring algorithm ported from MistServer’s MistMetaPlayer, evaluating codec support, browser compatibility, and source priority.
On iPhone and iPad—including iPads presenting a desktop Safari user agent—the
WebCodecs engine uses Canvas2D instead of uploading VideoFrame objects as WebGL
textures. Subtitle selection is handled once by the active engine, and literal cue
text remains literal even when it resembles JSON.
Caption controls touch only subtitle and caption text tracks. Chapter and metadata
tracks remain loaded, so host-side cuechange listeners continue to work while captions
are off.
WebCodecs renders captions from its own timed-metadata socket. MEWS and WHEP use the same timed-subtitle overlay renderer, so every subtitle offered by those transports has a visible rendering path with cue end-time and transport-duration timing. Caption enablement reflects the user’s requested state immediately, while the applied track remains separate. An early request is retried when tracks arrive, an absent explicit language is not replaced by a fallback, a second toggle cancels a pending request, and periodic stream snapshots do not restart the same active cue. If a selected track disappears and later returns, the controller clears the stale renderer and reapplies the request. The bundled React, Svelte, and web-component settings consume reactive controller track lists and route choices through this contract. Standalone Svelte controls without a controller callback expose only native browser text tracks, avoiding a selected state with no rendering owner.
Track selectors preserve Mist’s absolute identity (idx, or a numeric metadata key
when idx is absent). Tracks without a stable transport identity are omitted from
selection menus instead of using their filtered list position. Native WHEP also retains
desired quality and audio choices while signaling is disconnected and sends them again
whenever a replacement control channel opens. Mist implements tracks per output: MEWS,
Native WHEP, and Mist WebRTC accept video and audio selectors, not a subtitle key;
subtitle selection remains local to the controller-owned metadata renderer. WebCodecs
ws/video/h264 accepts video-only selection and restores Auto by removing the server
selector. ws/video/raw accepts no track commands, so the player hides unsupported quality
and audio controls instead of showing locally active no-op choices. WebCodecs replays
supported H264 selection, repeats its codec handshake, and independently restores
timed-metadata subscriptions.
WebCodecs binds selected subtitle subscriptions to the subtitle
type, and the framework subtitle renderers’ two-argument subscription remains compatible
with plain cues received before Mist track metadata arrives.
Thumbnails
Section titled “Thumbnails”Poster image
Section titled “Poster image”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 }}/>// Vanillaconst player = createPlayer({ ..., poster: "https://your-cdn.com/poster.jpg", autoplay: false,});Seek-bar sprite preview
Section titled “Seek-bar sprite preview”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.
Frame Stepping
Section titled “Frame Stepping”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)
Picture-in-Picture
Section titled “Picture-in-Picture”if (player.capabilities.pip) { player.togglePiP(); // or await player.requestPiP();}
player.on("pipChange", ({ isPiP }) => { console.log("PiP:", isPiP);});Fullscreen
Section titled “Fullscreen”if (player.capabilities.fullscreen) { player.toggleFullscreen(); // or await player.requestFullscreen();}
player.on("fullscreenChange", ({ isFullscreen }) => { console.log("Fullscreen:", isFullscreen);});Error Handling
Section titled “Error Handling”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 stateplayer.clearError();Tracks
Section titled “Tracks”Audio Tracks
Section titled “Audio Tracks”const audioTracks = player.getAudioTracks();player.selectAudioTrack(audioTracks[1].id);Text Tracks (Subtitles)
Section titled “Text Tracks (Subtitles)”const textTracks = player.getTextTracks();player.selectTextTrack(textTracks[0].id);Playback Statistics
Section titled “Playback Statistics”const stats = await player.getStats();Diagnostic Telemetry
Section titled “Diagnostic Telemetry”Browser telemetry is opt-in and intentionally lossy. Enable only the signals you need:
const player = createPlayer({ target: "#player", contentId: "pk_...", telemetry: { boot: true, // Startup waterfall and time to first frame session: true, // Rebuffering, frame, bitrate, and retention deltas },});With session enabled, the player generates a fresh ephemeral identifier for each
attach() and adds it to Mist playback, stream-info, and timed-metadata URLs as fwsid. FrameWorks uses
that value only to correlate browser QoE with edge connection diagnostics. It is not a
viewer or account identifier and is never a billing source. The analytics ingest removes
every query parameter except a validated fwsid, and always removes URL userinfo and the fragment, before
retaining a diagnostic request URL. This allowlist also excludes future credential names
without requiring a denylist update.
Session-level correlation is currently an internal support surface. The public analytics API exposes aggregate QoE and retention, not raw request URLs or per-session identities. The internal detail RPC evaluates and keyset-paginates the collapsed browser-session page once before looking up only those concrete connection IDs. All returned connection dimensions come from one canonical connect row; unmatched sessions report empty dimensions. The connection lookup filters both the selected client-session IDs and their concrete stream IDs, and scans at most one source-retention window ending at the requested slice. If that diagnostic enrichment query fails, the QoE page is still returned with empty connection fields.
Content Types
Section titled “Content Types”| Type | Behavior |
|---|---|
live | Live stream — shows live badge, penalizes seeking |
dvr | Live with DVR buffer — enables seeking within buffer |
clip | Short clip — seekable, may loop |
vod | Video on demand — full seeking, duration display |
<Player contentType="dvr" contentId="pk_..." /><Player contentType="clip" contentId="pk_..." /><Player contentType="vod" contentId="pk_..." />Source Resolution Modes
Section titled “Source Resolution Modes”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.
| Priority | Option | Resolution | When to Use |
|---|---|---|---|
| 1 | endpoints | None — uses the endpoints as-is | You already have resolved edge node URLs from your own orchestration layer |
| 2 | mistUrl | Fetches Mist JSON metadata directly from MistServer | Standalone setups pointing at a known MistServer node |
| 3 | gatewayUrl | Queries the FrameWorks Gateway GraphQL API | Provider-managed private Gateways or local development previews |
Gateway Resolution (default)
Section titled “Gateway Resolution (default)”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 FrameWorks provides a private Gateway endpoint or for a local
Gateway preview:
createPlayer({ target: "#player", contentId: "pk_abc123", gatewayUrl: "https://gateway.private.example/graphql",});mistUrl (direct MistServer)
Section titled “mistUrl (direct MistServer)”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",});endpoints (pre-resolved)
Section titled “endpoints (pre-resolved)”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: [], },});