init commit

This commit is contained in:
Carlos
2026-02-21 10:33:18 +01:00
parent c863a943ed
commit 9d955bf338
9512 changed files with 2015317 additions and 1305 deletions

View File

@@ -0,0 +1,10 @@
export declare const CLUSTER_MIN_VINT_WIDTH = 8;
export declare const createClusterSegment: (timestamp: number) => import("./matroska-utils").BytesAndOffset;
export declare const makeSimpleBlock: ({ bytes, trackNumber, timecodeRelativeToCluster, keyframe, invisible, lacing, }: {
bytes: Uint8Array;
trackNumber: number;
timecodeRelativeToCluster: number;
keyframe: boolean;
invisible: boolean;
lacing: number;
}) => Uint8Array<ArrayBufferLike>;

View File

@@ -0,0 +1,38 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeSimpleBlock = exports.createClusterSegment = exports.CLUSTER_MIN_VINT_WIDTH = void 0;
const matroska_utils_1 = require("./matroska-utils");
exports.CLUSTER_MIN_VINT_WIDTH = 8;
const createClusterSegment = (timestamp) => {
return (0, matroska_utils_1.makeMatroskaBytes)({
type: 'Cluster',
value: [
{
type: 'Timestamp',
minVintWidth: null,
value: {
value: timestamp,
byteLength: null,
},
},
],
minVintWidth: exports.CLUSTER_MIN_VINT_WIDTH,
});
};
exports.createClusterSegment = createClusterSegment;
const makeSimpleBlock = ({ bytes, trackNumber, timecodeRelativeToCluster, keyframe, invisible, lacing, }) => {
const simpleBlockHeader = (0, matroska_utils_1.matroskaToHex)('0xa3');
const headerByte = (Number(keyframe) << 7) | (Number(invisible) << 3) | (lacing << 1);
const body = (0, matroska_utils_1.combineUint8Arrays)([
(0, matroska_utils_1.getVariableInt)(trackNumber, null),
(0, matroska_utils_1.serializeUint16)(timecodeRelativeToCluster),
new Uint8Array([headerByte]),
bytes,
]);
return (0, matroska_utils_1.combineUint8Arrays)([
simpleBlockHeader,
(0, matroska_utils_1.getVariableInt)(body.length, null),
body,
]);
};
exports.makeSimpleBlock = makeSimpleBlock;

View File

@@ -0,0 +1,24 @@
import type { MediaParserInternalTypes, MediaParserLogLevel } from '@remotion/media-parser';
import { type MediaParserAudioSample, type MediaParserVideoSample } from '@remotion/media-parser';
export declare const timestampToClusterTimestamp: (timestamp: number, timescale: number) => number;
export declare const canFitInCluster: ({ clusterStartTimestamp, chunk, timescale, }: {
clusterStartTimestamp: number;
chunk: MediaParserAudioSample | MediaParserVideoSample;
timescale: number;
}) => boolean;
export declare const makeCluster: ({ writer, clusterStartTimestamp, timescale, logLevel, }: {
writer: MediaParserInternalTypes["Writer"];
clusterStartTimestamp: number;
timescale: number;
logLevel: MediaParserLogLevel;
}) => Promise<{
addSample: (chunk: MediaParserAudioSample | MediaParserVideoSample, trackNumber: number) => Promise<{
timecodeRelativeToCluster: number;
}>;
shouldMakeNewCluster: ({ isVideo, chunk, newT, }: {
newT: number;
chunk: MediaParserAudioSample | MediaParserVideoSample;
isVideo: boolean;
}) => boolean;
startTimestamp: number;
}>;

View File

