Skip to content

Sources API โ€‹

Movi-Player provides different source adapters for various input types.

Available Sources โ€‹

SourceUse CaseImport
HttpSourceRemote URLsmovi-player/demuxer
FileSourceLocal filesmovi-player/demuxer
Custom adapterAny other protocolimplement SourceAdapter

HttpSource โ€‹

For loading videos from HTTP/HTTPS URLs.

Basic Usage โ€‹

typescript
import { Demuxer, HttpSource } from "movi-player/demuxer";

const source = new HttpSource("https://example.com/video.mp4");
const demuxer = new Demuxer(source);

await demuxer.open();
console.log("Duration:", demuxer.getDuration());

With Player โ€‹

typescript
import { MoviPlayer } from "movi-player/player";

const player = new MoviPlayer({
  source: { type: "url", url: "https://example.com/video.mp4" },
  canvas: document.getElementById("canvas") as HTMLCanvasElement,
});

await player.load();

HttpSource is created internally for { type: "url" }. If you want to plug a pre-built HttpSource (or any other adapter instance) directly into MoviPlayer or <movi-player> โ€” for tweaked headers, alternate buffering, or a fully custom protocol โ€” use the sourceAdapter field instead. See Custom Sources.

CORS Requirements โ€‹

CORS

HttpSource requires the server to send proper CORS headers:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, HEAD
Access-Control-Expose-Headers: Content-Length, Content-Range

Features โ€‹

  • โœ… Range request support (seeking)
  • โœ… Automatic chunk caching
  • โœ… HEAD request for file size
  • โœ… Error recovery

FileSource โ€‹

For loading local files from the user's device.

Basic Usage โ€‹

typescript
import { Demuxer, FileSource } from "movi-player/demuxer";

const fileInput = document.getElementById("file") as HTMLInputElement;

fileInput.addEventListener("change", async (e) => {
  const file = (e.target as HTMLInputElement).files?.[0];
  if (!file) return;

  const source = new FileSource(file);
  const demuxer = new Demuxer(source);

  await demuxer.open();
  console.log("File:", file.name);
  console.log("Duration:", demuxer.getDuration());
});

With Player โ€‹

typescript
import { MoviPlayer } from "movi-player/player";

fileInput.addEventListener("change", async (e) => {
  const file = (e.target as HTMLInputElement).files?.[0];
  if (!file) return;

  const player = new MoviPlayer({
    source: { type: "file", file },
    canvas: document.getElementById("canvas") as HTMLCanvasElement,
  });

  await player.load();
  await player.play();
});

Features โ€‹

  • โœ… No CORS needed
  • โœ… Instant seeking (no network latency)
  • โœ… LRU cache for chunks
  • โœ… Memory efficient (2MB chunks)
  • โœ… Works offline
  • โœ… Revocation recovery (8s timeout per chunk read)

Handle Revocation (mobile) โ€‹

iOS Safari and Android Chrome silently revoke File handles after long backgrounding or memory pressure, leaving the demuxer hung forever waiting on a read that will never complete.

FileSource races each chunk read against an 8s timeout. The first time a read fails this way, it fires a one-shot onRevoked callback so the host can prompt for a re-pick. MoviPlayer re-emits this as a filerevoked event, and <movi-player> re-dispatches it as a DOM CustomEvent.

typescript
// Direct FileSource use:
const source = new FileSource(file);
source.setOnRevoked(({ offset, length, reason }) => {
  console.warn(`File handle revoked at byte ${offset} (${reason})`);
  promptUserToRepickFile();
});

// Via the player:
player.on("filerevoked", (info) => promptUserToRepickFile());

// Via the element:
el.addEventListener("filerevoked", (e: CustomEvent) => {
  promptUserToRepickFile();
});

Memory Management โ€‹

FileSource uses intelligent chunking:

typescript
// Internal configuration
const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB chunks
const MAX_CACHED_CHUNKS = 50; // ~100MB max cache

// LRU cache evicts least recently used chunks
// when cache is full

Source Interface โ€‹

All sources implement the SourceAdapter interface:

typescript
interface SourceAdapter {
  // Total size of the source in bytes
  getSize(): Promise<number>;

  // Read `length` bytes starting at `offset`. Must return an ArrayBuffer.
  read(offset: number, length: number): Promise<ArrayBuffer>;

  // Seek to a position (sources that need state can track it here)
  seek(offset: number): number;

  // Current read position
  getPosition(): number;

  // Close and release resources
  close(): void;

  // Stable, unique identifier โ€” used for the resume-position storage key
  getKey(): string;
}

