npm install playsvideoimport { PlaysVideoEngine } from 'playsvideo';PlaysVideoEngine wraps a standard <video> element — the same pattern as hls.js. The video element you pass in is the playback surface — all native HTMLVideoElement properties, methods, and events work exactly as usual. The engine just replaces video.src with a pipeline that can play formats the browser doesn't natively support (MKV, AVI, non-MP4 containers, non-native audio codecs, embedded subtitles, etc.).
The underlying video element is accessible as engine.video.
Everything you'd normally do with a <video> element works unchanged:
- Playback control:
video.play(),video.pause(),video.playbackRate - Seeking:
video.currentTime = 30 - Volume:
video.volume,video.muted - State:
video.paused,video.ended,video.readyState,video.duration - Events:
timeupdate,play,pause,ended,seeking,seeked,volumechange,canplay, etc. - UI:
video.controls, fullscreen, Picture-in-Picture, CSS styling - Text tracks:
video.textTracks(engine adds subtitle<track>elements automatically)
| Instead of | Use |
|---|---|
video.src = '...' |
engine.loadFile(file) or engine.loadUrl(url) |
external subtitle <track> plumbing |
engine.loadExternalSubtitle(file) |
| — | engine.destroy() to release resources |
| — | engine.addEventListener('loading' | 'ready' | 'error', ...) for engine lifecycle |
| — | engine.phase, engine.loading, engine.subtitleTracks for engine state |
const engine = new PlaysVideoEngine(video: HTMLVideoElement);Attaches to a <video> element. Respects the autoplay attribute — if set, playback starts automatically after loading. Otherwise, call video.play() manually.
Load a video from a local File object (from <input type="file">, drag-and-drop, File Handling API, etc.).
input.addEventListener('change', () => {
const file = input.files?.[0];
if (file) engine.loadFile(file);
});Load a video from an HTTP URL. The server must support CORS and HTTP range requests.
engine.loadUrl('https://example.com/video.mkv');Attach one user-provided external subtitle file to the current video and make it the active text track. Loading a new external subtitle replaces the previous external subtitle, but leaves embedded tracks intact.
Currently supports .srt and .vtt.
const subFile = subtitleInput.files?.[0];
if (subFile) {
await engine.loadExternalSubtitle(subFile);
}Remove any previously loaded external subtitle tracks.
engine.clearExternalSubtitles();Tear down the engine, terminate the worker, and release all resources. Safe to call multiple times.
| Property | Type | Description |
|---|---|---|
video |
HTMLVideoElement |
The underlying video element (readonly) |
phase |
'idle' | 'demuxing' | 'ready' | 'error' |
Current engine state |
loading |
boolean |
true while demuxing (shorthand for phase === 'demuxing') |
totalSegments |
number |
Number of segments in the plan (0 until ready) |
durationSec |
number |
Video duration in seconds (0 until ready) |
subtitleTracks |
SubtitleTrackInfo[] |
Embedded subtitle tracks (empty until ready) |
The engine extends EventTarget and emits a small number of lifecycle events. These are separate from the native video events (which fire on the <video> element as usual).
Fired when a file or URL starts loading.
engine.addEventListener('loading', (e) => {
// e.detail.file — the File object (if loadFile was called)
// e.detail.url — the URL string (if loadUrl was called)
});Fired when demuxing is complete and playback begins.
engine.addEventListener('ready', (e) => {
console.log(e.detail.totalSegments); // number of segments
console.log(e.detail.durationSec); // duration in seconds
console.log(e.detail.subtitleTracks); // SubtitleTrackInfo[]
});Fired on fatal errors (demux failure, unsupported format, playback error, etc.).
engine.addEventListener('error', (e) => {
console.error(e.detail.message);
});interface SubtitleTrackInfo {
index: number; // 0-based track index
codec: string; // original codec (e.g. 'subrip', 'ass')
language: string; // ISO 639-2/T code (e.g. 'eng', 'spa', 'und')
name: string | null; // user-visible track name, if any
disposition: {
default: boolean;
forced: boolean;
hearingImpaired: boolean;
};
}import { PlaysVideoEngine } from 'playsvideo';
const video = document.querySelector('video')!;
const status = document.querySelector('#status')!;
const engine = new PlaysVideoEngine(video);
// Engine lifecycle events
engine.addEventListener('loading', () => {
status.textContent = 'Loading...';
});
engine.addEventListener('ready', (e) => {
const { totalSegments, durationSec, subtitleTracks } = e.detail;
const mins = Math.floor(durationSec / 60);
const secs = Math.floor(durationSec % 60);
status.textContent = `${totalSegments} segments, ${mins}:${secs.toString().padStart(2, '0')}`;
if (subtitleTracks.length > 0) {
console.log(`${subtitleTracks.length} subtitle tracks`);
}
});
engine.addEventListener('error', (e) => {
status.textContent = `Error: ${e.detail.message}`;
});
// Standard <video> events work as usual
video.addEventListener('timeupdate', () => {
console.log(`Position: ${video.currentTime.toFixed(1)}s`);
});
// Load from file input
document.querySelector('input[type=file]')!.addEventListener('change', (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) engine.loadFile(file);
});
// Or load from URL
engine.loadUrl('https://example.com/video.mkv');- Calling
loadFileorloadUrlwhile already playing will cleanly tear down the previous session and start a new one. - Embedded subtitles (SRT, ASS/SSA) are automatically extracted and attached as
<track>elements on the video. - External subtitle import currently supports
.srtand.vtt. External.ass/.ssafiles are not yet rendered because the engine's external-file path converts imported subtitles to WebVTT, and the ASS/SSA parser path is still a placeholder. - Audio codecs unsupported by the active playback path are transcoded to AAC on the fly using a lightweight ffmpeg.wasm build that is lazy-loaded only when needed. Passthrough/native playback and remuxed HLS/MSE playback are evaluated separately, so a codec may be allowed for direct playback but still be transcoded in the remux pipeline.
- URL loading uses HTTP range requests for random access — the entire file is not downloaded upfront.