@@ -0,0 +1,76 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeCluster = exports.canFitInCluster = exports.timestampToClusterTimestamp = void 0;
const media_parser_1 = require("@remotion/media-parser");
const log_1 = require("../../log");
const cluster_segment_1 = require("./cluster-segment");
const matroska_utils_1 = require("./matroska-utils");
const maxClusterTimestamp = 2 ** 15;
const timestampToClusterTimestamp = (timestamp, timescale) => {
return Math.round((timestamp / timescale) * 1000);
};
exports.timestampToClusterTimestamp = timestampToClusterTimestamp;
const canFitInCluster = ({ clusterStartTimestamp, chunk, timescale, }) => {
const timecodeRelativeToCluster = (0, exports.timestampToClusterTimestamp)(chunk.timestamp, timescale) -
(0, exports.timestampToClusterTimestamp)(clusterStartTimestamp, timescale);
if (timecodeRelativeToCluster < 0) {
throw new Error(`timecodeRelativeToCluster is negative, tried to add ${chunk.timestamp} to ${clusterStartTimestamp}`);
}
return timecodeRelativeToCluster <= maxClusterTimestamp;
};
exports.canFitInCluster = canFitInCluster;
const makeCluster = async ({ writer, clusterStartTimestamp, timescale, logLevel, }) => {
log_1.Log.verbose(logLevel, `Making new Matroska cluster with timestamp ${clusterStartTimestamp}`);
const cluster = (0, cluster_segment_1.createClusterSegment)((0, exports.timestampToClusterTimestamp)(clusterStartTimestamp, timescale));
const clusterVIntPosition = writer.getWrittenByteCount() +
cluster.offsets.offset +
(0, matroska_utils_1.matroskaToHex)(media_parser_1.MediaParserInternals.matroskaElements.Cluster).byteLength;
let clusterSize = cluster.bytes.byteLength -
(0, matroska_utils_1.matroskaToHex)(media_parser_1.MediaParserInternals.matroskaElements.Cluster).byteLength -
cluster_segment_1.CLUSTER_MIN_VINT_WIDTH;
await writer.write(cluster.bytes);
const addSample = async (chunk, trackNumber) => {
const timecodeRelativeToCluster = (0, exports.timestampToClusterTimestamp)(chunk.timestamp, timescale) -
(0, exports.timestampToClusterTimestamp)(clusterStartTimestamp, timescale);
if (!(0, exports.canFitInCluster)({ clusterStartTimestamp, chunk, timescale })) {
throw new Error(`timecodeRelativeToCluster is too big: ${timecodeRelativeToCluster} > ${maxClusterTimestamp}`);
}
const keyframe = chunk.type === 'key';
const simpleBlock = (0, cluster_segment_1.makeSimpleBlock)({
bytes: chunk.data,
invisible: false,
keyframe,
lacing: 0,
trackNumber,
timecodeRelativeToCluster,
});
clusterSize += simpleBlock.byteLength;
await writer.updateDataAt(clusterVIntPosition, (0, matroska_utils_1.getVariableInt)(clusterSize, cluster_segment_1.CLUSTER_MIN_VINT_WIDTH));
await writer.write(simpleBlock);
return { timecodeRelativeToCluster };
};
const shouldMakeNewCluster = ({ isVideo, chunk, newT, }) => {
const newTimestamp = (0, exports.timestampToClusterTimestamp)(newT, timescale);
const oldTimestamp = (0, exports.timestampToClusterTimestamp)(clusterStartTimestamp, timescale);
const canFit = (0, exports.canFitInCluster)({
chunk,
clusterStartTimestamp,
timescale,
});
if (!canFit) {
// We must create a new cluster
// This is for example if we have an audio-only file
log_1.Log.verbose(logLevel, `Cannot fit ${chunk.timestamp} in cluster ${clusterStartTimestamp}. Creating new cluster`);
return true;
}
const keyframe = chunk.type === 'key';
// TODO: Timestamp falls apart when video only
return newTimestamp - oldTimestamp >= 2000 && keyframe && isVideo;
};
return {
addSample,
shouldMakeNewCluster,
startTimestamp: clusterStartTimestamp,
};
};
exports.makeCluster = makeCluster;

View File

@@ -0,0 +1,2 @@
import type { MediaParserAdvancedColor } from '@remotion/media-parser';
export declare const makeMatroskaColorBytes: ({ transfer: transferCharacteristics, matrix: matrixCoefficients, primaries, fullRange, }: MediaParserAdvancedColor) => import("./matroska-utils").BytesAndOffset;

