/*! * 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 { Demuxer } from './demuxer'; import { Input } from './input'; import { IsobmffDemuxer } from './isobmff/isobmff-demuxer'; import { EBMLId, MAX_HEADER_SIZE, MIN_HEADER_SIZE, readAsciiString, readElementHeader, readElementSize, readUnsignedInt, readVarIntSize, } from './matroska/ebml'; import { MatroskaDemuxer } from './matroska/matroska-demuxer'; import { Mp3Demuxer } from './mp3/mp3-demuxer'; import { FRAME_HEADER_SIZE } from '../shared/mp3-misc'; import { readId3, readNextFrameHeader } from './mp3/mp3-reader'; import { OggDemuxer } from './ogg/ogg-demuxer'; import { WaveDemuxer } from './wave/wave-demuxer'; import { MAX_FRAME_HEADER_SIZE, MIN_FRAME_HEADER_SIZE, readFrameHeader } from './adts/adts-reader'; import { AdtsDemuxer } from './adts/adts-demuxer'; import { readAscii } from './reader'; /** * Base class representing an input media file format. * @group Input formats * @public */ export abstract class InputFormat { /** @internal */ abstract _canReadInput(input: Input): Promise; /** @internal */ abstract _createDemuxer(input: Input): Demuxer; /** Returns the name of the input format. */ abstract get name(): string; /** Returns the typical base MIME type of the input format. */ abstract get mimeType(): string; } /** * Format representing files compatible with the ISO base media file format (ISOBMFF), like MP4 or MOV files. * @group Input formats * @public */ export abstract class IsobmffInputFormat extends InputFormat { /** @internal */ protected async _getMajorBrand(input: Input) { let slice = input._reader.requestSlice(0, 12); if (slice instanceof Promise) slice = await slice; if (!slice) return null; slice.skip(4); const fourCc = readAscii(slice, 4); if (fourCc !== 'ftyp') { return null; } return readAscii(slice, 4); } /** @internal */ _createDemuxer(input: Input) { return new IsobmffDemuxer(input); } } /** * MPEG-4 Part 14 (MP4) file format. * * Do not instantiate this class; use the {@link MP4} singleton instead. * * @group Input formats * @public */ export class Mp4InputFormat extends IsobmffInputFormat { /** @internal */ async _canReadInput(input: Input) { const majorBrand = await this._getMajorBrand(input); return !!majorBrand && majorBrand !== 'qt '; } get name() { return 'MP4'; } get mimeType() { return 'video/mp4'; } } /** * QuickTime File Format (QTFF), often called MOV. * * Do not instantiate this class; use the {@link QTFF} singleton instead. * * @group Input formats * @public */ export class QuickTimeInputFormat extends IsobmffInputFormat { /** @internal */ async _canReadInput(input: Input) { const majorBrand = await this._getMajorBrand(input); return majorBrand === 'qt '; } get name() { return 'QuickTime File Format'; } get mimeType() { return 'video/quicktime'; } } /** * Matroska file format. * * Do not instantiate this class; use the {@link MATROSKA} singleton instead. * * @group Input formats * @public */ export class MatroskaInputFormat extends InputFormat { /** @internal */ protected async isSupportedEBMLOfDocType(input: Input, desiredDocType: string) { let headerSlice = input._reader.requestSlice(0, MAX_HEADER_SIZE); if (headerSlice instanceof Promise) headerSlice = await headerSlice; if (!headerSlice) return false; const varIntSize = readVarIntSize(headerSlice); if (varIntSize === null) { return false; } if (varIntSize < 1 || varIntSize > 8) { return false; } const id = readUnsignedInt(headerSlice, varIntSize); if (id !== EBMLId.EBML) { return false; } const dataSize = readElementSize(headerSlice); if (dataSize === null) { return false; // Miss me with that shit } let dataSlice = input._reader.requestSlice(headerSlice.filePos, dataSize); if (dataSlice instanceof Promise) dataSlice = await dataSlice; if (!dataSlice) return false; const startPos = headerSlice.filePos; while (dataSlice.filePos <= startPos + dataSize - MIN_HEADER_SIZE) { const header = readElementHeader(dataSlice); if (!header) break; const { id, size } = header; const dataStartPos = dataSlice.filePos; if (size === null) return false; switch (id) { case EBMLId.EBMLVersion: { const ebmlVersion = readUnsignedInt(dataSlice, size); if (ebmlVersion !== 1) { return false; } }; break; case EBMLId.EBMLReadVersion: { const ebmlReadVersion = readUnsignedInt(dataSlice, size); if (ebmlReadVersion !== 1) { return false; } }; break; case EBMLId.DocType: { const docType = readAsciiString(dataSlice, size); if (docType !== desiredDocType) { return false; } }; break; case EBMLId.DocTypeVersion: { const docTypeVersion = readUnsignedInt(dataSlice, size); if (docTypeVersion > 4) { // Support up to Matroska v4 return false; } }; break; } dataSlice.filePos = dataStartPos + size; } return true; } /** @internal */ _canReadInput(input: Input) { return this.isSupportedEBMLOfDocType(input, 'matroska'); } /** @internal */ _createDemuxer(input: Input) { return new MatroskaDemuxer(input); } get name() { return 'Matroska'; } get mimeType() { return 'video/x-matroska'; } } /** * WebM file format, based on Matroska. * * Do not instantiate this class; use the {@link WEBM} singleton instead. * * @group Input formats * @public */ export class WebMInputFormat extends MatroskaInputFormat { /** @internal */ override _canReadInput(input: Input) { return this.isSupportedEBMLOfDocType(input, 'webm'); } override get name() { return 'WebM'; } override get mimeType() { return 'video/webm'; } } /** * MP3 file format. * * Do not instantiate this class; use the {@link MP3} singleton instead. * * @group Input formats * @public */ export class Mp3InputFormat extends InputFormat { /** @internal */ async _canReadInput(input: Input) { let slice = input._reader.requestSlice(0, 10); if (slice instanceof Promise) slice = await slice; if (!slice) return false; let currentPos = 0; const id3Tag = readId3(slice); if (id3Tag) { currentPos = slice.filePos + id3Tag.size; } const firstResult = await readNextFrameHeader(input._reader, currentPos, currentPos + 4096); if (!firstResult) { return false; } if (id3Tag) { // If there was an ID3 tag at the start, we can be pretty sure this is MP3 by now return true; } currentPos = firstResult.startPos += firstResult.header.totalSize; // Fine, we found one frame header, but we're still not entirely sure this is MP3. Let's check if we can find // another header right after it: const secondResult = await readNextFrameHeader(input._reader, currentPos, currentPos + FRAME_HEADER_SIZE); if (!secondResult) { return false; } const firstHeader = firstResult.header; const secondHeader = secondResult.header; // In a well-formed MP3 file, we'd expect these two frames to share some similarities: if (firstHeader.channel !== secondHeader.channel || firstHeader.sampleRate !== secondHeader.sampleRate) { return false; } // We have found two matching consecutive MP3 frames, a strong indicator that this is an MP3 file return true; } /** @internal */ _createDemuxer(input: Input) { return new Mp3Demuxer(input); } get name() { return 'MP3'; } get mimeType() { return 'audio/mpeg'; } } /** * WAVE file format, based on RIFF. * * Do not instantiate this class; use the {@link WAVE} singleton instead. * * @group Input formats * @public */ export class WaveInputFormat extends InputFormat { /** @internal */ async _canReadInput(input: Input) { let slice = input._reader.requestSlice(0, 12); if (slice instanceof Promise) slice = await slice; if (!slice) return false; const riffType = readAscii(slice, 4); if (riffType !== 'RIFF' && riffType !== 'RIFX' && riffType !== 'RF64') { return false; } slice.skip(4); const format = readAscii(slice, 4); return format === 'WAVE'; } /** @internal */ _createDemuxer(input: Input) { return new WaveDemuxer(input); } get name() { return 'WAVE'; } get mimeType() { return 'audio/wav'; } } /** * Ogg file format. * * Do not instantiate this class; use the {@link OGG} singleton instead. * * @group Input formats * @public */ export class OggInputFormat extends InputFormat { /** @internal */ async _canReadInput(input: Input) { let slice = input._reader.requestSlice(0, 4); if (slice instanceof Promise) slice = await slice; if (!slice) return false; return readAscii(slice, 4) === 'OggS'; } /** @internal */ _createDemuxer(input: Input) { return new OggDemuxer(input); } get name() { return 'Ogg'; } get mimeType() { return 'application/ogg'; } } /** * ADTS file format. * * Do not instantiate this class; use the {@link ADTS} singleton instead. * * @group Input formats * @public */ export class AdtsInputFormat extends InputFormat { /** @internal */ async _canReadInput(input: Input) { let slice = input._reader.requestSliceRange(0, MIN_FRAME_HEADER_SIZE, MAX_FRAME_HEADER_SIZE); if (slice instanceof Promise) slice = await slice; if (!slice) return false; const firstHeader = readFrameHeader(slice); if (!firstHeader) { return false; } slice = input._reader.requestSliceRange(firstHeader.frameLength, MIN_FRAME_HEADER_SIZE, MAX_FRAME_HEADER_SIZE); if (slice instanceof Promise) slice = await slice; if (!slice) return false; const secondHeader = readFrameHeader(slice); if (!secondHeader) { return false; } return firstHeader.objectType === secondHeader.objectType && firstHeader.samplingFrequencyIndex === secondHeader.samplingFrequencyIndex && firstHeader.channelConfiguration === secondHeader.channelConfiguration; } /** @internal */ _createDemuxer(input: Input) { return new AdtsDemuxer(input); } get name() { return 'ADTS'; } get mimeType() { return 'audio/aac'; } } /** * MP4 input format singleton. * @group Input formats * @public */ export const MP4 = new Mp4InputFormat(); /** * QuickTime File Format input format singleton. * @group Input formats * @public */ export const QTFF = new QuickTimeInputFormat(); /** * Matroska input format singleton. * @group Input formats * @public */ export const MATROSKA = new MatroskaInputFormat(); /** * WebM input format singleton. * @group Input formats * @public */ export const WEBM = new WebMInputFormat(); /** * MP3 input format singleton. * @group Input formats * @public */ export const MP3 = new Mp3InputFormat(); /** * WAVE input format singleton. * @group Input formats * @public */ export const WAVE = new WaveInputFormat(); /** * Ogg input format singleton. * @group Input formats * @public */ export const OGG = new OggInputFormat(); /** * ADTS input format singleton. * @group Input formats * @public */ export const ADTS = new AdtsInputFormat(); /** * List of all input format singletons. If you don't need to support all input formats, you should specify the * formats individually for better tree shaking. * @group Input formats * @public */ export const ALL_FORMATS: InputFormat[] = [MP4, QTFF, MATROSKA, WEBM, WAVE, OGG, MP3, ADTS];