What the Player Actually Calls โ€‹

Out of the six methods, only three are load-bearing for a custom adapter โ€” the rest are required by TypeScript but can be no-ops:

MethodRequiredWhere it's called
getSize()YesFile size cache + demuxer EOF check
read()YesEvery WASM/FFmpeg I/O request
getKey()YesResume-position storage key (must be stable across sessions)
seek()NoWASM probes for it via typeof === "function"; safe no-op
getPosition()NoOnly used inside instanceof HttpSource branches
close()NoCalled on destroy(); no-op if nothing to clean up

Creating Custom Sources โ€‹

Bring your own protocol โ€” WebSocket, WebRTC data channel, IndexedDB, custom encryption, anything โ€” without touching the demuxer or the UI:

typescript
import type { SourceAdapter } from "movi-player";

class MySource implements SourceAdapter {
  constructor(private url: string, private totalSize: number) {}

  async getSize() {
    return this.totalSize;
  }

  async read(offset: number, length: number): Promise<ArrayBuffer> {
    // Fetch bytes [offset, offset + length) from your protocol.
    // If offset + length > totalSize, return a TRUNCATED buffer โ€” do not throw.
    // FFmpeg's probe phase expects partial reads near EOF.
  }

  getKey() {
    return this.url; // Stable key for resume storage
  }

  // โ†“ TS requires these โ€” safe no-ops for read-only random-access adapters
  seek(o: number) { return o; }
  getPosition() { return 0; }
  close() {}
}

Plugging In โ€‹

Same adapter, three integration surfaces:

typescript
// 1. Demuxer (low-level, no playback) โ€” direct constructor argument
import { Demuxer } from "movi-player/demuxer";
const dm = new Demuxer(new MySource(url, size));
await dm.open();

// 2. MoviPlayer (programmatic, no UI) โ€” `sourceAdapter` config field
import { MoviPlayer } from "movi-player/player";
const player = new MoviPlayer({
  sourceAdapter: new MySource(url, size),
  canvas: document.querySelector("canvas")!,
});
await player.load();

// 3. <movi-player> custom element โ€” `sourceAdapter` property
const el = document.querySelector("movi-player");
el.sourceAdapter = new MySource(url, size);

When sourceAdapter is set, the standard source / src path is bypassed โ€” the player feeds bytes through your adapter directly. Setting src after clears the adapter (and vice versa) so the two stay mutually exclusive.

Random-access is mandatory

FFmpeg seeks to the end of the file to find the moov atom (MP4), then back to the start. Pure-streaming protocols like a single WebSocket frame won't work โ€” you need either server-side range queries or client-side buffering of the whole file before playback.

Return ArrayBuffer, not Uint8Array

The adapter contract is Promise<ArrayBuffer>. Returning Uint8Array works at runtime but forces a silent new Uint8Array(uint8array) copy inside the demuxer wrapper.

Source Selection โ€‹

SourceConfig requires an explicit type discriminant โ€” the player picks the right adapter from it:

typescript
// HTTP URL โ†’ HttpSource
await player.load({ type: "url", url: "https://example.com/video.mp4" });

// Local File โ†’ FileSource
await player.load({ type: "file", file: selectedFile });

// Encrypted endpoint โ†’ EncryptedHttpSource
await player.load({
  type: "encrypted",
  encrypted: {
    videoUrl: "/api/video",
    tokenUrl: "/api/token",
    videoId: "movie.mp4",
    fingerprint: await generateFingerprint(),
    sessionToken: jwt,
  },
});

Error Handling โ€‹

HttpSource Errors โ€‹

typescript
try {
  const source = new HttpSource(url);
  const demuxer = new Demuxer(source);
  await demuxer.open();
} catch (error) {
  if (error.message.includes("CORS")) {
    console.error("CORS error: Server must allow cross-origin requests");
  } else if (error.message.includes("404")) {
    console.error("File not found");
  } else if (error.message.includes("network")) {
    console.error("Network error");
  }
}

FileSource Errors โ€‹

typescript
try {
  const source = new FileSource(file);
  const demuxer = new Demuxer(source);
  await demuxer.open();
} catch (error) {
  if (error.message.includes("format")) {
    console.error("Unsupported file format");
  } else if (error.message.includes("corrupt")) {
    console.error("File may be corrupted");
  }
}

Performance Comparison โ€‹

MetricHttpSourceFileSource
Initial loadNetwork dependentInstant
Seeking~100-500ms<10ms
Memory~200MB~100-400MB
OfflineโŒโœ…
CORSRequiredNot needed