View File

@@ -0,0 +1,142 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeMatroskaColorBytes = void 0;
const truthy_1 = require("../../truthy");
const matroska_utils_1 = require("./matroska-utils");
const getRangeValue = ({ transferCharacteristics, matrixCoefficients, fullRange, }) => {
return transferCharacteristics && matrixCoefficients
? 3
: fullRange === true
? 2
: fullRange === false
? 1
: 0;
};
// https://w3c.github.io/webcodecs/#videocolorprimaries
const 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);
};
const 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);
};
const 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);
};
const makeMatroskaColorBytes = ({ transfer: transferCharacteristics, matrix: matrixCoefficients, primaries, fullRange, }) => {
const rangeValue = getRangeValue({
transferCharacteristics,
matrixCoefficients,
fullRange,
});
// https://datatracker.ietf.org/doc/draft-ietf-cellar-matroska/
// 5.1.4.1.28.27
const primariesValue = getPrimariesValue(primaries);
const transferChracteristicsValue = getTransferCharacteristicsValue(transferCharacteristics);
if (matrixCoefficients === 'rgb') {
throw new Error('Cannot encode Matroska in RGB');
}
const matrixCoefficientsValue = getMatrixCoefficientsValue(matrixCoefficients);
return (0, matroska_utils_1.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_1.truthy),
});
};
exports.makeMatroskaColorBytes = makeMatroskaColorBytes;

View File

@@ -0,0 +1,2 @@
import type { MediaFn, MediaFnGeneratorInput } from '../media-fn';
export declare const createMatroskaMedia: ({ writer, onBytesProgress, onMillisecondsProgress, filename, logLevel, progressTracker, }: MediaFnGeneratorInput) => Promise<MediaFn>;

View File

