// src/writers/web-fs.ts var createContent = async ({ filename }) => { const directoryHandle = await navigator.storage.getDirectory(); const actualFilename = `__remotion_mediaparser:${filename}`; const remove = async () => { try { await directoryHandle.removeEntry(actualFilename, { recursive: true }); } catch {} }; await remove(); const fileHandle = await directoryHandle.getFileHandle(actualFilename, { create: true }); const writable = await fileHandle.createWritable(); let written = 0; let writPromise = Promise.resolve(); const write = async (arr) => { await writable.write(arr); written += arr.byteLength; }; const updateDataAt = async (position, data) => { await writable.seek(position); await writable.write(data); await writable.seek(written); }; const writer = { write: (arr) => { writPromise = writPromise.then(() => write(arr)); return writPromise; }, finish: async () => { await writPromise; try { await writable.close(); } catch {} }, async getBlob() { const newHandle = await directoryHandle.getFileHandle(actualFilename, { create: true }); const newFile = await newHandle.getFile(); return newFile; }, getWrittenByteCount: () => written, updateDataAt: (position, data) => { writPromise = writPromise.then(() => updateDataAt(position, data)); return writPromise; }, remove }; return writer; }; var webFsWriter = { createContent }; var canUseWebFsWriter = async () => { if (!("storage" in navigator)) { return false; } if (!("getDirectory" in navigator.storage)) { return false; } try { const directoryHandle = await navigator.storage.getDirectory(); const fileHandle = await directoryHandle.getFileHandle("remotion-probe-web-fs-support", { create: true }); const canUse = fileHandle.createWritable !== undefined; return canUse; } catch { return false; } }; // src/writers/buffer-implementation/writer.ts var createContent2 = ({ filename, mimeType }) => { const buf = new ArrayBuffer(0, { maxByteLength: 2000000000 }); if (!buf.resize) { throw new Error("Could not create buffer writer"); } const write = (newData) => { const oldLength = buf.byteLength; const newLength = oldLength + newData.byteLength; buf.resize(newLength); const newArray = new Uint8Array(buf); newArray.set(newData, oldLength); }; const updateDataAt = (position, newData) => { const newArray = new Uint8Array(buf); newArray.set(newData, position); }; let writPromise = Promise.resolve(); let removed = false; const writer = { write: (arr) => { writPromise = writPromise.then(() => write(arr)); return writPromise; }, finish: async () => { await writPromise; if (removed) { return Promise.reject(new Error("Already called .remove() on the result")); } return Promise.resolve(); }, getBlob() { const arr = new Uint8Array(buf); return Promise.resolve(new File([arr.slice()], filename, { type: mimeType })); }, remove() { removed = true; return Promise.resolve(); }, getWrittenByteCount: () => buf.byteLength, updateDataAt: (position, newData) => { writPromise = writPromise.then(() => updateDataAt(position, newData)); return writPromise; } }; return Promise.resolve(writer); }; // src/writers/buffer.ts var bufferWriter = { createContent: createContent2 }; // src/resizing/calculate-new-size.ts var ensureMultipleOfTwo = ({ dimensions, needsToBeMultipleOfTwo }) => { if (!needsToBeMultipleOfTwo) { return dimensions; } return { width: Math.floor(dimensions.width / 2) * 2, height: Math.floor(dimensions.height / 2) * 2 }; }; var calculateNewSizeAfterResizing = ({ dimensions, resizeOperation, needsToBeMultipleOfTwo }) => { if (resizeOperation === null) { return ensureMultipleOfTwo({ dimensions, needsToBeMultipleOfTwo }); } if (resizeOperation.mode === "width") { return ensureMultipleOfTwo({ dimensions: { width: resizeOperation.width, height: Math.round(resizeOperation.width / dimensions.width * dimensions.height) }, needsToBeMultipleOfTwo }); } if (resizeOperation.mode === "height") { return ensureMultipleOfTwo({ dimensions: { width: Math.round(resizeOperation.height / dimensions.height * dimensions.width), height: resizeOperation.height }, needsToBeMultipleOfTwo }); } if (resizeOperation.mode === "max-height") { const height = Math.min(dimensions.height, resizeOperation.maxHeight); return ensureMultipleOfTwo({ dimensions: { width: Math.round(height / dimensions.height * dimensions.width), height }, needsToBeMultipleOfTwo }); } if (resizeOperation.mode === "max-width") { const width = Math.min(dimensions.width, resizeOperation.maxWidth); return ensureMultipleOfTwo({ dimensions: { width, height: Math.round(width / dimensions.width * dimensions.height) }, needsToBeMultipleOfTwo }); } if (resizeOperation.mode === "max-height-width") { const height = Math.min(dimensions.height, resizeOperation.maxHeight); const width = Math.min(dimensions.width, resizeOperation.maxWidth); const scale = Math.min(width / dimensions.width, height / dimensions.height); const actualWidth = Math.round(dimensions.width * scale); const actualHeight = Math.round(dimensions.height * scale); return ensureMultipleOfTwo({ dimensions: { height: actualHeight, width: actualWidth }, needsToBeMultipleOfTwo }); } if (resizeOperation.mode === "scale") { if (resizeOperation.scale <= 0) { throw new Error("Scale must be greater than 0"); } const width = Math.round(dimensions.width * resizeOperation.scale); const height = Math.round(dimensions.height * resizeOperation.scale); return ensureMultipleOfTwo({ dimensions: { width, height }, needsToBeMultipleOfTwo }); } throw new Error("Invalid resizing mode " + resizeOperation); }; // src/rotation.ts var calculateNewDimensionsFromRotate = ({ height, width, rotation }) => { const normalized = normalizeVideoRotation(rotation); const switchDimensions = normalized % 90 === 0 && normalized % 180 !== 0; const newHeight = switchDimensions ? width : height; const newWidth = switchDimensions ? height : width; return { height: newHeight, width: newWidth }; }; var calculateNewDimensionsFromRotateAndScale = ({ width, height, rotation, resizeOperation, needsToBeMultipleOfTwo }) => { const { height: newHeight, width: newWidth } = calculateNewDimensionsFromRotate({ height, rotation, width }); return calculateNewSizeAfterResizing({ dimensions: { height: newHeight, width: newWidth }, resizeOperation, needsToBeMultipleOfTwo }); }; // src/rotate-and-resize-video-frame.ts var normalizeVideoRotation = (rotation) => { return (rotation % 360 + 360) % 360; }; var rotateAndResizeVideoFrame = ({ frame, rotation, needsToBeMultipleOfTwo = false, resizeOperation }) => { const normalized = normalizeVideoRotation(rotation); const mustProcess = "rotation" in frame && frame.rotation !== 0; if (normalized === 0 && resizeOperation === null && !mustProcess) { return frame; } if (normalized % 90 !== 0) { throw new Error("Only 90 degree rotations are supported"); } const tentativeDimensions = calculateNewDimensionsFromRotateAndScale({ height: frame.displayHeight, width: frame.displayWidth, rotation, needsToBeMultipleOfTwo, resizeOperation }); if (normalized === 0 && tentativeDimensions.height === frame.displayHeight && tentativeDimensions.width === frame.displayWidth && !mustProcess) { return frame; } const canvasRotationToApply = normalizeVideoRotation(normalized); const { width, height } = calculateNewDimensionsFromRotateAndScale({ height: frame.displayHeight, width: frame.displayWidth, rotation: canvasRotationToApply, needsToBeMultipleOfTwo, resizeOperation }); const canvas = new OffscreenCanvas(width, height); const ctx = canvas.getContext("2d"); if (!ctx) { throw new Error("Could not get 2d context"); } canvas.width = width; canvas.height = height; if (canvasRotationToApply === 90) { ctx.translate(width, 0); } else if (canvasRotationToApply === 180) { ctx.translate(width, height); } else if (canvasRotationToApply === 270) { ctx.translate(0, height); } if (canvasRotationToApply !== 0) { ctx.rotate(canvasRotationToApply * (Math.PI / 180)); } if (frame.displayHeight !== height || frame.displayWidth !== width) { const dimensionsAfterRotate = calculateNewDimensionsFromRotate({ height: frame.displayHeight, rotation: canvasRotationToApply, width: frame.displayWidth }); ctx.scale(width / dimensionsAfterRotate.width, height / dimensionsAfterRotate.height); } ctx.drawImage(frame, 0, 0); return new VideoFrame(canvas, { displayHeight: height, displayWidth: width, duration: frame.duration ?? undefined, timestamp: frame.timestamp }); }; // src/audio-encoder.ts import { MediaParserAbortError } from "@remotion/media-parser"; // src/create/event-emitter.ts class IoEventEmitter { listeners = { input: [], output: [], processed: [], progress: [] }; 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 }); }); } } // src/create/with-resolvers.ts var withResolvers = function() { let resolve; let reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; }; var withResolversAndWaitForReturn = () => { const { promise, reject, resolve } = withResolvers(); const { promise: returnPromise, resolve: resolveReturn } = withResolvers(); return { getPromiseToImmediatelyReturn: () => { resolveReturn(undefined); return promise; }, reject: (reason) => { returnPromise.then(() => reject(reason)); }, resolve }; }; // src/log.ts import { MediaParserInternals } from "@remotion/media-parser"; var { Log } = MediaParserInternals; // src/io-manager/make-timeout-promise.ts var makeTimeoutPromise = ({ label, ms, controller }) => { const { promise, reject, resolve } = withResolvers(); let timeout = null; const set = () => { timeout = setTimeout(() => { reject(new Error(`${label()} (timed out after ${ms}ms)`)); }, ms); }; set(); const onPause = () => { if (timeout) { clearTimeout(timeout); } }; const onResume = () => { set(); }; if (controller) { controller.addEventListener("pause", onPause); controller.addEventListener("resume", onResume); } return { timeoutPromise: promise, clear: () => { if (timeout) { clearTimeout(timeout); } resolve(); if (controller) { controller.removeEventListener("pause", onPause); controller.removeEventListener("resume", onResume); } } }; }; // src/io-manager/io-synchronizer.ts var makeIoSynchronizer = ({ logLevel, label, controller }) => { const eventEmitter = new IoEventEmitter; let lastInput = 0; let lastOutput = 0; let inputsSinceLastOutput = 0; let inputs = []; let resolvers = []; const getQueuedItems = () => { inputs = inputs.filter((input) => Math.floor(input) > Math.floor(lastOutput) + 1); return inputs.length; }; const printState = (prefix) => { Log.trace(logLevel, `[${label}] ${prefix}, state: Last input = ${lastInput} Last output = ${lastOutput} Inputs since last output = ${inputsSinceLastOutput}, Queue = ${getQueuedItems()}`); }; const inputItem = (timestamp) => { lastInput = timestamp; inputsSinceLastOutput++; inputs.push(timestamp); eventEmitter.dispatchEvent("input", { timestamp }); printState("Input item"); }; const onOutput = (timestamp) => { lastOutput = timestamp; inputsSinceLastOutput = 0; eventEmitter.dispatchEvent("output", { timestamp }); printState("Got output"); }; const waitForOutput = () => { const { promise, resolve } = withResolvers(); const on = () => { eventEmitter.removeEventListener("output", on); resolve(); resolvers = resolvers.filter((resolver) => resolver !== resolve); }; eventEmitter.addEventListener("output", on); resolvers.push(resolve); return promise; }; const makeErrorBanner = () => { return [ `Waited too long for ${label} to finish:`, `${getQueuedItems()} queued items`, `inputs: ${JSON.stringify(inputs)}`, `last output: ${lastOutput}` ]; }; const waitForQueueSize = async (queueSize) => { if (getQueuedItems() <= queueSize) { return Promise.resolve(); } const { timeoutPromise, clear } = makeTimeoutPromise({ label: () => [ ...makeErrorBanner(), `wanted: <${queueSize} queued items`, `Report this at https://remotion.dev/report` ].join(` `), ms: 1e4, controller }); if (controller) { controller._internals._mediaParserController._internals.signal.addEventListener("abort", clear); } await Promise.race([ timeoutPromise, (async () => { while (getQueuedItems() > queueSize) { await waitForOutput(); } })() ]).finally(() => clear()); if (controller) { controller._internals._mediaParserController._internals.signal.removeEventListener("abort", clear); } }; const clearQueue = () => { inputs.length = 0; lastInput = 0; lastOutput = 0; inputsSinceLastOutput = 0; resolvers.forEach((resolver) => { return resolver(); }); resolvers.length = 0; inputs.length = 0; }; return { inputItem, onOutput, waitForQueueSize, clearQueue }; }; // src/audio-data/data-types.ts var getDataTypeForAudioFormat = (format) => { switch (format) { case "f32": return Float32Array; case "f32-planar": return Float32Array; case "s16": return Int16Array; case "s16-planar": return Int16Array; case "u8": return Uint8Array; case "u8-planar": return Uint8Array; case "s32": return Int32Array; case "s32-planar": return Int32Array; default: throw new Error(`Unsupported audio format: ${format}`); } }; // src/audio-data/is-planar-format.ts var isPlanarFormat = (format) => { return format.includes("-planar"); }; // src/convert-audiodata.ts var validateRange = (format, value) => { if (format === "f32" || format === "f32-planar") { if (value < -1 || value > 1) { throw new Error("All values in a Float32 array must be between -1 and 1"); } } }; var convertAudioData = ({ audioData, newSampleRate = audioData.sampleRate, format = audioData.format }) => { const { numberOfChannels, sampleRate: currentSampleRate, numberOfFrames: currentNumberOfFrames } = audioData; const ratio = currentSampleRate / newSampleRate; const newNumberOfFrames = Math.floor(currentNumberOfFrames / ratio); if (newNumberOfFrames === 0) { throw new Error("Cannot resample - the given sample rate would result in less than 1 sample"); } if (newSampleRate < 3000 || newSampleRate > 768000) { throw new Error("newSampleRate must be between 3000 and 768000"); } if (!format) { throw new Error("AudioData format is not set"); } if (format === audioData.format && newNumberOfFrames === currentNumberOfFrames) { return audioData.clone(); } const DataType = getDataTypeForAudioFormat(format); const isPlanar = isPlanarFormat(format); const planes = isPlanar ? numberOfChannels : 1; const srcChannels = new Array(planes).fill(true).map(() => new DataType((isPlanar ? 1 : numberOfChannels) * currentNumberOfFrames)); for (let i = 0;i < planes; i++) { audioData.clone().copyTo(srcChannels[i], { planeIndex: i, format }); } const data = new DataType(newNumberOfFrames * numberOfChannels); const chunkSize = currentNumberOfFrames / newNumberOfFrames; for (let newFrameIndex = 0;newFrameIndex < newNumberOfFrames; newFrameIndex++) { const start = Math.floor(newFrameIndex * chunkSize); const end = Math.max(Math.floor(start + chunkSize), start + 1); if (isPlanar) { for (let channelIndex = 0;channelIndex < numberOfChannels; channelIndex++) { const chunk = srcChannels[channelIndex].slice(start, end); const average = chunk.reduce((a, b) => { return a + b; }, 0) / chunk.length; validateRange(format, average); data[newFrameIndex + channelIndex * newNumberOfFrames] = average; } } else { const sampleCountAvg = end - start; for (let channelIndex = 0;channelIndex < numberOfChannels; channelIndex++) { const items = []; for (let k = 0;k < sampleCountAvg; k++) { const num = srcChannels[0][(start + k) * numberOfChannels + channelIndex]; items.push(num); } const average = items.reduce((a, b) => a + b, 0) / items.length; validateRange(format, average); data[newFrameIndex * numberOfChannels + channelIndex] = average; } } } const newAudioData = new AudioData({ data, format, numberOfChannels, numberOfFrames: newNumberOfFrames, sampleRate: newSampleRate, timestamp: audioData.timestamp }); return newAudioData; }; // src/wav-audio-encoder.ts var getWaveAudioEncoder = ({ onChunk, controller, config, ioSynchronizer }) => { return { close: () => { return Promise.resolve(); }, encode: (unconvertedAudioData) => { if (controller._internals._mediaParserController._internals.signal.aborted) { return Promise.resolve(); } const audioData = convertAudioData({ audioData: unconvertedAudioData, newSampleRate: config.sampleRate, format: "s16" }); unconvertedAudioData.close(); const chunk = { timestamp: audioData.timestamp, duration: audioData.duration, type: "key", copyTo: (destination) => audioData.copyTo(destination, { planeIndex: 0 }), byteLength: audioData.allocationSize({ planeIndex: 0 }) }; return onChunk(chunk); }, flush: () => Promise.resolve(), waitForFinish: () => Promise.resolve(), ioSynchronizer }; }; // src/audio-encoder.ts var createAudioEncoder = ({ onChunk, onError, codec, controller, config: audioEncoderConfig, logLevel, onNewAudioSampleRate }) => { if (controller._internals._mediaParserController._internals.signal.aborted) { throw new MediaParserAbortError("Not creating audio encoder, already aborted"); } const ioSynchronizer = makeIoSynchronizer({ logLevel, label: "Audio encoder", controller }); if (codec === "wav") { return getWaveAudioEncoder({ onChunk, controller, config: audioEncoderConfig, ioSynchronizer }); } const encoder = new AudioEncoder({ output: async (chunk) => { try { await onChunk(chunk); } catch (err) { onError(err); } ioSynchronizer.onOutput(chunk.timestamp); }, error(error) { onError(error); } }); const close = () => { controller._internals._mediaParserController._internals.signal.removeEventListener("abort", onAbort); if (encoder.state === "closed") { return; } encoder.close(); }; const onAbort = () => { close(); }; controller._internals._mediaParserController._internals.signal.addEventListener("abort", onAbort); if (codec !== "opus" && codec !== "aac") { throw new Error('Only `codec: "opus"` and `codec: "aac"` is supported currently'); } const wantedSampleRate = audioEncoderConfig.sampleRate; const encodeFrame = (audioData) => { if (encoder.state === "closed") { return; } if (encoder.state === "unconfigured") { if (audioData.sampleRate === wantedSampleRate) { encoder.configure(audioEncoderConfig); } else { encoder.configure({ ...audioEncoderConfig, sampleRate: audioData.sampleRate }); onNewAudioSampleRate(audioData.sampleRate); } } encoder.encode(audioData); ioSynchronizer.inputItem(audioData.timestamp); }; return { encode: (audioData) => { encodeFrame(audioData); }, waitForFinish: async () => { await encoder.flush(); await ioSynchronizer.waitForQueueSize(0); }, close, flush: async () => { await encoder.flush(); }, ioSynchronizer }; }; // src/is-different-video-codec.ts var isSameVideoCodec = ({ inputVideoCodec, outputCodec }) => { if (outputCodec === "h264") { return inputVideoCodec === "h264"; } if (outputCodec === "h265") { return inputVideoCodec === "h265"; } if (outputCodec === "vp8") { return inputVideoCodec === "vp8"; } if (outputCodec === "vp9") { return inputVideoCodec === "vp9"; } throw new Error(`Unsupported output codec: ${outputCodec}`); }; var isSameAudioCodec = ({ inputAudioCodec, outputCodec }) => { if (outputCodec === "aac") { return inputAudioCodec === "aac"; } if (outputCodec === "opus") { return inputAudioCodec === "opus"; } if (outputCodec === "wav") { return inputAudioCodec === "pcm-f32" || inputAudioCodec === "pcm-s16" || inputAudioCodec === "pcm-s24" || inputAudioCodec === "pcm-s32" || inputAudioCodec === "pcm-u8"; } throw new Error(`Unsupported output codec: ${outputCodec}`); }; // src/can-copy-audio-track.ts var canCopyAudioTrack = ({ inputCodec, outputContainer, inputContainer, outputAudioCodec }) => { if (outputAudioCodec) { if (!isSameAudioCodec({ inputAudioCodec: inputCodec, outputCodec: outputAudioCodec })) { return false; } } if (outputContainer === "webm") { return inputCodec === "opus"; } if (outputContainer === "mp4") { return inputCodec === "aac" && (inputContainer === "mp4" || inputContainer === "avi" || inputContainer === "m3u8"); } if (outputContainer === "wav") { return false; } throw new Error(`Unhandled container: ${outputContainer}`); }; // src/can-copy-video-track.ts var canCopyVideoTrack = ({ outputContainer, rotationToApply, inputContainer, resizeOperation, inputTrack, outputVideoCodec }) => { if (normalizeVideoRotation(inputTrack.rotation) !== normalizeVideoRotation(rotationToApply)) { return false; } if (outputVideoCodec) { if (!isSameVideoCodec({ inputVideoCodec: inputTrack.codecEnum, outputCodec: outputVideoCodec })) { return false; } } const needsToBeMultipleOfTwo = inputTrack.codecEnum === "h264"; const newDimensions = calculateNewDimensionsFromRotateAndScale({ height: inputTrack.height, resizeOperation, rotation: rotationToApply, width: inputTrack.width, needsToBeMultipleOfTwo }); if (newDimensions.height !== inputTrack.height || newDimensions.width !== inputTrack.width) { return false; } if (outputContainer === "webm") { return inputTrack.codecEnum === "vp8" || inputTrack.codecEnum === "vp9"; } if (outputContainer === "mp4") { return (inputTrack.codecEnum === "h264" || inputTrack.codecEnum === "h265") && (inputContainer === "mp4" || inputContainer === "avi" || inputContainer === "m3u8" && inputTrack.m3uStreamFormat === "mp4"); } if (outputContainer === "wav") { return false; } throw new Error(`Unhandled codec: ${outputContainer}`); }; // src/audio-decoder-config.ts var getAudioDecoderConfig = async (config) => { if (config.codec === "pcm-s16") { return config; } if (config.codec === "pcm-s24") { return config; } if (typeof AudioDecoder === "undefined") { return null; } if (typeof EncodedAudioChunk === "undefined") { return null; } if ((await AudioDecoder.isConfigSupported(config)).supported) { return config; } return null; }; // src/audio-encoder-config.ts var getCodecString = (audioCodec) => { if (audioCodec === "opus") { return "opus"; } if (audioCodec === "aac") { return "mp4a.40.02"; } if (audioCodec === "wav") { return "wav-should-not-to-into-audio-encoder"; } throw new Error(`Unsupported audio codec: ${audioCodec}`); }; var getAudioEncoderConfig = async (config) => { const actualConfig = { ...config, codec: getCodecString(config.codec) }; if (config.codec === "wav") { return actualConfig; } if (typeof AudioEncoder === "undefined") { return null; } if ((await AudioEncoder.isConfigSupported(actualConfig)).supported) { return actualConfig; } const maybeItIsTheSampleRateThatIsTheProblem = config.sampleRate !== 48000 && config.sampleRate !== 44100; if (maybeItIsTheSampleRateThatIsTheProblem) { return getAudioEncoderConfig({ ...config, sampleRate: config.sampleRate === 22050 ? 44100 : 48000 }); } return null; }; // src/can-reencode-audio-track.ts var canReencodeAudioTrack = async ({ track, audioCodec, bitrate, sampleRate }) => { const audioDecoderConfig = await getAudioDecoderConfig(track); if (audioCodec === "wav" && audioDecoderConfig) { return true; } const audioEncoderConfig = await getAudioEncoderConfig({ codec: audioCodec, numberOfChannels: track.numberOfChannels, sampleRate: sampleRate ?? track.sampleRate, bitrate }); return Boolean(audioDecoderConfig && audioEncoderConfig); }; // src/video-decoder-config.ts var getVideoDecoderConfigWithHardwareAcceleration = async (config) => { if (typeof VideoDecoder === "undefined") { return null; } const hardware = { ...config, hardwareAcceleration: "prefer-hardware" }; if ((await VideoDecoder.isConfigSupported(hardware)).supported) { return hardware; } const software = { ...config, hardwareAcceleration: "prefer-software" }; if ((await VideoDecoder.isConfigSupported(software)).supported) { return software; } return null; }; // src/browser-quirks.ts var isFirefox = () => { return navigator.userAgent.toLowerCase().indexOf("firefox") > -1; }; var isSafari = () => { return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); }; // src/choose-correct-avc1-profile.ts var chooseCorrectAvc1Profile = ({ width, height, fps }) => { const profiles = [ { level: "3.1", hex: "1F", width: 1280, height: 720, fps: 30 }, { level: "3.2", hex: "20", width: 1280, height: 1024, fps: 42.2 }, { level: "4.0", hex: "28", width: 2048, height: 1024, fps: 30 }, { level: "4.1", hex: "29", width: 2048, height: 1024, fps: 30 }, { level: "4.2", hex: "2A", width: 2048, height: 1080, fps: 60 }, { level: "5.0", hex: "32", width: 3672, height: 1536, fps: 26.7 }, { level: "5.1", hex: "33", width: 4096, height: 2304, fps: 26.7 }, { level: "5.2", hex: "34", width: 4096, height: 2304, fps: 56.3 }, { level: "6.0", hex: "3C", width: 8192, height: 4320, fps: 30.2 }, { level: "6.1", hex: "3D", width: 8192, height: 4320, fps: 60.4 }, { level: "6.2", hex: "3E", width: 8192, height: 4320, fps: 120.8 } ]; const profile = profiles.find((p) => { if (width > p.width) { return false; } if (height > p.height) { return false; } const fallbackFps = fps ?? 60; return fallbackFps <= p.fps; }); if (!profile) { throw new Error(`No suitable AVC1 profile found for ${width}x${height}@${fps}fps`); } return `avc1.6400${profile.hex}`; }; // src/hevc-levels.ts var hevcLevels = [ { level: "3.1", maxBitrateMainTier: 1e4, maxBitrateHighTier: null, maxResolutionsAndFrameRates: [ { width: 720, height: 480, fps: 84.3 }, { width: 720, height: 576, fps: 75 }, { width: 960, height: 540, fps: 60 }, { width: 1280, height: 720, fps: 33.7 } ] }, { level: "4", maxBitrateMainTier: 12000, maxBitrateHighTier: 30000, maxResolutionsAndFrameRates: [ { width: 1280, height: 720, fps: 68 }, { width: 1920, height: 1080, fps: 32 }, { width: 2048, height: 1080, fps: 30 } ] }, { level: "4.1", maxBitrateMainTier: 20000, maxBitrateHighTier: 50000, maxResolutionsAndFrameRates: [ { width: 1280, height: 720, fps: 136 }, { width: 1920, height: 1080, fps: 64 }, { width: 2048, height: 1080, fps: 60 } ] }, { level: "5", maxBitrateMainTier: 25000, maxBitrateHighTier: 1e5, maxResolutionsAndFrameRates: [ { width: 1920, height: 1080, fps: 128 }, { width: 2048, height: 1080, fps: 120 }, { width: 3840, height: 2160, fps: 32 }, { width: 4096, height: 2160, fps: 30 } ] }, { level: "5.1", maxBitrateMainTier: 40000, maxBitrateHighTier: 160000, maxResolutionsAndFrameRates: [ { width: 1920, height: 1080, fps: 256 }, { width: 2048, height: 1080, fps: 240 }, { width: 3840, height: 2160, fps: 64 }, { width: 4096, height: 2160, fps: 60 } ] }, { level: "5.2", maxBitrateMainTier: 60000, maxBitrateHighTier: 240000, maxResolutionsAndFrameRates: [ { width: 2048, height: 1080, fps: 300 }, { width: 3840, height: 2160, fps: 128 }, { width: 4096, height: 2160, fps: 120 } ] }, { level: "6", maxBitrateMainTier: 60000, maxBitrateHighTier: 240000, maxResolutionsAndFrameRates: [ { width: 3840, height: 2160, fps: 128 }, { width: 4096, height: 2160, fps: 120 }, { width: 7680, height: 4320, fps: 32 }, { width: 8192, height: 4320, fps: 30 } ] }, { level: "6.1", maxBitrateMainTier: 120000, maxBitrateHighTier: 480000, maxResolutionsAndFrameRates: [ { width: 3840, height: 2160, fps: 256 }, { width: 4096, height: 2160, fps: 240 }, { width: 7680, height: 4320, fps: 64 }, { width: 8192, height: 4320, fps: 60 } ] }, { level: "6.2", maxBitrateMainTier: 240000, maxBitrateHighTier: 800000, maxResolutionsAndFrameRates: [ { width: 3840, height: 2160, fps: 512 }, { width: 4096, height: 2160, fps: 480 }, { width: 7680, height: 4320, fps: 128 }, { width: 8192, height: 4320, fps: 120 } ] } ]; // src/choose-correct-hevc-profile.ts var chooseCorrectHevcProfile = ({ width, height, fps }) => { const profile = hevcLevels.find((p) => { return p.maxResolutionsAndFrameRates.some((max) => { if (width > max.width) { return false; } if (height > max.height) { return false; } const fallbackFps = fps ?? 60; return fallbackFps <= max.fps; }); }); if (!profile) { throw new Error(`No suitable HEVC profile found for ${width}x${height}@${fps}fps`); } return `hvc1.${1}.${0}.${"L"}${Math.round(Number(profile.level) * 30)}.${"b0"}`; }; // src/get-codec-string.ts var getCodecStringForEncoder = ({ codec, fps, height, width }) => { if (codec === "h264") { return chooseCorrectAvc1Profile({ fps, height, width }); } if (codec === "h265") { return chooseCorrectHevcProfile({ fps, height, width }); } if (codec === "vp8") { return "vp8"; } if (codec === "vp9") { return "vp09.00.10.08"; } throw new Error(`Unknown codec: ${codec}`); }; // src/video-encoder-config.ts var getVideoEncoderConfig = async ({ codec, height, width, fps }) => { if (typeof VideoEncoder === "undefined") { return null; } const config = { codec: getCodecStringForEncoder({ codec, fps, height, width }), height, width, bitrate: isSafari() ? 3000000 : undefined, bitrateMode: codec === "vp9" && !isSafari() ? "quantizer" : undefined, framerate: fps ?? undefined }; const hardware = { ...config, hardwareAcceleration: "prefer-hardware" }; if ((await VideoEncoder.isConfigSupported(hardware)).supported) { return hardware; } const software = { ...config, hardwareAcceleration: "prefer-software" }; if ((await VideoEncoder.isConfigSupported(software)).supported) { return software; } return null; }; // src/can-reencode-video-track.ts var canReencodeVideoTrack = async ({ videoCodec, track, resizeOperation, rotate }) => { const { height, width } = calculateNewDimensionsFromRotateAndScale({ height: track.displayAspectHeight, resizeOperation, rotation: rotate ?? 0, needsToBeMultipleOfTwo: videoCodec === "h264", width: track.displayAspectWidth }); const videoEncoderConfig = await getVideoEncoderConfig({ codec: videoCodec, height, width, fps: track.fps }); const videoDecoderConfig = await getVideoDecoderConfigWithHardwareAcceleration(track); return Boolean(videoDecoderConfig && videoEncoderConfig); }; // src/convert-media.ts import { defaultSelectM3uAssociatedPlaylists, defaultSelectM3uStreamFn, MediaParserAbortError as MediaParserAbortError3, MediaParserInternals as MediaParserInternals9 } from "@remotion/media-parser"; import { webReader } from "@remotion/media-parser/web"; // src/auto-select-writer.ts var autoSelectWriter = async (writer, logLevel) => { if (writer) { Log.verbose(logLevel, "Using writer provided by user"); return writer; } Log.verbose(logLevel, "Determining best writer"); const hasNavigator = typeof navigator !== "undefined"; if (!hasNavigator) { Log.verbose(logLevel, "No navigator API detected, using buffer writer"); return bufferWriter; } const isOffline = !navigator.onLine; if (isOffline) { Log.verbose(logLevel, "Offline mode detected, using buffer writer"); return bufferWriter; } try { const { promise: timeout, reject, resolve } = withResolvers(); const time = setTimeout(() => reject(new Error("WebFS check timeout")), 2000); const webFsSupported = await Promise.race([canUseWebFsWriter(), timeout]); resolve(); clearTimeout(time); if (webFsSupported) { Log.verbose(logLevel, "Using WebFS writer because it is supported"); return webFsWriter; } } catch (err) { Log.verbose(logLevel, `WebFS check failed: ${err}. Falling back to buffer writer`); } Log.verbose(logLevel, "Using buffer writer because WebFS writer is not supported or unavailable"); return bufferWriter; }; // src/calculate-progress.ts var calculateProgress = ({ millisecondsWritten, expectedOutputDurationInMs }) => { if (expectedOutputDurationInMs === null) { return null; } return millisecondsWritten / expectedOutputDurationInMs; }; // src/create/iso-base-media/create-iso-base-media.ts import { MediaParserInternals as MediaParserInternals3 } from "@remotion/media-parser"; // src/create/matroska/matroska-utils.ts import { MediaParserInternals as MediaParserInternals2 } from "@remotion/media-parser"; var getIdForName = (name) => { const value = Object.entries(MediaParserInternals2.matroskaElements).find(([key]) => key === name)?.[1]; if (!value) { throw new Error(`Could not find id for name ${name}`); } return value; }; function putUintDynamic(number, minimumLength) { if (number < 0) { throw new Error("This function is designed for non-negative integers only."); } const length = Math.max(minimumLength ?? 1, Math.ceil(Math.log2(number + 1) / 8)); const bytes = new Uint8Array(length); for (let i = 0;i < length; i++) { bytes[length - 1 - i] = number >> 8 * i & 255; } return bytes; } var makeFromStructure = (fields) => { if ("bytes" in fields) { return fields; } const arrays = []; const struct = MediaParserInternals2.ebmlMap[getIdForName(fields.type)]; if (struct.type === "uint8array") { return { bytes: fields.value, offsets: { offset: 0, children: [], field: fields.type } }; } if (struct.type === "children") { const children = []; let bytesWritten = 0; for (const item of fields.value) { const { bytes, offsets } = makeMatroskaBytes(item); arrays.push(bytes); children.push(incrementOffsetAndChildren(offsets, bytesWritten)); bytesWritten += bytes.byteLength; } return { bytes: combineUint8Arrays(arrays), offsets: { offset: 0, children, field: fields.type } }; } if (struct.type === "string") { return { bytes: new TextEncoder().encode(fields.value), offsets: { children: [], offset: 0, field: fields.type } }; } if (struct.type === "uint") { return { bytes: putUintDynamic(fields.value.value, fields.value.byteLength), offsets: { children: [], offset: 0, field: fields.type } }; } if (struct.type === "hex-string") { const hex = fields.value.substring(2); const arr = new Uint8Array(hex.length / 2); for (let i = 0;i < hex.length; i += 2) { const byte = parseInt(hex.substring(i, i + 2), 16); arr[i / 2] = byte; } return { bytes: arr, offsets: { children: [], offset: 0, field: fields.type } }; } if (struct.type === "float") { const value = fields.value; if (value.size === "32") { const dataView = new DataView(new ArrayBuffer(4)); dataView.setFloat32(0, value.value); return { bytes: new Uint8Array(dataView.buffer), offsets: { children: [], offset: 0, field: fields.type } }; } const dataView2 = new DataView(new ArrayBuffer(8)); dataView2.setFloat64(0, value.value); return { bytes: new Uint8Array(dataView2.buffer), offsets: { children: [], offset: 0, field: fields.type } }; } throw new Error("Unexpected type"); }; var combineUint8Arrays = (arrays) => { if (arrays.length === 0) { return new Uint8Array([]); } if (arrays.length === 1) { return arrays[0]; } let totalLength = 0; for (const array of arrays) { totalLength += array.length; } const result = new Uint8Array(totalLength); let offset = 0; for (const array of arrays) { result.set(array, offset); offset += array.length; } return result; }; var incrementOffsetAndChildren = (offset, increment) => { return { offset: offset.offset + increment, children: offset.children.map((c) => incrementOffsetAndChildren(c, increment)), field: offset.field }; }; var matroskaToHex = (matrId) => { const numbers = new Uint8Array((matrId.length - 2) / 2); for (let i = 2;i < matrId.length; i += 2) { const hex = matrId.substring(i, i + 2); numbers[(i - 2) / 2] = parseInt(hex, 16); } return numbers; }; var measureEBMLVarInt = (value) => { if (value < (1 << 7) - 1) { return 1; } if (value < (1 << 14) - 1) { return 2; } if (value < (1 << 21) - 1) { return 3; } if (value < (1 << 28) - 1) { return 4; } if (value < 2 ** 35 - 1) { return 5; } if (value < 2 ** 42 - 1) { return 6; } throw new Error("EBML VINT size not supported " + value); }; var getVariableInt = (value, minWidth) => { const width = Math.max(measureEBMLVarInt(value), minWidth ?? 0); switch (width) { case 1: return new Uint8Array([1 << 7 | value]); case 2: return new Uint8Array([1 << 6 | value >> 8, value]); case 3: return new Uint8Array([1 << 5 | value >> 16, value >> 8, value]); case 4: return new Uint8Array([ 1 << 4 | value >> 24, value >> 16, value >> 8, value ]); case 5: return new Uint8Array([ 1 << 3 | value / 2 ** 32 & 7, value >> 24, value >> 16, value >> 8, value ]); case 6: return new Uint8Array([ 1 << 2 | value / 2 ** 40 & 3, value / 2 ** 32 | 0, value >> 24, value >> 16, value >> 8, value ]); case 7: return new Uint8Array([ 1 << 1 | value / 2 ** 48 & 1, value / 2 ** 40 | 0, value / 2 ** 32 | 0, value >> 24, value >> 16, value >> 8, value ]); case 8: return new Uint8Array([ 1 << 0 | value / 2 ** 56 & 1, value / 2 ** 48 | 0, value / 2 ** 40 | 0, value / 2 ** 32 | 0, value >> 24, value >> 16, value >> 8, value ]); default: throw new Error("Bad EBML VINT size " + width); } }; var makeMatroskaBytes = (fields) => { if ("bytes" in fields) { return fields; } const value = makeFromStructure(fields); const header = matroskaToHex(getIdForName(fields.type)); const size = getVariableInt(value.bytes.length, fields.minVintWidth); const bytes = combineUint8Arrays([header, size, value.bytes]); return { bytes, offsets: { offset: value.offsets.offset, field: value.offsets.field, children: value.offsets.children.map((c) => { return incrementOffsetAndChildren(c, header.byteLength + size.byteLength); }) } }; }; var padMatroskaBytes = (fields, totalLength) => { const regular = makeMatroskaBytes(fields); const paddingLength = totalLength - regular.bytes.byteLength - matroskaToHex(MediaParserInternals2.matroskaElements.Void).byteLength; if (paddingLength < 0) { throw new Error("ooops"); } const padding = makeMatroskaBytes({ type: "Void", value: new Uint8Array(paddingLength).fill(0), minVintWidth: null }); return [ regular, { bytes: padding.bytes, offsets: incrementOffsetAndChildren(padding.offsets, regular.bytes.length) } ]; }; function serializeUint16(value) { const buffer = new ArrayBuffer(2); const view = new DataView(buffer); view.setUint16(0, value); return new Uint8Array(buffer); } // src/create/iso-base-media/primitives.ts var stringsToUint8Array = (str) => { return new TextEncoder().encode(str); }; var numberTo32BitUIntOrInt = (num) => { return new Uint8Array([ num >> 24 & 255, num >> 16 & 255, num >> 8 & 255, num & 255 ]); }; var numberTo64BitUIntOrInt = (num) => { const bigNum = BigInt(num); return new Uint8Array([ Number(bigNum >> 56n & 0xffn), Number(bigNum >> 48n & 0xffn), Number(bigNum >> 40n & 0xffn), Number(bigNum >> 32n & 0xffn), Number(bigNum >> 24n & 0xffn), Number(bigNum >> 16n & 0xffn), Number(bigNum >> 8n & 0xffn), Number(bigNum & 0xffn) ]); }; var numberTo32BitUIntOrIntLeading128 = (num) => { const arr = [ num >> 24 & 255, num >> 16 & 255, num >> 8 & 255, num & 255 ]; for (const i in arr) { if (arr[i] === 0) { arr[i] = 128; } else { break; } } return new Uint8Array(arr); }; var numberTo16BitUIntOrInt = (num) => { return new Uint8Array([num >> 8 & 255, num & 255]); }; var setFixedPointSignedOrUnsigned1616Number = (num) => { const val = Math.round(num * 2 ** 16); return numberTo32BitUIntOrInt(val); }; var setFixedPointSigned230Number = (num) => { const val = Math.round(num * 2 ** 30); return numberTo32BitUIntOrInt(val); }; var addSize = (arr) => { return combineUint8Arrays([numberTo32BitUIntOrInt(arr.length + 4), arr]); }; var addLeading128Size = (arr) => { return combineUint8Arrays([ numberTo32BitUIntOrIntLeading128(arr.length), arr ]); }; var floatTo16Point1632Bit = (number) => { const fixedNumber = Number(number.toFixed(2)); const result = new Uint8Array(4); const tens = Math.floor(fixedNumber / 10); const ones = Math.floor(fixedNumber % 10); const tenths = Math.floor(fixedNumber * 10 % 10); const hundredths = Math.floor(fixedNumber * 100 % 10); result[0] = tens; result[1] = ones; result[2] = tenths; result[3] = hundredths; return result; }; var floatTo16Point16_16Bit = (number) => { const fixedNumber = Number(number.toFixed(2)); const result = new Uint8Array(2); const ones = Math.floor(fixedNumber % 10); const tenths = Math.floor(fixedNumber * 10 % 10); result[0] = ones; result[1] = tenths; return result; }; var serializeMatrix = (matrix) => { return combineUint8Arrays([ setFixedPointSignedOrUnsigned1616Number(matrix[0]), setFixedPointSignedOrUnsigned1616Number(matrix[1]), setFixedPointSigned230Number(matrix[2]), setFixedPointSignedOrUnsigned1616Number(matrix[3]), setFixedPointSignedOrUnsigned1616Number(matrix[4]), setFixedPointSigned230Number(matrix[5]), setFixedPointSignedOrUnsigned1616Number(matrix[6]), setFixedPointSignedOrUnsigned1616Number(matrix[7]), setFixedPointSigned230Number(matrix[8]) ]); }; var stringToPascalString = (str) => { const buffer = new Uint8Array(32); for (let i = 0;i < Math.min(str.length, 32); i++) { buffer[i] = str.charCodeAt(i); } return buffer; }; var padIsoBaseMediaBytes = (data, totalLength) => { if (data.length - 8 > totalLength) { throw new Error(`Data is longer than the total length: ${data.length - 8} > ${totalLength}. Set the 'expectedDurationInSeconds' value to avoid this problem: https://www.remotion.dev/docs/webcodecs/convert-media#expecteddurationinseconds`); } if (data.length - 8 === totalLength) { return data; } return combineUint8Arrays([ data, addSize(combineUint8Arrays([ stringsToUint8Array("free"), new Uint8Array(totalLength - (data.length - 8)) ])) ]); }; var IDENTITY_MATRIX = [1, 0, 0, 0, 1, 0, 0, 0, 1]; // src/create/iso-base-media/create-ftyp.ts var createFtyp = ({ majorBrand, minorBrand, compatibleBrands }) => { const type = stringsToUint8Array("ftyp"); const majorBrandArr = stringsToUint8Array(majorBrand); const minorBrandArr = numberTo32BitUIntOrInt(minorBrand); const compatibleBrandsArr = combineUint8Arrays(compatibleBrands.map((b) => stringsToUint8Array(b))); return addSize(combineUint8Arrays([ type, majorBrandArr, minorBrandArr, compatibleBrandsArr ])); }; var createIsoBaseMediaFtyp = ({ majorBrand, minorBrand, compatibleBrands }) => { return createFtyp({ compatibleBrands, majorBrand, minorBrand }); }; // src/create/iso-base-media/mp4-header.ts import { VERSION } from "@remotion/media-parser"; // src/create/iso-base-media/create-ilst.ts var createIlst = (items) => { return addSize(combineUint8Arrays([ stringsToUint8Array("ilst"), ...items ])); }; // src/create/iso-base-media/create-moov.ts var createMoov = ({ mvhd, traks, udta }) => { return addSize(combineUint8Arrays([ stringsToUint8Array("moov"), mvhd, ...traks, udta ])); }; // src/from-unix-timestamp.ts var fromUnixTimestamp = (value) => { if (value === null) { return 0; } const baseDate = new Date("1904-01-01T00:00:00Z"); return Math.floor(value / 1000 - baseDate.getTime() / 1000); }; // src/create/iso-base-media/create-mvhd.ts var createMvhd = ({ timescale, durationInUnits, rate, volume, nextTrackId, matrix, creationTime, modificationTime }) => { if (matrix.length !== 9) { throw new Error("Matrix must be 9 elements long"); } const content = combineUint8Arrays([ stringsToUint8Array("mvhd"), new Uint8Array([0]), new Uint8Array([0, 0, 0]), creationTime === null ? numberTo32BitUIntOrInt(0) : numberTo32BitUIntOrInt(fromUnixTimestamp(creationTime)), modificationTime === null ? numberTo32BitUIntOrInt(0) : numberTo32BitUIntOrInt(fromUnixTimestamp(modificationTime)), numberTo32BitUIntOrInt(timescale), numberTo32BitUIntOrInt(durationInUnits), floatTo16Point1632Bit(rate), floatTo16Point16_16Bit(volume), new Uint8Array([0, 0]), new Uint8Array([0, 0, 0, 0]), new Uint8Array([0, 0, 0, 0]), serializeMatrix(matrix), combineUint8Arrays(new Array(6).fill(new Uint8Array([0, 0, 0, 0]))), numberTo32BitUIntOrInt(nextTrackId) ]); return addSize(content); }; // src/create/iso-base-media/create-udta.ts var createUdta = (children) => { return addSize(combineUint8Arrays([ stringsToUint8Array("udta"), children ])); }; // src/create/iso-base-media/header-length.ts var calculateAReasonableMp4HeaderLength = ({ expectedDurationInSeconds, expectedFrameRate }) => { if (expectedDurationInSeconds === null) { return 2048000; } const assumedFrameRate = expectedFrameRate ?? 60; const frameRateMultiplier = assumedFrameRate / 30; const bytesPerSecond = 3.7 * 1024 * 1024 / 6000; const bytesWithSafetyMargin = bytesPerSecond * 1.2 * frameRateMultiplier; const calculatedBytes = Math.max(50 * 1024, Math.ceil(expectedDurationInSeconds * bytesWithSafetyMargin)); return calculatedBytes; }; // src/create/iso-base-media/ilst/create-cmt.ts var createCmt = (comment) => { return addSize(combineUint8Arrays([ new Uint8Array([169, 99, 109, 116]), addSize(combineUint8Arrays([ stringsToUint8Array("data"), new Uint8Array([0, 0]), new Uint8Array([0, 1]), new Uint8Array([0, 0]), new Uint8Array([0, 0]), stringsToUint8Array(comment) ])) ])); }; // src/create/iso-base-media/ilst/create-too.ts var createToo = (value) => { return addSize(combineUint8Arrays([ new Uint8Array([169, 116, 111, 111]), addSize(combineUint8Arrays([ new Uint8Array([100, 97, 116, 97]), new Uint8Array([0, 0]), new Uint8Array([0, 1]), new Uint8Array([0, 0]), new Uint8Array([0, 0]), stringsToUint8Array(value) ])) ])); }; // src/create/iso-base-media/trak/mdia/minf/stbl/stsd/create-avcc.ts var createAvccBox = (privateData) => { if (!privateData) { throw new Error("privateData is required"); } return addSize(combineUint8Arrays([ stringsToUint8Array("avcC"), privateData ])); }; // src/create/iso-base-media/trak/mdia/minf/stbl/stsd/create-hvcc.ts var createHvccBox = (privateData) => { if (!privateData) { throw new Error("privateData is required"); } return addSize(combineUint8Arrays([ stringsToUint8Array("hvcC"), privateData ])); }; // src/create/iso-base-media/trak/mdia/minf/stbl/stsd/create-pasp.ts var createPasp = (x, y) => { return addSize(combineUint8Arrays([ stringsToUint8Array("pasp"), numberTo32BitUIntOrInt(x), numberTo32BitUIntOrInt(y) ])); }; // src/create/iso-base-media/codec-specific/avc1.ts var createAvc1Data = ({ avccBox, pasp, width, height, horizontalResolution, verticalResolution, compressorName, depth }) => { return addSize(combineUint8Arrays([ stringsToUint8Array("avc1"), new Uint8Array([0, 0, 0, 0, 0, 0]), new Uint8Array([0, 1]), new Uint8Array([0, 0]), new Uint8Array([0, 0]), new Uint8Array([0, 0, 0, 0]), new Uint8Array([0, 0, 0, 0]), new Uint8Array([0, 0, 0, 0]), numberTo16BitUIntOrInt(width), numberTo16BitUIntOrInt(height), setFixedPointSignedOrUnsigned1616Number(horizontalResolution), setFixedPointSignedOrUnsigned1616Number(verticalResolution), new Uint8Array([0, 0, 0, 0]), numberTo16BitUIntOrInt(1), stringToPascalString(compressorName), numberTo16BitUIntOrInt(depth), numberTo16BitUIntOrInt(-1), avccBox, pasp ])); }; // src/create/iso-base-media/codec-specific/hvc1.ts var createHvc1Data = ({ compressorName, depth, height, horizontalResolution, hvccBox, pasp, verticalResolution, width }) => { return addSize(combineUint8Arrays([ stringsToUint8Array("hvc1"), new Uint8Array([0, 0, 0, 0, 0, 0]), new Uint8Array([0, 1]), new Uint8Array([0, 0]), new Uint8Array([0, 0]), new Uint8Array([0, 0, 0, 0]), new Uint8Array([0, 0, 0, 0]), new Uint8Array([0, 0, 0, 0]), numberTo16BitUIntOrInt(width), numberTo16BitUIntOrInt(height), setFixedPointSignedOrUnsigned1616Number(horizontalResolution), setFixedPointSignedOrUnsigned1616Number(verticalResolution), new Uint8Array([0, 0, 0, 0]), numberTo16BitUIntOrInt(1), stringToPascalString(compressorName), numberTo16BitUIntOrInt(depth), numberTo16BitUIntOrInt(-1), hvccBox, pasp ])); }; // src/create/iso-base-media/codec-specific/mp4a.ts var createMp4a = ({ sampleRate, channelCount, avgBitrate, maxBitrate, codecPrivate }) => { if (!codecPrivate) { throw new Error("Need codecPrivate for mp4a"); } const esdsAtom = addSize(combineUint8Arrays([ stringsToUint8Array("esds"), new Uint8Array([0]), new Uint8Array([0, 0, 0]), new Uint8Array([3]), addLeading128Size(combineUint8Arrays([ numberTo16BitUIntOrInt(2), new Uint8Array([0]), new Uint8Array([4]), addLeading128Size(combineUint8Arrays([ new Uint8Array([64]), new Uint8Array([21]), new Uint8Array([0, 0, 0]), numberTo32BitUIntOrInt(maxBitrate), numberTo32BitUIntOrInt(avgBitrate), new Uint8Array([5]), addLeading128Size(codecPrivate) ])), new Uint8Array([6]), addLeading128Size(new Uint8Array([2])) ])) ])); return addSize(combineUint8Arrays([ stringsToUint8Array("mp4a"), new Uint8Array([0, 0, 0, 0, 0, 0]), numberTo16BitUIntOrInt(1), numberTo16BitUIntOrInt(0), numberTo16BitUIntOrInt(0), new Uint8Array([0, 0, 0, 0]), numberTo16BitUIntOrInt(channelCount), numberTo16BitUIntOrInt(16), numberTo16BitUIntOrInt(0), numberTo16BitUIntOrInt(0), setFixedPointSignedOrUnsigned1616Number(sampleRate), esdsAtom ])); }; // src/create/iso-base-media/codec-specific/create-codec-specific-data.ts var createCodecSpecificData = (track) => { if (track.type === "video") { if (track.codec === "h264") { if (!track.codecPrivate) { return new Uint8Array([]); } return createAvc1Data({ avccBox: createAvccBox(track.codecPrivate), compressorName: "WebCodecs", depth: 24, horizontalResolution: 72, verticalResolution: 72, height: track.height, width: track.width, pasp: createPasp(1, 1), type: "avc1-data" }); } if (track.codec === "h265") { if (!track.codecPrivate) { return new Uint8Array([]); } return createHvc1Data({ hvccBox: createHvccBox(track.codecPrivate), compressorName: "WebCodecs", depth: 24, horizontalResolution: 72, verticalResolution: 72, height: track.height, width: track.width, pasp: createPasp(1, 1), type: "hvc1-data" }); } throw new Error("Unsupported codec specific data " + track.codec); } if (track.type === "audio") { return createMp4a({ type: "mp4a-data", avgBitrate: 128 * 1024, maxBitrate: 128 * 1024, channelCount: track.numberOfChannels, sampleRate: track.sampleRate, codecPrivate: track.codecPrivate }); } throw new Error("Unsupported codec specific data " + track); }; // src/create/iso-base-media/create-mdia.ts var createMdia = ({ mdhd, hdlr, minf }) => { return addSize(combineUint8Arrays([ stringsToUint8Array("mdia"), mdhd, hdlr, minf ])); }; // src/truthy.ts function truthy(value) { return Boolean(value); } // src/create/iso-base-media/create-trak.ts var createTrak = ({ tkhd, mdia }) => { return addSize(combineUint8Arrays([ stringsToUint8Array("trak"), tkhd, mdia ].filter(truthy))); }; // src/create/iso-base-media/mdia/create-mdhd.ts var createMdhd = ({ creationTime, modificationTime, timescale, duration }) => { return addSize(combineUint8Arrays([ stringsToUint8Array("mdhd"), new Uint8Array([0]), new Uint8Array([0, 0, 0]), creationTime === null ? numberTo32BitUIntOrInt(0) : numberTo32BitUIntOrInt(fromUnixTimestamp(creationTime)), modificationTime === null ? numberTo32BitUIntOrInt(0) : numberTo32BitUIntOrInt(fromUnixTimestamp(modificationTime)), numberTo32BitUIntOrInt(timescale), numberTo32BitUIntOrInt(Math.round(duration / 1000 * timescale)), new Uint8Array([85, 196]), new Uint8Array([0, 0]) ])); }; // src/create/iso-base-media/trak/create-tkhd.ts var TKHD_FLAGS = { TRACK_ENABLED: 1, TRACK_IN_MOVIE: 2, TRACK_IN_PREVIEW: 4, TRACK_IN_POSTER: 8 }; var createTkhdForAudio = ({ creationTime, modificationTime, flags, trackId, duration, volume, timescale }) => { return addSize(combineUint8Arrays([ stringsToUint8Array("tkhd"), new Uint8Array([0]), new Uint8Array([0, 0, flags]), creationTime === null ? numberTo32BitUIntOrInt(0) : numberTo32BitUIntOrInt(fromUnixTimestamp(creationTime)), modificationTime === null ? numberTo32BitUIntOrInt(0) : numberTo32BitUIntOrInt(fromUnixTimestamp(modificationTime)), numberTo32BitUIntOrInt(trackId), new Uint8Array([0, 0, 0, 0]), numberTo32BitUIntOrInt(Math.round(duration / 1000 * timescale)), new Uint8Array([0, 0, 0, 0]), new Uint8Array([0, 0, 0, 0]), new Uint8Array([0, 0]), new Uint8Array([0, 1]), floatTo16Point16_16Bit(volume), new Uint8Array([0, 0]), serializeMatrix(IDENTITY_MATRIX), setFixedPointSignedOrUnsigned1616Number(0), setFixedPointSignedOrUnsigned1616Number(0) ])); }; var createTkhdForVideo = ({ creationTime, modificationTime, duration, trackId, volume, matrix, width, height, flags, timescale }) => { const content = combineUint8Arrays([ stringsToUint8Array("tkhd"), new Uint8Array([0]), new Uint8Array([0, 0, flags]), creationTime === null ? numberTo32BitUIntOrInt(0) : numberTo32BitUIntOrInt(fromUnixTimestamp(creationTime)), modificationTime === null ? numberTo32BitUIntOrInt(0) : numberTo32BitUIntOrInt(fromUnixTimestamp(modificationTime)), numberTo32BitUIntOrInt(trackId), new Uint8Array([0, 0, 0, 0]), numberTo32BitUIntOrInt(duration / 1000 * timescale), new Uint8Array([0, 0, 0, 0]), new Uint8Array([0, 0, 0, 0]), new Uint8Array([0, 0]), new Uint8Array([0, 0]), floatTo16Point16_16Bit(volume), new Uint8Array([0, 0]), serializeMatrix(matrix), setFixedPointSignedOrUnsigned1616Number(width), setFixedPointSignedOrUnsigned1616Number(height) ]); return addSize(content); }; // src/create/iso-base-media/trak/mdia/minf/create-dinf.ts var createDinf = () => { return addSize(combineUint8Arrays([ stringsToUint8Array("dinf"), addSize(combineUint8Arrays([ stringsToUint8Array("dref"), new Uint8Array([0]), new Uint8Array([0, 0, 0]), new Uint8Array([0, 0, 0, 1]), addSize(combineUint8Arrays([ stringsToUint8Array("url "), new Uint8Array([0]), new Uint8Array([0, 0, 1]) ])) ])) ])); }; // src/create/iso-base-media/trak/mdia/create-minf.ts var createMinf = ({ vmhdAtom, stblAtom }) => { return addSize(combineUint8Arrays([ stringsToUint8Array("minf"), vmhdAtom, createDinf(), stblAtom ])); }; // src/create/iso-base-media/trak/mdia/minf/create-smhd.ts var createSmhd = () => { return addSize(combineUint8Arrays([ stringsToUint8Array("smhd"), new Uint8Array([0]), new Uint8Array([0, 0, 0]), new Uint8Array([0, 0]), new Uint8Array([0, 0]) ])); }; // src/create/iso-base-media/trak/mdia/minf/stbl/create-ctts.ts var makeEntry = (entry) => { return combineUint8Arrays([ numberTo32BitUIntOrInt(entry.sampleCount), numberTo32BitUIntOrInt(entry.sampleOffset) ]); }; var createCttsBox = (samplePositions) => { const offsets = samplePositions.map((s) => s.timestamp - s.decodingTimestamp); const entries = []; let lastOffset = null; for (const offset of offsets) { if (lastOffset === offset) { entries[entries.length - 1].sampleCount++; } else { entries.push({ sampleCount: 1, sampleOffset: offset }); } lastOffset = offset; } const needsCtts = entries.length > 0 && entries.some((e) => e.sampleOffset !== 0); if (!needsCtts) { return null; } return addSize(combineUint8Arrays([ stringsToUint8Array("ctts"), new Uint8Array([0]), new Uint8Array([0, 0, 0]), numberTo32BitUIntOrInt(entries.length), ...entries.map((e) => makeEntry(e)) ])); }; // src/create/iso-base-media/trak/mdia/minf/stbl/create-stco.ts var createStcoAtom = (samplePositions) => { const chunkOffsets = []; let lastChunk; let needs64Bit = false; for (const sample of samplePositions) { if (lastChunk !== sample.chunk) { chunkOffsets.push(sample.offset); } if (sample.offset > 2 ** 32) { needs64Bit = true; } lastChunk = sample.chunk; } return addSize(combineUint8Arrays([ stringsToUint8Array(needs64Bit ? "co64" : "stco"), new Uint8Array([0]), new Uint8Array([0, 0, 0]), numberTo32BitUIntOrInt(chunkOffsets.length), combineUint8Arrays(chunkOffsets.map((offset) => needs64Bit ? numberTo64BitUIntOrInt(offset) : numberTo32BitUIntOrInt(offset))) ])); }; // src/create/iso-base-media/trak/mdia/minf/stbl/create-stsc.ts var createEntry = (entry) => { return combineUint8Arrays([ numberTo32BitUIntOrInt(entry.firstChunk), numberTo32BitUIntOrInt(entry.samplesPerChunk), numberTo32BitUIntOrInt(entry.sampleDescriptionIndex) ]); }; var createStsc = (samplePositions) => { const entries = []; const deduplicateLastEntry = () => { const lastEntry = entries[entries.length - 1]; const secondToLastEntry = entries[entries.length - 2]; if (lastEntry && secondToLastEntry && lastEntry.samplesPerChunk === secondToLastEntry.samplesPerChunk && lastEntry.sampleDescriptionIndex === secondToLastEntry.sampleDescriptionIndex) { const lastIndex = entries.length - 1; entries.length = lastIndex; } }; let lastChunk; for (const samplePosition of samplePositions) { if (samplePosition.chunk === lastChunk) { entries[entries.length - 1].samplesPerChunk++; } else { deduplicateLastEntry(); entries.push({ firstChunk: samplePosition.chunk, samplesPerChunk: 1, sampleDescriptionIndex: 1 }); lastChunk = samplePosition.chunk; } } deduplicateLastEntry(); return addSize(combineUint8Arrays([ stringsToUint8Array("stsc"), new Uint8Array([0]), new Uint8Array([0, 0, 0]), numberTo32BitUIntOrInt(entries.length), ...entries.map((e) => createEntry(e)) ])); }; // src/create/iso-base-media/trak/mdia/minf/stbl/create-stss.ts var createStss = (samplePositions) => { const samples = samplePositions.map((sample, i) => [sample.isKeyframe, i]).filter((s) => s[0]).map((s) => s[1] + 1); return addSize(combineUint8Arrays([ stringsToUint8Array("stss"), new Uint8Array([0]), new Uint8Array([0, 0, 0]), numberTo32BitUIntOrInt(samples.length), ...samples.map((sample) => numberTo32BitUIntOrInt(sample)) ])); }; // src/create/iso-base-media/trak/mdia/minf/stbl/create-stsz.ts var createStsz = (samplePositions) => { const sampleSizes = samplePositions.map((samplePosition) => samplePosition.size); return addSize(combineUint8Arrays([ stringsToUint8Array("stsz"), new Uint8Array([0]), new Uint8Array([0, 0, 0]), numberTo32BitUIntOrInt(0), numberTo32BitUIntOrInt(sampleSizes.length), ...sampleSizes.map((size) => numberTo32BitUIntOrInt(size)) ])); }; // src/create/iso-base-media/trak/mdia/minf/stbl/create-stts.ts var makeEntry2 = (entry) => { if (entry.sampleOffset < 0) { throw new Error("negative sample offset in stts " + entry.sampleOffset); } return combineUint8Arrays([ numberTo32BitUIntOrInt(entry.sampleCount), numberTo32BitUIntOrInt(entry.sampleOffset) ]); }; var createSttsAtom = (samplePositions) => { let lastDuration = null; const durations = samplePositions.map((_, i, a) => { if (a[i].duration === undefined || a[i].duration === 0) { if (a[i + 1] === undefined) { return a[i].decodingTimestamp - (a[i - 1]?.decodingTimestamp ?? a[i].decodingTimestamp); } return a[i + 1].decodingTimestamp - a[i].decodingTimestamp; } return a[i].duration; }); const entries = []; for (const duration of durations) { if (duration === lastDuration) { entries[entries.length - 1].sampleCount++; } else { entries.push({ sampleCount: 1, sampleOffset: duration }); } lastDuration = duration; } return addSize(combineUint8Arrays([ stringsToUint8Array("stts"), new Uint8Array([0]), new Uint8Array([0, 0, 0]), numberTo32BitUIntOrInt(entries.length), ...entries.map((e) => makeEntry2(e)) ])); }; // src/create/iso-base-media/trak/mdia/minf/stbl/stsd/create-avc1.ts var createStsdData = (codecSpecificData) => { return addSize(combineUint8Arrays([ stringsToUint8Array("stsd"), new Uint8Array([0]), new Uint8Array([0, 0, 0]), new Uint8Array([0, 0, 0, 1]), codecSpecificData ])); }; // src/create/iso-base-media/trak/mdia/minf/create-stbl.ts var createStbl = ({ samplePositions, codecSpecificData, isVideo }) => { const sorted = samplePositions.slice().sort((a, b) => a.decodingTimestamp - b.decodingTimestamp); return addSize(combineUint8Arrays([ stringsToUint8Array("stbl"), createStsdData(codecSpecificData), createSttsAtom(sorted), isVideo ? createStss(samplePositions) : null, createCttsBox(samplePositions), createStsc(samplePositions), createStsz(samplePositions), createStcoAtom(samplePositions), isVideo ? null : new Uint8Array([ 0, 0, 0, 26, 115, 103, 112, 100, 1, 0, 0, 0, 114, 111, 108, 108, 0, 0, 0, 2, 0, 0, 0, 1, 255, 255, 0, 0, 0, 28, 115, 98, 103, 112, 0, 0, 0, 0, 114, 111, 108, 108, 0, 0, 0, 1, 0, 0, 10, 25, 0, 0, 0, 1 ]) ].filter(truthy))); }; // src/create/iso-base-media/trak/mdia/minf/create-vmhd.ts var createVmhd = () => { return addSize(combineUint8Arrays([ stringsToUint8Array("vmhd"), new Uint8Array([0]), new Uint8Array([0, 0, 1]), new Uint8Array([0, 0]), new Uint8Array([0, 0, 0, 0, 0, 0]) ])); }; // src/create/iso-base-media/udta/meta/create-hdlr.ts var createHdlr = (type) => { return addSize(combineUint8Arrays([ stringsToUint8Array("hdlr"), new Uint8Array([0]), new Uint8Array([0, 0, 0]), new Uint8Array([0, 0, 0, 0]), stringsToUint8Array(type === "mdir" ? "mdir" : type === "video" ? "vide" : "soun"), type === "mdir" ? numberTo32BitUIntOrInt(1634758764) : new Uint8Array([0, 0, 0, 0]), new Uint8Array([0, 0, 0, 0]), new Uint8Array([0, 0, 0, 0]), stringsToUint8Array(type === "mdir" ? "\x00" : type === "video" ? "VideoHandler\x00" : "SoundHandler\x00") ])); }; // src/create/iso-base-media/serialize-track.ts var serializeTrack = ({ track, durationInUnits, samplePositions, timescale }) => { if (track.codec !== "h264" && track.codec !== "h265" && track.codec !== "aac") { throw new Error("Currently only H.264 and AAC is supported"); } return createTrak({ tkhd: track.codec === "aac" ? createTkhdForAudio({ creationTime: Date.now(), flags: TKHD_FLAGS.TRACK_ENABLED | TKHD_FLAGS.TRACK_IN_MOVIE, modificationTime: Date.now(), duration: durationInUnits, trackId: track.trackNumber, volume: 1, timescale }) : track.type === "video" ? createTkhdForVideo({ creationTime: Date.now(), modificationTime: Date.now(), duration: durationInUnits, flags: TKHD_FLAGS.TRACK_ENABLED | TKHD_FLAGS.TRACK_IN_MOVIE, height: track.height, width: track.width, matrix: IDENTITY_MATRIX, trackId: track.trackNumber, volume: 0, timescale }) : new Uint8Array(stringsToUint8Array("wrong")), mdia: createMdia({ mdhd: createMdhd({ creationTime: null, modificationTime: null, duration: durationInUnits, timescale: track.timescale }), hdlr: track.type === "video" ? createHdlr("video") : createHdlr("audio"), minf: createMinf({ stblAtom: createStbl({ samplePositions, isVideo: track.type === "video", codecSpecificData: createCodecSpecificData(track) }), vmhdAtom: track.type === "audio" ? createSmhd() : createVmhd() }) }) }); }; // src/create/iso-base-media/udta/create-meta.ts var createMeta = ({ hdlr, ilst }) => { return addSize(combineUint8Arrays([ stringsToUint8Array("meta"), new Uint8Array([0]), new Uint8Array([0, 0, 0]), hdlr, ilst ])); }; // src/create/iso-base-media/mp4-header.ts var createPaddedMoovAtom = ({ durationInUnits, trackInfo, timescale, expectedDurationInSeconds, logLevel, expectedFrameRate }) => { const headerLength = calculateAReasonableMp4HeaderLength({ expectedDurationInSeconds, expectedFrameRate }); if (expectedDurationInSeconds !== null) { Log.verbose(logLevel, `Expecting duration of the video to be ${expectedDurationInSeconds} seconds, allocating ${headerLength} bytes for the MP4 header.`); } else { Log.verbose(logLevel, `No duration was provided, allocating ${headerLength} bytes for the MP4 header.`); } return padIsoBaseMediaBytes(createMoov({ mvhd: createMvhd({ timescale, durationInUnits, matrix: IDENTITY_MATRIX, nextTrackId: trackInfo.map((t) => t.track.trackNumber).reduce((a, b) => Math.max(a, b), 0) + 1, rate: 1, volume: 1, creationTime: Date.now(), modificationTime: Date.now() }), traks: trackInfo.map((track) => { return serializeTrack({ timescale, track: track.track, durationInUnits, samplePositions: track.samplePositions }); }), udta: createUdta(createMeta({ hdlr: createHdlr("mdir"), ilst: createIlst([ createToo("WebCodecs"), createCmt(`Made with @remotion/webcodecs ${VERSION}`) ]) })) }), headerLength); }; // src/create/iso-base-media/create-iso-base-media.ts var CONTAINER_TIMESCALE = 1000; var createIsoBaseMedia = async ({ writer, onBytesProgress, onMillisecondsProgress, logLevel, filename, progressTracker, expectedDurationInSeconds, expectedFrameRate }) => { const header = createIsoBaseMediaFtyp({ compatibleBrands: ["isom", "iso2", "avc1", "mp42"], majorBrand: "isom", minorBrand: 512 }); const w = await writer.createContent({ filename, mimeType: "video/mp4", logLevel }); await w.write(header); let globalDurationInUnits = 0; const lowestTrackTimestamps = {}; const trackDurations = {}; const currentTracks = []; const samplePositions = []; const sampleChunkIndices = []; const moovOffset = w.getWrittenByteCount(); const getPaddedMoovAtom = () => { return createPaddedMoovAtom({ durationInUnits: globalDurationInUnits, trackInfo: currentTracks.map((track) => { return { track, durationInUnits: trackDurations[track.trackNumber] ?? 0, samplePositions: samplePositions[track.trackNumber] ?? [], timescale: track.timescale }; }), timescale: CONTAINER_TIMESCALE, expectedDurationInSeconds, logLevel, expectedFrameRate }); }; await w.write(getPaddedMoovAtom()); let mdatSize = 8; const mdatSizeOffset = w.getWrittenByteCount(); await w.write(combineUint8Arrays([ numberTo32BitUIntOrInt(mdatSize), stringsToUint8Array("mdat") ])); const updateMdatSize = async () => { await w.updateDataAt(mdatSizeOffset, numberTo32BitUIntOrInt(mdatSize)); onBytesProgress(w.getWrittenByteCount()); }; const operationProm = { current: Promise.resolve() }; const updateMoov = async () => { await w.updateDataAt(moovOffset, getPaddedMoovAtom()); onBytesProgress(w.getWrittenByteCount()); }; const addCodecPrivateToTrack = ({ trackNumber, codecPrivate }) => { currentTracks.forEach((track) => { if (track.trackNumber === trackNumber) { track.codecPrivate = codecPrivate; } }); }; let lastChunkWasVideo = false; const addSample = async ({ chunk, trackNumber, isVideo, codecPrivate }) => { const position = w.getWrittenByteCount(); await w.write(chunk.data); mdatSize += chunk.data.length; onBytesProgress(w.getWrittenByteCount()); progressTracker.setPossibleLowestTimestamp(Math.min(chunk.timestamp, chunk.decodingTimestamp ?? Infinity)); progressTracker.updateTrackProgress(trackNumber, chunk.timestamp); if (codecPrivate) { addCodecPrivateToTrack({ trackNumber, codecPrivate }); } const currentTrack = currentTracks.find((t) => t.trackNumber === trackNumber); if (!currentTrack) { throw new Error(`Tried to add sample to track ${trackNumber}, but it doesn't exist`); } if (!lowestTrackTimestamps[trackNumber] || chunk.timestamp < lowestTrackTimestamps[trackNumber]) { lowestTrackTimestamps[trackNumber] = chunk.timestamp; } if (typeof lowestTrackTimestamps[trackNumber] !== "number") { throw new Error(`Tried to add sample to track ${trackNumber}, but it has no timestamp`); } const newDurationInMicroSeconds = chunk.timestamp + (chunk.duration ?? 0) - lowestTrackTimestamps[trackNumber]; const newDurationInTrackTimeUnits = Math.round(newDurationInMicroSeconds / (1e6 / currentTrack.timescale)); trackDurations[trackNumber] = newDurationInTrackTimeUnits; const newDurationInMilliseconds = Math.round(newDurationInMicroSeconds / 1e6 * CONTAINER_TIMESCALE); if (newDurationInMilliseconds > globalDurationInUnits) { globalDurationInUnits = newDurationInMilliseconds; onMillisecondsProgress(newDurationInMilliseconds); } if (!samplePositions[trackNumber]) { samplePositions[trackNumber] = []; } if (typeof sampleChunkIndices[trackNumber] === "undefined") { sampleChunkIndices[trackNumber] = 0; } if (isVideo && chunk.type === "key") { sampleChunkIndices[trackNumber]++; } else if (!isVideo && samplePositions[trackNumber].length % 22 === 0) { sampleChunkIndices[trackNumber]++; } else if (lastChunkWasVideo !== isVideo) { sampleChunkIndices[trackNumber]++; } const samplePositionToAdd = { isKeyframe: chunk.type === "key", offset: position, chunk: sampleChunkIndices[trackNumber], timestamp: Math.round(chunk.timestamp / 1e6 * currentTrack.timescale), decodingTimestamp: Math.round(chunk.decodingTimestamp / 1e6 * currentTrack.timescale), duration: Math.round((chunk.duration ?? 0) / 1e6 * currentTrack.timescale), size: chunk.data.length, bigEndian: false, chunkSize: null }; lastChunkWasVideo = isVideo; samplePositions[trackNumber].push(samplePositionToAdd); }; const addTrack = (track) => { const trackNumber = currentTracks.length + 1; currentTracks.push({ ...track, trackNumber }); progressTracker.registerTrack(trackNumber); return Promise.resolve({ trackNumber }); }; const waitForFinishPromises = []; return { getBlob: () => { return w.getBlob(); }, remove: async () => { await w.remove(); }, addSample: ({ chunk, trackNumber, isVideo, codecPrivate }) => { operationProm.current = operationProm.current.then(() => { return addSample({ chunk, trackNumber, isVideo, codecPrivate }); }); return operationProm.current; }, addTrack: (track) => { operationProm.current = operationProm.current.then(() => addTrack(track)); return operationProm.current; }, updateTrackSampleRate: ({ sampleRate, trackNumber }) => { currentTracks.forEach((track) => { if (track.trackNumber === trackNumber) { if (track.type !== "audio") { throw new Error(`Tried to update sample rate of track ${trackNumber}, but it's not an audio track`); } track.sampleRate = sampleRate; } }); }, addWaitForFinishPromise: (promise) => { waitForFinishPromises.push(promise); }, async waitForFinish() { MediaParserInternals3.Log.verbose(logLevel, "All write operations queued. Waiting for finish..."); await Promise.all(waitForFinishPromises.map((p) => p())); MediaParserInternals3.Log.verbose(logLevel, "Cleanup tasks executed"); await operationProm.current; await updateMoov(); await updateMdatSize(); MediaParserInternals3.Log.verbose(logLevel, "All write operations done. Waiting for finish..."); await w.finish(); } }; }; // src/create/matroska/create-matroska-media.ts import { MediaParserInternals as MediaParserInternals5 } from "@remotion/media-parser"; // src/create/matroska/cluster.ts import { MediaParserInternals as MediaParserInternals4 } from "@remotion/media-parser"; // src/create/matroska/cluster-segment.ts var CLUSTER_MIN_VINT_WIDTH = 8; var createClusterSegment = (timestamp) => { return makeMatroskaBytes({ type: "Cluster", value: [ { type: "Timestamp", minVintWidth: null, value: { value: timestamp, byteLength: null } } ], minVintWidth: CLUSTER_MIN_VINT_WIDTH }); }; var makeSimpleBlock = ({ bytes, trackNumber, timecodeRelativeToCluster, keyframe, invisible, lacing }) => { const simpleBlockHeader = matroskaToHex("0xa3"); const headerByte = Number(keyframe) << 7 | Number(invisible) << 3 | lacing << 1; const body = combineUint8Arrays([ getVariableInt(trackNumber, null), serializeUint16(timecodeRelativeToCluster), new Uint8Array([headerByte]), bytes ]); return combineUint8Arrays([ simpleBlockHeader, getVariableInt(body.length, null), body ]); }; // src/create/matroska/cluster.ts var maxClusterTimestamp = 2 ** 15; var timestampToClusterTimestamp = (timestamp, timescale) => { return Math.round(timestamp / timescale * 1000); }; var canFitInCluster = ({ clusterStartTimestamp, chunk, timescale }) => { const timecodeRelativeToCluster = timestampToClusterTimestamp(chunk.timestamp, timescale) - timestampToClusterTimestamp(clusterStartTimestamp, timescale); if (timecodeRelativeToCluster < 0) { throw new Error(`timecodeRelativeToCluster is negative, tried to add ${chunk.timestamp} to ${clusterStartTimestamp}`); } return timecodeRelativeToCluster <= maxClusterTimestamp; }; var makeCluster = async ({ writer, clusterStartTimestamp, timescale, logLevel }) => { Log.verbose(logLevel, `Making new Matroska cluster with timestamp ${clusterStartTimestamp}`); const cluster = createClusterSegment(timestampToClusterTimestamp(clusterStartTimestamp, timescale)); const clusterVIntPosition = writer.getWrittenByteCount() + cluster.offsets.offset + matroskaToHex(MediaParserInternals4.matroskaElements.Cluster).byteLength; let clusterSize = cluster.bytes.byteLength - matroskaToHex(MediaParserInternals4.matroskaElements.Cluster).byteLength - CLUSTER_MIN_VINT_WIDTH; await writer.write(cluster.bytes); const addSample = async (chunk, trackNumber) => { const timecodeRelativeToCluster = timestampToClusterTimestamp(chunk.timestamp, timescale) - timestampToClusterTimestamp(clusterStartTimestamp, timescale); if (!canFitInCluster({ clusterStartTimestamp, chunk, timescale })) { throw new Error(`timecodeRelativeToCluster is too big: ${timecodeRelativeToCluster} > ${maxClusterTimestamp}`); } const keyframe = chunk.type === "key"; const simpleBlock = makeSimpleBlock({ bytes: chunk.data, invisible: false, keyframe, lacing: 0, trackNumber, timecodeRelativeToCluster }); clusterSize += simpleBlock.byteLength; await writer.updateDataAt(clusterVIntPosition, getVariableInt(clusterSize, CLUSTER_MIN_VINT_WIDTH)); await writer.write(simpleBlock); return { timecodeRelativeToCluster }; }; const shouldMakeNewCluster = ({ isVideo, chunk, newT }) => { const newTimestamp = timestampToClusterTimestamp(newT, timescale); const oldTimestamp = timestampToClusterTimestamp(clusterStartTimestamp, timescale); const canFit = canFitInCluster({ chunk, clusterStartTimestamp, timescale }); if (!canFit) { Log.verbose(logLevel, `Cannot fit ${chunk.timestamp} in cluster ${clusterStartTimestamp}. Creating new cluster`); return true; } const keyframe = chunk.type === "key"; return newTimestamp - oldTimestamp >= 2000 && keyframe && isVideo; }; return { addSample, shouldMakeNewCluster, startTimestamp: clusterStartTimestamp }; }; // src/create/matroska/make-duration-with-padding.ts var makeDurationWithPadding = (newDuration) => { return makeMatroskaBytes({ type: "Duration", value: { value: newDuration, size: "64" }, minVintWidth: 8 }); }; // src/create/matroska/matroska-cues.ts var createMatroskaCues = (cues) => { if (cues.length === 0) { return null; } return makeMatroskaBytes({ type: "Cues", minVintWidth: null, value: cues.map((cue) => { return { type: "CuePoint", value: [ { type: "CueTime", minVintWidth: null, value: { value: cue.time, byteLength: null } }, { type: "CueTrackPositions", value: [ { type: "CueTrack", minVintWidth: null, value: { value: cue.trackNumber, byteLength: null } }, { type: "CueClusterPosition", minVintWidth: null, value: { value: cue.clusterPosition, byteLength: null } } ], minVintWidth: null } ], minVintWidth: null }; }) }); }; // src/create/matroska/matroska-header.ts var makeMatroskaHeader = () => { return makeMatroskaBytes({ type: "Header", value: [ { minVintWidth: null, type: "EBMLVersion", value: { value: 1, byteLength: null } }, { minVintWidth: null, type: "EBMLReadVersion", value: { value: 1, byteLength: null } }, { type: "EBMLMaxIDLength", value: { byteLength: null, value: 4 }, minVintWidth: null }, { type: "EBMLMaxSizeLength", value: { byteLength: null, value: 8 }, minVintWidth: null }, { type: "DocType", value: "webm", minVintWidth: null }, { type: "DocTypeVersion", value: { byteLength: null, value: 4 }, minVintWidth: null }, { type: "DocTypeReadVersion", value: { byteLength: null, value: 2 }, minVintWidth: null } ], minVintWidth: null }); }; // src/create/matroska/matroska-info.ts var makeMatroskaInfo = ({ timescale }) => { return makeMatroskaBytes({ type: "Info", value: [ { type: "TimestampScale", value: { value: timescale, byteLength: null }, minVintWidth: null }, { type: "MuxingApp", value: "@remotion/webcodecs", minVintWidth: null }, { type: "WritingApp", value: "@remotion/webcodecs", minVintWidth: null }, makeDurationWithPadding(0) ], minVintWidth: null }); }; // src/create/matroska/matroska-seek.ts var createMatroskaSeekHead = (seeks) => { return padMatroskaBytes(makeMatroskaBytes({ type: "SeekHead", minVintWidth: null, value: seeks.map((seek) => { return { type: "Seek", minVintWidth: null, value: [ { type: "SeekID", minVintWidth: null, value: seek.hexString }, { type: "SeekPosition", minVintWidth: null, value: { value: seek.byte, byteLength: null } } ] }; }) }), 200); }; // src/create/matroska/matroska-segment.ts var MATROSKA_SEGMENT_MIN_VINT_WIDTH = 8; var createMatroskaSegment = (children) => { return makeMatroskaBytes({ type: "Segment", value: children, minVintWidth: MATROSKA_SEGMENT_MIN_VINT_WIDTH }); }; // src/create/matroska/color.ts var getRangeValue = ({ transferCharacteristics, matrixCoefficients, fullRange }) => { return transferCharacteristics && matrixCoefficients ? 3 : fullRange === true ? 2 : fullRange === false ? 1 : 0; }; var getPrimariesValue = (primaries) => { if (primaries === null) { return null; } if (primaries === "bt709") { return 1; } if (primaries === "bt470bg") { return 5; } if (primaries === "smpte170m") { return 6; } if (primaries === "bt2020") { return 9; } if (primaries === "smpte432") { return 12; } throw new Error("Unknown primaries " + primaries); }; var getTransferCharacteristicsValue = (transferCharacteristics) => { if (transferCharacteristics === null) { return null; } if (transferCharacteristics === "bt709") { return 1; } if (transferCharacteristics === "smpte170m") { return 6; } if (transferCharacteristics === "iec61966-2-1") { return 13; } if (transferCharacteristics === "linear") { return 8; } if (transferCharacteristics === "pq") { return 16; } if (transferCharacteristics === "hlg") { return 18; } throw new Error("Unknown transfer characteristics " + transferCharacteristics); }; var getMatrixCoefficientsValue = (matrixCoefficients) => { if (matrixCoefficients === null) { return null; } if (matrixCoefficients === "rgb") { return 0; } if (matrixCoefficients === "bt709") { return 1; } if (matrixCoefficients === "bt470bg") { return 5; } if (matrixCoefficients === "smpte170m") { return 6; } if (matrixCoefficients === "bt2020-ncl") { return 9; } throw new Error("Unknown matrix coefficients " + matrixCoefficients); }; var makeMatroskaColorBytes = ({ transfer: transferCharacteristics, matrix: matrixCoefficients, primaries, fullRange }) => { const rangeValue = getRangeValue({ transferCharacteristics, matrixCoefficients, fullRange }); const primariesValue = getPrimariesValue(primaries); const transferChracteristicsValue = getTransferCharacteristicsValue(transferCharacteristics); if (matrixCoefficients === "rgb") { throw new Error("Cannot encode Matroska in RGB"); } const matrixCoefficientsValue = getMatrixCoefficientsValue(matrixCoefficients); return makeMatroskaBytes({ type: "Colour", minVintWidth: null, value: [ transferChracteristicsValue === null ? null : { type: "TransferCharacteristics", value: { value: transferChracteristicsValue, byteLength: null }, minVintWidth: null }, matrixCoefficientsValue === null ? null : { type: "MatrixCoefficients", value: { value: matrixCoefficientsValue, byteLength: null }, minVintWidth: null }, primariesValue === null ? null : { type: "Primaries", value: { value: primariesValue, byteLength: null }, minVintWidth: null }, { type: "Range", value: { value: rangeValue, byteLength: null }, minVintWidth: null } ].filter(truthy) }); }; // src/create/matroska/matroska-trackentry.ts var makeMatroskaVideoBytes = ({ color, width, height }) => { return makeMatroskaBytes({ type: "Video", value: [ { type: "PixelWidth", value: { value: width, byteLength: null }, minVintWidth: null }, { type: "PixelHeight", value: { value: height, byteLength: null }, minVintWidth: null }, { type: "FlagInterlaced", value: { value: 2, byteLength: null }, minVintWidth: null }, makeMatroskaColorBytes(color) ], minVintWidth: null }); }; var makeVideoCodecId = (codecId) => { if (codecId === "vp8") { return "V_VP8"; } if (codecId === "vp9") { return "V_VP9"; } if (codecId === "h264") { return "V_MPEG4/ISO/AVC"; } if (codecId === "av1") { return "V_AV1"; } if (codecId === "h265") { return "V_MPEGH/ISO/HEVC"; } if (codecId === "prores") { return "V_PRORES"; } throw new Error(`Unknown codec: ${codecId}`); }; var makeAudioCodecId = (codecId) => { if (codecId === "opus") { return "A_OPUS"; } if (codecId === "aac") { return "A_AAC"; } if (codecId === "ac3") { return "A_AC3"; } if (codecId === "mp3") { return "A_MPEG/L3"; } if (codecId === "vorbis") { return "A_VORBIS"; } if (codecId === "flac") { return "A_FLAC"; } if (codecId === "pcm-u8") { return "A_PCM/INT/LIT"; } if (codecId === "pcm-s16") { return "A_PCM/INT/LIT"; } if (codecId === "pcm-s24") { return "A_PCM/INT/LIT"; } if (codecId === "pcm-s32") { return "A_PCM/INT/LIT"; } if (codecId === "pcm-f32") { return "A_PCM/INT/LIT"; } if (codecId === "aiff") { throw new Error("aiff is not supported in Matroska"); } throw new Error(`Unknown codec: ${codecId}`); }; var makeMatroskaAudioTrackEntryBytes = ({ trackNumber, codec, numberOfChannels, sampleRate, codecPrivate }) => { return makeMatroskaBytes({ type: "TrackEntry", minVintWidth: null, value: [ { type: "TrackNumber", value: { value: trackNumber, byteLength: null }, minVintWidth: null }, { type: "TrackType", value: { value: 2, byteLength: null }, minVintWidth: null }, { type: "CodecID", value: makeAudioCodecId(codec), minVintWidth: null }, { type: "Audio", value: [ { type: "Channels", minVintWidth: null, value: { value: numberOfChannels, byteLength: null } }, { type: "SamplingFrequency", minVintWidth: null, value: { value: sampleRate, size: "64" } }, { type: "BitDepth", minVintWidth: null, value: { value: 32, byteLength: null } } ], minVintWidth: null }, codecPrivate ? { type: "CodecPrivate", minVintWidth: null, value: codecPrivate } : null ].filter(Boolean) }); }; var makeMatroskaVideoTrackEntryBytes = ({ color, width, height, trackNumber, codec, codecPrivate }) => { return makeMatroskaBytes({ type: "TrackEntry", minVintWidth: null, value: [ { type: "TrackNumber", value: { value: trackNumber, byteLength: null }, minVintWidth: null }, { type: "Language", value: "und", minVintWidth: null }, { type: "CodecID", value: makeVideoCodecId(codec), minVintWidth: null }, { type: "TrackType", value: { value: 1, byteLength: null }, minVintWidth: null }, makeMatroskaVideoBytes({ color, width, height }), codecPrivate ? { type: "CodecPrivate", minVintWidth: null, value: codecPrivate } : null ].filter(Boolean) }); }; var makeMatroskaTracks = (tracks) => { const bytesArr = tracks.map((t) => { const bytes = t.type === "video" ? makeMatroskaVideoTrackEntryBytes(t) : makeMatroskaAudioTrackEntryBytes(t); return bytes; }); return padMatroskaBytes(makeMatroskaBytes({ type: "Tracks", value: bytesArr, minVintWidth: null }), 500); }; // src/create/matroska/create-matroska-media.ts var { matroskaElements } = MediaParserInternals5; var timescale = 1e6; var createMatroskaMedia = async ({ writer, onBytesProgress, onMillisecondsProgress, filename, logLevel, progressTracker }) => { const header = makeMatroskaHeader(); const w = await writer.createContent({ filename, mimeType: "video/webm", logLevel }); await w.write(header.bytes); const matroskaInfo = makeMatroskaInfo({ timescale }); const currentTracks = []; const seeks = []; const cues = []; const trackNumbers = []; const matroskaSegment = createMatroskaSegment([ ...createMatroskaSeekHead(seeks), matroskaInfo, ...makeMatroskaTracks(currentTracks) ]); const infoSegment = matroskaSegment.offsets.children.find((o) => o.field === "Info"); const durationOffset = (infoSegment?.children.find((c) => c.field === "Duration")?.offset ?? 0) + w.getWrittenByteCount(); const tracksOffset = (matroskaSegment.offsets.children.find((o) => o.field === "Tracks")?.offset ?? 0) + w.getWrittenByteCount(); const seekHeadOffset = (matroskaSegment.offsets.children.find((o) => o.field === "SeekHead")?.offset ?? 0) + w.getWrittenByteCount(); const infoOffset = (infoSegment?.offset ?? 0) + w.getWrittenByteCount(); if (!seekHeadOffset) { throw new Error("could not get seek offset"); } if (!durationOffset) { throw new Error("could not get duration offset"); } if (!tracksOffset) { throw new Error("could not get tracks offset"); } if (!infoOffset) { throw new Error("could not get tracks offset"); } seeks.push({ hexString: matroskaElements.Info, byte: infoOffset - seekHeadOffset }); seeks.push({ hexString: matroskaElements.Tracks, byte: tracksOffset - seekHeadOffset }); const updateSeekWrite = async () => { const updatedSeek = createMatroskaSeekHead(seeks); await w.updateDataAt(seekHeadOffset, combineUint8Arrays(updatedSeek.map((b) => b.bytes))); onBytesProgress(w.getWrittenByteCount()); }; const segmentOffset = w.getWrittenByteCount(); const updateSegmentSize = async (size) => { const data = getVariableInt(size, MATROSKA_SEGMENT_MIN_VINT_WIDTH); await w.updateDataAt(segmentOffset + matroskaToHex(matroskaElements.Segment).byteLength, data); onBytesProgress(w.getWrittenByteCount()); }; await w.write(matroskaSegment.bytes); const clusterOffset = w.getWrittenByteCount(); let currentCluster = await makeCluster({ writer: w, clusterStartTimestamp: 0, timescale, logLevel }); seeks.push({ hexString: matroskaElements.Cluster, byte: clusterOffset - seekHeadOffset }); const getClusterOrMakeNew = async ({ chunk, isVideo }) => { progressTracker.setPossibleLowestTimestamp(Math.min(chunk.timestamp, chunk.decodingTimestamp ?? Infinity)); const smallestProgress = progressTracker.getSmallestProgress(); if (!currentCluster.shouldMakeNewCluster({ newT: smallestProgress, isVideo, chunk })) { return { cluster: currentCluster, isNew: false, smallestProgress }; } currentCluster = await makeCluster({ writer: w, clusterStartTimestamp: smallestProgress, timescale, logLevel }); return { cluster: currentCluster, isNew: true, smallestProgress }; }; const updateDuration = async (newDuration) => { const blocks = makeDurationWithPadding(newDuration); await w.updateDataAt(durationOffset, blocks.bytes); onBytesProgress(w.getWrittenByteCount()); }; const addSample = async ({ chunk, trackNumber, isVideo }) => { const offset = w.getWrittenByteCount(); const { cluster, isNew, smallestProgress } = await getClusterOrMakeNew({ chunk, isVideo }); const newDuration = Math.round((chunk.timestamp + (chunk.duration ?? 0)) / 1000); await updateDuration(newDuration); const { timecodeRelativeToCluster } = await cluster.addSample(chunk, trackNumber); if (isNew) { if (offset === null) { throw new Error("offset is null"); } cues.push({ time: timestampToClusterTimestamp(smallestProgress, timescale) + timecodeRelativeToCluster, clusterPosition: offset - seekHeadOffset, trackNumber }); } if (chunk.type === "key") { progressTracker.updateTrackProgress(trackNumber, chunk.timestamp); } onBytesProgress(w.getWrittenByteCount()); onMillisecondsProgress(newDuration); }; const addTrack = async (track) => { currentTracks.push(track); const newTracks = makeMatroskaTracks(currentTracks); progressTracker.registerTrack(track.trackNumber); await w.updateDataAt(tracksOffset, combineUint8Arrays(newTracks.map((b) => b.bytes))); }; const operationProm = { current: Promise.resolve() }; const waitForFinishPromises = []; return { updateTrackSampleRate: ({ sampleRate, trackNumber }) => { currentTracks.forEach((track) => { if (track.trackNumber === trackNumber) { if (track.type !== "audio") { throw new Error("track is not audio"); } track.sampleRate = sampleRate; } }); }, getBlob: () => { return w.getBlob(); }, remove: async () => { await w.remove(); }, addSample: ({ chunk, trackNumber, isVideo }) => { operationProm.current = operationProm.current.then(() => addSample({ chunk, trackNumber, isVideo })); return operationProm.current; }, addTrack: (track) => { const trackNumber = currentTracks.length + 1; operationProm.current = operationProm.current.then(() => addTrack({ ...track, trackNumber })); trackNumbers.push(trackNumber); return operationProm.current.then(() => ({ trackNumber })); }, addWaitForFinishPromise: (promise) => { waitForFinishPromises.push(promise); }, async waitForFinish() { await Promise.all(waitForFinishPromises.map((p) => p())); await operationProm.current; const cuesBytes = createMatroskaCues(cues); if (cuesBytes) { seeks.push({ hexString: matroskaElements.Cues, byte: w.getWrittenByteCount() - seekHeadOffset }); await w.write(cuesBytes.bytes); } await updateSeekWrite(); const segmentSize = w.getWrittenByteCount() - segmentOffset - matroskaToHex(matroskaElements.Segment).byteLength - MATROSKA_SEGMENT_MIN_VINT_WIDTH; await updateSegmentSize(segmentSize); await w.finish(); } }; }; // src/create/wav/create-wav.ts var numberTo32BiIntLittleEndian = (num) => { return new Uint8Array([ num & 255, num >> 8 & 255, num >> 16 & 255, num >> 24 & 255 ]); }; var numberTo16BitLittleEndian = (num) => { return new Uint8Array([num & 255, num >> 8 & 255]); }; var BIT_DEPTH = 16; var BYTES_PER_SAMPLE = BIT_DEPTH / 8; var createWav = async ({ filename, logLevel, onBytesProgress, onMillisecondsProgress, writer, progressTracker }) => { const w = await writer.createContent({ filename, mimeType: "audio/wav", logLevel }); await w.write(new Uint8Array([82, 73, 70, 70])); const sizePosition = w.getWrittenByteCount(); await w.write(new Uint8Array([0, 0, 0, 0])); await w.write(new Uint8Array([87, 65, 86, 69])); await w.write(new Uint8Array([102, 109, 116, 32])); await w.write(new Uint8Array([16, 0, 0, 0])); await w.write(new Uint8Array([1, 0])); const channelNumPosition = w.getWrittenByteCount(); await w.write(new Uint8Array([1, 0])); const sampleRatePosition = w.getWrittenByteCount(); await w.write(new Uint8Array([0, 0, 0, 0])); const byteRatePosition = w.getWrittenByteCount(); await w.write(new Uint8Array([0, 0, 0, 0])); const blockAlignPosition = w.getWrittenByteCount(); await w.write(new Uint8Array([0, 0])); await w.write(numberTo16BitLittleEndian(BIT_DEPTH)); await w.write(new Uint8Array([100, 97, 116, 97])); const dataSizePosition = w.getWrittenByteCount(); await w.write(new Uint8Array([0, 0, 0, 0])); const operationProm = { current: Promise.resolve() }; const updateSize = async () => { const size = w.getWrittenByteCount() - sizePosition - 4; await w.updateDataAt(sizePosition, numberTo32BiIntLittleEndian(size)); const dataSize = w.getWrittenByteCount() - dataSizePosition - 4; await w.updateDataAt(dataSizePosition, numberTo32BiIntLittleEndian(dataSize)); }; const updateChannelNum = async (numberOfChannels) => { await w.updateDataAt(channelNumPosition, new Uint8Array([numberOfChannels, 0])); }; const updateSampleRate = async (sampleRate) => { await w.updateDataAt(sampleRatePosition, numberTo32BiIntLittleEndian(sampleRate)); }; const updateByteRate = async ({ sampleRate, numberOfChannels }) => { await w.updateDataAt(byteRatePosition, numberTo32BiIntLittleEndian(sampleRate * numberOfChannels + BYTES_PER_SAMPLE)); }; const updateBlockAlign = async (numberOfChannels) => { await w.updateDataAt(blockAlignPosition, new Uint8Array(numberTo16BitLittleEndian(numberOfChannels * BYTES_PER_SAMPLE))); }; const addSample = async (chunk) => { Log.trace(logLevel, "Adding sample", chunk); await w.write(chunk.data); onMillisecondsProgress((chunk.timestamp + (chunk.duration ?? 0)) / 1000); onBytesProgress(w.getWrittenByteCount()); }; const waitForFinishPromises = []; return { getBlob: () => { return w.getBlob(); }, remove: () => { return w.remove(); }, addSample: ({ chunk, trackNumber }) => { if (trackNumber !== 1) { throw new Error("Only one track supported for WAV"); } operationProm.current = operationProm.current.then(() => addSample(chunk)); progressTracker.updateTrackProgress(trackNumber, chunk.timestamp); return operationProm.current; }, updateTrackSampleRate: () => { throw new Error("updateTrackSampleRate() not implemented for WAV encoder"); }, addWaitForFinishPromise: (promise) => { waitForFinishPromises.push(promise); }, async waitForFinish() { Log.verbose(logLevel, "All write operations queued. Waiting for finish..."); await Promise.all(waitForFinishPromises.map((p) => p())); await operationProm.current; await updateSize(); await w.finish(); }, addTrack: async (track) => { if (track.type !== "audio") { throw new Error("Only audio tracks supported for WAV"); } await updateChannelNum(track.numberOfChannels); await updateSampleRate(track.sampleRate); await updateByteRate({ sampleRate: track.sampleRate, numberOfChannels: track.numberOfChannels }); await updateBlockAlign(track.numberOfChannels); progressTracker.registerTrack(1); return Promise.resolve({ trackNumber: 1 }); } }; }; // src/create-media.ts var createMedia = (params) => { if (params.container === "mp4") { return createIsoBaseMedia(params); } if (params.container === "wav") { return createWav(params); } if (params.container === "webm") { return createMatroskaMedia(params); } throw new Error(`Unsupported container: ${params.container}`); }; // src/create/progress-tracker.ts var makeProgressTracker = () => { const trackNumberProgresses = {}; const eventEmitter = new IoEventEmitter; let startingTimestamp = null; const setPossibleLowestTimestamp = (timestamp) => { if (startingTimestamp === null) { startingTimestamp = timestamp; } else { startingTimestamp = Math.min(startingTimestamp, timestamp); } }; const getSmallestProgress = () => { const progressValues = Object.values(trackNumberProgresses).map((p) => { if (p !== null) { return p; } if (startingTimestamp === null) { throw new Error("No progress values to calculate smallest progress from"); } return startingTimestamp; }); return Math.min(...progressValues); }; return { registerTrack: (trackNumber) => { trackNumberProgresses[trackNumber] = null; }, getSmallestProgress, updateTrackProgress: (trackNumber, progress) => { if (trackNumberProgresses[trackNumber] === undefined) { throw new Error(`Tried to update progress for a track that was not registered: ${trackNumber}`); } trackNumberProgresses[trackNumber] = progress; eventEmitter.dispatchEvent("progress", { smallestProgress: getSmallestProgress() }); }, setPossibleLowestTimestamp }; }; // src/generate-output-filename.ts var generateOutputFilename = (source, container) => { const filename = typeof source === "string" ? source : source instanceof File ? source.name : "converted"; const behindSlash = filename.split("/").pop(); const withoutExtension = behindSlash.split(".").slice(0, -1).join("."); return `${withoutExtension}.${container}`; }; // src/get-available-containers.ts var availableContainers = ["webm", "mp4", "wav"]; var getAvailableContainers = () => { return availableContainers; }; // src/get-available-video-codecs.ts var availableVideoCodecs = ["vp8", "vp9", "h264", "h265"]; var getAvailableVideoCodecs = ({ container }) => { if (container === "mp4") { return ["h264", "h265"]; } if (container === "webm") { return ["vp8", "vp9"]; } if (container === "wav") { return []; } throw new Error(`Unsupported container: ${container}`); }; // src/copy-audio-track.ts var copyAudioTrack = async ({ state, track, logLevel, onMediaStateUpdate, progressTracker }) => { const addedTrack = await state.addTrack({ type: "audio", codec: track.codecEnum, numberOfChannels: track.numberOfChannels, sampleRate: track.sampleRate, codecPrivate: track.codecData?.data ?? null, timescale: track.originalTimescale }); Log.verbose(logLevel, `Copying audio track ${track.trackId} as track ${addedTrack.trackNumber}. Timescale = ${track.originalTimescale}, codec = ${track.codecEnum} (${track.codec}) `); return async (audioSample) => { progressTracker.setPossibleLowestTimestamp(Math.min(audioSample.timestamp, audioSample.decodingTimestamp ?? Infinity)); await state.addSample({ chunk: audioSample, trackNumber: addedTrack.trackNumber, isVideo: false, codecPrivate: track.codecData?.data ?? null }); onMediaStateUpdate?.((prevState) => { return { ...prevState, encodedAudioFrames: prevState.encodedAudioFrames + 1 }; }); }; }; // src/default-on-audio-track-handler.ts import { MediaParserInternals as MediaParserInternals6 } from "@remotion/media-parser"; var DEFAULT_BITRATE = 128000; var defaultOnAudioTrackHandler = async ({ track, defaultAudioCodec, logLevel, canCopyTrack }) => { const bitrate = DEFAULT_BITRATE; if (canCopyTrack) { MediaParserInternals6.Log.verbose(logLevel, `Track ${track.trackId} (audio): Can copy track, therefore copying`); return Promise.resolve({ type: "copy" }); } if (defaultAudioCodec === null) { MediaParserInternals6.Log.verbose(logLevel, `Track ${track.trackId} (audio): Container does not support audio, dropping audio`); return Promise.resolve({ type: "drop" }); } const canReencode = await canReencodeAudioTrack({ audioCodec: defaultAudioCodec, track, bitrate, sampleRate: null }); if (canReencode) { MediaParserInternals6.Log.verbose(logLevel, `Track ${track.trackId} (audio): Cannot copy, but re-encode, therefore re-encoding`); return Promise.resolve({ type: "reencode", bitrate, audioCodec: defaultAudioCodec, sampleRate: null }); } MediaParserInternals6.Log.verbose(logLevel, `Track ${track.trackId} (audio): Can neither re-encode nor copy, failing render`); return Promise.resolve({ type: "fail" }); }; // src/get-default-audio-codec.ts var getDefaultAudioCodec = ({ container }) => { if (container === "webm") { return "opus"; } if (container === "mp4") { return "aac"; } if (container === "wav") { return "wav"; } throw new Error(`Unhandled container: ${container}`); }; // src/reencode-audio-track.ts import { MediaParserInternals as MediaParserInternals7 } from "@remotion/media-parser"; // src/convert-encoded-chunk.ts var convertEncodedChunk = (chunk) => { const arr = new Uint8Array(chunk.byteLength); chunk.copyTo(arr); return { data: arr, duration: chunk.duration ?? undefined, timestamp: chunk.timestamp, type: chunk.type, decodingTimestamp: chunk.timestamp, offset: 0 }; }; // src/flush-pending.ts var makeFlushPending = () => { const { promise, resolve, reject } = withResolvers(); return { promise, resolve, reject }; }; // src/get-wave-audio-decoder.ts var getBytesPerSample = (sampleFormat) => { if (sampleFormat === "s16") { return 2; } if (sampleFormat === "s24") { return 4; } throw new Error(`Unsupported sample format: ${sampleFormat}`); }; function uint8_24le_to_uint32(u8) { if (u8.length % 3 !== 0) { throw new Error("Input length must be a multiple of 3"); } const count = u8.length / 3; const out = new Uint32Array(count); let j = 0; for (let i = 0;i < count; i++) { const b0 = u8[j++]; const b1 = u8[j++]; const b2 = u8[j++]; out[i] = (b0 | b1 << 8 | b2 << 16) << 8; } return out; } var getAudioData = (audioSample) => { if (audioSample instanceof EncodedAudioChunk) { const data = new Uint8Array(audioSample.byteLength); audioSample.copyTo(data); return data; } return audioSample.data; }; var getWaveAudioDecoder = ({ onFrame, config, sampleFormat, ioSynchronizer, onError }) => { const processSample = async (audioSample) => { const bytesPerSample = getBytesPerSample(sampleFormat); const rawData = getAudioData(audioSample); const data = sampleFormat === "s24" && rawData instanceof Uint8Array ? uint8_24le_to_uint32(rawData) : rawData; const numberOfFrames = data.byteLength / bytesPerSample / config.numberOfChannels; const audioData = new AudioData({ data, format: sampleFormat === "s16" ? "s16" : "s32", numberOfChannels: config.numberOfChannels, numberOfFrames, sampleRate: config.sampleRate, timestamp: audioSample.timestamp }); try { await onFrame(audioData); } catch (err) { audioData.close(); onError(err); } }; let lastReset = null; let mostRecentSampleInput = null; return { close() { return Promise.resolve(); }, decode(audioSample) { mostRecentSampleInput = audioSample.timestamp; return processSample(audioSample); }, flush: () => Promise.resolve(), waitForQueueToBeLessThan: ioSynchronizer.waitForQueueSize, reset: () => { lastReset = Date.now(); }, checkReset: () => ({ wasReset: () => lastReset !== null && lastReset > Date.now() }), getMostRecentSampleInput: () => mostRecentSampleInput }; }; // src/undecodable-error.ts class VideoUndecodableError extends Error { config; constructor({ message, config }) { super(message); this.name = "VideoUndecodableError"; this.config = config; if (Error.captureStackTrace) { Error.captureStackTrace(this, VideoUndecodableError); } } } class AudioUndecodableError extends Error { config; constructor({ message, config }) { super(message); this.name = "AudioUndecodableError"; this.config = config; if (Error.captureStackTrace) { Error.captureStackTrace(this, AudioUndecodableError); } } } // src/create-audio-decoder.ts var internalCreateAudioDecoder = async ({ onFrame, onError, controller, config, logLevel }) => { if (controller && controller._internals._mediaParserController._internals.signal.aborted) { throw new Error("Not creating audio decoder, already aborted"); } const ioSynchronizer = makeIoSynchronizer({ logLevel, label: "Audio decoder", controller }); let mostRecentSampleReceived = null; if (config.codec === "pcm-s16") { return getWaveAudioDecoder({ onFrame, config, sampleFormat: "s16", logLevel, ioSynchronizer, onError }); } if (config.codec === "pcm-s24") { return getWaveAudioDecoder({ onFrame, config, sampleFormat: "s24", logLevel, ioSynchronizer, onError }); } const audioDecoder = new AudioDecoder({ async output(frame) { try { await onFrame(frame); } catch (err) { frame.close(); onError(err); } ioSynchronizer.onOutput(frame.timestamp + (frame.duration ?? 0)); }, error(error) { onError(error); } }); const close = () => { if (controller) { controller._internals._mediaParserController._internals.signal.removeEventListener("abort", onAbort); } if (audioDecoder.state === "closed") { return; } audioDecoder.close(); }; const onAbort = () => { close(); }; if (controller) { controller._internals._mediaParserController._internals.signal.addEventListener("abort", onAbort); } const isConfigSupported = await AudioDecoder.isConfigSupported(config); if (!isConfigSupported) { throw new AudioUndecodableError({ message: "Audio cannot be decoded by this browser", config }); } audioDecoder.configure(config); const decode = async (audioSample) => { if (audioDecoder.state === "closed") { return; } try { await controller?._internals._mediaParserController._internals.checkForAbortAndPause(); } catch (err) { onError(err); return; } mostRecentSampleReceived = audioSample.timestamp; const chunk = audioSample instanceof EncodedAudioChunk ? audioSample : new EncodedAudioChunk(audioSample); audioDecoder.decode(chunk); if (chunk.byteLength > 16) { ioSynchronizer.inputItem(chunk.timestamp); } }; let flushPending = null; const lastReset = null; return { decode, close, flush: () => { if (flushPending) { throw new Error("Flush already pending"); } const pendingFlush = makeFlushPending(); flushPending = pendingFlush; Promise.resolve().then(() => { return audioDecoder.flush(); }).catch(() => {}).finally(() => { pendingFlush.resolve(); flushPending = null; }); return pendingFlush.promise; }, waitForQueueToBeLessThan: ioSynchronizer.waitForQueueSize, reset: () => { audioDecoder.reset(); audioDecoder.configure(config); }, checkReset: () => { const initTime = Date.now(); return { wasReset: () => lastReset !== null && lastReset > initTime }; }, getMostRecentSampleInput() { return mostRecentSampleReceived; } }; }; var createAudioDecoder = ({ track, onFrame, onError, controller, logLevel }) => { return internalCreateAudioDecoder({ onFrame, onError, controller: controller ?? null, config: track, logLevel: logLevel ?? "error" }); }; // src/processing-queue.ts function processingQueue({ onOutput, logLevel, label, onError, controller }) { const ioSynchronizer = makeIoSynchronizer({ logLevel, label, controller }); let queue = Promise.resolve(); let stopped = false; const input = (item) => { if (stopped) { return; } if (controller._internals._mediaParserController._internals.signal.aborted) { stopped = true; return; } const { timestamp } = item; ioSynchronizer.inputItem(timestamp); queue = queue.then(() => { if (stopped) { return; } if (controller._internals._mediaParserController._internals.signal.aborted) { stopped = true; return; } return onOutput(item); }).then(() => { ioSynchronizer.onOutput(timestamp); return Promise.resolve(); }).catch((err) => { stopped = true; onError(err); }); }; return { input, ioSynchronizer }; } // src/reencode-audio-track.ts var reencodeAudioTrack = async ({ audioOperation, track, logLevel, abortConversion, state, controller, onMediaStateUpdate, onAudioData, progressTracker }) => { if (audioOperation.type !== "reencode") { throw new Error(`Audio track with ID ${track.trackId} could not be resolved with a valid operation. Received ${JSON.stringify(audioOperation)}, but must be either "copy", "reencode", "drop" or "fail"`); } const audioEncoderConfig = await getAudioEncoderConfig({ numberOfChannels: track.numberOfChannels, sampleRate: audioOperation.sampleRate ?? track.sampleRate, codec: audioOperation.audioCodec, bitrate: audioOperation.bitrate }); const audioDecoderConfig = await getAudioDecoderConfig({ codec: track.codec, numberOfChannels: track.numberOfChannels, sampleRate: track.sampleRate, description: track.description }); Log.verbose(logLevel, "Audio encoder config", audioEncoderConfig); Log.verbose(logLevel, "Audio decoder config", audioDecoderConfig ?? track); if (!audioEncoderConfig) { abortConversion(new Error(`Could not configure audio encoder of track ${track.trackId}`)); return null; } if (!audioDecoderConfig) { abortConversion(new Error(`Could not configure audio decoder of track ${track.trackId}`)); return null; } const codecPrivate = audioOperation.audioCodec === "aac" ? MediaParserInternals7.createAacCodecPrivate({ audioObjectType: 2, sampleRate: audioOperation.sampleRate ?? audioEncoderConfig.sampleRate, channelConfiguration: audioEncoderConfig.numberOfChannels, codecPrivate: null }) : null; const { trackNumber } = await state.addTrack({ type: "audio", codec: audioOperation.audioCodec === "wav" ? "pcm-s16" : audioOperation.audioCodec, numberOfChannels: audioEncoderConfig.numberOfChannels, sampleRate: audioOperation.sampleRate ?? audioEncoderConfig.sampleRate, codecPrivate, timescale: track.originalTimescale }); const audioEncoder = createAudioEncoder({ onNewAudioSampleRate: (sampleRate) => { state.updateTrackSampleRate({ sampleRate, trackNumber }); }, onChunk: async (chunk) => { await state.addSample({ chunk: convertEncodedChunk(chunk), trackNumber, isVideo: false, codecPrivate }); onMediaStateUpdate?.((prevState) => { return { ...prevState, encodedAudioFrames: prevState.encodedAudioFrames + 1 }; }); }, onError: (err) => { abortConversion(new Error(`Audio encoder of track ${track.trackId} failed (see .cause of this error)`, { cause: err })); }, codec: audioOperation.audioCodec, controller, config: audioEncoderConfig, logLevel }); const audioProcessingQueue = processingQueue({ controller, label: "AudioData processing queue", logLevel, onError(error) { abortConversion(new Error(`Audio decoder of track ${track.trackId} failed. Config: ${JSON.stringify(audioDecoderConfig)} (see .cause of this error)`, { cause: error })); }, onOutput: async (audioData) => { const newAudioData = onAudioData ? await onAudioData?.({ audioData, track }) : audioData; if (newAudioData !== audioData) { if (newAudioData.duration !== audioData.duration) { throw new Error(`onAudioData returned a different duration than the input audio data. Original duration: ${audioData.duration}, new duration: ${newAudioData.duration}`); } if (newAudioData.numberOfChannels !== audioData.numberOfChannels) { throw new Error(`onAudioData returned a different number of channels than the input audio data. Original channels: ${audioData.numberOfChannels}, new channels: ${newAudioData.numberOfChannels}`); } if (newAudioData.sampleRate !== audioData.sampleRate) { throw new Error(`onAudioData returned a different sample rate than the input audio data. Original sample rate: ${audioData.sampleRate}, new sample rate: ${newAudioData.sampleRate}`); } if (newAudioData.format !== audioData.format) { throw new Error(`onAudioData returned a different format than the input audio data. Original format: ${audioData.format}, new format: ${newAudioData.format}`); } if (newAudioData.timestamp !== audioData.timestamp) { throw new Error(`onAudioData returned a different timestamp than the input audio data. Original timestamp: ${audioData.timestamp}, new timestamp: ${newAudioData.timestamp}`); } audioData.close(); } await controller._internals._mediaParserController._internals.checkForAbortAndPause(); await audioEncoder.ioSynchronizer.waitForQueueSize(10); await controller._internals._mediaParserController._internals.checkForAbortAndPause(); audioEncoder.encode(newAudioData); onMediaStateUpdate?.((prevState) => { return { ...prevState, decodedAudioFrames: prevState.decodedAudioFrames + 1 }; }); newAudioData.close(); } }); const audioDecoder = await internalCreateAudioDecoder({ onFrame: async (audioData) => { await controller._internals._mediaParserController._internals.checkForAbortAndPause(); await audioProcessingQueue.ioSynchronizer.waitForQueueSize(10); audioProcessingQueue.input(audioData); }, onError(error) { abortConversion(new Error(`Audio decoder of track ${track.trackId} failed. Config: ${JSON.stringify(audioDecoderConfig)} (see .cause of this error)`, { cause: error })); }, controller, config: audioDecoderConfig, logLevel }); state.addWaitForFinishPromise(async () => { Log.verbose(logLevel, "Waiting for audio decoder to finish"); await audioDecoder.flush(); Log.verbose(logLevel, "Audio decoder finished"); audioDecoder.close(); await audioProcessingQueue.ioSynchronizer.waitForQueueSize(0); Log.verbose(logLevel, "Audio processing queue finished"); await audioEncoder.waitForFinish(); Log.verbose(logLevel, "Audio encoder finished"); audioEncoder.close(); }); return async (audioSample) => { progressTracker.setPossibleLowestTimestamp(Math.min(audioSample.timestamp, audioSample.decodingTimestamp ?? Infinity)); await controller._internals._mediaParserController._internals.checkForAbortAndPause(); await audioDecoder.waitForQueueToBeLessThan(10); audioDecoder.decode(audioSample); }; }; // src/on-audio-track.ts var makeAudioTrackHandler = ({ state, defaultAudioCodec: audioCodec, controller, abortConversion, onMediaStateUpdate, onAudioTrack, logLevel, outputContainer, onAudioData, progressTracker }) => async ({ track, container: inputContainer }) => { const canCopyTrack = canCopyAudioTrack({ inputCodec: track.codecEnum, outputContainer, inputContainer, outputAudioCodec: audioCodec }); const audioOperation = await (onAudioTrack ?? defaultOnAudioTrackHandler)({ defaultAudioCodec: audioCodec ?? getDefaultAudioCodec({ container: outputContainer }), track, logLevel, outputContainer, inputContainer, canCopyTrack }); if (audioOperation.type === "drop") { return null; } if (audioOperation.type === "fail") { throw new Error(`Audio track with ID ${track.trackId} resolved with {"type": "fail"}. This could mean that this audio track could neither be copied to the output container or re-encoded. You have the option to drop the track instead of failing it: https://remotion.dev/docs/webcodecs/track-transformation`); } if (audioOperation.type === "copy") { return copyAudioTrack({ logLevel, onMediaStateUpdate, state, track, progressTracker }); } return reencodeAudioTrack({ abortConversion, controller, logLevel, onMediaStateUpdate, audioOperation, onAudioData, state, track, progressTracker }); }; // src/copy-video-track.ts var copyVideoTrack = async ({ logLevel, state, track, onMediaStateUpdate, progressTracker }) => { Log.verbose(logLevel, `Copying video track with codec ${track.codec} and timescale ${track.originalTimescale}`); const videoTrack = await state.addTrack({ type: "video", color: track.advancedColor, width: track.codedWidth, height: track.codedHeight, codec: track.codecEnum, codecPrivate: track.codecData?.data ?? null, timescale: track.originalTimescale }); return async (sample) => { progressTracker.setPossibleLowestTimestamp(Math.min(sample.timestamp, sample.decodingTimestamp ?? Infinity)); await state.addSample({ chunk: sample, trackNumber: videoTrack.trackNumber, isVideo: true, codecPrivate: track.codecData?.data ?? null }); onMediaStateUpdate?.((prevState) => { return { ...prevState, decodedVideoFrames: prevState.decodedVideoFrames + 1 }; }); }; }; // src/default-on-video-track-handler.ts import { MediaParserInternals as MediaParserInternals8 } from "@remotion/media-parser"; var defaultOnVideoTrackHandler = async ({ track, defaultVideoCodec, logLevel, rotate, canCopyTrack, resizeOperation }) => { if (canCopyTrack) { MediaParserInternals8.Log.verbose(logLevel, `Track ${track.trackId} (video): Can copy, therefore copying`); return Promise.resolve({ type: "copy" }); } if (defaultVideoCodec === null) { MediaParserInternals8.Log.verbose(logLevel, `Track ${track.trackId} (video): Is audio container, therefore dropping video`); return Promise.resolve({ type: "drop" }); } const canReencode = await canReencodeVideoTrack({ videoCodec: defaultVideoCodec, track, resizeOperation, rotate }); if (canReencode) { MediaParserInternals8.Log.verbose(logLevel, `Track ${track.trackId} (video): Cannot copy, but re-enconde, therefore re-encoding`); return Promise.resolve({ type: "reencode", videoCodec: defaultVideoCodec, rotate: undefined, resize: resizeOperation }); } MediaParserInternals8.Log.verbose(logLevel, `Track ${track.trackId} (video): Can neither copy nor re-encode, therefore failing`); return Promise.resolve({ type: "fail" }); }; // src/get-default-video-codec.ts var getDefaultVideoCodec = ({ container }) => { if (container === "webm") { return "vp8"; } if (container === "mp4") { return "h264"; } if (container === "wav") { return null; } throw new Error(`Unhandled container: ${container}`); }; // src/arraybuffer-to-uint8-array.ts var arrayBufferToUint8Array = (buffer) => { return buffer ? new Uint8Array(buffer) : null; }; // src/create-video-decoder.ts var internalCreateVideoDecoder = async ({ onFrame, onError, controller, config, logLevel }) => { if (controller && controller._internals._mediaParserController._internals.signal.aborted) { throw new Error("Not creating audio decoder, already aborted"); } const ioSynchronizer = makeIoSynchronizer({ logLevel, label: "Video decoder", controller }); let mostRecentSampleReceived = null; const videoDecoder = new VideoDecoder({ async output(frame) { try { await onFrame(frame); } catch (err) { onError(err); frame.close(); } ioSynchronizer.onOutput(frame.timestamp); }, error(error) { onError(error); } }); const close = () => { if (controller) { controller._internals._mediaParserController._internals.signal.removeEventListener("abort", onAbort); } if (videoDecoder.state === "closed") { return; } videoDecoder.close(); }; const onAbort = () => { close(); }; if (controller) { controller._internals._mediaParserController._internals.signal.addEventListener("abort", onAbort); } const isConfigSupported = await VideoDecoder.isConfigSupported(config); if (!isConfigSupported) { throw new VideoUndecodableError({ message: "Video cannot be decoded by this browser", config }); } videoDecoder.configure(config); const decode = async (sample) => { if (videoDecoder.state === "closed") { return; } try { await controller?._internals._mediaParserController._internals.checkForAbortAndPause(); } catch (err) { onError(err); return; } mostRecentSampleReceived = sample.timestamp; const encodedChunk = sample instanceof EncodedVideoChunk ? sample : new EncodedVideoChunk(sample); videoDecoder.decode(encodedChunk); ioSynchronizer.inputItem(sample.timestamp); }; let flushPending = null; let lastReset = null; return { decode, close, flush: () => { if (flushPending) { throw new Error("Flush already pending"); } const pendingFlush = makeFlushPending(); flushPending = pendingFlush; Promise.resolve().then(() => { return videoDecoder.flush(); }).catch(() => {}).finally(() => { pendingFlush.resolve(); flushPending = null; }); return pendingFlush.promise; }, waitForQueueToBeLessThan: ioSynchronizer.waitForQueueSize, reset: () => { lastReset = Date.now(); flushPending?.resolve(); ioSynchronizer.clearQueue(); videoDecoder.reset(); videoDecoder.configure(config); }, checkReset: () => { const initTime = Date.now(); return { wasReset: () => lastReset !== null && lastReset > initTime }; }, getMostRecentSampleInput() { return mostRecentSampleReceived; } }; }; var createVideoDecoder = ({ onFrame, onError, controller, track, logLevel }) => { return internalCreateVideoDecoder({ onFrame, onError, controller: controller ?? null, config: track, logLevel: logLevel ?? "info" }); }; // src/convert-to-correct-videoframe.ts var needsToCorrectVideoFrame = ({ videoFrame, outputCodec }) => { if (videoFrame.format === null) { return true; } if (videoFrame.format === "I420P10") { return true; } return isFirefox() && videoFrame.format === "BGRX" && outputCodec === "h264"; }; var convertToCorrectVideoFrame = ({ videoFrame, outputCodec }) => { if (!needsToCorrectVideoFrame({ videoFrame, outputCodec })) { return videoFrame; } const canvas = new OffscreenCanvas(videoFrame.displayWidth, videoFrame.displayHeight); canvas.width = videoFrame.displayWidth; canvas.height = videoFrame.displayHeight; const ctx = canvas.getContext("2d"); if (!ctx) { throw new Error("Could not get 2d context"); } ctx.drawImage(videoFrame, 0, 0); return new VideoFrame(canvas, { displayHeight: videoFrame.displayHeight, displayWidth: videoFrame.displayWidth, duration: videoFrame.duration, timestamp: videoFrame.timestamp }); }; // src/on-frame.ts var processFrame = async ({ frame: unrotatedFrame, onVideoFrame, track, outputCodec, rotation, resizeOperation }) => { const rotated = rotateAndResizeVideoFrame({ rotation, frame: unrotatedFrame, resizeOperation, needsToBeMultipleOfTwo: outputCodec === "h264" }); if (unrotatedFrame !== rotated) { unrotatedFrame.close(); } const userProcessedFrame = onVideoFrame ? await onVideoFrame({ frame: rotated, track }) : rotated; if (userProcessedFrame.displayWidth !== rotated.displayWidth) { throw new Error(`Returned VideoFrame of track ${track.trackId} has different displayWidth (${userProcessedFrame.displayWidth}) than the input frame (${rotated.displayWidth})`); } if (userProcessedFrame.displayHeight !== rotated.displayHeight) { throw new Error(`Returned VideoFrame of track ${track.trackId} has different displayHeight (${userProcessedFrame.displayHeight}) than the input frame (${rotated.displayHeight})`); } if (userProcessedFrame.timestamp !== rotated.timestamp && !isSafari()) { throw new Error(`Returned VideoFrame of track ${track.trackId} has different timestamp (${userProcessedFrame.timestamp}) than the input frame (${rotated.timestamp}). When calling new VideoFrame(), pass {timestamp: frame.timestamp} as second argument`); } if ((userProcessedFrame.duration ?? 0) !== (rotated.duration ?? 0)) { throw new Error(`Returned VideoFrame of track ${track.trackId} has different duration (${userProcessedFrame.duration}) than the input frame (${rotated.duration}). When calling new VideoFrame(), pass {duration: frame.duration} as second argument`); } if (rotated !== userProcessedFrame) { rotated.close(); } const fixedFrame = convertToCorrectVideoFrame({ videoFrame: userProcessedFrame, outputCodec }); if (fixedFrame !== userProcessedFrame) { userProcessedFrame.close(); } return fixedFrame; }; // src/sort-video-frames.ts var MAX_QUEUE_SIZE = 5; var videoFrameSorter = ({ controller, onOutput }) => { const frames = []; const releaseFrame = async () => { await controller._internals._mediaParserController._internals.checkForAbortAndPause(); const frame = frames.shift(); if (frame) { await onOutput(frame); } }; const sortFrames = () => { frames.sort((a, b) => a.timestamp - b.timestamp); }; const releaseIfQueueFull = async () => { if (frames.length >= MAX_QUEUE_SIZE) { sortFrames(); await releaseFrame(); } }; const addFrame = (frame) => { frames.push(frame); }; const inputFrame = async (frame) => { addFrame(frame); await releaseIfQueueFull(); }; const onAbort = () => { while (frames.length > 0) { const frame = frames.shift(); if (frame) { frame.close(); } } frames.length = 0; }; const flush = async () => { sortFrames(); while (frames.length > 0) { await releaseFrame(); } controller._internals._mediaParserController._internals.signal.removeEventListener("abort", onAbort); }; controller._internals._mediaParserController._internals.signal.addEventListener("abort", onAbort); let promise = Promise.resolve(); return { inputFrame: (frame) => { promise = promise.then(() => inputFrame(frame)); }, waitUntilProcessed: () => promise, flush }; }; // src/video-encoder.ts import { MediaParserAbortError as MediaParserAbortError2 } from "@remotion/media-parser"; var createVideoEncoder = ({ onChunk, onError, controller, config, logLevel, outputCodec, keyframeInterval }) => { if (controller._internals._mediaParserController._internals.signal.aborted) { throw new MediaParserAbortError2("Not creating video encoder, already aborted"); } const ioSynchronizer = makeIoSynchronizer({ logLevel, label: "Video encoder", controller }); const encoder = new VideoEncoder({ error(error) { onError(error); }, async output(chunk, metadata) { const timestamp = chunk.timestamp + (chunk.duration ?? 0); try { await onChunk(chunk, metadata ?? null); } catch (err) { onError(err); } ioSynchronizer.onOutput(timestamp); } }); const close = () => { controller._internals._mediaParserController._internals.signal.removeEventListener("abort", onAbort); if (encoder.state === "closed") { return; } encoder.close(); }; const onAbort = () => { close(); }; controller._internals._mediaParserController._internals.signal.addEventListener("abort", onAbort); Log.verbose(logLevel, "Configuring video encoder", config); encoder.configure(config); let framesProcessed = 0; const encodeFrame = (frame) => { if (encoder.state === "closed") { return; } const keyFrame = framesProcessed % keyframeInterval === 0; encoder.encode(convertToCorrectVideoFrame({ videoFrame: frame, outputCodec }), { keyFrame, vp9: { quantizer: 36 } }); ioSynchronizer.inputItem(frame.timestamp); framesProcessed++; }; return { encode: (frame) => { encodeFrame(frame); }, waitForFinish: async () => { await encoder.flush(); await ioSynchronizer.waitForQueueSize(0); }, close, flush: async () => { await encoder.flush(); }, ioSynchronizer }; }; // src/reencode-video-track.ts var reencodeVideoTrack = async ({ videoOperation, rotate, track, logLevel, abortConversion, onMediaStateUpdate, controller, onVideoFrame, state, progressTracker }) => { if (videoOperation.type !== "reencode") { throw new Error(`Video track with ID ${track.trackId} could not be resolved with a valid operation. Received ${JSON.stringify(videoOperation)}, but must be either "copy", "reencode", "drop" or "fail"`); } const rotation = videoOperation.rotate ?? rotate; const { height: newHeight, width: newWidth } = calculateNewDimensionsFromRotateAndScale({ width: track.codedWidth, height: track.codedHeight, rotation, needsToBeMultipleOfTwo: videoOperation.videoCodec === "h264", resizeOperation: videoOperation.resize ?? null }); const videoEncoderConfig = await getVideoEncoderConfig({ codec: videoOperation.videoCodec, height: newHeight, width: newWidth, fps: track.fps }); const videoDecoderConfig = await getVideoDecoderConfigWithHardwareAcceleration(track); Log.verbose(logLevel, "Video encoder config", videoEncoderConfig); Log.verbose(logLevel, "Video decoder config", videoDecoderConfig ?? track); if (videoEncoderConfig === null) { abortConversion(new Error(`Could not configure video encoder of track ${track.trackId}`)); return null; } if (videoDecoderConfig === null) { abortConversion(new Error(`Could not configure video decoder of track ${track.trackId}`)); return null; } const { trackNumber } = await state.addTrack({ type: "video", color: track.advancedColor, width: newWidth, height: newHeight, codec: videoOperation.videoCodec, codecPrivate: null, timescale: track.originalTimescale }); Log.verbose(logLevel, `Created new video track with ID ${trackNumber}, codec ${videoOperation.videoCodec} and timescale ${track.originalTimescale}`); const videoEncoder = createVideoEncoder({ onChunk: async (chunk, metadata) => { await state.addSample({ chunk: convertEncodedChunk(chunk), trackNumber, isVideo: true, codecPrivate: arrayBufferToUint8Array(metadata?.decoderConfig?.description ?? null) }); onMediaStateUpdate?.((prevState) => { return { ...prevState, encodedVideoFrames: prevState.encodedVideoFrames + 1 }; }); }, onError: (err) => { abortConversion(new Error(`Video encoder of track ${track.trackId} failed (see .cause of this error)`, { cause: err })); }, controller, config: videoEncoderConfig, logLevel, outputCodec: videoOperation.videoCodec, keyframeInterval: 40 }); const videoProcessingQueue = processingQueue({ controller, label: "VideoFrame processing queue", logLevel, onError: (err) => { abortConversion(new Error(`VideoFrame processing queue of track ${track.trackId} failed (see .cause of this error)`, { cause: err })); }, onOutput: async (frame) => { await controller._internals._mediaParserController._internals.checkForAbortAndPause(); const processedFrame = await processFrame({ frame, track, onVideoFrame, outputCodec: videoOperation.videoCodec, rotation, resizeOperation: videoOperation.resize ?? null }); await controller._internals._mediaParserController._internals.checkForAbortAndPause(); await videoEncoder.ioSynchronizer.waitForQueueSize(10); await controller._internals._mediaParserController._internals.checkForAbortAndPause(); videoEncoder.encode(processedFrame); processedFrame.close(); } }); const frameSorter = videoFrameSorter({ controller, onOutput: async (frame) => { await controller._internals._mediaParserController._internals.checkForAbortAndPause(); await videoProcessingQueue.ioSynchronizer.waitForQueueSize(10); videoProcessingQueue.input(frame); } }); const videoDecoder = await createVideoDecoder({ track: videoDecoderConfig, onFrame: async (frame) => { await frameSorter.waitUntilProcessed(); frameSorter.inputFrame(frame); }, onError: (err) => { abortConversion(new Error(`Video decoder of track ${track.trackId} failed (see .cause of this error)`, { cause: err })); }, controller, logLevel }); state.addWaitForFinishPromise(async () => { Log.verbose(logLevel, "Waiting for video decoder to finish"); await videoDecoder.flush(); videoDecoder.close(); Log.verbose(logLevel, "Video decoder finished. Waiting for encoder to finish"); await frameSorter.flush(); Log.verbose(logLevel, "Frame sorter flushed"); await videoProcessingQueue.ioSynchronizer.waitForQueueSize(0); Log.verbose(logLevel, "Video processing queue finished"); await videoEncoder.waitForFinish(); videoEncoder.close(); Log.verbose(logLevel, "Video encoder finished"); }); return async (chunk) => { progressTracker.setPossibleLowestTimestamp(Math.min(chunk.timestamp, chunk.decodingTimestamp ?? Infinity)); await controller._internals._mediaParserController._internals.checkForAbortAndPause(); await videoDecoder.waitForQueueToBeLessThan(15); if (chunk.type === "key") { await videoDecoder.flush(); } videoDecoder.decode(chunk); }; }; // src/on-video-track.ts var makeVideoTrackHandler = ({ state, onVideoFrame, onMediaStateUpdate, abortConversion, controller, defaultVideoCodec, onVideoTrack, logLevel, outputContainer, rotate, resizeOperation, progressTracker }) => async ({ track, container: inputContainer }) => { if (controller._internals._mediaParserController._internals.signal.aborted) { throw new Error("Aborted"); } const canCopyTrack = canCopyVideoTrack({ inputContainer, outputContainer, rotationToApply: rotate, inputTrack: track, resizeOperation, outputVideoCodec: defaultVideoCodec }); const videoOperation = await (onVideoTrack ?? defaultOnVideoTrackHandler)({ track, defaultVideoCodec: defaultVideoCodec ?? getDefaultVideoCodec({ container: outputContainer }), logLevel, outputContainer, rotate, inputContainer, canCopyTrack, resizeOperation }); if (videoOperation.type === "drop") { return null; } if (videoOperation.type === "fail") { throw new Error(`Video track with ID ${track.trackId} resolved with {"type": "fail"}. This could mean that this video track could neither be copied to the output container or re-encoded. You have the option to drop the track instead of failing it: https://remotion.dev/docs/webcodecs/track-transformation`); } if (videoOperation.type === "copy") { return copyVideoTrack({ logLevel, onMediaStateUpdate, state, track, progressTracker }); } return reencodeVideoTrack({ videoOperation, abortConversion, controller, logLevel, rotate, track, onVideoFrame, state, onMediaStateUpdate, progressTracker }); }; // src/throttled-state-update.ts var throttledStateUpdate = ({ updateFn, everyMilliseconds, signal }) => { let currentState = { decodedAudioFrames: 0, decodedVideoFrames: 0, encodedVideoFrames: 0, encodedAudioFrames: 0, bytesWritten: 0, millisecondsWritten: 0, expectedOutputDurationInMs: null, overallProgress: 0 }; if (!updateFn) { return { get: () => currentState, update: null, stopAndGetLastProgress: () => {} }; } let lastUpdated = null; const callUpdateIfChanged = () => { if (currentState === lastUpdated) { return; } updateFn(currentState); lastUpdated = currentState; }; const interval = setInterval(() => { callUpdateIfChanged(); }, everyMilliseconds); const onAbort = () => { clearInterval(interval); }; signal?.addEventListener("abort", onAbort, { once: true }); return { get: () => currentState, update: (fn) => { currentState = fn(currentState); }, stopAndGetLastProgress: () => { clearInterval(interval); signal?.removeEventListener("abort", onAbort); return currentState; } }; }; // src/webcodecs-controller.ts import { mediaParserController } from "@remotion/media-parser"; var webcodecsController = () => { const controller = mediaParserController(); return { abort: controller.abort, pause: controller.pause, resume: controller.resume, addEventListener: controller.addEventListener, removeEventListener: controller.removeEventListener, _internals: { _mediaParserController: controller } }; }; // src/convert-media.ts var convertMedia = async function({ src, onVideoFrame, onAudioData, onProgress: onProgressDoNotCallDirectly, audioCodec, container, videoCodec, controller = webcodecsController(), onAudioTrack: userAudioResolver, onVideoTrack: userVideoResolver, reader, fields, logLevel = "info", writer, progressIntervalInMs, rotate, resize, onAudioCodec, onContainer, onDimensions, onDurationInSeconds, onFps, onImages, onInternalStats, onIsHdr, onKeyframes, onLocation, onMetadata, onMimeType, onName, onNumberOfAudioChannels, onRotation, onSampleRate, onSize, onSlowAudioBitrate, onSlowDurationInSeconds, onSlowFps, onSlowKeyframes, onSlowNumberOfFrames, onSlowVideoBitrate, onSlowStructure, onTracks, onUnrotatedDimensions, onVideoCodec, onM3uStreams, selectM3uStream, selectM3uAssociatedPlaylists, expectedDurationInSeconds, expectedFrameRate, seekingHints, ...more }) { if (controller._internals._mediaParserController._internals.signal.aborted) { return Promise.reject(new MediaParserAbortError3("Aborted")); } if (availableContainers.indexOf(container) === -1) { return Promise.reject(new TypeError(`Only the following values for "container" are supported currently: ${JSON.stringify(availableContainers)}`)); } if (videoCodec && availableVideoCodecs.indexOf(videoCodec) === -1) { return Promise.reject(new TypeError(`Only the following values for "videoCodec" are supported currently: ${JSON.stringify(availableVideoCodecs)}`)); } const { resolve, reject, getPromiseToImmediatelyReturn } = withResolversAndWaitForReturn(); const abortConversion = (errCause) => { reject(errCause); if (!controller._internals._mediaParserController._internals.signal.aborted) { controller.abort(); } }; const onUserAbort = () => { abortConversion(new MediaParserAbortError3("Conversion aborted by user")); }; controller._internals._mediaParserController._internals.signal.addEventListener("abort", onUserAbort); const throttledState = throttledStateUpdate({ updateFn: onProgressDoNotCallDirectly ?? null, everyMilliseconds: progressIntervalInMs ?? 100, signal: controller._internals._mediaParserController._internals.signal }); const progressTracker = makeProgressTracker(); const state = await createMedia({ container, filename: generateOutputFilename(src, container), writer: await autoSelectWriter(writer, logLevel), onBytesProgress: (bytesWritten) => { throttledState.update?.((prevState) => { return { ...prevState, bytesWritten }; }); }, onMillisecondsProgress: (millisecondsWritten) => { throttledState.update?.((prevState) => { if (millisecondsWritten > prevState.millisecondsWritten) { return { ...prevState, millisecondsWritten, overallProgress: calculateProgress({ millisecondsWritten: prevState.millisecondsWritten, expectedOutputDurationInMs: prevState.expectedOutputDurationInMs }) }; } return prevState; }); }, logLevel, progressTracker, expectedDurationInSeconds: expectedDurationInSeconds ?? null, expectedFrameRate: expectedFrameRate ?? null }); const onVideoTrack = makeVideoTrackHandler({ progressTracker, state, onVideoFrame: onVideoFrame ?? null, onMediaStateUpdate: throttledState.update ?? null, abortConversion, controller, defaultVideoCodec: videoCodec ?? null, onVideoTrack: userVideoResolver ?? null, logLevel, outputContainer: container, rotate: rotate ?? 0, resizeOperation: resize ?? null }); const onAudioTrack = makeAudioTrackHandler({ progressTracker, abortConversion, defaultAudioCodec: audioCodec ?? null, controller, onMediaStateUpdate: throttledState.update ?? null, state, onAudioTrack: userAudioResolver ?? null, logLevel, outputContainer: container, onAudioData: onAudioData ?? null }); MediaParserInternals9.internalParseMedia({ logLevel, src, onVideoTrack, onAudioTrack, controller: controller._internals._mediaParserController, fields: { ...fields, durationInSeconds: true }, reader: reader ?? webReader, ...more, onDurationInSeconds: (durationInSeconds) => { if (durationInSeconds === null) { return null; } const casted = more; if (casted.onDurationInSeconds) { casted.onDurationInSeconds(durationInSeconds); } const expectedOutputDurationInMs = durationInSeconds * 1000; throttledState.update?.((prevState) => { return { ...prevState, expectedOutputDurationInMs, overallProgress: calculateProgress({ millisecondsWritten: prevState.millisecondsWritten, expectedOutputDurationInMs }) }; }); }, acknowledgeRemotionLicense: true, mode: "query", onDiscardedData: null, onError: () => ({ action: "fail" }), onParseProgress: null, progressIntervalInMs: null, onAudioCodec: onAudioCodec ?? null, onContainer: onContainer ?? null, onDimensions: onDimensions ?? null, onFps: onFps ?? null, onImages: onImages ?? null, onInternalStats: onInternalStats ?? null, onIsHdr: onIsHdr ?? null, onKeyframes: onKeyframes ?? null, onLocation: onLocation ?? null, onMetadata: onMetadata ?? null, onMimeType: onMimeType ?? null, onName: onName ?? null, onNumberOfAudioChannels: onNumberOfAudioChannels ?? null, onRotation: onRotation ?? null, onSampleRate: onSampleRate ?? null, onSize: onSize ?? null, onSlowAudioBitrate: onSlowAudioBitrate ?? null, onSlowDurationInSeconds: onSlowDurationInSeconds ?? null, onSlowFps: onSlowFps ?? null, onSlowKeyframes: onSlowKeyframes ?? null, onSlowNumberOfFrames: onSlowNumberOfFrames ?? null, onSlowVideoBitrate: onSlowVideoBitrate ?? null, onSlowStructure: onSlowStructure ?? null, onTracks: onTracks ?? null, onUnrotatedDimensions: onUnrotatedDimensions ?? null, onVideoCodec: onVideoCodec ?? null, apiName: "convertMedia()", onM3uStreams: onM3uStreams ?? null, selectM3uStream: selectM3uStream ?? defaultSelectM3uStreamFn, selectM3uAssociatedPlaylists: selectM3uAssociatedPlaylists ?? defaultSelectM3uAssociatedPlaylists, makeSamplesStartAtZero: false, m3uPlaylistContext: null, seekingHints: seekingHints ?? null }).then(() => { return state.waitForFinish(); }).then(() => { resolve({ save: state.getBlob, remove: state.remove, finalState: throttledState.get() }); }).then(() => {}).catch((err) => { reject(err); }).finally(() => { throttledState.stopAndGetLastProgress(); }); return getPromiseToImmediatelyReturn().finally(() => { controller._internals._mediaParserController._internals.signal.removeEventListener("abort", onUserAbort); }); }; // src/extract-frames.ts import { parseMedia } from "@remotion/media-parser"; // src/internal-extract-frames.ts import { MediaParserAbortError as MediaParserAbortError4, WEBCODECS_TIMESCALE, hasBeenAborted, mediaParserController as mediaParserController2 } from "@remotion/media-parser"; var internalExtractFrames = ({ src, onFrame, signal, timestampsInSeconds, acknowledgeRemotionLicense, logLevel, parseMediaImplementation }) => { const controller = mediaParserController2(); const expectedFrames = []; const resolvers = withResolvers(); const abortListener = () => { controller.abort(); resolvers.reject(new MediaParserAbortError4("Aborted by user")); }; signal?.addEventListener("abort", abortListener, { once: true }); let dur = null; let lastFrame; let lastFrameEmitted; parseMediaImplementation({ src: new URL(src, window.location.href), acknowledgeRemotionLicense, controller, logLevel, onDurationInSeconds(durationInSeconds) { dur = durationInSeconds; }, onVideoTrack: async ({ track, container }) => { const timestampTargetsUnsorted = typeof timestampsInSeconds === "function" ? await timestampsInSeconds({ track, container, durationInSeconds: dur }) : timestampsInSeconds; const timestampTargets = timestampTargetsUnsorted.sort((a, b) => a - b); if (timestampTargets.length === 0) { throw new Error("expected at least one timestamp to extract but found zero"); } controller.seek(timestampTargets[0]); const decoder = await createVideoDecoder({ onFrame: (frame) => { Log.trace(logLevel, "Received frame with timestamp", frame.timestamp); if (expectedFrames.length === 0) { frame.close(); return; } if (frame.timestamp < expectedFrames[0] - 1) { if (lastFrame) { lastFrame.close(); } lastFrame = frame; return; } if (expectedFrames[0] + 6667 < frame.timestamp && lastFrame && lastFrame !== lastFrameEmitted) { onFrame(lastFrame); lastFrameEmitted = lastFrame; expectedFrames.shift(); lastFrame = frame; return; } expectedFrames.shift(); onFrame(frame); if (lastFrame && lastFrame !== lastFrameEmitted) { lastFrame.close(); } lastFrameEmitted = frame; lastFrame = frame; }, onError: (e) => { controller.abort(); try { decoder.close(); } catch {} resolvers.reject(e); }, track }); const queued = []; const doProcess = async () => { expectedFrames.push(timestampTargets.shift() * WEBCODECS_TIMESCALE); while (queued.length > 0) { const sam = queued.shift(); if (!sam) { throw new Error("Sample is undefined"); } await decoder.waitForQueueToBeLessThan(20); Log.trace(logLevel, "Decoding sample", sam.timestamp); await decoder.decode(sam); } }; return async (sample) => { const nextTimestampWeWant = timestampTargets[0]; Log.trace(logLevel, `Received ${sample.type} sample with dts`, sample.decodingTimestamp, "and cts", sample.timestamp); if (sample.type === "key") { await decoder.flush(); queued.length = 0; } queued.push(sample); if (sample.decodingTimestamp >= timestampTargets[timestampTargets.length - 1] * WEBCODECS_TIMESCALE) { await doProcess(); await decoder.flush(); controller.abort(); return; } if (nextTimestampWeWant === undefined) { throw new Error("this should not happen"); } if (sample.decodingTimestamp >= nextTimestampWeWant * WEBCODECS_TIMESCALE) { await doProcess(); if (timestampTargets.length === 0) { await decoder.flush(); controller.abort(); } } return async () => { await doProcess(); await decoder.flush(); if (lastFrame && lastFrameEmitted !== lastFrame) { lastFrame.close(); } }; }; } }).then(() => { resolvers.resolve(); }).catch((e) => { if (!hasBeenAborted(e)) { resolvers.reject(e); } else { resolvers.resolve(); } }).finally(() => { if (lastFrame && lastFrameEmitted !== lastFrame) { lastFrame.close(); } signal?.removeEventListener("abort", abortListener); }); return resolvers.promise; }; // src/extract-frames.ts var extractFrames = (options) => { return internalExtractFrames({ ...options, signal: options.signal ?? null, acknowledgeRemotionLicense: options.acknowledgeRemotionLicense ?? false, logLevel: options.logLevel ?? "info", parseMediaImplementation: parseMedia }); }; // src/get-available-audio-codecs.ts var getAvailableAudioCodecs = ({ container }) => { if (container === "mp4") { return ["aac"]; } if (container === "webm") { return ["opus"]; } if (container === "wav") { return ["wav"]; } throw new Error(`Unsupported container: ${container}`); }; // src/get-partial-audio-data.ts import { hasBeenAborted as hasBeenAborted2, mediaParserController as mediaParserController3, parseMedia as parseMedia2 } from "@remotion/media-parser"; var extractOverlappingAudioSamples = ({ sample, fromSeconds, toSeconds, channelIndex, timescale: timescale2 }) => { const chunkStartInSeconds = sample.timestamp / timescale2; const chunkDuration = sample.numberOfFrames / sample.sampleRate; const chunkEndInSeconds = chunkStartInSeconds + chunkDuration; const overlapStartSecond = Math.max(chunkStartInSeconds, fromSeconds); const overlapEndSecond = Math.min(chunkEndInSeconds, toSeconds); if (overlapStartSecond >= overlapEndSecond) { return null; } const { numberOfChannels } = sample; const samplesPerChannel = sample.numberOfFrames; let data; if (numberOfChannels === 1) { data = new Float32Array(samplesPerChannel); sample.copyTo(data, { format: "f32", planeIndex: 0 }); } else { const interleaved = new Float32Array(samplesPerChannel * numberOfChannels); sample.copyTo(interleaved, { format: "f32", planeIndex: 0 }); data = new Float32Array(samplesPerChannel); for (let i = 0;i < samplesPerChannel; i++) { data[i] = interleaved[i * numberOfChannels + channelIndex]; } } const startSampleInChunk = Math.floor((overlapStartSecond - chunkStartInSeconds) * sample.sampleRate); const endSampleInChunk = Math.ceil((overlapEndSecond - chunkStartInSeconds) * sample.sampleRate); return data.slice(startSampleInChunk, endSampleInChunk); }; var BUFFER_IN_SECONDS = 0.1; var getPartialAudioData = async ({ src, fromSeconds, toSeconds, channelIndex, signal }) => { const controller = mediaParserController3(); const audioSamples = []; if (signal.aborted) { throw new Error("Operation was aborted"); } const { resolve: resolveAudioDecode, promise: audioDecodePromise } = Promise.withResolvers(); const onAbort = () => { controller.abort(); resolveAudioDecode(); }; signal.addEventListener("abort", onAbort, { once: true }); try { const seekFromSeconds = Math.max(0, fromSeconds - BUFFER_IN_SECONDS); if (seekFromSeconds > 0) { controller.seek(seekFromSeconds); } await parseMedia2({ acknowledgeRemotionLicense: true, src, controller, onAudioTrack: async ({ track }) => { if (signal.aborted) { return null; } const audioDecoder = await createAudioDecoder({ track, onFrame: (sample) => { if (signal.aborted) { sample.close(); return; } const trimmedData = extractOverlappingAudioSamples({ sample, fromSeconds, toSeconds, channelIndex, timescale: track.timescale }); if (trimmedData) { audioSamples.push(trimmedData); } sample.close(); }, onError(error) { resolveAudioDecode(); throw error; } }); return async (sample) => { if (signal.aborted) { audioDecoder.close(); controller.abort(); return; } if (!audioDecoder) { throw new Error("No audio decoder found"); } const fromSecondsWithBuffer = Math.max(0, fromSeconds - BUFFER_IN_SECONDS); const toSecondsWithBuffer = toSeconds + BUFFER_IN_SECONDS; const time = sample.timestamp / track.timescale; if (time < fromSecondsWithBuffer) { return; } if (time >= toSecondsWithBuffer) { audioDecoder.flush().then(() => { audioDecoder.close(); resolveAudioDecode(); }); controller.abort(); return; } await audioDecoder.waitForQueueToBeLessThan(10); audioDecoder.decode(sample); return () => { audioDecoder.flush().then(() => { audioDecoder.close(); resolveAudioDecode(); }); }; }; } }); } catch (err) { const isAbortedByTimeCutoff = hasBeenAborted2(err); if (!isAbortedByTimeCutoff && !signal.aborted) { throw err; } } finally { signal.removeEventListener("abort", onAbort); } await audioDecodePromise; const totalSamples = audioSamples.reduce((sum, sample) => sum + sample.length, 0); const result = new Float32Array(totalSamples); let offset = 0; for (const audioSample of audioSamples) { result.set(audioSample, offset); offset += audioSample.length; } return result; }; // src/index.ts var WebCodecsInternals = { rotateAndResizeVideoFrame, normalizeVideoRotation }; export { webcodecsController, rotateAndResizeVideoFrame, getPartialAudioData, getDefaultVideoCodec, getDefaultAudioCodec, getAvailableVideoCodecs, getAvailableContainers, getAvailableAudioCodecs, extractFrames, defaultOnVideoTrackHandler, defaultOnAudioTrackHandler, createVideoEncoder, createVideoDecoder, createAudioEncoder, createAudioDecoder, convertMedia, convertAudioData, canReencodeVideoTrack, canReencodeAudioTrack, canCopyVideoTrack, canCopyAudioTrack, WebCodecsInternals, VideoUndecodableError, AudioUndecodableError };