6lvkro's Atelier

mkvpeek

The README of mkvpeek 0.2.0.

mkvpeek

npm demo

MKV tracks and subtitles in pure JavaScript, reading a fraction of the file.

Measured on a 2.3 GB file on a local NVMe drive
ffprobe -show_streams video.mkv 1.3 MB read 22 ms
peekTracks("video.mkv") 135 KB read 0.9 ms
ffmpeg -i video.mkv -map 0:s:0 -c:s copy out.ass 2.3 GB read 659 ms
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

Install

Terminal window
npm install mkvpeek

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
} of await
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
error
instanceof
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
}

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
} of await
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.

urlSource
(
any
url
, {
UrlOptions.headers?: Readonly<Record<string, string>> | undefined

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.

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.

openContainer
(
any
path
);
const
const subtitles: Track<TrackType>[]
subtitles
=
await using container: Container
container
.
Container.tracks: Track<TrackType>[]
tracks
.
Array<Track<TrackType>>.filter(predicate: (value: Track<TrackType>, index: number, array: Track<TrackType>[]) => unknown, thisArg?: any): Track<TrackType>[] (+1 overload)

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.

filter
((
track: Track<TrackType>
track
) =>
track: Track<TrackType>
track
.
Track<TrackType>.type: TrackType
type
=== "subtitle");
for await (const
const frame: Frame
frame
of
await using container: Container
container
.
Container.frames(tracks: readonly Track[], options?: FrameOptions): AsyncIterableIterator<Frame>

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
error
instanceof
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.

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.

License

MIT