@@ -0,0 +1,206 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createMatroskaMedia = void 0;
const media_parser_1 = require("@remotion/media-parser");
const cluster_1 = require("./cluster");
const make_duration_with_padding_1 = require("./make-duration-with-padding");
const matroska_cues_1 = require("./matroska-cues");
const matroska_header_1 = require("./matroska-header");
const matroska_info_1 = require("./matroska-info");
const matroska_seek_1 = require("./matroska-seek");
const matroska_segment_1 = require("./matroska-segment");
const matroska_trackentry_1 = require("./matroska-trackentry");
const matroska_utils_1 = require("./matroska-utils");
const { matroskaElements } = media_parser_1.MediaParserInternals;
const timescale = 1000000;
const createMatroskaMedia = async ({ writer, onBytesProgress, onMillisecondsProgress, filename, logLevel, progressTracker, }) => {
const header = (0, matroska_header_1.makeMatroskaHeader)();
const w = await writer.createContent({
filename,
mimeType: 'video/webm',
logLevel,
});
await w.write(header.bytes);
const matroskaInfo = (0, matroska_info_1.makeMatroskaInfo)({
timescale,
});
const currentTracks = [];
const seeks = [];
const cues = [];
const trackNumbers = [];
const matroskaSegment = (0, matroska_segment_1.createMatroskaSegment)([
...(0, matroska_seek_1.createMatroskaSeekHead)(seeks),
matroskaInfo,
...(0, matroska_trackentry_1.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 = (0, matroska_seek_1.createMatroskaSeekHead)(seeks);
await w.updateDataAt(seekHeadOffset, (0, matroska_utils_1.combineUint8Arrays)(updatedSeek.map((b) => b.bytes)));
onBytesProgress(w.getWrittenByteCount());
};
const segmentOffset = w.getWrittenByteCount();
const updateSegmentSize = async (size) => {
const data = (0, matroska_utils_1.getVariableInt)(size, matroska_segment_1.MATROSKA_SEGMENT_MIN_VINT_WIDTH);
await w.updateDataAt(segmentOffset + (0, matroska_utils_1.matroskaToHex)(matroskaElements.Segment).byteLength, data);
onBytesProgress(w.getWrittenByteCount());
};
await w.write(matroskaSegment.bytes);
const clusterOffset = w.getWrittenByteCount();
let currentCluster = await (0, cluster_1.makeCluster)({
writer: w,
clusterStartTimestamp: 0,
timescale,
logLevel,
});
seeks.push({
hexString: matroskaElements.Cluster,
byte: clusterOffset - seekHeadOffset,
});
const getClusterOrMakeNew = async ({ chunk, isVideo, }) => {
// In Safari, samples can arrive out of order, e.g public/bigbuckbunny.mp4
// Therefore, only updating track number progress if it is a keyframe
// to allow for timestamps to be lower than the previous one
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 (0, cluster_1.makeCluster)({
writer: w,
clusterStartTimestamp: smallestProgress,
timescale,
logLevel,
});
return {
cluster: currentCluster,
isNew: true,
smallestProgress,
};
};
const updateDuration = async (newDuration) => {
const blocks = (0, make_duration_with_padding_1.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: (0, cluster_1.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 = (0, matroska_trackentry_1.makeMatroskaTracks)(currentTracks);
progressTracker.registerTrack(track.trackNumber);
await w.updateDataAt(tracksOffset, (0, matroska_utils_1.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 = (0, matroska_cues_1.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 -
(0, matroska_utils_1.matroskaToHex)(matroskaElements.Segment).byteLength -
matroska_segment_1.MATROSKA_SEGMENT_MIN_VINT_WIDTH;
await updateSegmentSize(segmentSize);
await w.finish();
},
};
};
exports.createMatroskaMedia = createMatroskaMedia;

View File

@@ -0,0 +1 @@
export declare const makeDurationWithPadding: (newDuration: number) => import("./matroska-utils").BytesAndOffset;

View File

@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeDurationWithPadding = void 0;
const matroska_utils_1 = require("./matroska-utils");
const makeDurationWithPadding = (newDuration) => {
return (0, matroska_utils_1.makeMatroskaBytes)({
type: 'Duration',
value: {
value: newDuration,
size: '64',
},
minVintWidth: 8,
});
};
exports.makeDurationWithPadding = makeDurationWithPadding;

View File

@@ -0,0 +1,7 @@
import type { BytesAndOffset } from './matroska-utils';
export type Cue = {
time: number;
clusterPosition: number;
trackNumber: number;
};
export declare const createMatroskaCues: (cues: Cue[]) => BytesAndOffset | null;

View File

@@ -0,0 +1,52 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createMatroskaCues = void 0;
const matroska_utils_1 = require("./matroska-utils");
const createMatroskaCues = (cues) => {
if (cues.length === 0) {
return null;
}
return (0, matroska_utils_1.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,
};
}),
});
};
exports.createMatroskaCues = createMatroskaCues;

View File

@@ -0,0 +1 @@
export declare const makeMatroskaHeader: () => import("./matroska-utils").BytesAndOffset;

View File

@@ -0,0 +1,66 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeMatroskaHeader = void 0;
const matroska_utils_1 = require("./matroska-utils");
const makeMatroskaHeader = () => {
return (0, matroska_utils_1.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,
});
};
exports.makeMatroskaHeader = makeMatroskaHeader;

View File

@@ -0,0 +1,3 @@
export declare const makeMatroskaInfo: ({ timescale }: {
timescale: number;
}) => import("./matroska-utils").BytesAndOffset;

View File

@@ -0,0 +1,33 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeMatroskaInfo = void 0;
const make_duration_with_padding_1 = require("./make-duration-with-padding");
const matroska_utils_1 = require("./matroska-utils");
const makeMatroskaInfo = ({ timescale }) => {
return (0, matroska_utils_1.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,
},
(0, make_duration_with_padding_1.makeDurationWithPadding)(0),
],
minVintWidth: null,
});
};
exports.makeMatroskaInfo = makeMatroskaInfo;

View File

