UNPKG

@remotion/media-parser

Version:

A pure JavaScript library for parsing video files

1,912 lines (1,887 loc) 520 kB
// src/errors.ts class IsAnImageError extends Error { imageType; dimensions; mimeType; sizeInBytes; fileName; constructor({ dimensions, imageType, message, mimeType, sizeInBytes, fileName }) { super(message); this.name = "IsAnImageError"; this.imageType = imageType; this.dimensions = dimensions; this.mimeType = mimeType; this.sizeInBytes = sizeInBytes; this.fileName = fileName; if (Error.captureStackTrace) { Error.captureStackTrace(this, IsAnImageError); } } } class IsAPdfError extends Error { mimeType; sizeInBytes; fileName; constructor({ message, mimeType, sizeInBytes, fileName }) { super(message); this.name = "IsAPdfError"; this.mimeType = mimeType; this.sizeInBytes = sizeInBytes; this.fileName = fileName; if (Error.captureStackTrace) { Error.captureStackTrace(this, IsAPdfError); } } } class IsAnUnsupportedFileTypeError extends Error { mimeType; sizeInBytes; fileName; constructor({ message, mimeType, sizeInBytes, fileName }) { super(message); this.name = "IsAnUnsupportedFileTypeError"; this.mimeType = mimeType; this.sizeInBytes = sizeInBytes; this.fileName = fileName; if (Error.captureStackTrace) { Error.captureStackTrace(this, IsAnUnsupportedFileTypeError); } } } class MediaParserAbortError extends Error { constructor(message) { super(message); this.name = "MediaParserAbortError"; this.cause = undefined; } } // src/log.ts var logLevels = ["trace", "verbose", "info", "warn", "error"]; var getNumberForLogLevel = (level) => { return logLevels.indexOf(level); }; var isEqualOrBelowLogLevel = (currentLevel, level) => { return getNumberForLogLevel(currentLevel) <= getNumberForLogLevel(level); }; var Log = { trace: (logLevel, ...args) => { if (isEqualOrBelowLogLevel(logLevel, "trace")) { return console.log(...args); } }, verbose: (logLevel, ...args) => { if (isEqualOrBelowLogLevel(logLevel, "verbose")) { return console.log(...args); } }, info: (logLevel, ...args) => { if (isEqualOrBelowLogLevel(logLevel, "info")) { return console.log(...args); } }, warn: (logLevel, ...args) => { if (isEqualOrBelowLogLevel(logLevel, "warn")) { return console.warn(...args); } }, error: (...args) => { return console.error(...args); } }; // src/readers/fetch/get-body-and-reader.ts var getLengthAndReader = async ({ canLiveWithoutContentLength, res, ownController, requestedWithoutRange }) => { const length = res.headers.get("content-length"); const contentLength = length === null ? null : parseInt(length, 10); if (requestedWithoutRange || canLiveWithoutContentLength && contentLength === null) { const buffer = await res.arrayBuffer(); const encoded = new Uint8Array(buffer); let streamCancelled = false; const stream = new ReadableStream({ start(controller) { if (ownController.signal.aborted) { return; } if (streamCancelled) { return; } try { controller.enqueue(encoded); controller.close(); } catch {} }, cancel() { streamCancelled = true; } }); return { contentLength: encoded.byteLength, reader: { reader: stream.getReader(), abort: () => { ownController.abort(); return Promise.resolve(); } }, needsContentRange: false }; } if (!res.body) { throw new Error("No body"); } const reader = res.body.getReader(); return { reader: { reader, abort: () => { ownController.abort(); return Promise.resolve(); } }, contentLength, needsContentRange: true }; }; // src/readers/fetch/resolve-url.ts var resolveUrl = (src) => { try { const resolvedUrl = typeof window !== "undefined" && typeof window.location !== "undefined" ? new URL(src, window.location.origin) : new URL(src); return resolvedUrl; } catch { return src; } }; // src/readers/from-fetch.ts function parseContentRange(input) { const matches = input.match(/^(\w+) ((\d+)-(\d+)|\*)\/(\d+|\*)$/); if (!matches) return null; const [, unit, , start, end, size] = matches; const range = { unit, start: start != null ? Number(start) : null, end: end != null ? Number(end) : null, size: size === "*" ? null : Number(size) }; if (range.start === null && range.end === null && range.size === null) { return null; } return range; } var validateContentRangeAndDetectIfSupported = ({ requestedRange, parsedContentRange, statusCode }) => { if (statusCode === 206) { return { supportsContentRange: true }; } if (typeof requestedRange === "number" && parsedContentRange?.start !== requestedRange) { if (requestedRange === 0) { return { supportsContentRange: false }; } throw new Error(`Range header (${requestedRange}) does not match content-range header (${parsedContentRange?.start})`); } if (requestedRange !== null && typeof requestedRange !== "number" && (parsedContentRange?.start !== requestedRange[0] || parsedContentRange?.end !== requestedRange[1])) { throw new Error(`Range header (${requestedRange}) does not match content-range header (${parsedContentRange?.start})`); } return { supportsContentRange: true }; }; var makeFetchRequest = async ({ range, src, controller }) => { const resolvedUrl = resolveUrl(src); const resolvedUrlString = resolvedUrl.toString(); if (!resolvedUrlString.startsWith("https://") && !resolvedUrlString.startsWith("blob:") && !resolvedUrlString.startsWith("http://")) { return Promise.reject(new Error(`${resolvedUrlString} is not a URL - needs to start with http:// or https:// or blob:. If you want to read a local file, pass \`reader: nodeReader\` to parseMedia().`)); } const ownController = new AbortController; const cache = typeof navigator !== "undefined" && navigator.userAgent.includes("Cloudflare-Workers") ? undefined : "no-store"; const requestedRange = range === null ? 0 : range; const asString = typeof resolvedUrl === "string" ? resolvedUrl : resolvedUrl.pathname; const requestWithoutRange = asString.endsWith(".m3u8"); const canLiveWithoutContentLength = asString.endsWith(".m3u8") || asString.endsWith(".ts"); const headers = requestedRange === 0 && requestWithoutRange ? {} : typeof requestedRange === "number" ? { Range: `bytes=${requestedRange}-` } : { Range: `bytes=${`${requestedRange[0]}-${requestedRange[1]}`}` }; const res = await fetch(resolvedUrl, { headers, signal: ownController.signal, cache }); const contentRange = res.headers.get("content-range"); const parsedContentRange = contentRange ? parseContentRange(contentRange) : null; const { supportsContentRange } = validateContentRangeAndDetectIfSupported({ requestedRange, parsedContentRange, statusCode: res.status }); if (controller) { controller._internals.signal.addEventListener("abort", () => { ownController.abort(new MediaParserAbortError("Aborted by user")); }, { once: true }); } if (res.status.toString().startsWith("4") || res.status.toString().startsWith("5")) { throw new Error(`Server returned status code ${res.status} for ${resolvedUrl} and range ${requestedRange}`); } const contentDisposition = res.headers.get("content-disposition"); const name = contentDisposition?.match(/filename="([^"]+)"/)?.[1]; const { contentLength, needsContentRange, reader } = await getLengthAndReader({ canLiveWithoutContentLength, res, ownController, requestedWithoutRange: requestWithoutRange }); const contentType = res.headers.get("content-type"); return { contentLength, needsContentRange, reader, name, contentType, supportsContentRange }; }; var cacheKey = ({ src, range }) => { return `${src}-${JSON.stringify(range)}`; }; var makeFetchRequestOrGetCached = ({ range, src, controller, logLevel, prefetchCache }) => { const key = cacheKey({ src, range }); const cached = prefetchCache.get(key); if (cached) { Log.verbose(logLevel, `Reading from preload cache for ${key}`); return cached; } Log.verbose(logLevel, `Fetching ${key}`); const result = makeFetchRequest({ range, src, controller }); prefetchCache.set(key, result); return result; }; var fetchReadContent = async ({ src, range, controller, logLevel, prefetchCache }) => { if (typeof src !== "string" && src instanceof URL === false) { throw new Error("src must be a string when using `fetchReader`"); } const fallbackName = src.toString().split("/").pop(); const res = makeFetchRequestOrGetCached({ range, src, controller, logLevel, prefetchCache }); const key = cacheKey({ src, range }); prefetchCache.delete(key); const { reader, contentLength, needsContentRange, name, supportsContentRange, contentType } = await res; if (controller) { controller._internals.signal.addEventListener("abort", () => { reader.reader.cancel().catch(() => {}); }, { once: true }); } return { reader, contentLength, contentType, name: name ?? fallbackName, supportsContentRange, needsContentRange }; }; var fetchPreload = ({ src, range, logLevel, prefetchCache }) => { if (typeof src !== "string" && src instanceof URL === false) { throw new Error("src must be a string when using `fetchReader`"); } const key = cacheKey({ src, range }); if (prefetchCache.has(key)) { return prefetchCache.get(key); } makeFetchRequestOrGetCached({ range, src, controller: null, logLevel, prefetchCache }); }; var fetchReadWholeAsText = async (src) => { if (typeof src !== "string" && src instanceof URL === false) { throw new Error("src must be a string when using `fetchReader`"); } const res = await fetch(src); if (!res.ok) { throw new Error(`Failed to fetch ${src} (HTTP code: ${res.status})`); } return res.text(); }; var fetchCreateAdjacentFileSource = (relativePath, src) => { if (typeof src !== "string" && src instanceof URL === false) { throw new Error("src must be a string or URL when using `fetchReader`"); } return new URL(relativePath, src).toString(); }; // src/readers/from-web-file.ts var webFileReadContent = ({ src, range, controller }) => { if (typeof src === "string" || src instanceof URL) { throw new Error("`inputTypeFileReader` only supports `File` objects"); } const part = range === null ? src : typeof range === "number" ? src.slice(range) : src.slice(range[0], range[1] + 1); const stream = part.stream(); const streamReader = stream.getReader(); if (controller) { controller._internals.signal.addEventListener("abort", () => { streamReader.cancel(); }, { once: true }); } return Promise.resolve({ reader: { reader: streamReader, async abort() { try { await streamReader.cancel(); } catch {} return Promise.resolve(); } }, contentLength: src.size, name: src instanceof File ? src.name : src.toString(), supportsContentRange: true, contentType: src.type, needsContentRange: true }); }; var webFileReadWholeAsText = () => { throw new Error("`webFileReader` cannot read auxiliary files."); }; var webFileCreateAdjacentFileSource = () => { throw new Error("`webFileReader` cannot create adjacent file sources."); }; // src/readers/web.ts var webReader = { read: (params) => { if (params.src instanceof Blob) { return webFileReadContent(params); } return fetchReadContent(params); }, createAdjacentFileSource: (relativePath, src) => { if (src instanceof Blob) { return webFileCreateAdjacentFileSource(relativePath, src); } return fetchCreateAdjacentFileSource(relativePath, src); }, readWholeAsText: (src) => { if (src instanceof Blob) { return webFileReadWholeAsText(src); } return fetchReadWholeAsText(src); }, preload: ({ range, src, logLevel, prefetchCache }) => { if (src instanceof Blob) { return; } return fetchPreload({ range, src, logLevel, prefetchCache }); } }; // src/containers/m3u/select-stream.ts var selectAssociatedPlaylists = async ({ playlists, fn, skipAudioTracks }) => { if (playlists.length < 1) { return Promise.resolve([]); } const streams = await fn({ associatedPlaylists: playlists }); if (!Array.isArray(streams)) { throw new Error("Expected an array of associated playlists"); } const selectedStreams = []; for (const stream of streams) { if (stream.isAudio && skipAudioTracks) { continue; } if (!playlists.find((playlist) => playlist.src === stream.src)) { throw new Error(`The associated playlist ${JSON.stringify(streams)} cannot be selected because it was not in the list of selectable playlists`); } selectedStreams.push(stream); } return selectedStreams; }; var defaultSelectM3uAssociatedPlaylists = ({ associatedPlaylists }) => { if (associatedPlaylists.length === 1) { return associatedPlaylists; } return associatedPlaylists.filter((playlist) => playlist.default); }; var selectStream = async ({ streams, fn }) => { if (streams.length < 1) { throw new Error("No streams found"); } const selectedStreamId = await fn({ streams }); const selectedStream = streams.find((stream) => stream.id === selectedStreamId); if (!selectedStream) { throw new Error(`No stream with the id ${selectedStreamId} found`); } return Promise.resolve(selectedStream); }; var defaultSelectM3uStreamFn = ({ streams }) => { return Promise.resolve(streams[0].id); }; // src/with-resolvers.ts var withResolvers = function() { let resolve; let reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; }; // src/controller/emitter.ts class MediaParserEmitter { listeners = { pause: [], resume: [], abort: [], seek: [] }; readyPromise; #markAsReady; constructor() { const { promise, resolve } = withResolvers(); this.readyPromise = promise; this.#markAsReady = resolve; } markAsReady = () => { this.#markAsReady(); }; addEventListener = (name, callback) => { this.listeners[name].push(callback); }; removeEventListener = (name, callback) => { this.listeners[name] = this.listeners[name].filter((l) => l !== callback); }; dispatchEvent(dispatchName, context) { this.listeners[dispatchName].forEach((callback) => { callback({ detail: context }); }); } dispatchPause = () => { this.readyPromise = this.readyPromise.then(() => { this.dispatchEvent("pause", undefined); }); }; dispatchResume = () => { this.readyPromise = this.readyPromise.then(() => { this.dispatchEvent("resume", undefined); }); }; dispatchAbort = (reason) => { this.readyPromise = this.readyPromise.then(() => { this.dispatchEvent("abort", { reason }); }); }; dispatchSeek = (seek) => { this.readyPromise = this.readyPromise.then(() => { this.dispatchEvent("seek", { seek }); }); }; } // src/controller/pause-signal.ts var makePauseSignal = (emitter) => { const waiterFns = []; let paused = false; return { pause: () => { if (paused) { return; } emitter.dispatchPause(); paused = true; }, resume: () => { if (!paused) { return; } paused = false; for (const waiterFn of waiterFns) { waiterFn(); } waiterFns.length = 0; emitter.dispatchResume(); }, waitUntilResume: () => { return new Promise((resolve) => { if (!paused) { resolve(); } else { waiterFns.push(resolve); } }); } }; }; // src/controller/performed-seeks-stats.ts var performedSeeksStats = () => { const performedSeeks = []; const markLastSeekAsUserInitiated = () => { if (performedSeeks.length > 0) { performedSeeks[performedSeeks.length - 1].type = "user-initiated"; } }; return { recordSeek: (seek) => { performedSeeks.push(seek); }, getPerformedSeeks: () => { return performedSeeks; }, markLastSeekAsUserInitiated }; }; // src/controller/seek-signal.ts var makeSeekSignal = (emitter) => { let seek = null; return { seek: (seekRequest) => { seek = seekRequest; emitter.dispatchSeek(seekRequest); }, getSeek() { return seek; }, clearSeekIfStillSame(previousSeek) { if (seek === previousSeek) { seek = null; return { hasChanged: false }; } return { hasChanged: true }; } }; }; // src/controller/media-parser-controller.ts var mediaParserController = () => { const abortController = new AbortController; const emitter = new MediaParserEmitter; const pauseSignal = makePauseSignal(emitter); const seekSignal = makeSeekSignal(emitter); const performedSeeksSignal = performedSeeksStats(); const checkForAbortAndPause = async () => { if (abortController.signal.aborted) { const err = new MediaParserAbortError("Aborted"); if (abortController.signal.reason) { err.cause = abortController.signal.reason; } throw err; } await pauseSignal.waitUntilResume(); }; let seekingHintResolution = null; let simulateSeekResolution = null; const getSeekingHints = () => { if (!seekingHintResolution) { throw new Error("The mediaParserController() was not yet used in a parseMedia() call"); } return seekingHintResolution(); }; const simulateSeek = (seekInSeconds) => { if (!simulateSeekResolution) { throw new Error("The mediaParserController() was not yet used in a parseMedia() call"); } return simulateSeekResolution(seekInSeconds); }; const attachSeekingHintResolution = (callback) => { if (seekingHintResolution) { throw new Error("The mediaParserController() was used in multiple parseMedia() calls. Create a separate controller for each call."); } seekingHintResolution = callback; }; const attachSimulateSeekResolution = (callback) => { if (simulateSeekResolution) { throw new Error("The mediaParserController() was used in multiple parseMedia() calls. Create a separate controller for each call."); } simulateSeekResolution = callback; }; return { abort: (reason) => { abortController.abort(reason); emitter.dispatchAbort(reason); }, seek: seekSignal.seek, simulateSeek, pause: pauseSignal.pause, resume: pauseSignal.resume, addEventListener: emitter.addEventListener, removeEventListener: emitter.removeEventListener, getSeekingHints, _internals: { signal: abortController.signal, checkForAbortAndPause, seekSignal, markAsReadyToEmitEvents: emitter.markAsReady, performedSeeksSignal, attachSeekingHintResolution, attachSimulateSeekResolution } }; }; // src/containers/m3u/get-streams.ts var isIndependentSegments = (structure) => { if (structure === null || structure.type !== "m3u") { return false; } return structure.boxes.some((box) => box.type === "m3u-independent-segments" || box.type === "m3u-stream-info"); }; var getM3uStreams = ({ structure, originalSrc, readerInterface }) => { if (structure === null || structure.type !== "m3u") { return null; } const boxes = []; for (let i = 0;i < structure.boxes.length; i++) { const str = structure.boxes[i]; if (str.type === "m3u-stream-info") { const next = structure.boxes[i + 1]; if (next.type !== "m3u-text-value") { throw new Error("Expected m3u-text-value"); } const associatedPlaylists = []; if (str.audio) { const match = structure.boxes.filter((box) => { return box.type === "m3u-media-info" && box.groupId === str.audio; }); for (const audioTrack of match) { associatedPlaylists.push({ autoselect: audioTrack.autoselect, channels: audioTrack.channels, default: audioTrack.default, groupId: audioTrack.groupId, language: audioTrack.language, name: audioTrack.name, src: readerInterface.createAdjacentFileSource(audioTrack.uri, originalSrc), id: associatedPlaylists.length, isAudio: true }); } } boxes.push({ src: readerInterface.createAdjacentFileSource(next.value, originalSrc), averageBandwidthInBitsPerSec: str.averageBandwidthInBitsPerSec, bandwidthInBitsPerSec: str.bandwidthInBitsPerSec, codecs: str.codecs, dimensions: str.dimensions, associatedPlaylists }); } } if (boxes.length === 0) { return null; } const sorted = boxes.slice().sort((a, b) => { const aResolution = a.dimensions ? a.dimensions.width * a.dimensions.height : 0; const bResolution = b.dimensions ? b.dimensions.width * b.dimensions.height : 0; if (aResolution === bResolution) { const bandwidthA = a.averageBandwidthInBitsPerSec ?? a.bandwidthInBitsPerSec ?? 0; const bandwidthB = b.averageBandwidthInBitsPerSec ?? b.bandwidthInBitsPerSec ?? 0; return bandwidthB - bandwidthA; } return bResolution - aResolution; }); return sorted.map((box, index) => ({ ...box, id: index })); }; var m3uHasStreams = (state) => { const structure = state.structure.getStructureOrNull(); if (!structure) { return false; } if (structure.type !== "m3u") { return true; } return state.m3u.hasFinishedManifest(); }; // src/state/iso-base-media/precomputed-moof.ts var precomputedMoofState = () => { let moofBoxes = []; return { getMoofBoxes: () => moofBoxes, setMoofBoxes: (boxes) => { moofBoxes = boxes; } }; }; var toMoofBox = (box) => { if (box.type !== "regular-box") { throw new Error("expected regular bpx"); } return { offset: box.offset, trafBoxes: box.children.filter((c) => c.type === "regular-box" && c.boxType === "traf"), size: box.boxSize }; }; var deduplicateMoofBoxesByOffset = (moofBoxes) => { return moofBoxes.filter((m, i, arr) => i === arr.findIndex((t) => t.offset === m.offset)); }; // src/containers/iso-base-media/traversal.ts var getMoovFromFromIsoStructure = (structure) => { const moovBox = structure.boxes.find((s) => s.type === "moov-box"); if (!moovBox || moovBox.type !== "moov-box") { return null; } return moovBox; }; var getMoovBoxFromState = ({ structureState, isoState, mp4HeaderSegment, mayUsePrecomputed }) => { const got = isoState.moov.getMoovBoxAndPrecomputed(); if (got && (mayUsePrecomputed || !got.precomputed)) { return got.moovBox; } if (mp4HeaderSegment) { return getMoovFromFromIsoStructure(mp4HeaderSegment); } const structure = structureState.getIsoStructure(); return getMoovFromFromIsoStructure(structure); }; var getMoofBoxes = (main) => { const moofBoxes = main.filter((s) => s.type === "regular-box" && s.boxType === "moof"); return moofBoxes.map((m) => toMoofBox(m)); }; var getMvhdBox = (moovBox) => { const mvHdBox = moovBox.children.find((s) => s.type === "mvhd-box"); if (!mvHdBox || mvHdBox.type !== "mvhd-box") { return null; } return mvHdBox; }; var getTraks = (moovBox) => { return moovBox.children.filter((s) => s.type === "trak-box"); }; var getTkhdBox = (trakBox) => { const tkhdBox = trakBox.children.find((s) => s.type === "tkhd-box"); return tkhdBox; }; var getMdiaBox = (trakBox) => { const mdiaBox = trakBox.children.find((s) => s.type === "regular-box" && s.boxType === "mdia"); if (!mdiaBox || mdiaBox.type !== "regular-box") { return null; } return mdiaBox; }; var getMdhdBox = (trakBox) => { const mdiaBox = getMdiaBox(trakBox); if (!mdiaBox) { return null; } const mdhdBox = mdiaBox.children.find((c) => c.type === "mdhd-box"); return mdhdBox; }; var getStblBox = (trakBox) => { const mdiaBox = getMdiaBox(trakBox); if (!mdiaBox) { return null; } const minfBox = mdiaBox.children.find((s) => s.type === "regular-box" && s.boxType === "minf"); if (!minfBox || minfBox.type !== "regular-box") { return null; } const stblBox = minfBox.children.find((s) => s.type === "regular-box" && s.boxType === "stbl"); if (!stblBox || stblBox.type !== "regular-box") { return null; } return stblBox; }; var getStsdBox = (trakBox) => { const stblBox = getStblBox(trakBox); if (!stblBox || stblBox.type !== "regular-box") { return null; } const stsdBox = stblBox.children.find((s) => s.type === "stsd-box"); return stsdBox; }; var getVideoDescriptors = (trakBox) => { const stsdBox = getStsdBox(trakBox); if (!stsdBox) { return null; } const descriptors = stsdBox.samples.map((s) => { return s.type === "video" ? s.descriptors.map((d) => { return d.type === "avcc-box" ? d.privateData : d.type === "hvcc-box" ? d.privateData : null; }) : []; }); return descriptors.flat(1).filter(Boolean)[0] ?? null; }; var getStcoBox = (trakBox) => { const stblBox = getStblBox(trakBox); if (!stblBox || stblBox.type !== "regular-box") { return null; } const stcoBox = stblBox.children.find((s) => s.type === "stco-box"); return stcoBox; }; var getSttsBox = (trakBox) => { const stblBox = getStblBox(trakBox); if (!stblBox || stblBox.type !== "regular-box") { return null; } const sttsBox = stblBox.children.find((s) => s.type === "stts-box"); return sttsBox; }; var getCttsBox = (trakBox) => { const stblBox = getStblBox(trakBox); if (!stblBox || stblBox.type !== "regular-box") { return null; } const cttsBox = stblBox.children.find((s) => s.type === "ctts-box"); return cttsBox; }; var getStszBox = (trakBox) => { const stblBox = getStblBox(trakBox); if (!stblBox || stblBox.type !== "regular-box") { return null; } const stszBox = stblBox.children.find((s) => s.type === "stsz-box"); return stszBox; }; var getStscBox = (trakBox) => { const stblBox = getStblBox(trakBox); if (!stblBox || stblBox.type !== "regular-box") { return null; } const stcoBox = stblBox.children.find((b) => b.type === "stsc-box"); return stcoBox; }; var getStssBox = (trakBox) => { const stblBox = getStblBox(trakBox); if (!stblBox || stblBox.type !== "regular-box") { return null; } const stssBox = stblBox.children.find((b) => b.type === "stss-box"); return stssBox; }; var getTfdtBox = (segment) => { if (segment.type !== "regular-box" || segment.boxType !== "traf") { throw new Error("Expected traf-box"); } const tfhdBox = segment.children.find((c) => c.type === "tfdt-box"); if (!tfhdBox || tfhdBox.type !== "tfdt-box") { throw new Error("Expected tfhd-box"); } return tfhdBox; }; var getTfhdBox = (segment) => { if (segment.type !== "regular-box" || segment.boxType !== "traf") { throw new Error("Expected traf-box"); } const tfhdBox = segment.children.find((c) => c.type === "tfhd-box"); if (!tfhdBox || tfhdBox.type !== "tfhd-box") { throw new Error("Expected tfhd-box"); } return tfhdBox; }; var getTrunBoxes = (segment) => { if (segment.type !== "regular-box" || segment.boxType !== "traf") { throw new Error("Expected traf-box"); } const trunBoxes = segment.children.filter((c) => c.type === "trun-box"); return trunBoxes; }; var getMvexBox = (moovAtom) => { const mvexBox = moovAtom.children.find((s) => s.type === "regular-box" && s.boxType === "mvex"); if (!mvexBox || mvexBox.type !== "regular-box") { return null; } return mvexBox; }; var getTrexBoxes = (moovAtom) => { const mvexBox = getMvexBox(moovAtom); if (!mvexBox) { return []; } const trexBoxes = mvexBox.children.filter((c) => c.type === "trex-box"); return trexBoxes; }; var getTfraBoxesFromMfraBoxChildren = (mfraBoxChildren) => { const tfraBoxes = mfraBoxChildren.filter((b) => b.type === "tfra-box"); return tfraBoxes; }; var getTfraBoxes = (structure) => { const mfraBox = structure.find((b) => b.type === "regular-box" && b.boxType === "mfra"); if (!mfraBox) { return []; } return getTfraBoxesFromMfraBoxChildren(mfraBox.children); }; var getTrakBoxByTrackId = (moovBox, trackId) => { const trakBoxes = getTraks(moovBox); return trakBoxes.find((t) => { const tkhd = getTkhdBox(t); if (!tkhd) { return false; } return tkhd.trackId === trackId; }) ?? null; }; var getElstBox = (trakBox) => { const edtsBox = trakBox.children.find((s) => s.type === "regular-box" && s.boxType === "edts"); if (!edtsBox || edtsBox.type !== "regular-box") { return null; } const elstBox = edtsBox.children.find((s) => s.type === "elst-box"); return elstBox; }; // src/containers/riff/traversal.ts var isRiffAvi = (structure) => { return structure.boxes.some((box) => box.type === "riff-header" && box.fileType === "AVI"); }; var getHdlrBox = (structure) => { return structure.boxes.find((box) => box.type === "list-box" && box.listType === "hdrl"); }; var getAvihBox = (structure) => { const hdlrBox = getHdlrBox(structure); if (!hdlrBox) { return null; } return hdlrBox.children.find((box) => box.type === "avih-box"); }; var getStrlBoxes = (structure) => { const hdlrBox = getHdlrBox(structure); if (!hdlrBox) { return []; } return hdlrBox.children.filter((box) => box.type === "list-box" && box.listType === "strl"); }; var getStrhBox = (strlBoxChildren) => { return strlBoxChildren.find((box) => box.type === "strh-box"); }; // src/is-audio-structure.ts var isAudioStructure = (structure) => { if (structure.type === "mp3") { return true; } if (structure.type === "wav") { return true; } if (structure.type === "aac") { return true; } if (structure.type === "flac") { return true; } if (structure.type === "iso-base-media") { return false; } if (structure.type === "matroska") { return false; } if (structure.type === "transport-stream") { return false; } if (structure.type === "riff") { return false; } if (structure.type === "m3u") { return false; } throw new Error(`Unhandled structure type: ${structure}`); }; // src/get-fps.ts var calculateFps = ({ sttsBox, timeScale, durationInSamples }) => { let totalSamples = 0; for (const sample of sttsBox.sampleDistribution) { totalSamples += sample.sampleCount; } if (totalSamples === 0) { return null; } const durationInSeconds = durationInSamples / timeScale; const fps = totalSamples / durationInSeconds; return fps; }; var trakBoxContainsAudio = (trakBox) => { const stsd = getStsdBox(trakBox); if (!stsd) { return false; } const videoSample = stsd.samples.find((s) => s.type === "audio"); if (!videoSample || videoSample.type !== "audio") { return false; } return true; }; var trakBoxContainsVideo = (trakBox) => { const stsd = getStsdBox(trakBox); if (!stsd) { return false; } const videoSample = stsd.samples.find((s) => s.type === "video"); if (!videoSample || videoSample.type !== "video") { return false; } return true; }; var getTimescaleAndDuration = (trakBox) => { const mdhdBox = getMdhdBox(trakBox); if (mdhdBox) { return { timescale: mdhdBox.timescale, duration: mdhdBox.duration }; } return null; }; var getFpsFromMp4TrakBox = (trakBox) => { const timescaleAndDuration = getTimescaleAndDuration(trakBox); if (!timescaleAndDuration) { return null; } const sttsBox = getSttsBox(trakBox); if (!sttsBox) { return null; } return calculateFps({ sttsBox, timeScale: timescaleAndDuration.timescale, durationInSamples: timescaleAndDuration.duration }); }; var getFpsFromIsoMaseMedia = (state) => { const moovBox = getMoovBoxFromState({ structureState: state.structure, isoState: state.iso, mp4HeaderSegment: state.m3uPlaylistContext?.mp4HeaderSegment ?? null, mayUsePrecomputed: true }); if (!moovBox) { return null; } const trackBoxes = getTraks(moovBox); const trackBox = trackBoxes.find(trakBoxContainsVideo); if (!trackBox) { return null; } return getFpsFromMp4TrakBox(trackBox); }; var getFpsFromAvi = (structure) => { const strl = getStrlBoxes(structure); for (const s of strl) { const strh = getStrhBox(s.children); if (!strh) { throw new Error("No strh box"); } if (strh.fccType === "auds") { continue; } return strh.rate; } return null; }; var getFps = (state) => { const segments = state.structure.getStructure(); if (segments.type === "iso-base-media") { return getFpsFromIsoMaseMedia(state); } if (segments.type === "riff") { return getFpsFromAvi(segments); } if (segments.type === "matroska") { return null; } if (segments.type === "transport-stream") { return null; } if (segments.type === "m3u") { return null; } if (segments.type === "mp3" || segments.type === "wav" || segments.type === "flac" || segments.type === "aac") { return null; } throw new Error("Cannot get fps, not implemented: " + segments); }; var hasFpsSuitedForSlowFps = (state) => { try { return getFps(state) !== null; } catch { return false; } }; var hasFps = (state) => { const structure = state.structure.getStructure(); if (isAudioStructure(structure)) { return true; } if (structure.type === "matroska") { return true; } if (structure.type === "transport-stream") { return true; } if (structure.type === "m3u") { return true; } return hasFpsSuitedForSlowFps(state); }; // src/get-sample-aspect-ratio.ts var getStsdVideoConfig = (trakBox) => { const stsdBox = getStsdBox(trakBox); if (!stsdBox) { return null; } const videoConfig = stsdBox.samples.find((s) => s.type === "video"); if (!videoConfig || videoConfig.type !== "video") { return null; } return videoConfig; }; var getAvccBox = (trakBox) => { const videoConfig = getStsdVideoConfig(trakBox); if (!videoConfig) { return null; } const avccBox = videoConfig.descriptors.find((c) => c.type === "avcc-box"); if (!avccBox || avccBox.type !== "avcc-box") { return null; } return avccBox; }; var getAv1CBox = (trakBox) => { const videoConfig = getStsdVideoConfig(trakBox); if (!videoConfig) { return null; } const av1cBox = videoConfig.descriptors.find((c) => c.type === "av1C-box"); if (!av1cBox || av1cBox.type !== "av1C-box") { return null; } return av1cBox; }; var getPaspBox = (trakBox) => { const videoConfig = getStsdVideoConfig(trakBox); if (!videoConfig) { return null; } const paspBox = videoConfig.descriptors.find((c) => c.type === "pasp-box"); if (!paspBox || paspBox.type !== "pasp-box") { return null; } return paspBox; }; var getHvccBox = (trakBox) => { const videoConfig = getStsdVideoConfig(trakBox); if (!videoConfig) { return null; } const hvccBox = videoConfig.descriptors.find((c) => c.type === "hvcc-box"); if (!hvccBox || hvccBox.type !== "hvcc-box") { return null; } return hvccBox; }; var getSampleAspectRatio = (trakBox) => { const paspBox = getPaspBox(trakBox); if (!paspBox) { return { numerator: 1, denominator: 1 }; } return { numerator: paspBox.hSpacing, denominator: paspBox.vSpacing }; }; var getColrBox = (videoSample) => { const colrBox = videoSample.descriptors.find((c) => c.type === "colr-box"); if (!colrBox || colrBox.type !== "colr-box") { return null; } return colrBox; }; var applyTkhdBox = (aspectRatioApplied, tkhdBox) => { if (tkhdBox === null || tkhdBox.rotation === 0) { return { displayAspectWidth: aspectRatioApplied.width, displayAspectHeight: aspectRatioApplied.height, width: aspectRatioApplied.width, height: aspectRatioApplied.height, rotation: 0 }; } return { width: tkhdBox.width, height: tkhdBox.height, rotation: tkhdBox.rotation, displayAspectWidth: aspectRatioApplied.width, displayAspectHeight: aspectRatioApplied.height }; }; var applyAspectRatios = ({ dimensions, sampleAspectRatio, displayAspectRatio }) => { if (displayAspectRatio.numerator === 0) { return dimensions; } if (displayAspectRatio.denominator === 0) { return dimensions; } const newWidth = Math.round(dimensions.width * sampleAspectRatio.numerator / sampleAspectRatio.denominator); const newHeight = Math.floor(newWidth / (displayAspectRatio.numerator / displayAspectRatio.denominator)); return { width: Math.floor(newWidth), height: newHeight }; }; function gcd(a, b) { return b === 0 ? a : gcd(b, a % b); } function reduceFraction(numerator, denominator) { const greatestCommonDivisor = gcd(Math.abs(numerator), Math.abs(denominator)); return { numerator: numerator / greatestCommonDivisor, denominator: denominator / greatestCommonDivisor }; } var getDisplayAspectRatio = ({ sampleAspectRatio, nativeDimensions }) => { const num = Math.round(nativeDimensions.width * sampleAspectRatio.numerator); const den = Math.round(nativeDimensions.height * sampleAspectRatio.denominator); return reduceFraction(num, den); }; // src/containers/avc/color.ts var getMatrixCoefficientsFromIndex = (index) => { if (index === 0) { return "rgb"; } if (index === 1) { return "bt709"; } if (index === 5) { return "bt470bg"; } if (index === 6) { return "smpte170m"; } if (index === 9) { return "bt2020-ncl"; } return null; }; var getTransferCharacteristicsFromIndex = (index) => { if (index === 1) { return "bt709"; } if (index === 6) { return "smpte170m"; } if (index === 8) { return "linear"; } if (index === 13) { return "iec61966-2-1"; } if (index === 16) { return "pq"; } if (index === 18) { return "hlg"; } return null; }; var getPrimariesFromIndex = (index) => { if (index === 1) { return "bt709"; } if (index === 5) { return "bt470bg"; } if (index === 6) { return "smpte170m"; } if (index === 9) { return "bt2020"; } if (index === 12) { return "smpte432"; } return null; }; // src/containers/webm/segments/all-segments.ts var matroskaElements = { Header: "0x1a45dfa3", EBMLMaxIDLength: "0x42f2", EBMLVersion: "0x4286", EBMLReadVersion: "0x42f7", EBMLMaxSizeLength: "0x42f3", DocType: "0x4282", DocTypeVersion: "0x4287", DocTypeReadVersion: "0x4285", Segment: "0x18538067", SeekHead: "0x114d9b74", Seek: "0x4dbb", SeekID: "0x53ab", SeekPosition: "0x53ac", Info: "0x1549a966", SegmentUUID: "0x73a4", SegmentFilename: "0x7384", PrevUUID: "0x3cb923", PrevFilename: "0x3c83ab", NextUUID: "0x3eb923", NextFilename: "0x3e83bb", SegmentFamily: "0x4444", ChapterTranslate: "0x6924", ChapterTranslateID: "0x69a5", ChapterTranslateCodec: "0x69bf", ChapterTranslateEditionUID: "0x69fc", TimestampScale: "0x2ad7b1", Duration: "0x4489", DateUTC: "0x4461", Title: "0x7ba9", MuxingApp: "0x4d80", WritingApp: "0x5741", Cluster: "0x1f43b675", Timestamp: "0xe7", SilentTracks: "0x5854", SilentTrackNumber: "0x58d7", Position: "0xa7", PrevSize: "0xab", SimpleBlock: "0xa3", BlockGroup: "0xa0", Block: "0xa1", BlockVirtual: "0xa2", BlockAdditions: "0x75a1", BlockMore: "0xa6", BlockAdditional: "0xa5", BlockAddID: "0xee", BlockDuration: "0x9b", ReferencePriority: "0xfa", ReferenceBlock: "0xfb", ReferenceVirtual: "0xfd", CodecState: "0xa4", DiscardPadding: "0x75a2", Slices: "0x8e", TimeSlice: "0xe8", LaceNumber: "0xcc", FrameNumber: "0xcd", BlockAdditionID: "0xcb", Delay: "0xce", SliceDuration: "0xcf", ReferenceFrame: "0xc8", ReferenceOffset: "0xc9", ReferenceTimestamp: "0xca", EncryptedBlock: "0xaf", Tracks: "0x1654ae6b", TrackEntry: "0xae", TrackNumber: "0xd7", TrackUID: "0x73c5", TrackType: "0x83", FlagEnabled: "0xb9", FlagDefault: "0x88", FlagForced: "0x55aa", FlagHearingImpaired: "0x55ab", FlagVisualImpaired: "0x55ac", FlagTextDescriptions: "0x55ad", FlagOriginal: "0x55ae", FlagCommentary: "0x55af", FlagLacing: "0x9c", MinCache: "0x6de7", MaxCache: "0x6df8", DefaultDuration: "0x23e383", DefaultDecodedFieldDuration: "0x234e7a", TrackTimestampScale: "0x23314f", TrackOffset: "0x537f", MaxBlockAdditionID: "0x55ee", BlockAdditionMapping: "0x41e4", BlockAddIDValue: "0x41f0", BlockAddIDName: "0x41a4", BlockAddIDType: "0x41e7", BlockAddIDExtraData: "0x41ed", Name: "0x536e", Language: "0x22b59c", LanguageBCP47: "0x22b59d", CodecID: "0x86", CodecPrivate: "0x63a2", CodecName: "0x258688", AttachmentLink: "0x7446", CodecSettings: "0x3a9697", CodecInfoURL: "0x3b4040", CodecDownloadURL: "0x26b240", CodecDecodeAll: "0xaa", TrackOverlay: "0x6fab", CodecDelay: "0x56aa", SeekPreRoll: "0x56bb", TrackTranslate: "0x6624", TrackTranslateTrackID: "0x66a5", TrackTranslateCodec: "0x66bf", TrackTranslateEditionUID: "0x66fc", Video: "0xe0", FlagInterlaced: "0x9a", FieldOrder: "0x9d", StereoMode: "0x53b8", AlphaMode: "0x53c0", OldStereoMode: "0x53b9", PixelWidth: "0xb0", PixelHeight: "0xba", PixelCropBottom: "0x54aa", PixelCropTop: "0x54bb", PixelCropLeft: "0x54cc", PixelCropRight: "0x54dd", DisplayWidth: "0x54b0", DisplayHeight: "0x54ba", DisplayUnit: "0x54b2", AspectRatioType: "0x54b3", UncompressedFourCC: "0x2eb524", GammaValue: "0x2fb523", FrameRate: "0x2383e3", Colour: "0x55b0", MatrixCoefficients: "0x55b1", BitsPerChannel: "0x55b2", ChromaSubsamplingHorz: "0x55b3", ChromaSubsamplingVert: "0x55b4", CbSubsamplingHorz: "0x55b5", CbSubsamplingVert: "0x55b6", ChromaSitingHorz: "0x55b7", ChromaSitingVert: "0x55b8", Range: "0x55b9", TransferCharacteristics: "0x55ba", Primaries: "0x55bb", MaxCLL: "0x55bc", MaxFALL: "0x55bd", MasteringMetadata: "0x55d0", PrimaryRChromaticityX: "0x55d1", PrimaryRChromaticityY: "0x55d2", PrimaryGChromaticityX: "0x55d3", PrimaryGChromaticityY: "0x55d4", PrimaryBChromaticityX: "0x55d5", PrimaryBChromaticityY: "0x55d6", WhitePointChromaticityX: "0x55d7", WhitePointChromaticityY: "0x55d8", LuminanceMax: "0x55d9", LuminanceMin: "0x55da", Projection: "0x7670", ProjectionType: "0x7671", ProjectionPrivate: "0x7672", ProjectionPoseYaw: "0x7673", ProjectionPosePitch: "0x7674", ProjectionPoseRoll: "0x7675", Audio: "0xe1", SamplingFrequency: "0xb5", OutputSamplingFrequency: "0x78b5", Channels: "0x9f", ChannelPositions: "0x7d7b", BitDepth: "0x6264", Emphasis: "0x52f1", TrackOperation: "0xe2", TrackCombinePlanes: "0xe3", TrackPlane: "0xe4", TrackPlaneUID: "0xe5", TrackPlaneType: "0xe6", TrackJoinBlocks: "0xe9", TrackJoinUID: "0xed", TrickTrackUID: "0xc0", TrickTrackSegmentUID: "0xc1", TrickTrackFlag: "0xc6", TrickMasterTrackUID: "0xc7", TrickMasterTrackSegmentUID: "0xc4", ContentEncodings: "0x6d80", ContentEncoding: "0x6240", ContentEncodingOrder: "0x5031", ContentEncodingScope: "0x5032", ContentEncodingType: "0x5033", ContentCompression: "0x5034", ContentCompAlgo: "0x4254", ContentCompSettings: "0x4255", ContentEncryption: "0x5035", ContentEncAlgo: "0x47e1", ContentEncKeyID: "0x47e2", ContentEncAESSettings: "0x47e7", AESSettingsCipherMode: "0x47e8", ContentSignature: "0x47e3", ContentSigKeyID: "0x47e4", ContentSigAlgo: "0x47e5", ContentSigHashAlgo: "0x47e6", Cues: "0x1c53bb6b", CuePoint: "0xbb", CueTime: "0xb3", CueTrackPositions: "0xb7", CueTrack: "0xf7", CueClusterPosition: "0xf1", CueRelativePosition: "0xf0", CueDuration: "0xb2", CueBlockNumber: "0x5378", CueCodecState: "0xea", CueReference: "0xdb", CueRefTime: "0x96", CueRefCluster: "0x97", CueRefNumber: "0x535f", CueRefCodecState: "0xeb", Attachments: "0x1941a469", AttachedFile: "0x61a7", FileDescription: "0x467e", FileName: "0x466e", FileMediaType: "0x4660", FileData: "0x465c", FileUID: "0x46ae", FileReferral: "0x4675", FileUsedStartTime: "0x4661", FileUsedEndTime: "0x4662", Chapters: "0x1043a770", EditionEntry: "0x45b9", EditionUID: "0x45bc", EditionFlagHidden: "0x45bd", EditionFlagDefault: "0x45db", EditionFlagOrdered: "0x45dd", EditionDisplay: "0x4520", EditionString: "0x4521", EditionLanguageIETF: "0x45e4", ChapterAtom: "0xb6", ChapterUID: "0x73c4", ChapterStringUID: "0x5654", ChapterTimeStart: "0x91", ChapterTimeEnd: "0x92", ChapterFlagHidden: "0x98", ChapterFlagEnabled: "0x4598", ChapterSegmentUUID: "0x6e67", ChapterSkipType: "0x4588", ChapterSegmentEditionUID: "0x6ebc", ChapterPhysicalEquiv: "0x63c3", ChapterTrack: "0x8f", ChapterTrackUID: "0x89", ChapterDisplay: "0x80", ChapString: "0x85", ChapLanguage: "0x437c", ChapLanguageBCP47: "0x437d", ChapCountry: "0x437e", ChapProcess: "0x6944", ChapProcessCodecID: "0x6955", ChapProcessPrivate: "0x450d", ChapProcessCommand: "0x6911", ChapProcessTime: "0x6922", ChapProcessData: "0x6933", Tags: "0x1254c367", Tag: "0x7373", Targets: "0x63c0", TargetTypeValue: "0x68ca", TargetType: "0x63ca", TagTrackUID: "0x63c5", TagEditionUID: "0x63c9", TagChapterUID: "0x63c4", TagAttachmentUID: "0x63c6", SimpleTag: "0x67c8", TagName: "0x45a3", TagLanguage: "0x447a", TagLanguageBCP47: "0x447b", TagDefault: "0x4484", TagDefaultBogus: "0x44b4", TagString: "0x4487", TagBinary: "0x4485", Void: "0xec", Crc32: "0xbf" }; var matroskaIds = Object.values(matroskaElements); var knownIdsWithOneLength = matroskaIds.filter((id) => id.length === 4); var knownIdsWithTwoLength = matroskaIds.filter((id) => id.length === 6); var knownIdsWithThreeLength = matroskaIds.filter((id) => id.length === 8); var ebmlVersion = { name: "EBMLVersion", type: "uint" }; var ebmlReadVersion = { name: "EBMLReadVersion", type: "uint" }; var ebmlMaxIdLength = { name: "EBMLMaxIDLength", type: "uint" }; var ebmlMaxSizeLength = { name: "EBMLMaxSizeLength", type: "uint" }; var docType = { name: "DocType", type: "string" }; var docTypeVersion = { name: "DocTypeVersion", type: "uint" }; var docTypeReadVersion = { name: "DocTypeReadVersion", type: "uint" }; var voidEbml = { name: "Void", type: "uint8array" }; var matroskaHeader = { name: "Header", type: "children" }; var seekId = { name: "SeekID", type: "hex-string" }; var _name = { name: "Name", type: "string" }; var minCache = { name: "MinCache", type: "uint" }; var maxCache = { name: "MaxCache", type: "uint" }; var seekPosition = { name: "SeekPosition", type: "uint" }; var seek = { name: "Seek", type: "children" }; var seekHead = { name: "SeekHead", type: "children" }; var trackType = { name: "TrackType", type: "uint" }; var widthType = { name: "PixelWidth", type: "uint" }; var heightType = { name: "PixelHeight", type: "uint" }; var muxingApp = { name: "MuxingApp", type: "string" }; var duration = { name: "Duration", type: "float" }; var timestampScale = { name: "TimestampScale", type: "uint" }; var infoType = { name: "Info", type: "children" }; var titleType = { name: "Title", type: "string" }; var tagTrackUidType = { name: "TagTrackUID", type: "hex-string" }; var samplingFrequency = { name: "SamplingFrequency", type: "float" }; var channels = { name: "Channels", type: "uint" }; var alphaMode = { name: "AlphaMode", type: "uint" }; var interlaced = { name: "FlagInterlaced", type: "uint" }; var bitDepth = { name: "BitDepth", type: "uint" }; var displayWidth = { name: "DisplayWidth", type: "uint" }; var displayHeight = { name: "DisplayHeight", type: "uint" }; var displayUnit = { name: "DisplayUnit", type: "uint" }; var flagLacing = { name: "FlagLacing", type: "uint" }; var tagSegment = { name: "Tag", type: "children" }; var tags = { name: "Tags", type: "children" }; var trackNumber = { name: "TrackNumber", type: "uint" }; var trackUID = { name: "TrackUID", type: "hex-string" }; var color = { name: "Colour", type: "children" }; var transferCharacteristics = { name: "TransferCharacteristics", type: "uint" }; var matrixCoefficients = { name: "MatrixCoefficients", type: "uint" }; var primaries = { name: "Primaries", type: "uint" }; var range = { name: "Range", type: "uint" }; var ChromaSitingHorz = { name: "ChromaSitingHorz", type: "uint" }; var ChromaSitingVert = { name: "ChromaSitingVert", type: "uint" }; var language = { name: "Language", type: "string" }; var defaultDuration = { name: "DefaultDuration", type: "uint" }; var codecPrivate = { name: "CodecPrivate", type: "uint8array" }; var blockAdditionsSegment = { name: "BlockAdditions", type: "uint8array" }; var maxBlockAdditionIdSegment = { name: "MaxBlockAdditionID", type: "uint" }; var audioSegment = { name: "Audio", type: "children" }; var videoSegment = { name: "Video", type: "children" }; var flagDefault = { name: "FlagDefault", type: "uint" }; var referenceBlock = { name: "ReferenceBlock", type: "uint" }; var blockDurationSegment = { name: "BlockDuration", type: "uint" }; var codecName = { name: "CodecName", type: "string" }; var trackTimestampScale = { name: "TrackTimestampScale", type: "float" }; var trackEntry = { name: "TrackEntry", type: "children" }; var tracks = { name: "Tracks", type: "children" }; var block = { name: "Block", ty