subtitleFile(firstSubtitle, frames) 501 KB read 24 ms
mkvpeek is timed inside a running Node process, from the open to the first subtitle track written out; the two tools, from spawn to exit.
When to use it
You want the track list and the container info without an external demuxer
You need to read a container in the browser with no WASM dependency
The container sits on a server that allows Range requests
You want a subtitle track, quickly and with fewer reads (or less transfer)
Install
Terminal window
npm installmkvpeek
Node 22.1 or later, no other dependencies.
In the browser, mkvpeek/browser is the same reader minus file paths.
Usage
import{
function peekTracks(target: Target, options?: Abortable): Promise<Listing>
Peeks at a container's track list.
It reads only the header, answers every type of track in one array,
and throws a refusal as a RefusalError, as openContainer does.
peekTracks}from"mkvpeek";
// A file path (Node only), a URL whose server allows Range requests, or bytes
const{
const tracks: Track<TrackType>[]
tracks,
const info: ContainerInfo
info}=await
function peekTracks(target: Target, options?: Abortable): Promise<Listing>
Peeks at a container's track list.
It reads only the header, answers every type of track in one array,
and throws a refusal as a RefusalError, as openContainer does.
peekTracks("video.mkv");
import{
import writeFile
writeFile}from"node:fs/promises";
import{
function openContainer(target: Target, options?: Abortable): Promise<Container>
Opens a container for its tracks' frames, and keeps the target open until close.
It reads what peekTracks reads with info,
and throws a refusal at the open or at a read as a RefusalError.
openContainer,
class RefusalError
A refusal a peek, an open, a read of frames or attachments throws.
RefusalError,
function subtitleFiles(container: Container, options?: SubtitleOptions): Promise<SubtitleFile[]>
Writes out every track of container.tracks whose format formats names and that readable answers true for.
It writes nothing where that read is refused.
subtitleFiles}from"mkvpeek";
try{
await using
await using container: Container
container=await
function openContainer(target: Target, options?: Abortable): Promise<Container>
Opens a container for its tracks' frames, and keeps the target open until close.
It reads what peekTracks reads with info,
and throws a refusal at the open or at a read as a RefusalError.
openContainer("video.mkv");
for(const{
const track: Track<TrackType>
track,
const format: SubtitleFormat
format,
const bytes: Uint8Array<ArrayBufferLike>
UTF-8 for the text formats.
bytes}ofawait
function subtitleFiles(container: Container, options?: SubtitleOptions): Promise<SubtitleFile[]>
Writes out every track of container.tracks whose format formats names and that readable answers true for.
It writes nothing where that read is refused.
subtitleFiles(
await using container: Container
container)){
await
import writeFile
writeFile(`${
const track: Track<TrackType>
track.
Track<TrackType>.index: number
The position in the track list.
The same count as ffmpeg's 0:N.
index}.${
const format: SubtitleFormat
format}`,
const bytes: Uint8Array<ArrayBufferLike>
UTF-8 for the text formats.
bytes);
}
}catch(
var error: unknown
error){
if(!(
var error: unknown
errorinstanceof
class RefusalError
A refusal a peek, an open, a read of frames or attachments throws.
RefusalError))throw
var error: unknown
error;
// error.code says why
}
peekTracks reads only the header, and answers every track with the container’s info.
openContainer keeps the file open for frames, readable and attachments, until close or the end of the await using block.
subtitleFiles reads every ASS, SSA, SubRip and WebVTT track in one plan and writes each out; formats names others, such as sup for PGS.
subtitleFile writes one track from its frames, in the format subtitleFormat names.
A refusal is thrown as a RefusalError, whose code says why.
Node 22 runs await using only through a compiler such as TypeScript or esbuild.
In plain JavaScript there, close the container in a finally.
const
const container: any
container=await
any
openContainer("video.mkv");
try{
for(const{
const track: any
track,
const format: any
format,
const bytes: any
bytes}ofawait
any
subtitleFiles(
const container: any
container)){
await
any
writeFile(`${
const track: any
track.
any
index}.${
const format: any
format}`,
const bytes: any
bytes);
}
}finally{
await
const container: any
container.
any
close();
}
For request options such as headers, open the source with urlSource; whoever opens a source closes it.
blobSource reads a File, or any other Blob, as it goes.
import{
function blobSource(blob: Blob, options?: BlobOptions): Source
A source over a Blob, such as a picked File, read as on a local disk.
WARN: a file on a network share wants trip: "mount", or a walk reads nearly all of it over the link.
blobSource,
function openContainer(target: Target, options?: Abortable): Promise<Container>
Opens a container for its tracks' frames, and keeps the target open until close.
It reads what peekTracks reads with info,
and throws a refusal at the open or at a read as a RefusalError.
openContainer,
function peekTracks(target: Target, options?: Abortable): Promise<Listing>
Peeks at a container's track list.
It reads only the header, answers every type of track in one array,
and throws a refusal as a RefusalError, as openContainer does.
peekTracks,
function urlSource(url: string, options?: UrlOptions): Promise<Source & AsyncDisposal>
An HTTP source over node:http, or over fetch when one is passed.
A server that answers a Range request with anything but 206 fails the read as source-failed.
urlSource}from"mkvpeek";
await using
await using source: Source & Record<typeof Symbol.asyncDispose,()=> Promise<void>>
source=await
function urlSource(url: string, options?: UrlOptions): Promise<Source & AsyncDisposal>
An HTTP source over node:http, or over fetch when one is passed.
A server that answers a Range request with anything but 206 fails the read as source-failed.
Sent with every request, but for Host, Range and Accept-Encoding, which the transport owns.
headers});
const{
const tracks: Track<TrackType>[]
tracks}=await
function peekTracks(target: Target, options?: Abortable): Promise<Listing>
Peeks at a container's track list.
It reads only the header, answers every type of track in one array,
and throws a refusal as a RefusalError, as openContainer does.
peekTracks(
await using source: Source & Record<typeof Symbol.asyncDispose,()=> Promise<void>>
source);
const
const container: Container
container=await
function openContainer(target: Target, options?: Abortable): Promise<Container>
Opens a container for its tracks' frames, and keeps the target open until close.
It reads what peekTracks reads with info,
and throws a refusal at the open or at a read as a RefusalError.
openContainer(
function blobSource(blob: Blob, options?: BlobOptions): Source
A source over a Blob, such as a picked File, read as on a local disk.
WARN: a file on a network share wants trip: "mount", or a walk reads nearly all of it over the link.
blobSource(
any
file));
blobSource reads as from a local disk, since a page is never told where a file lives; for a file on a network share, pass blobSource(file, { trip: "mount" }).
Constraints and read path selection
Frames are, for now, the subtitle tracks’ alone. Where a track’s frames are stored encoded, it undoes zlib and header stripping, and refuses a read of any other encoding, or of another kind of track, before it fetches a frame.
By default the fast path (index) follows the file’s Cues unless the file contradicts them, and then backs off to the slow path (walk), which visits every cluster.
A short Cues list on a track that states no frame count can go unnoticed; strict: true trusts only what the file proves, at the cost of more reads.
Only walk streams its frames; index vouches for all of them before handing any over.
On a high-latency link walk loses its speed to round trips; downloading the file once and passing the bytes can be faster.
A server has to answer a Range request with 206.
This reads fewer containers than the external tools, so where one is available, read by index and hand refused files to it.
import{
function openContainer(target: Target, options?: Abortable): Promise<Container>
Opens a container for its tracks' frames, and keeps the target open until close.
It reads what peekTracks reads with info,
and throws a refusal at the open or at a read as a RefusalError.
openContainer,
class RefusalError
A refusal a peek, an open, a read of frames or attachments throws.
RefusalError,
const worthFallback:(code: RefusalCode)=> boolean
Whether a file this reader refused is worth handing to another tool, ffmpeg say.
Yes for a refusal that read the container; no where the bytes did not arrive or the call was wrong.
worthFallback}from"mkvpeek";
try{
await using
await using container: Container
container=await
function openContainer(target: Target, options?: Abortable): Promise<Container>
Opens a container for its tracks' frames, and keeps the target open until close.
It reads what peekTracks reads with info,
and throws a refusal at the open or at a read as a RefusalError.
Returns the elements of an array that meet the condition specified in a callback function.
@param ― predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.
@param ― thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
Reads the frames of the given tracks in one plan and in file order.
Before a byte is read, it refuses a track that is not readable,
and throws a TypeError for a track that does not state, value for value, one of the tracks listed at the open.
frames(
const subtitles: Track<TrackType>[]
subtitles,{
FrameOptions.via?: FrameVia |undefined
via:"index"}))
any
use(
const frame: Frame
frame);
}catch(
var error: unknown
error){
if(!(
var error: unknown
errorinstanceof
class RefusalError
A refusal a peek, an open, a read of frames or attachments throws.
RefusalError)||!
function worthFallback(code: RefusalCode): boolean
Whether a file this reader refused is worth handing to another tool, ffmpeg say.
Yes for a refusal that read the container; no where the bytes did not arrive or the call was wrong.
worthFallback(
var error: RefusalError
error.
RefusalError.code: RefusalCode
code))throw
var error: unknown
error;
await
any
demux(
any
path);
}
Compared with FFmpeg
Output is verified against ffprobe and ffmpeg, with four deliberate exceptions.
A track that states no language: ffprobe emits eng; the package emits null.
A frame that states no duration, or zero: ffmpeg may fill one in from a later frame; the package emits null or 0.
A WebVTT block that does not split the way ffmpeg splits one: ffmpeg drops the cue; the package keeps it.
Media that does not start where the first cluster does: every timestamp differs from ffmpeg’s by that gap.
Anything else the spec defines that a served read distorts or loses is a bug, apart from what a short Cues list hides by default; under strict, only these four remain.