@@ -0,0 +1,6 @@
import type { MediaParserInternalTypes } from '@remotion/media-parser';
export type Seek = {
hexString: MediaParserInternalTypes['MatroskaElement'];
byte: number;
};
export declare const createMatroskaSeekHead: (seeks: Seek[]) => import("./matroska-utils").BytesAndOffset[];

View File

@@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createMatroskaSeekHead = void 0;
const matroska_utils_1 = require("./matroska-utils");
const createMatroskaSeekHead = (seeks) => {
return (0, matroska_utils_1.padMatroskaBytes)((0, matroska_utils_1.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);
};
exports.createMatroskaSeekHead = createMatroskaSeekHead;

View File

@@ -0,0 +1,3 @@
import type { BytesAndOffset } from './matroska-utils';
export declare const MATROSKA_SEGMENT_MIN_VINT_WIDTH = 8;
export declare const createMatroskaSegment: (children: BytesAndOffset[]) => BytesAndOffset;

View File

@@ -0,0 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createMatroskaSegment = exports.MATROSKA_SEGMENT_MIN_VINT_WIDTH = void 0;
const matroska_utils_1 = require("./matroska-utils");
exports.MATROSKA_SEGMENT_MIN_VINT_WIDTH = 8;
const createMatroskaSegment = (children) => {
return (0, matroska_utils_1.makeMatroskaBytes)({
type: 'Segment',
value: children,
minVintWidth: exports.MATROSKA_SEGMENT_MIN_VINT_WIDTH,
});
};
exports.createMatroskaSegment = createMatroskaSegment;

View File

@@ -0,0 +1,10 @@
import type { MediaParserAdvancedColor } from '@remotion/media-parser';
import type { MakeTrackAudio, MakeTrackVideo } from '../make-track-info';
export declare const makeMatroskaVideoBytes: ({ color, width, height, }: {
color: MediaParserAdvancedColor;
width: number;
height: number;
}) => import("./matroska-utils").BytesAndOffset;
export declare const makeMatroskaAudioTrackEntryBytes: ({ trackNumber, codec, numberOfChannels, sampleRate, codecPrivate, }: MakeTrackAudio) => import("./matroska-utils").BytesAndOffset;
export declare const makeMatroskaVideoTrackEntryBytes: ({ color, width, height, trackNumber, codec, codecPrivate, }: MakeTrackVideo) => import("./matroska-utils").BytesAndOffset;
export declare const makeMatroskaTracks: (tracks: (MakeTrackAudio | MakeTrackVideo)[]) => import("./matroska-utils").BytesAndOffset[];

View File

@@ -0,0 +1,229 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeMatroskaTracks = exports.makeMatroskaVideoTrackEntryBytes = exports.makeMatroskaAudioTrackEntryBytes = exports.makeMatroskaVideoBytes = void 0;
const color_1 = require("./color");
const matroska_utils_1 = require("./matroska-utils");
const makeMatroskaVideoBytes = ({ color, width, height, }) => {
return (0, matroska_utils_1.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: {
// https://datatracker.ietf.org/doc/draft-ietf-cellar-matroska/
// 5.1.4.1.28.1.
value: 2, // 2 - progressive, no interlaced
byteLength: null,
},
minVintWidth: null,
},
(0, color_1.makeMatroskaColorBytes)(color),
],
minVintWidth: null,
});
};
exports.makeMatroskaVideoBytes = makeMatroskaVideoBytes;
const 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}`);
};
const 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}`);
};
const makeMatroskaAudioTrackEntryBytes = ({ trackNumber, codec, numberOfChannels, sampleRate, codecPrivate, }) => {
return (0, matroska_utils_1.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),
});
};
exports.makeMatroskaAudioTrackEntryBytes = makeMatroskaAudioTrackEntryBytes;
const makeMatroskaVideoTrackEntryBytes = ({ color, width, height, trackNumber, codec, codecPrivate, }) => {
return (0, matroska_utils_1.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, // 'video'
byteLength: null,
},
minVintWidth: null,
},
(0, exports.makeMatroskaVideoBytes)({
color,
width,
height,
}),
codecPrivate
? {
type: 'CodecPrivate',
minVintWidth: null,
value: codecPrivate,
}
: null,
].filter(Boolean),
});
};
exports.makeMatroskaVideoTrackEntryBytes = makeMatroskaVideoTrackEntryBytes;
const makeMatroskaTracks = (tracks) => {
const bytesArr = tracks.map((t) => {
const bytes = t.type === 'video'
? (0, exports.makeMatroskaVideoTrackEntryBytes)(t)
: (0, exports.makeMatroskaAudioTrackEntryBytes)(t);
return bytes;
});
return (0, matroska_utils_1.padMatroskaBytes)((0, matroska_utils_1.makeMatroskaBytes)({
type: 'Tracks',
value: bytesArr,
minVintWidth: null,
}), 500);
};
exports.makeMatroskaTracks = makeMatroskaTracks;

