Files
story-studio/remotion/node_modules/@remotion/webcodecs/dist/esm/index.mjs
2026-02-21 10:33:18 +01:00

5878 lines
162 KiB
JavaScript

// 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
};