UNPKG

mediabunny

Version:

Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.

392 lines (391 loc) 12.6 kB
/*! * Copyright (c) 2025-present, Vanilagy and contributors * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { AUDIO_CODECS, NON_PCM_AUDIO_CODECS, PCM_AUDIO_CODECS, SUBTITLE_CODECS, VIDEO_CODECS, } from './codec.js'; import { IsobmffMuxer } from './isobmff/isobmff-muxer.js'; import { MatroskaMuxer } from './matroska/matroska-muxer.js'; import { Mp3Muxer } from './mp3/mp3-muxer.js'; import { OggMuxer } from './ogg/ogg-muxer.js'; import { WaveMuxer } from './wave/wave-muxer.js'; /** * Base class representing an output media file format. * @public */ export class OutputFormat { /** Returns a list of video codecs that this output format can contain. */ getSupportedVideoCodecs() { return this.getSupportedCodecs() .filter(codec => VIDEO_CODECS.includes(codec)); } /** Returns a list of audio codecs that this output format can contain. */ getSupportedAudioCodecs() { return this.getSupportedCodecs() .filter(codec => AUDIO_CODECS.includes(codec)); } /** Returns a list of subtitle codecs that this output format can contain. */ getSupportedSubtitleCodecs() { return this.getSupportedCodecs() .filter(codec => SUBTITLE_CODECS.includes(codec)); } /** @internal */ // eslint-disable-next-line @typescript-eslint/no-unused-vars _codecUnsupportedHint(codec) { return ''; } } /** * Format representing files compatible with the ISO base media file format (ISOBMFF), like MP4 or MOV files. * @public */ export class IsobmffOutputFormat extends OutputFormat { constructor(options = {}) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object.'); } if (options.fastStart !== undefined && ![false, 'in-memory', 'fragmented'].includes(options.fastStart)) { throw new TypeError('options.fastStart, when provided, must be false, "in-memory", or "fragmented".'); } if (options.minimumFragmentDuration !== undefined && (!Number.isFinite(options.minimumFragmentDuration) || options.minimumFragmentDuration < 0)) { throw new TypeError('options.minimumFragmentDuration, when provided, must be a non-negative number.'); } if (options.onFtyp !== undefined && typeof options.onFtyp !== 'function') { throw new TypeError('options.onFtyp, when provided, must be a function.'); } if (options.onMoov !== undefined && typeof options.onMoov !== 'function') { throw new TypeError('options.onMoov, when provided, must be a function.'); } if (options.onMdat !== undefined && typeof options.onMdat !== 'function') { throw new TypeError('options.onMdat, when provided, must be a function.'); } if (options.onMoof !== undefined && typeof options.onMoof !== 'function') { throw new TypeError('options.onMoof, when provided, must be a function.'); } super(); this._options = options; } getSupportedTrackCounts() { return { video: { min: 0, max: Infinity }, audio: { min: 0, max: Infinity }, subtitle: { min: 0, max: Infinity }, total: { min: 1, max: 2 ** 32 - 1 }, // Have fun reaching this one }; } get supportsVideoRotationMetadata() { return true; } /** @internal */ _createMuxer(output) { return new IsobmffMuxer(output, this); } } /** * MPEG-4 Part 14 (MP4) file format. Supports all codecs except PCM audio codecs. * @public */ export class Mp4OutputFormat extends IsobmffOutputFormat { /** @internal */ get _name() { return 'MP4'; } get fileExtension() { return '.mp4'; } get mimeType() { return 'video/mp4'; } getSupportedCodecs() { return [ ...VIDEO_CODECS, ...NON_PCM_AUDIO_CODECS, // These are supported via ISO/IEC 23003-5 'pcm-s16', 'pcm-s16be', 'pcm-s24', 'pcm-s24be', 'pcm-s32', 'pcm-s32be', 'pcm-f32', 'pcm-f32be', 'pcm-f64', 'pcm-f64be', ...SUBTITLE_CODECS, ]; } /** @internal */ _codecUnsupportedHint(codec) { if (new MovOutputFormat().getSupportedCodecs().includes(codec)) { return ' Switching to MOV will grant support for this codec.'; } return ''; } } /** * QuickTime File Format (QTFF), often called MOV. Supports all video and audio codecs, but not subtitle codecs. * @public */ export class MovOutputFormat extends IsobmffOutputFormat { /** @internal */ get _name() { return 'MOV'; } get fileExtension() { return '.mov'; } get mimeType() { return 'video/quicktime'; } getSupportedCodecs() { return [ ...VIDEO_CODECS, ...AUDIO_CODECS, ]; } /** @internal */ _codecUnsupportedHint(codec) { if (new Mp4OutputFormat().getSupportedCodecs().includes(codec)) { return ' Switching to MP4 will grant support for this codec.'; } return ''; } } /** * Matroska file format. * @public */ export class MkvOutputFormat extends OutputFormat { constructor(options = {}) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object.'); } if (options.appendOnly !== undefined && typeof options.appendOnly !== 'boolean') { throw new TypeError('options.appendOnly, when provided, must be a boolean.'); } if (options.minimumClusterDuration !== undefined && (!Number.isFinite(options.minimumClusterDuration) || options.minimumClusterDuration < 0)) { throw new TypeError('options.minimumClusterDuration, when provided, must be a non-negative number.'); } if (options.onEbmlHeader !== undefined && typeof options.onEbmlHeader !== 'function') { throw new TypeError('options.onEbmlHeader, when provided, must be a function.'); } if (options.onSegmentHeader !== undefined && typeof options.onSegmentHeader !== 'function') { throw new TypeError('options.onHeader, when provided, must be a function.'); } if (options.onCluster !== undefined && typeof options.onCluster !== 'function') { throw new TypeError('options.onCluster, when provided, must be a function.'); } super(); this._options = options; } /** @internal */ _createMuxer(output) { return new MatroskaMuxer(output, this); } /** @internal */ get _name() { return 'Matroska'; } getSupportedTrackCounts() { return { video: { min: 0, max: Infinity }, audio: { min: 0, max: Infinity }, subtitle: { min: 0, max: Infinity }, total: { min: 1, max: 127 }, }; } get fileExtension() { return '.mkv'; } get mimeType() { return 'video/x-matroska'; } getSupportedCodecs() { return [ ...VIDEO_CODECS, ...NON_PCM_AUDIO_CODECS, ...PCM_AUDIO_CODECS.filter(codec => !['pcm-s8', 'pcm-f32be', 'pcm-f64be', 'ulaw', 'alaw'].includes(codec)), ...SUBTITLE_CODECS, ]; } get supportsVideoRotationMetadata() { // While it technically does support it with ProjectionPoseRoll, many players appear to ignore this value return false; } } /** * WebM file format, based on Matroska. * @public */ export class WebMOutputFormat extends MkvOutputFormat { getSupportedCodecs() { return [ ...VIDEO_CODECS.filter(codec => ['vp8', 'vp9', 'av1'].includes(codec)), ...AUDIO_CODECS.filter(codec => ['opus', 'vorbis'].includes(codec)), ...SUBTITLE_CODECS, ]; } /** @internal */ get _name() { return 'WebM'; } get fileExtension() { return '.webm'; } get mimeType() { return 'video/webm'; } /** @internal */ _codecUnsupportedHint(codec) { if (new MkvOutputFormat().getSupportedCodecs().includes(codec)) { return ' Switching to MKV will grant support for this codec.'; } return ''; } } /** * MP3 file format. * @public */ export class Mp3OutputFormat extends OutputFormat { constructor(options = {}) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object.'); } if (options.xingHeader !== undefined && typeof options.xingHeader !== 'boolean') { throw new TypeError('options.xingHeader, when provided, must be a boolean.'); } if (options.onXingFrame !== undefined && typeof options.onXingFrame !== 'function') { throw new TypeError('options.onXingFrame, when provided, must be a function.'); } super(); this._options = options; } /** @internal */ _createMuxer(output) { return new Mp3Muxer(output, this); } /** @internal */ get _name() { return 'MP3'; } getSupportedTrackCounts() { return { video: { min: 0, max: 0 }, audio: { min: 1, max: 1 }, subtitle: { min: 0, max: 0 }, total: { min: 1, max: 1 }, }; } get fileExtension() { return '.mp3'; } get mimeType() { return 'audio/mpeg'; } getSupportedCodecs() { return ['mp3']; } get supportsVideoRotationMetadata() { return false; } } /** * WAVE file format, based on RIFF. * @public */ export class WavOutputFormat extends OutputFormat { constructor(options = {}) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object.'); } if (options.large !== undefined && typeof options.large !== 'boolean') { throw new TypeError('options.large, when provided, must be a boolean.'); } if (options.onHeader !== undefined && typeof options.onHeader !== 'function') { throw new TypeError('options.onHeader, when provided, must be a function.'); } super(); this._options = options; } /** @internal */ _createMuxer(output) { return new WaveMuxer(output, this); } /** @internal */ get _name() { return 'WAVE'; } getSupportedTrackCounts() { return { video: { min: 0, max: 0 }, audio: { min: 1, max: 1 }, subtitle: { min: 0, max: 0 }, total: { min: 1, max: 1 }, }; } get fileExtension() { return '.wav'; } get mimeType() { return 'audio/wav'; } getSupportedCodecs() { return [ ...PCM_AUDIO_CODECS.filter(codec => ['pcm-s16', 'pcm-s24', 'pcm-s32', 'pcm-f32', 'pcm-u8', 'ulaw', 'alaw'].includes(codec)), ]; } get supportsVideoRotationMetadata() { return false; } } /** * Ogg file format. * @public */ export class OggOutputFormat extends OutputFormat { constructor(options = {}) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object.'); } if (options.onPage !== undefined && typeof options.onPage !== 'function') { throw new TypeError('options.onPage, when provided, must be a function.'); } super(); this._options = options; } /** @internal */ _createMuxer(output) { return new OggMuxer(output, this); } /** @internal */ get _name() { return 'Ogg'; } getSupportedTrackCounts() { return { video: { min: 0, max: 0 }, audio: { min: 0, max: Infinity }, subtitle: { min: 0, max: 0 }, total: { min: 1, max: 2 ** 32 }, }; } get fileExtension() { return '.ogg'; } get mimeType() { return 'application/ogg'; } getSupportedCodecs() { return [ ...AUDIO_CODECS.filter(codec => ['vorbis', 'opus'].includes(codec)), ]; } get supportsVideoRotationMetadata() { return false; } }