View File

@@ -0,0 +1,34 @@
import type { _InternalEbmlValue, MediaParserInternalTypes } from '@remotion/media-parser';
import { MediaParserInternals } from '@remotion/media-parser';
type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
export declare const getIdForName: (name: string) => EbmlMapKey;
export declare const combineUint8Arrays: (arrays: Uint8Array[]) => Uint8Array<ArrayBufferLike>;
export type OffsetAndChildren = {
offset: number;
children: OffsetAndChildren[];
field: keyof typeof MediaParserInternals.matroskaElements;
};
export declare const incrementOffsetAndChildren: (offset: OffsetAndChildren, increment: number) => OffsetAndChildren;
export declare const matroskaToHex: (matrId: (typeof MediaParserInternals.matroskaElements)[keyof typeof MediaParserInternals.matroskaElements]) => Uint8Array<ArrayBufferLike>;
export type BytesAndOffset = {
bytes: Uint8Array;
offsets: OffsetAndChildren;
};
export type EbmlValueOrUint8Array<T extends MediaParserInternalTypes['Ebml']> = Uint8Array | _InternalEbmlValue<T, PossibleEbmlOrUint8Array>;
export type EbmlParsedOrUint8Array<T extends MediaParserInternalTypes['Ebml']> = {
type: T['name'];
value: EbmlValueOrUint8Array<T>;
minVintWidth: number | null;
};
export declare const measureEBMLVarInt: (value: number) => 1 | 2 | 4 | 3 | 5 | 6;
export declare const getVariableInt: (value: number, minWidth: number | null) => Uint8Array<ArrayBuffer>;
export declare const makeMatroskaBytes: (fields: PossibleEbmlOrUint8Array) => BytesAndOffset;
export type PossibleEbmlOrUint8Array = Prettify<{
[key in keyof typeof MediaParserInternals.ebmlMap]: EbmlParsedOrUint8Array<(typeof MediaParserInternals.ebmlMap)[key]>;
}[keyof typeof MediaParserInternals.ebmlMap]> | BytesAndOffset;
export type EbmlMapKey = keyof typeof MediaParserInternals.ebmlMap;
export declare const padMatroskaBytes: (fields: PossibleEbmlOrUint8Array, totalLength: number) => BytesAndOffset[];
export declare function serializeUint16(value: number): Uint8Array;
export {};

View File

