Sources API โ
Movi-Player provides different source adapters for various input types.
Available Sources โ
| Source | Use Case | Import |
|---|---|---|
HttpSource | Remote URLs | movi-player/demuxer |
FileSource | Local files | movi-player/demuxer |
| Custom adapter | Any other protocol | implement SourceAdapter |
HttpSource โ
For loading videos from HTTP/HTTPS URLs.
Basic Usage โ
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 โ
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-RangeFeatures โ
- โ 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 โ
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 โ
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.
// 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:
// 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 fullSource Interface โ
All sources implement the SourceAdapter interface:
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:
| Method | Required | Where it's called |
|---|---|---|
getSize() | Yes | File size cache + demuxer EOF check |
read() | Yes | Every WASM/FFmpeg I/O request |
getKey() | Yes | Resume-position storage key (must be stable across sessions) |
seek() | No | WASM probes for it via typeof === "function"; safe no-op |
getPosition() | No | Only used inside instanceof HttpSource branches |
close() | No | Called 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:
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:
// 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:
// 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 โ
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 โ
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 โ
| Metric | HttpSource | FileSource |
|---|---|---|
| Initial load | Network dependent | Instant |
| Seeking | ~100-500ms | <10ms |
| Memory | ~200MB | ~100-400MB |
| Offline | โ | โ |
| CORS | Required | Not needed |