diff --git a/AGENTS.md b/AGENTS.md index 3837d41..0dc3cb8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,13 +2,13 @@ ## Project Overview -`@plutotcool/fsv` is a TypeScript library that defines and implements the **FSV (Fast Scrubbing Video)** format — a custom binary video format optimized for frame-accurate scrubbing on the web, with optional alpha-channel support. +`@plutotcool/fsv` is a TypeScript library that defines and implements the **FSV (Fast Scrubbing Video)** format — a video container format optimized for frame-accurate scrubbing on the web, using H.264 and H.265 codecs with optional alpha-channel support. The library ships two distinct APIs: | API | Environment | Purpose | |-----|-------------|---------| -| **Conversion** | Node.js | Converts any video file to `.fsv` using ffmpeg (`node-av`) for encoding and config extraction | +| **Conversion** | Node.js | Converts any video file to `.fsv` (H.264/H.265 container) using ffmpeg (`node-av`) for encoding and config extraction | | **Decoding / Rendering** | Browser | Loads, demuxes, decodes, and WebGL2-renders `.fsv` files using native browser WebCodecs and WebGL2 APIs | The package is published to the GitHub Packages registry (`https://npm.pkg.github.com`) under the `@plutotcool` scope. @@ -185,8 +185,8 @@ The manifest is serialized as a compact flat number array: each frame is 4 conse - Uses `node-av` (ffmpeg bindings) for decoding and encoding - Uses a `FilterComplexAPI` for pixel format conversion to `yuv420p` - For alpha videos, uses ffmpeg's `alphaextract` filter to produce separate color + alpha streams -- Supported output codecs: `libx264`, `libx265`, `libvpx-vp8`, `libvpx-vp9` -- Default output codec: `libx264` → mp4 container +- Supported output codecs: `libx264` (H.264), `libx265` (H.265) +- Default output codec: `libx264` - Default encoder settings: `crf=20`, `gopSize=5`, `maxBFrames=0`, `threadType=FF_THREAD_FRAME` - Sets `AV_CODEC_FLAG_GLOBAL_HEADER` on each encoder so SPS/PPS go into extradata (not inlined in keyframes) - Collects raw packets in memory as `Packet[]` — no temp files or intermediate container @@ -200,7 +200,7 @@ The manifest is serialized as a compact flat number array: each frame is 4 conse ### Muxer (Node.js only — `src/core/Muxer.ts`) - Accepts `Packet[]` arrays from Converter (no container demuxing) -- Converts Annex B → AVCC bitstream for H.264/H.265 packets (VP8/VP9 pass through unchanged) +- Converts Annex B → AVCC bitstream for H.264/H.265 packets - Packs packets and manifest directly into FSV binary format - Reads width/height from `config.codedWidth`/`config.codedHeight` for manifest serialization diff --git a/package.json b/package.json index 0c8eb86..91c6abe 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,10 @@ "import": "./dist/cli/main.mjs", "require": "./dist/cli/main.cjs" }, + "./core/Codec": { + "import": "./dist/core/Codec.mjs", + "require": "./dist/core/Codec.cjs" + }, "./core/Converter": { "import": "./dist/core/Converter.mjs", "require": "./dist/core/Converter.cjs" diff --git a/readme.md b/readme.md index db18c25..a552dd3 100644 --- a/readme.md +++ b/readme.md @@ -4,6 +4,11 @@ FSV (short for "Fast Scrubbing Video") is a video format optimized for fast scrubbing on the web, supporting alpha channel. It has been inspired by the [ActiveFrame](https://github.com/activetheory/activeframe) project. +More specifically, FSV is a container for h264 and h265. It is designed to be +easily demuxable on the browser, and contains additional metadata to allow for +frame-accurate scrubbing and efficient seeking. See the [format](#format) +section for more details. + [See the demo](https://plutotcool-fsv.vercel.app) @@ -78,8 +83,8 @@ await Converter.convert('input.webm', 'output.fsv', { The input file codec is automatically detected if not provided. Yet the conversion may fail (especially for alpha videos) if the input codec is not -compatible with the conversion process. For VP8/VP9 videos, you may need to -explicitly set the input codec to `libvpx-vp8` or `libvpx-vp9`. +compatible with the conversion process. For VP8/VP9 input videos, you may +need to explicitly set the input codec to `libvpx-vp8` or `libvpx-vp9`. ## Rendering diff --git a/src/cli/convert.ts b/src/cli/convert.ts index f6da83e..046233d 100644 --- a/src/cli/convert.ts +++ b/src/cli/convert.ts @@ -1,5 +1,6 @@ import { defineCommand } from 'citty' -import { Converter, FF_ENCODER_LIBX264, type ConvertOptions } from '~/core/Converter' +import { Converter, type ConvertOptions } from '~/core/Converter' +import { resolveCodec } from '~/core/Codec' import { Logger } from '~/core/Logger' export default defineCommand({ @@ -26,7 +27,7 @@ export default defineCommand({ }, 'output-codec': { type: 'string', - default: FF_ENCODER_LIBX264, + default: 'libx264', valueHint: 'codec', description: 'Codec to use for encoding the video tracks.' }, @@ -60,7 +61,7 @@ export default defineCommand({ }), alpha: args.alpha, - outputCodec: args['output-codec'], + outputCodec: resolveCodec(args['output-codec']), inputCodec: args['input-codec'] === 'auto' ? undefined : args['input-codec'] as ConvertOptions['inputCodec'], diff --git a/src/core/Codec.ts b/src/core/Codec.ts new file mode 100644 index 0000000..0929efa --- /dev/null +++ b/src/core/Codec.ts @@ -0,0 +1,38 @@ +import { + FF_ENCODER_LIBX264 as H264, + FF_ENCODER_LIBX265 as H265 +} from 'node-av/constants' + +export const CODECS = [ + H264, + H265 +] + +export const DEFAULT_CODEC = H264 + +export { + H264, + H265 +} + +export type Codec = typeof CODECS[number] + +export function isCodec(codec: unknown): codec is Codec { + return CODECS.includes(codec as Codec) +} + +export function assertCodec(codec: unknown): asserts codec is Codec { + if (!isCodec(codec)) { + throw new Error(`Unsupported codec: ${codec}. Supported codecs: ${CODECS}`) + } +} + +export function resolveCodec(codec: unknown): Codec { + if (!codec) { + return DEFAULT_CODEC + } + + assertCodec(codec) + + return codec +} diff --git a/src/core/Converter.ts b/src/core/Converter.ts index 5a9a105..20ab6a8 100644 --- a/src/core/Converter.ts +++ b/src/core/Converter.ts @@ -5,7 +5,6 @@ import type { FFDecoderCodec, FFEncoderCodec, AVCodecID, - Codec, Stream } from 'node-av' @@ -22,10 +21,6 @@ import { import { AV_PIX_FMT_YUV420P, AV_CODEC_FLAG_GLOBAL_HEADER, - FF_ENCODER_LIBX264, - FF_ENCODER_LIBX265, - FF_ENCODER_LIBVPX_VP8, - FF_ENCODER_LIBVPX_VP9, FF_THREAD_FRAME, AVCOL_PRI_BT709, AVCOL_PRI_BT470BG, @@ -48,29 +43,13 @@ import { } from 'node-av/constants' import type { Packet } from './Packet' -import { Muxer as FSVMuxer, type MuxOptions } from './Muxer' +import { Muxer as FSVMuxer } from './Muxer' +import { assertCodec, DEFAULT_CODEC, type Codec } from './Codec' -const DEFAULT_OUTPUT_CODEC = FF_ENCODER_LIBX264 const ALPHA_SPLIT_FILTER = ( '[0:v]split[v1][v2];[v1]format=yuv420p[color];[v2]alphaextract,format=yuv420p[alpha]' ) -export const OUTPUT_CODECS = [ - FF_ENCODER_LIBX264, - FF_ENCODER_LIBX265, - FF_ENCODER_LIBVPX_VP8, - FF_ENCODER_LIBVPX_VP9, - 'libvpx-vp9', - 'libvpx-vp8' -] - -export { - FF_ENCODER_LIBX264, - FF_ENCODER_LIBX265, - FF_ENCODER_LIBVPX_VP8, - FF_ENCODER_LIBVPX_VP9 -} - export interface ConvertOptions { /** * Whether to include an alpha track in the output. @@ -95,7 +74,7 @@ export interface ConvertOptions { /** * The output video codec. */ - outputCodec?: typeof OUTPUT_CODECS[number] + outputCodec?: Codec /** * Optional encoder settings to override the defaults. @@ -165,7 +144,6 @@ async function convert( const encoded = await transcode(input, options) const muxed = FSVMuxer.mux(encoded.color, encoded.alpha, { config: encoded.config, - codec: encoded.codec, duration: encoded.duration }) @@ -189,7 +167,6 @@ interface TranscodeOutput { color: Packet[] alpha?: Packet[] config: VideoDecoderConfig - codec: MuxOptions['codec'] duration: number } @@ -204,7 +181,7 @@ async function transcode(source: string | Buffer, { const { inputFormat, inputCodec, - outputCodec = DEFAULT_OUTPUT_CODEC, + outputCodec = DEFAULT_CODEC, encoder: encoderOptions, logger } = options @@ -212,10 +189,7 @@ async function transcode(source: string | Buffer, { logger?.start(`Starting encoding`) try { - if (!OUTPUT_CODECS.includes(outputCodec)) { - logger?.error(`Supported output codecs: ${OUTPUT_CODECS}`) - throw new Error(`Unsupported output codec "${outputCodec}"`) - } + assertCodec(outputCodec) logger?.info(`Opening input with format ${inputFormat || '[auto]'}`) await using input = await Demuxer.open(source, { @@ -315,13 +289,12 @@ async function transcode(source: string | Buffer, { logger?.success(`Encoding completed`) - return { - color: packets, - alpha: undefined, - config, - codec: outputCodecToMuxCodec(outputCodec), - duration: streamDurationToMicroseconds(videoStream) - } + return { + color: packets, + alpha: undefined, + config, + duration: streamDurationToMicroseconds(videoStream) + } } catch (error) { logger?.error('Encoding failed') throw error @@ -331,17 +304,14 @@ async function transcode(source: string | Buffer, { async function transcodeAlpha(source: string | Buffer, { inputFormat, inputCodec, - outputCodec = DEFAULT_OUTPUT_CODEC, + outputCodec = DEFAULT_CODEC, encoder: encoderOptions, logger }: Exclude = {}): Promise { logger?.start(`Starting encoding`) try { - if (!OUTPUT_CODECS.includes(outputCodec)) { - logger?.error(`Supported output codecs: ${OUTPUT_CODECS}`) - throw new Error(`Unsupported output codec "${outputCodec}"`) - } + assertCodec(outputCodec) logger?.info(`Opening input with format ${inputFormat || '[auto]'}`) await using input = await Demuxer.open(source, { @@ -482,13 +452,12 @@ async function transcodeAlpha(source: string | Buffer, { logger?.success(`Encoding completed`) - return { - color: colorPackets, - alpha: alphaPackets, - config, - codec: outputCodecToMuxCodec(outputCodec), - duration: streamDurationToMicroseconds(videoStream) - } + return { + color: colorPackets, + alpha: alphaPackets, + config, + duration: streamDurationToMicroseconds(videoStream) + } } catch (error) { logger?.error('Encoding failed') throw error @@ -619,7 +588,8 @@ function mapTransferCharacteristic(value: number): string | undefined { } /** - * Maps an ffmpeg color space (matrix coefficients) value to a WebCodecs matrix string. + * Maps an ffmpeg color space (matrix coefficients) value to a WebCodecs matrix + * string. */ function mapColorMatrix(value: number): string | undefined { switch (value) { @@ -683,17 +653,15 @@ function streamDurationToMicroseconds(stream: Stream): number { return 0 } - return Number(duration * BigInt(timeBase.num) * 1_000_000n / BigInt(timeBase.den)) + return Number( + duration * BigInt(timeBase.num) * 1_000_000n / BigInt(timeBase.den) + ) } function resolveEncoderOptions( stream: Stream, encoderOptions?: Partial ): EncoderOptions { - const format = resolveOutputFormat( - (encoderOptions as ConvertOptions | undefined)?.outputCodec - ) - return { threadCount: 0, threadType: FF_THREAD_FRAME, @@ -703,77 +671,19 @@ function resolveEncoderOptions( frameRate: stream.avgFrameRate, ...encoderOptions, options: { - ...( - format === 'mp4' ? { - crf: 20, - preset: 'slower', - tune: 'fastdecode', - profile: 'baseline', - level: '5.1', - sc_threshold: '0', - movflags: '+faststart', - refs: '1', - pixel_format: AV_PIX_FMT_YUV420P - } : - - format === 'webm' ? { - crf: 20, - b: '0', - deadline: 'good', - 'cpu-used': '2', - 'row-mt': '1', - 'tile-columns': '2', - 'tile-rows': '1', - 'frame-parallel': '1', - pixel_format: AV_PIX_FMT_YUV420P - } : - - null - ), + ...{ + crf: 20, + preset: 'slower', + tune: 'fastdecode', + profile: 'baseline', + level: '5.1', + sc_threshold: '0', + movflags: '+faststart', + refs: '1', + pixel_format: AV_PIX_FMT_YUV420P + }, ...encoderOptions?.options } } as EncoderOptions } - -function resolveOutputFormat( - codec: ConvertOptions['outputCodec'] = DEFAULT_OUTPUT_CODEC -) { - switch (codec) { - case FF_ENCODER_LIBX264: - case FF_ENCODER_LIBX265: - return 'mp4' - - case FF_ENCODER_LIBVPX_VP8: - case FF_ENCODER_LIBVPX_VP9: - case 'libvpx-vp8': - case 'libvpx-vp9': - return 'webm' - - default: - throw new Error(`Unsupported output codec "${codec}"`) - } -} - -function outputCodecToMuxCodec( - codec: ConvertOptions['outputCodec'] = DEFAULT_OUTPUT_CODEC -): MuxOptions['codec'] { - switch (codec) { - case FF_ENCODER_LIBX264: - return 'h264' - - case FF_ENCODER_LIBX265: - return 'h265' - - case FF_ENCODER_LIBVPX_VP8: - case 'libvpx-vp8': - return 'vp8' - - case FF_ENCODER_LIBVPX_VP9: - case 'libvpx-vp9': - return 'vp9' - - default: - throw new Error(`Unsupported output codec "${codec}"`) - } -} diff --git a/src/core/Muxer.ts b/src/core/Muxer.ts index 220ec5c..bcf8e15 100644 --- a/src/core/Muxer.ts +++ b/src/core/Muxer.ts @@ -15,14 +15,6 @@ export interface MuxOptions { */ config: VideoDecoderConfig - /** - * The output codec identifier. - * - * Used to decide whether Annex B → AVCC conversion is required. - * H.264 and H.265 packets require conversion; VP8 and VP9 do not. - */ - codec: 'h264' | 'h265' | 'vp8' | 'vp9' - /** * Total duration of the video in microseconds. */ @@ -39,9 +31,8 @@ export const Muxer = { /** * Muxes raw encoded video packets into fsv format. * - * For H.264 and H.265, each packet's Annex B bitstream is converted to AVCC - * format (4-byte big-endian length prefixes) before being packed. VP8 and VP9 - * packets are stored as-is. + * Each packet's Annex B bitstream is converted to AVCC format + * (4-byte big-endian length prefixes) before being packed. * * @param packets The encoded color (or only) track packets. * @param alphaPackets Optional encoded alpha track packets. @@ -74,18 +65,13 @@ function mux( function muxTrack(packets: Packet[], { config, - codec, duration }: MuxOptions): Buffer { - const needsConversion = codec === 'h264' || codec === 'h265' - const chunks: Buffer[] = [] const frames: ManifestFrame[] = [] for (const packet of packets) { - const data = needsConversion - ? Buffer.from(annexBToAVCC(packet.data)) - : packet.data + const data = Buffer.from(annexBToAVCC(packet.data)) const previous = frames[frames.length - 1] @@ -122,11 +108,7 @@ function muxTrack(packets: Packet[], { * * Annex B uses start codes (0x00 0x00 0x00 0x01 or 0x00 0x00 0x01) as NAL * unit delimiters. AVCC replaces each start code with a 4-byte big-endian NAL - * unit length prefix, which is the format expected by WebCodecs for H.264 and - * H.265. - * - * VP8 and VP9 bitstreams do not use start codes and should be passed through - * unchanged — do not call this function for those codecs. + * unit length prefix, which is the format expected by WebCodecs. * * @param data The Annex B bitstream buffer. * @returns A new buffer containing the equivalent AVCC-formatted data. diff --git a/src/core/Packet.ts b/src/core/Packet.ts index 395619e..ebad97d 100644 --- a/src/core/Packet.ts +++ b/src/core/Packet.ts @@ -4,9 +4,6 @@ export interface Packet { /** * The raw encoded packet data. - * - * For H.264 and H.265 this is an Annex B bitstream (start-code delimited). - * For VP8 and VP9 this is the raw codec bitstream (no conversion needed). */ data: Buffer diff --git a/tests/Converter.test.ts b/tests/Converter.test.ts index a7a8dd6..3a8f524 100644 --- a/tests/Converter.test.ts +++ b/tests/Converter.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest' import { Converter } from '../src/core/Converter' import { Demuxer } from '../src/core/Demuxer' +import { H264, H265 } from '../src/core/Codec' describe('Converter', () => { describe('mp4/H.264 input (non-alpha)', () => { @@ -32,39 +33,39 @@ describe('Converter', () => { expect(fsv.frames[0].chunk.type).toBe('key') }) - it('converts with libx265 output codec', async () => { + it('converts with libx264 output codec', async () => { const fsv = demux(await Converter.convert(H264_MP4_FIXTURE, { - outputCodec: 'libx265', - encoder: { - options: { - // libx265 does not support H.264-specific profile/tune values; - // override them so the encoder initialises successfully. - profile: undefined, - tune: undefined, - preset: 'ultrafast' - } - } + outputCodec: H264 })) expect(fsv.width).toBe(320) expect(fsv.frames.length).toBeGreaterThan(0) + + const nalUnitType = getH264NalUnitType(fsv.frames[0].chunk) + expect(nalUnitType).toBe(5) // IDR slice }) - it('converts with libvpx-vp9 output codec', async () => { + it('converts with libx265 output codec', async () => { const fsv = demux(await Converter.convert(H264_MP4_FIXTURE, { - outputCodec: 'libvpx-vp9', + outputCodec: H265, encoder: { options: { - // VP9 does not support tune:fastdecode or H.264 profile values; + // @todo: Adjust default encoder options depending on the chosen output codec + // See issue #22 + // libx265 does not support H.264-specific profile/tune values; // override them so the encoder initialises successfully. profile: undefined, - tune: undefined + tune: undefined, + preset: 'ultrafast' } } })) expect(fsv.width).toBe(320) expect(fsv.frames.length).toBeGreaterThan(0) + + const nalUnitType = getH265NalUnitType(fsv.frames[0].chunk) + expect([19, 20]).toContain(nalUnitType) // IDR_W_RADL (19) or IDR_N_LP (20) }) it('throws for a non-existent input path', async () => { @@ -77,7 +78,7 @@ describe('Converter', () => { await expect( // eslint-disable-next-line @typescript-eslint/no-explicit-any Converter.convert(H264_MP4_FIXTURE, { outputCodec: 'libfoo' as any }) - ).rejects.toThrow(/[Uu]nsupported/) + ).rejects.toThrow(/Unsupported/) }) }) @@ -164,3 +165,47 @@ const FRAME_TOLERANCE = 2 function demux(buf: Buffer) { return Demuxer.demux(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer) } + +function getH264NalUnitType(chunk: EncodedVideoChunk): number { + const data = new Uint8Array(chunk.byteLength) + chunk.copyTo(data) + // AVCC format: each NAL unit is preceded by a 4-byte big-endian length. + // H.264 NAL unit header is 1 byte: forbidden_zero_bit(1) | nal_ref_idc(2) | nal_unit_type(5) + // Iterate all NAL units; prefer returning an IDR slice (type 5) over other types. + let firstType = -1 + let offset = 0 + while (offset + 4 < data.length) { + const nalLen = ((data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]) >>> 0 + if (nalLen === 0 || offset + 4 + nalLen > data.length) break + const nalType = data[offset + 4] & 0x1F + if (firstType === -1) firstType = nalType + if (nalType === 5) return 5 // IDR slice — return immediately + offset += 4 + nalLen + } + return firstType +} + +function getH265NalUnitType(chunk: EncodedVideoChunk): number { + const data = new Uint8Array(chunk.byteLength) + chunk.copyTo(data) + // AVCC format: each NAL unit is preceded by a 4-byte big-endian length. + // H.265 NAL unit header is 2 bytes (big-endian 16-bit value): + // bit 15 : forbidden_zero_bit + // bits 14–9 : nal_unit_type (6 bits) → (header >> 9) & 0x3F + // bits 8–3 : nuh_layer_id + // bits 2–0 : nuh_temporal_id_plus1 + // IDR_W_RADL = 19, IDR_N_LP = 20 + // Iterate all NAL units; prefer returning an IDR type over other types. + let firstType = -1 + let offset = 0 + while (offset + 5 < data.length) { + const nalLen = ((data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]) >>> 0 + if (nalLen === 0 || offset + 4 + nalLen > data.length) break + const header = (data[offset + 4] << 8) | data[offset + 5] + const nalType = (header >> 9) & 0x3F + if (firstType === -1) firstType = nalType + if (nalType === 19 || nalType === 20) return nalType // IDR_W_RADL or IDR_N_LP + offset += 4 + nalLen + } + return firstType +} diff --git a/tests/Demuxer.test.ts b/tests/Demuxer.test.ts index ff019ec..edb8f00 100644 --- a/tests/Demuxer.test.ts +++ b/tests/Demuxer.test.ts @@ -149,7 +149,7 @@ describe('Demuxer', () => { }) const config: VideoDecoderConfig = { - codec: 'vp09.00.10.08', + codec: 'avc1.42001f', codedWidth: 320, codedHeight: 240, optimizeForLatency: true @@ -166,7 +166,6 @@ function makePackets(count: number, payloadSize = 32): Packet[] { function makeNonAlphaFSV(packetCount = 5): Buffer { return Muxer.mux(makePackets(packetCount), undefined, { config, - codec: 'vp9', duration: packetCount * 100000 }) } @@ -174,7 +173,6 @@ function makeNonAlphaFSV(packetCount = 5): Buffer { function makeAlphaFSV(packetCount = 5): Buffer { return Muxer.mux(makePackets(packetCount), makePackets(packetCount, 16), { config, - codec: 'vp9', duration: packetCount * 100000 }) } diff --git a/tests/Muxer.test.ts b/tests/Muxer.test.ts index 2b0458c..a292819 100644 --- a/tests/Muxer.test.ts +++ b/tests/Muxer.test.ts @@ -7,7 +7,10 @@ import type { Packet } from '../src/core/Packet' describe('Muxer', () => { describe('non-alpha mux (H.264)', () => { const packets = makePackets(5, () => makeAnnexBPacket(0x65, 20)) - const buffer = Muxer.mux(packets, undefined, { config, codec: 'h264', duration: 500000 }) + const buffer = Muxer.mux(packets, undefined, { + config, + duration: 500000 + }) it('returns a Buffer', () => { expect(Buffer.isBuffer(buffer)).toBe(true) @@ -67,7 +70,10 @@ describe('Muxer', () => { describe('alpha mux (H.264)', () => { const colorPackets = makePackets(5, () => makeAnnexBPacket(0x65, 20)) const alphaPackets = makePackets(5, () => makeAnnexBPacket(0x65, 15)) - const buffer = Muxer.mux(colorPackets, alphaPackets, { config, codec: 'h264', duration: 500000 }) + const buffer = Muxer.mux(colorPackets, alphaPackets, { + config, + duration: 500000 + }) it('first 4 bytes = color track buffer offset (non-zero)', () => { const alphaOffset = buffer.readUInt32LE(0) @@ -92,44 +98,6 @@ describe('Muxer', () => { expect(alphaManifest.width).toBe(320) }) }) - - describe('VP8/VP9 pass-through (no Annex B conversion)', () => { - it('VP8 packet data is stored unchanged', () => { - const raw = makeVpxPacket(32) - const packets: Packet[] = [{ - data: raw, - timestamp: 0, - isKeyFrame: true - }] - const buf = Muxer.mux(packets, undefined, { config, codec: 'vp8', duration: 100000 }) - - const manifestLength = buf.readUInt32LE(4) - const manifest = parseManifest(buf.subarray(8, 8 + manifestLength).toString('utf8')) - const frame = manifest.frames[0] - const dataStart = 8 + manifestLength - const stored = buf.subarray(dataStart + frame.offset, dataStart + frame.offset + frame.byteLength) - - expect(Buffer.from(stored)).toEqual(raw) - }) - - it('VP9 packet data is stored unchanged', () => { - const raw = makeVpxPacket(64) - const packets: Packet[] = [{ - data: raw, - timestamp: 0, - isKeyFrame: true - }] - const buf = Muxer.mux(packets, undefined, { config, codec: 'vp9', duration: 100000 }) - - const manifestLength = buf.readUInt32LE(4) - const manifest = parseManifest(buf.subarray(8, 8 + manifestLength).toString('utf8')) - const frame = manifest.frames[0] - const dataStart = 8 + manifestLength - const stored = buf.subarray(dataStart + frame.offset, dataStart + frame.offset + frame.byteLength) - - expect(Buffer.from(stored)).toEqual(raw) - }) - }) }) const config: VideoDecoderConfig = { @@ -161,6 +129,3 @@ function makeAnnexBPacket(nalType: number, payloadSize: number): Buffer { return buf } -function makeVpxPacket(size: number): Buffer { - return Buffer.alloc(size, 0xcd) -} diff --git a/tsconfig.json b/tsconfig.json index 670df90..71aa750 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,7 @@ }, "include": [ "src", + "tests", "@types" ] }