@@ -0,0 +1,291 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.padMatroskaBytes = exports.makeMatroskaBytes = exports.getVariableInt = exports.measureEBMLVarInt = exports.matroskaToHex = exports.incrementOffsetAndChildren = exports.combineUint8Arrays = exports.getIdForName = void 0;
exports.serializeUint16 = serializeUint16;
const media_parser_1 = require("@remotion/media-parser");
const getIdForName = (name) => {
const value = Object.entries(media_parser_1.MediaParserInternals.matroskaElements).find(([key]) => key === name)?.[1];
if (!value) {
throw new Error(`Could not find id for name ${name}`);
}
return value;
};
exports.getIdForName = getIdForName;
function putUintDynamic(number, minimumLength) {
if (number < 0) {
throw new Error('This function is designed for non-negative integers only.');
}
// Calculate the minimum number of bytes needed to store the integer
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++) {
// Extract each byte from the number
bytes[length - 1 - i] = (number >> (8 * i)) & 0xff;
}
return bytes;
}
const makeFromStructure = (fields) => {
if ('bytes' in fields) {
return fields;
}
const arrays = [];
const struct = media_parser_1.MediaParserInternals.ebmlMap[(0, exports.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 } = (0, exports.makeMatroskaBytes)(item);
arrays.push(bytes);
children.push((0, exports.incrementOffsetAndChildren)(offsets, bytesWritten));
bytesWritten += bytes.byteLength;
}
return {
bytes: (0, exports.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');
};
const 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;
};
exports.combineUint8Arrays = combineUint8Arrays;
const incrementOffsetAndChildren = (offset, increment) => {
return {
offset: offset.offset + increment,
children: offset.children.map((c) => (0, exports.incrementOffsetAndChildren)(c, increment)),
field: offset.field,
};
};
exports.incrementOffsetAndChildren = incrementOffsetAndChildren;
const 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;
};
exports.matroskaToHex = matroskaToHex;
// https://github.com/Vanilagy/webm-muxer/blob/main/src/ebml.ts#L101
const measureEBMLVarInt = (value) => {
if (value < (1 << 7) - 1) {
/** Top bit is set, leaving 7 bits to hold the integer, but we can't store
* 127 because "all bits set to one" is a reserved value. Same thing for the
* other cases below:
*/
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);
};
exports.measureEBMLVarInt = measureEBMLVarInt;
const getVariableInt = (value, minWidth) => {
const width = Math.max((0, exports.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:
/**
* JavaScript converts its doubles to 32-bit integers for bitwise
* operations, so we need to do a division by 2^32 instead of a
* right-shift of 32 to retain those top 3 bits
*/
return new Uint8Array([
(1 << 3) | ((value / 2 ** 32) & 0x7),
value >> 24,
value >> 16,
value >> 8,
value,
]);
case 6:
return new Uint8Array([
(1 << 2) | ((value / 2 ** 40) & 0x3),
(value / 2 ** 32) | 0,
value >> 24,
value >> 16,
value >> 8,
value,
]);
case 7:
return new Uint8Array([
(1 << 1) | ((value / 2 ** 48) & 0x1),
(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) & 0x1),
(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);
}
};
exports.getVariableInt = getVariableInt;
const makeMatroskaBytes = (fields) => {
if ('bytes' in fields) {
return fields;
}
const value = makeFromStructure(fields);
const header = (0, exports.matroskaToHex)((0, exports.getIdForName)(fields.type));
const size = (0, exports.getVariableInt)(value.bytes.length, fields.minVintWidth);
const bytes = (0, exports.combineUint8Arrays)([header, size, value.bytes]);
return {
bytes,
offsets: {
offset: value.offsets.offset,
field: value.offsets.field,
children: value.offsets.children.map((c) => {
return (0, exports.incrementOffsetAndChildren)(c, header.byteLength + size.byteLength);
}),
},
};
};
exports.makeMatroskaBytes = makeMatroskaBytes;
const padMatroskaBytes = (fields, totalLength) => {
const regular = (0, exports.makeMatroskaBytes)(fields);
const paddingLength = totalLength -
regular.bytes.byteLength -
(0, exports.matroskaToHex)(media_parser_1.MediaParserInternals.matroskaElements.Void).byteLength;
if (paddingLength < 0) {
throw new Error('ooops');
}
const padding = (0, exports.makeMatroskaBytes)({
type: 'Void',
value: new Uint8Array(paddingLength).fill(0),
minVintWidth: null,
});
return [
regular,
{
bytes: padding.bytes,
offsets: (0, exports.incrementOffsetAndChildren)(padding.offsets, regular.bytes.length),
},
];
};
exports.padMatroskaBytes = padMatroskaBytes;
function serializeUint16(value) {
const buffer = new ArrayBuffer(2);
const view = new DataView(buffer);
view.setUint16(0, value);
return new Uint8Array(buffer);
}