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
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; // Required โ { type, url } or { type, file }
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)
drm?: boolean; // DRM mode for HLS (native <video> + EME)
licenseUrl?: string; // Widevine/FairPlay license server URL
licenseHeaders?: Record<string, string>; // Auth headers for license requests
}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",
});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 4.0 recommended)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
| "seeking" // Seeking in progress
| "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.
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;
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 };
// Frame-level (advanced)
frame: DecodedVideoFrame;
audio: DecodedAudioFrame;
subtitle: SubtitleCue;
}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: February 5, 2026