Skip to content

Player Documentation โ€‹

Movi Streaming Video Library - Player Component


Table of Contents โ€‹

  1. Overview
  2. Architecture
  3. API Reference
  4. Configuration
  5. Playback Control
  6. Track Management
  7. Events
  8. A/V Synchronization
  9. Usage Examples
  10. Performance
  11. 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 Master

API Reference โ€‹

Constructor โ€‹

typescript
constructor(config: PlayerConfig)

Parameters:

typescript
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:

typescript
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.

typescript
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:

typescript
// 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:

typescript
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:

typescript
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 speed
    • 1.0 = normal speed (default)
    • 2.0 = double speed

Example:

typescript
player.setPlaybackRate(1.5); // 1.5x speed

setVolume(volume: number): void โ€‹

Sets audio volume.

Parameters:

  • volume - Volume level (0.0 to 1.0)
    • 0.0 = muted
    • 1.0 = maximum (default)

Example:

typescript
player.setVolume(0.5); // 50% volume

setMuted(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.

typescript
player.setMuted(true);
console.log(player.getMuted()); // true

setStableAudio(enabled: boolean): void / getStableAudio(): boolean โ€‹

Enable/disable the loudness-normalization compressor (driven by DynamicsCompressorNode). Same toggle as the stablevolume attribute on <movi-player>.

typescript
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.

typescript
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.

typescript
player.setMaxBufferSize(400); // 400 MB ahead

rotateVideo(): 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.

typescript
player.rotateVideo();          // โ†’ 90
player.setVideoRotation(180);
console.log(player.getVideoRotation()); // 180

setFitMode(mode): void โ€‹

Set the canvas fit policy. mode is "contain" | "cover" | "fill" | "zoom" | "control".

typescript
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.

typescript
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.

typescript
type PlayerState =
  | "idle" // Not loaded
  | "loading" // Loading source
  | "ready" // Loaded, paused
  | "playing" // Active playback
  | "seeking" // Seeking in progress
  | "ended" // Playback finished
  | "error"; // Error occurred

getVolume(): 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.

typescript
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".

typescript
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().

typescript
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.

typescript
const spanish = player.getSubtitleTracks().find((t) => t.language === "spa");
if (spanish) await player.selectSubtitleTrack(spanish.id);

await player.selectSubtitleTrack(null); // Off

Preview / 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.

typescript
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.maxSizeMB defaults to ~100 MB; tune via setMaxBufferSize() at runtime.
  • enablePreviews: true is required if you plan to call getPreviewFrame().

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:

  1. Check State: Skip if paused/seeking
  2. Read Packets: Demux next video/audio/subtitle packets
  3. Decode: Send packets to appropriate decoders
  4. Buffer Management: Apply back-pressure if buffers full
  5. Frame Presentation: Renderer handles timing
  6. 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 โ€‹

typescript
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) โ€‹

typescript
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 โ€‹

typescript
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 โ€‹

typescript
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:

typescript
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 โ€‹

  1. Loose Sync (Default)

    • Video uses wall clock for smooth presentation
    • Periodic corrections from audio clock
    • ยฑ50ms tolerance before correction
  2. 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 buffering event (if implemented)

Usage Examples โ€‹

Basic Playback โ€‹

typescript
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 โ€‹

typescript
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 โ€‹

typescript
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 โ€‹

typescript
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 โ€‹

typescript
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:

  1. Codec support: await navigator.mediaCapabilities.decodingInfo(...)
  2. Browser compatibility: WebCodecs requires Chrome 94+, Edge 94+, Safari 16.4+
  3. CORS headers: Cross-origin videos need Access-Control-Allow-Origin

Debug:

typescript
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:

typescript
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:

typescript
// 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:

typescript
// Show loading indicator during seek
player.on("seeking", () => {
  showLoadingSpinner();
});

player.on("seeked", () => {
  hideLoadingSpinner();
});

Best Practices โ€‹

1. Always Destroy Player โ€‹

typescript
// React example
useEffect(() => {
  const player = new MoviPlayer({ canvas });

  return () => {
    player.destroy(); // Cleanup on unmount
  };
}, []);

2. Handle Errors Gracefully โ€‹

typescript
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 โ€‹

typescript
// 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

Released under the Apache-2.0 License. Privacy Policy