Player Documentation โ
Movi Streaming Video Library - Player Component
Table of Contents โ
- Overview
- Architecture
- API Reference
- Configuration
- Playback Control
- Track Management
- Events
- A/V Synchronization
- Usage Examples
- Performance
- Troubleshooting
Overview โ
The MoviPlayer is the main orchestrator component that coordinates:
- Source Management: HTTP/File data streaming
- Demuxing: Container parsing and packet extraction
- Decoding: Video/Audio/Subtitle decoding (hardware + software fallback)
- Rendering: Canvas (WebGL2) and Audio (Web Audio API) output
- Synchronization: Audio-master A/V sync with frame-perfect timing
- State Management: Playback state machine with error recovery
Key File: src/core/MoviPlayer.ts
Key Features โ
โ Hardware-First Decoding: WebCodecs with automatic software fallback โ Pull-Based Streaming: Memory-efficient, handles multi-GB files โ HDR Support: BT.2020/PQ/HLG with Display-P3 rendering โ Multi-Track: Runtime audio/video/subtitle track switching โ Intelligent Seeking: Keyframe-based with post-seek throttling โ Preview Generation: Isolated WASM instance for thumbnails โ Wake Lock: Prevents screen sleep during playback โ Pitch-Preserving Time-Stretch: Signalsmith Stretch for clean non-1x playback โ Audio-Only Mode: Dedicated strip UI with embedded cover art extraction โ Cover Art: Automatic extraction from MP3/MP4/FLAC/MKV metadata
Architecture โ
Component Hierarchy โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MoviPlayer โ
โ (EventEmitter Core) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
โ โ HttpSource / โโโ>โ Demuxer โโโ>โ TrackManager โ โ
โ โ FileSource โ โ (FFmpeg) โ โ โ โ
โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Decoding Pipeline โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ
โ โ MoviVideoDecoder MoviAudioDecoder SubtitleDec โ โ
โ โ (WebCodecsโSW) (WebCodecsโSW) (Text/Image) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Rendering Pipeline โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ
โ โ CanvasRenderer AudioRenderer โ โ
โ โ (WebGL2 + P3) (Web Audio API) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
โ โ Clock โ โ StateManagerโ โ WakeLock โ โ
โ โ (A/V Sync) โ โ (FSM + Error)โ โ (Screen) โ โ
โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโData Flow โ
HTTP/File โ Source โ Demuxer โ Packets โ Decoders โ Frames โ Renderers โ Output
โ โ
TrackManager Clock (A/V Sync)
โ โ
StreamIndex Audio MasterAPI Reference โ
Constructor โ
constructor(config: PlayerConfig)Parameters:
interface PlayerConfig {
source?: SourceConfig; // { type, url } or { type, file } โ optional when sourceAdapter is set
sourceAdapter?: SourceAdapter; // Pre-built adapter โ overrides `source` (custom protocols)
audioSource?: SourceConfig; // Separate audio for split video+audio sources
audioTracks?: AudioSourceEntry[]; // Multi-language audio with metadata
subtitleTracks?: SubtitleSourceEntry[]; // External subtitles (VTT/SRT) with metadata
canvas?: HTMLCanvasElement | OffscreenCanvas;
renderer?: "canvas"; // Only "canvas" today (MSE pathway is HLS-internal)
decoder?: "auto" | "software"; // Default "auto" โ hardware first, software fallback
cache?: { maxSizeMB: number };
wasmBinary?: Uint8Array; // Pre-loaded WASM (skips fetch)
enablePreviews?: boolean; // Enable thumbnail-pipeline preview frames
frameRate?: number; // Override fps (0 = auto from metadata)
headers?: Record<string, string>; // Custom HTTP headers for all media requests (manifest + segments + progressive + thumbnails + encrypted)
audioOnly?: boolean; // Data saver: skip video decode; adaptive streams fetch an audio-only rendition
drm?: boolean; // DRM mode for adaptive streams (native <video> + EME)
licenseUrl?: string; // Widevine/PlayReady/FairPlay license server URL
licenseHeaders?: Record<string, string>; // Auth headers for license requests
lcevc?: boolean; // Enable MPEG-5 LCEVC decoding (needs lcevc_dec.js)
lcevcUrl?: string; // URL to lazy-load the lcevc_dec.js decoder library
}Example:
import { MoviPlayer } from "movi-player/player";
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
const player = new MoviPlayer({
source: { type: "url", url: "video.mp4" },
canvas: canvas,
renderer: "canvas",
});With a custom SourceAdapter (any protocol โ WebSocket, IndexedDB, custom encryption, etc.):
import { MoviPlayer } from "movi-player/player";
import type { SourceAdapter } from "movi-player";
const player = new MoviPlayer({
sourceAdapter: myCustomAdapter, // wins over `source` when present
canvas,
});
await player.load();See Sources โ Creating Custom Sources for the adapter contract.
Methods โ
load(sourceConfig?: SourceConfig): Promise<void> โ
Loads the media source set in the constructor (or the override passed here) and initializes the playback pipeline. SourceConfig requires a type discriminant.
interface SourceConfig {
type: "url" | "file" | "encrypted";
url?: string; // For type: "url"
file?: File; // For type: "file"
headers?: Record<string, string>;
encrypted?: { videoUrl, tokenUrl, videoId, fingerprint, sessionToken, ... };
}Returns: void. Inspect tracks/duration via getMediaInfo(), getDuration(), and getTracks() after the promise resolves.
Examples:
// Use the source from the constructor
await player.load();
// Override the source on an idle instance
await player.load({ type: "url", url: "https://example.com/video.mp4" });
const info = player.getMediaInfo();
console.log(`Loaded: ${info?.duration}s`);play(): Promise<void> โ
Starts playback from current position.
Behavior:
- Acquires wake lock (prevents screen sleep)
- Starts demuxing loop
- Begins rendering at 60Hz
- Returns when playback starts (not when it ends)
Example:
await player.play();
console.log("Playback started");pause(): void โ
Pauses playback immediately.
Behavior:
- Stops demuxing loop
- Releases wake lock
- Preserves current time
- Keeps last frame visible
seek(timestamp: number): Promise<void> โ
Seeks to a specific timestamp.
Parameters:
timestamp- Target time in seconds (must be โฅ 0 and โค duration)
Behavior:
- Seeks demuxer to nearest keyframe before timestamp
- Flushes video/audio decoders
- Skips packets until reaching target time
- Post-seek throttle (200ms) prevents rapid seeks
Example:
await player.seek(120.5); // Seek to 2:00.5
console.log(`Seeked to: ${player.getCurrentTime()}s`);Note: Actual position after seek may be slightly before target due to keyframe alignment.
setPlaybackRate(rate: number): void โ
Adjusts playback speed.
Parameters:
rate- Speed multiplier (0.25 to 2.0)0.5= half speed1.0= normal speed (default)2.0= double speed
Example:
player.setPlaybackRate(1.5); // 1.5x speedsetVolume(volume: number): void โ
Sets audio volume.
Parameters:
volume- Volume level (0.0 to 1.0)0.0= muted1.0= maximum (default)
Example:
player.setVolume(0.5); // 50% volumesetMuted(muted: boolean): void / getMuted(): boolean โ
Toggle/read mute state independently from setVolume(). Muting also disables the audio decode path on the renderer to save CPU.
player.setMuted(true);
console.log(player.getMuted()); // truesetStableAudio(enabled: boolean): void / getStableAudio(): boolean โ
Enable/disable the loudness-normalization compressor (driven by DynamicsCompressorNode). Same toggle as the stablevolume attribute on <movi-player>.
player.setStableAudio(true);setHDREnabled(enabled: boolean): void / isHDRSupported(): boolean โ
Force HDR rendering on/off, and check whether the content is HDR. Note that effective rendering also depends on browser/display capability โ see the HDR guide.
if (player.isHDRSupported()) {
player.setHDREnabled(true);
}setMaxBufferSize(megabytes: number): void โ
Adjusts the prefetch window in megabytes (HTTP and encrypted sources). Mirrors the buffersize HTML attribute.
player.setMaxBufferSize(400); // 400 MB aheadrotateVideo(): number / getVideoRotation(): number / setVideoRotation(deg: number): void โ
Rotate the canvas output. rotateVideo() cycles 0 โ 90 โ 180 โ 270 and returns the new angle. setVideoRotation() jumps directly to a value.
player.rotateVideo(); // โ 90
player.setVideoRotation(180);
console.log(player.getVideoRotation()); // 180setFitMode(mode): void โ
Set the canvas fit policy. mode is "contain" | "cover" | "fill" | "zoom" | "control".
player.setFitMode("cover");setLetterboxColor(r: number, g: number, b: number): void โ
Override the WebGL letterbox color (each channel 0..1). Used internally by ambient mode; expose for custom UI tints.
destroy(): void โ
Destroys the player and releases all resources.
Behavior:
- Closes demuxer (frees WASM memory)
- Destroys decoders
- Clears frame queues
- Releases wake lock
- Removes all event listeners
Important: Always call this before removing player instance.
player.destroy();Getters โ
getCurrentTime(): number โ
Returns current playback position in seconds.
getDuration(): number โ
Returns total media duration in seconds.
getState(): PlayerState โ
Returns current player state.
type PlayerState =
| "idle" // Not loaded
| "loading" // Loading source
| "ready" // Loaded, paused
| "playing" // Active playback
| "paused" // Paused
| "seeking" // Seeking in progress
| "buffering" // Waiting for data
| "ended" // Playback finished
| "error"; // Error occurredgetVolume(): number โ
Returns current volume (0.0 to 1.0).
getPlaybackRate(): number โ
Returns current playback rate multiplier.
getMediaInfo(): MediaInfo | null โ
Returns media metadata (null if not loaded).
getContentDispositionFilename(): string | null / getMetadataTitle(): string | null โ
Filename hinted by the server's Content-Disposition header, and the title carried in container metadata (e.g., MKV's <Title>). The element uses these to populate the in-player overlay and tab title.
getChapters(): Array<{ title, start, end }> โ
Chapters parsed from the source's metadata. Empty array when none are present.
for (const ch of player.getChapters()) {
console.log(`${ch.start}-${ch.end}: ${ch.title}`);
}getTracks(): Track[] โ
Returns all tracks (video, audio, subtitle).
getVideoTracks(): VideoTrack[] โ
Returns video tracks only.
getAudioTracks(): AudioTrack[] โ
Returns audio tracks only.
getSubtitleTracks(): SubtitleTrack[] โ
Returns subtitle tracks only.
getAudioLangs(): { lang, label, active }[] / selectAudioLang(lang: string): boolean โ
Language-keyed accessors for muxed audio tracks โ easier than working with numeric trackId when you just want "switch to Hindi".
player.selectAudioLang("hi");getSubtitleLangs(): { lang, label, active }[] / selectSubtitleLang(lang: string \| null): Promise<boolean> โ
Same idea for subtitles. Pass null to disable.
useMuxedAudio(): void / isNativeAudioActive(): boolean / hasNativeAudio(): boolean โ
Switch between muxed-in audio and a separately-loaded native audio element (the split-source path). hasNativeAudio() reports whether a separate audio element exists; isNativeAudioActive() whether it's currently driving playback.
hasAudibleSource(): boolean โ
Unified gate that returns true when the player has any audible output โ covers muxed audio tracks, a separate native <audio> element (split source), and HLS audio that lives inside the hidden native <video>. Use this instead of getAudioTracks().length when deciding whether to show a mute button or accept volume hotkeys.
if (player.hasAudibleSource()) {
showVolumeButton();
}getCoverArt(): ImageBitmap | null โ
Returns the embedded cover art extracted from the media file at load time, or null if the source has no embedded artwork or extraction failed. The caller owns the bitmap and must call close() on it when done.
const art = player.getCoverArt();
if (art) {
imgElement.src = await createImageBitmap(art).then(bmp => {
const canvas = document.createElement("canvas");
canvas.width = bmp.width;
canvas.height = bmp.height;
canvas.getContext("2d")!.drawImage(bmp, 0, 0);
bmp.close();
return canvas.toDataURL();
});
}Cover art is extracted from ID3v2 APIC (MP3), covr atom (MP4), FLAC PICTURE block, or MKV attachments. The player emits a coverart event when extraction completes โ the element listens for this and displays it in the audio-strip overlay.
setSubtitleDelay(seconds: number): void / getSubtitleDelay(): number โ
Shifts subtitle timing relative to video. Sign matches VLC and mpv: positive values shift subtitles later, negative shifts them earlier. Applied at the renderer's active-cue check, so the same offset works for text and image (PGS/DVB) cues without re-decoding.
player.setSubtitleDelay(0.5); // Subtitles 500ms later
console.log(player.getSubtitleDelay()); // 0.5Persistence
The offset is not persisted to SettingsStorage โ sync drift is per-source, so a global value would mis-shift unrelated videos.
Track Selection โ
selectAudioTrack(trackId: number): boolean โ
Switches to a different audio track by numeric ID. Returns true on success. For language-keyed switching see selectAudioLang().
const tracks = player.getAudioTracks();
const english = tracks.find((t) => t.language === "eng");
if (english) player.selectAudioTrack(english.id);Video tracks
There is no selectVideoTrack() on MoviPlayer โ for HLS quality switching, use the HLS-wrapped path or the <movi-player> quality menu. Multi-quality non-HLS sources are not yet supported at the programmatic-API level.
selectSubtitleTrack(trackId: number | null): Promise<boolean> โ
Enables a subtitle track or disables subtitles.
const spanish = player.getSubtitleTracks().find((t) => t.language === "spa");
if (spanish) await player.selectSubtitleTrack(spanish.id);
await player.selectSubtitleTrack(null); // OffPreview / Timeline Generation โ
getPreviewFrame(time: number): Promise<Blob | null> โ
Generates a single thumbnail at a given timestamp using an isolated WASM instance, so it doesn't disturb the main playback decoders. Returns null if generation fails.
const blob = await player.getPreviewFrame(60);
if (blob) imgElement.src = URL.createObjectURL(blob);generateTimeline(...): Promise<...> โ
Pre-renders a strip of N evenly-spaced thumbnails for the timeline scrubber. Driven by the T keyboard shortcut and the timeline UI. See src/core/MoviPlayer.ts:2437 for the current signature.
Buffer / Cache Inspection โ
getBufferedTime(): number โ
Returns the highest contiguous buffered time (in seconds) starting from the current position. This is what the seek-bar buffered indicator reads.
getCachedTimeRanges(): Array<{ start, end }> โ
All cached time ranges, not just the contiguous one. Useful for drawing multi-segment buffer indicators.
getCacheStats() / getNetworkSpeed(): number / getStats() โ
Diagnostic counters surfaced in the "Stats for nerds" overlay (I key) โ bytes cached, current network throughput, codec/decoder/buffer health, etc.
getBufferStartBytes() / getBufferEndBytes() / getBufferStartTime() / getBufferEndTime() โ
Byte- and time-level edges of the active buffer window. Used by the encrypted source's prefetch/refill heuristics; rarely needed in app code.
Source / Renderer Inspection โ
getSource(): SourceAdapter | null / isFileSource(): boolean / isHttpSource(): boolean โ
Reflect on the currently-loaded source. getSource() returns the underlying HttpSource/EncryptedHttpSource/FileSource instance (advanced).
isSoftwareDecoding(): boolean โ
true if the player is currently using the software (FFmpeg WASM) decoder path โ either because hardware decode failed or because the source forced it.
getHLSVideoElement(): HTMLVideoElement | null โ
The native <video> element used for HLS/DRM playback paths, or null for the canvas pipeline.
resizeCanvas(width: number, height: number): void โ
Notify the renderer that the canvas dimensions changed. The element calls this from its ResizeObserver; you only need it when driving MoviPlayer directly.
Subtitle Surface โ
setSubtitleOverlay(overlay: HTMLElement | null): void โ
Mount/unmount the DOM node that subtitle text renders into. The element passes its shadow-root overlay automatically.
setSubtitleControlsPadding(padding: number): void โ
Push subtitles up by N pixels so they don't sit under the controls bar when controls are visible.
Configuration โ
See the Constructor section above for the full PlayerConfig shape. Highlights:
decoder: "auto"(default) tries hardware (WebCodecs) first and falls back to software (FFmpeg WASM) on failure. Force"software"only when hardware decode is producing visual artifacts.renderer: "canvas"is currently the only option โ DRM/HLS paths internally manage their own native<video>element when needed.cache.maxSizeMBdefaults to ~100 MB; tune viasetMaxBufferSize()at runtime.enablePreviews: trueis required if you plan to callgetPreviewFrame().
Playback Control โ
State Machine โ
โโโโโโโโ
โ idle โ
โโโโโฌโโโ
โ load()
โผ
โโโโโโโโโโโ
โ loading โ
โโโโโโฌโโโโโ
โ success
โผ
โโโโโโโโ play() โโโโโโโโโโโ
โready โโโโโโโโโโโโค playing โ
โโโโฌโโโโ pause() โโโโโโฌโโโโโ
โ โ end
โ seek() โผ
โโโโโโโโโ>โโโโโโโโโโโ
โ โ seeking โ
โ โโโโโโฌโโโโโ
โ โ complete
โโโโโโโโโโโโโโโโ
โ error
โผ
โโโโโโโโโ
โ error โ
โโโโโโโโโPlayback Loop โ
The player runs an internal requestAnimationFrame loop:
- Check State: Skip if paused/seeking
- Read Packets: Demux next video/audio/subtitle packets
- Decode: Send packets to appropriate decoders
- Buffer Management: Apply back-pressure if buffers full
- Frame Presentation: Renderer handles timing
- Repeat: Until paused or ended
Track Management โ
Multi-Track Architecture โ
File: src/core/TrackManager.ts
Features:
- Runtime track switching without rebuffering
- Automatic selection (first video/audio, no subtitle)
- Track filtering by type, language, codec
Track Selection Strategy โ
class TrackManager {
// Default selection on load
autoSelectTracks() {
this.selectedVideoTrack = videoTracks[0];
this.selectedAudioTrack = audioTracks[0];
this.selectedSubtitleTrack = null; // Disabled by default
}
// User selection
selectVideoTrack(trackId: number) {
// Flush current video decoder
// Switch to new track
// Continue playback seamlessly
}
}Events โ
The player extends EventEmitter and fires the events declared in PlayerEventMap. See the full Events Reference for every event, payload type, and example handler โ including the MoviElement DOM event mirror.
Event Types (summary) โ
interface PlayerEventMap {
// Lifecycle
loadStart: void;
loadEnd: void;
preloadComplete: void;
stateChange: PlayerState;
ended: void;
error: Error;
// Progress
timeUpdate: number;
durationChange: number;
seeking: number;
seeked: number;
bufferUpdate: { start: number; end: number }[]; // reserved (not yet emitted)
// Tracks
tracksChange: Track[];
audioTrackChange: { lang: string; label: string };
subtitleTrackChange:
| { lang: string; label: string }
| { lang: null; label: null };
// Audio
coverArt: ImageBitmap | null; // Embedded cover art extracted at load (close() the bitmap when done)
// Frame-level (advanced)
frame: DecodedVideoFrame;
audio: DecodedAudioFrame;
subtitle: SubtitleCue;
// Source recovery
filerevoked: { offset: number; length: number; reason: string };
}filerevoked
Mobile browsers (iOS Safari, Android Chrome) silently revoke File handles after long backgrounding or memory pressure, leaving the demuxer hung forever. FileSource races each chunk read against an 8s timeout and fires filerevoked once so app code can prompt the user to re-pick the file. The <movi-player> element re-dispatches this as a DOM filerevoked CustomEvent.
Event Subscription โ
player.on("stateChange", (state) => console.log("State:", state));
player.on("timeUpdate", (t) => updateProgressBar(t / player.getDuration()));
player.on("error", (err) => showErrorMessage(err.message));
// Unsubscribe
const handler = () => console.log("Paused");
player.on("stateChange", handler);
player.off("stateChange", handler);Event names are camelCase
MoviPlayer events are camelCase (loadStart, timeUpdate, stateChange). The DOM-level events on <movi-player> use HTML-style lowercase (loadstart, timeupdate, statechange). Don't mix the two โ see the events reference for both tables side-by-side.
A/V Synchronization โ
Audio-Master Sync Model โ
File: src/core/Clock.ts
Principle: Audio is the master clock, video syncs to audio
Why Audio-Master?
- Audio glitches are very noticeable (pops, clicks)
- Video frame drops are less noticeable (smooth motion blur)
- Web Audio API provides high-precision timing
Sync Implementation โ
class Clock {
// Get current playback time from audio renderer
getTime(): number {
if (this.audioRenderer.isHealthy()) {
return this.audioRenderer.getAudioClock();
}
// Fallback to wall clock if audio unhealthy
return this.wallClockTime;
}
}CanvasRenderer Sync:
presentFrame() {
const audioTime = this.getAudioTime();
const frame = this.frameQueue[0];
if (frame.timestamp <= audioTime) {
// Audio ahead or in sync โ present frame
this.renderFrame(frame);
this.frameQueue.shift();
} else {
// Video ahead โ wait for audio to catch up
// Check again next RAF
}
}Sync Modes โ
Loose Sync (Default)
- Video uses wall clock for smooth presentation
- Periodic corrections from audio clock
- ยฑ50ms tolerance before correction
Tight Sync (Optional)
- Every frame checked against audio time
- More accurate, may cause frame drops
Buffer Health โ
Video Buffer: 120 frames (~2s at 60fps, ~4s at 30fps) Audio Buffer: 2 seconds of audio
If buffers drain:
- Player enters buffering state
- Playback pauses until buffers refill
- Fires
bufferingevent (if implemented)
Usage Examples โ
Basic Playback โ
import { MoviPlayer } from "movi-player/player";
const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;
const player = new MoviPlayer({
source: { type: "url", url: "https://example.com/video.mp4" },
canvas: canvas,
renderer: "canvas",
});
// Load and play
async function playVideo() {
try {
await player.load();
const info = player.getMediaInfo();
console.log(`Loaded: ${info?.duration}s`);
await player.play();
} catch (error) {
console.error("Failed to play:", error);
}
}
playVideo();Progress Bar โ
const progressBar = document.getElementById("progress") as HTMLInputElement;
const timeDisplay = document.getElementById("time") as HTMLSpanElement;
player.on("timeUpdate", (currentTime: number) => {
const duration = player.getDuration();
const percent = (currentTime / duration) * 100;
progressBar.value = percent.toString();
timeDisplay.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
});
progressBar.addEventListener("input", () => {
const percent = parseFloat(progressBar.value);
const duration = player.getDuration();
const timestamp = (percent / 100) * duration;
player.seek(timestamp);
});
function formatTime(seconds: number): string {
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, "0")}`;
}Multi-Language Audio โ
async function setupAudioTracks() {
await player.load();
const audioTracks = player.getAudioTracks();
const selector = document.getElementById("audioTrack") as HTMLSelectElement;
// Populate dropdown
audioTracks.forEach((track) => {
const option = document.createElement("option");
option.value = track.id.toString();
option.textContent = `${track.language || "Unknown"} (${track.codec})`;
selector.appendChild(option);
});
// Handle selection
selector.addEventListener("change", () => {
const trackId = parseInt(selector.value);
player.selectAudioTrack(trackId);
});
}HDR Detection โ
async function checkHDR() {
await player.load();
const videoTrack = player.getVideoTracks()[0];
const isHDR =
videoTrack.colorPrimaries === "bt2020" &&
(videoTrack.colorTransfer === "smpte2084" || // HDR10
videoTrack.colorTransfer === "arib-std-b67"); // HLG
if (isHDR) {
console.log("HDR content detected!");
console.log(`Transfer: ${videoTrack.colorTransfer}`);
console.log(`Primaries: ${videoTrack.colorPrimaries}`);
}
}Thumbnail Generation โ
async function generateThumbnails(url: string, count: number) {
const player = new MoviPlayer({ canvas });
await player.load();
const duration = player.getDuration();
const interval = duration / (count + 1);
const thumbnails: Blob[] = [];
for (let i = 1; i <= count; i++) {
const timestamp = interval * i;
const thumbnail = await player.getPreviewFrame(timestamp);
if (thumbnail) thumbnails.push(thumbnail);
}
player.destroy();
return thumbnails;
}
// Usage
const thumbs = await generateThumbnails("video.mp4", 10);
thumbs.forEach((blob, i) => {
const img = document.createElement("img");
img.src = URL.createObjectURL(blob);
document.body.appendChild(img);
});Performance โ
Hardware Decoding โ
WebCodecs API provides access to platform hardware decoders:
Supported Codecs (hardware):
- H.264/AVC (all platforms)
- H.265/HEVC (macOS, Windows, Android)
- VP9 (Chrome, Edge)
- AV1 (modern browsers)
Fallback: If hardware fails, player automatically switches to software decoding (FFmpeg WASM).
Memory Usage โ
Typical 4K HEVC Playback:
- WASM heap: ~50MB
- Video frame queue: ~120 frames ร ~12MB = ~1.4GB (YUV 4:2:0)
- Audio buffer: ~2s ร 48kHz ร 2ch ร 4B = ~384KB
- Total: ~1.5GB (mostly video frames)
Optimization:
- Frame queue size adapts to frame rate
- Decoder buffer limits prevent overflow
- Back-pressure stops demuxing when buffers full
Seeking Performance โ
Keyframe Seeking:
- Fast: 100-300ms (index-based)
- Used for most seeks
Non-Keyframe Seeking:
- Slower: 500-2000ms (decode from last keyframe)
- Rare (only when seeking to exact timestamp)
Post-Seek Throttle:
- 200ms delay prevents rapid seeks
- Improves UX on low-end devices
Troubleshooting โ
Video Not Playing โ
Check:
- Codec support:
await navigator.mediaCapabilities.decodingInfo(...) - Browser compatibility: WebCodecs requires Chrome 94+, Edge 94+, Safari 16.4+
- CORS headers: Cross-origin videos need
Access-Control-Allow-Origin
Debug:
player.on("error", (error) => {
console.error("Error details:", error);
console.log("Current state:", player.getState());
console.log("Media info:", player.getMediaInfo());
});Audio/Video Out of Sync โ
Causes:
- Decoder lag (software decode of 4K)
- Buffer underrun
- Incorrect PTS in source file
Debug:
player.on("frame", (frame) => {
const audioClock = audioRenderer.getAudioClock();
const drift = frame.timestamp - audioClock;
console.log(`A/V drift: ${drift * 1000}ms`);
});Fix:
- Enable hardware decoding
- Reduce quality (lower resolution track)
- Increase buffer sizes
High Memory Usage โ
Causes:
- Large frame queue for 4K/8K
- Memory leak (frames not closed)
Fix:
// Reduce frame queue (edit CanvasRenderer)
private static readonly MAX_FRAME_QUEUE = 60; // Default: 120
// Ensure player destroyed when done
window.addEventListener('beforeunload', () => {
player.destroy();
});Seeking is Slow โ
Causes:
- Non-seekable stream (no index)
- Large GOP size (keyframes far apart)
Workaround:
// Show loading indicator during seek
player.on("seeking", () => {
showLoadingSpinner();
});
player.on("seeked", () => {
hideLoadingSpinner();
});Best Practices โ
1. Always Destroy Player โ
// React example
useEffect(() => {
const player = new MoviPlayer({ canvas });
return () => {
player.destroy(); // Cleanup on unmount
};
}, []);2. Handle Errors Gracefully โ
player.on("error", async (error) => {
console.error("Playback error:", error);
// Try recovery
try {
await player.seek(0);
await player.play();
} catch {
showErrorMessage("Playback failed");
}
});3. Optimize for Mobile โ
// Detect mobile and shrink the prefetch window
const isMobile = /iPhone|iPad|Android/i.test(navigator.userAgent);
if (isMobile) {
player.setMaxBufferSize(80); // 80 MB prefetch instead of the default 250
}Multi-quality switching is HLS-only at the programmatic-API level; for a non-HLS source, swap src to a lower-bitrate file when bandwidth/device dictates.
See Also โ
Last Updated: June 2, 2026