Allow for out-of-presentation-order chunks, enabling P and B frames

This commit is contained in:
Vanilagy
2024-11-12 23:24:43 +01:00
parent d01d32f08c
commit a065a5ef12
13 changed files with 682 additions and 427 deletions
+26 -2
View File
@@ -1,7 +1,7 @@
export interface EBMLElement {
id: number,
size?: number,
data: number | string | Uint8Array | EBMLFloat32 | EBMLFloat64 | (EBML | null)[]
data: number | string | Uint8Array | EBMLFloat32 | EBMLFloat64 | EBMLSignedInt | (EBML | null)[]
}
export type EBML = EBMLElement | Uint8Array | (EBML | null)[];
@@ -24,6 +24,15 @@ export class EBMLFloat64 {
}
}
/** Wrapper around a number to be able to differentiate it in the writer. */
export class EBMLSignedInt {
value: number;
constructor(value: number) {
this.value = value;
}
}
/** Defines some of the EBML IDs used by Matroska files. */
export enum EBMLId {
EBML = 0x1a45dfa3,
@@ -83,7 +92,6 @@ export enum EBMLId {
}
export const measureUnsignedInt = (value: number) => {
// Force to 32-bit unsigned integer
if (value < (1 << 8)) {
return 1;
} else if (value < (1 << 16)) {
@@ -99,6 +107,22 @@ export const measureUnsignedInt = (value: number) => {
}
};
export const measureSignedInt = (value: number) => {
if (value >= -(1 << 6) && value < (1 << 6)) {
return 1;
} else if (value >= -(1 << 13) && value < (1 << 13)) {
return 2;
} else if (value >= -(1 << 20) && value < (1 << 20)) {
return 3;
} else if (value >= -(1 << 27) && value < (1 << 27)) {
return 4;
} else if (value >= -(2**34) && value < 2**34) {
return 5;
} else {
return 6;
}
};
export const measureEBMLVarInt = (value: number) => {
if (value < (1 << 7) - 1) {
/** Top bit is set, leaving 7 bits to hold the integer, but we can't store
+32 -19
View File
@@ -4,7 +4,7 @@ import { Output, OutputAudioTrack, OutputTrack, OutputVideoTrack } from '../outp
import { MkvOutputFormat, WebMOutputFormat } from '../output_format';
import { AudioCodec, VideoCodec } from '../source';
import { Writer } from '../writer';
import { EBML, EBMLElement, EBMLFloat32, EBMLFloat64, EBMLId, measureEBMLVarInt, measureUnsignedInt } from './ebml';
import { EBML, EBMLElement, EBMLFloat32, EBMLFloat64, EBMLId, EBMLSignedInt, measureEBMLVarInt, measureSignedInt, measureUnsignedInt } from './ebml';
const VIDEO_TRACK_TYPE = 1;
const AUDIO_TRACK_TYPE = 2;
@@ -41,7 +41,7 @@ type MatroskaTrackData = {
chunkQueue: InternalMediaChunk[],
firstTimestamp: number | null,
lastTimestamp: number | null,
lastKeyFrameTimestamp: number | null,
lastWrittenTimestamp: number | null
} & ({
track: OutputVideoTrack,
@@ -133,7 +133,7 @@ export class MatroskaMuxer extends Muxer {
this.#writer.write(this.#helper);
}
#writeUnsignedInt(value: number, width: number = measureUnsignedInt(value)) {
#writeUnsignedInt(value: number, width = measureUnsignedInt(value)) {
let pos = 0;
// Each case falls through:
@@ -159,7 +159,16 @@ export class MatroskaMuxer extends Muxer {
this.#writer.write(this.#helper.subarray(0, pos));
}
writeEBMLVarInt(value: number, width: number = measureEBMLVarInt(value)) {
#writeSignedInt(value: number, width = measureSignedInt(value)) {
if (value < 0) {
// Two's complement stuff
value += 2 ** (width * 8);
}
this.#writeUnsignedInt(value, width);
}
writeEBMLVarInt(value: number, width = measureEBMLVarInt(value)) {
let pos = 0;
switch (width) {
@@ -265,6 +274,10 @@ export class MatroskaMuxer extends Muxer {
} else if (data.data instanceof EBMLFloat64) {
this.writeEBMLVarInt(8);
this.#writeFloat64(data.data.value);
} else if (data.data instanceof EBMLSignedInt) {
let size = data.size ?? measureSignedInt(data.data.value);
this.writeEBMLVarInt(size);
this.#writeSignedInt(data.data.value, size);
}
}
}
@@ -362,7 +375,7 @@ export class MatroskaMuxer extends Muxer {
{ id: EBMLId.CodecID, data: CODEC_STRING_MAP[trackData.track.source.codec] },
(trackData.info.decoderConfig.description ? { id: EBMLId.CodecPrivate, data: toUint8Array(trackData.info.decoderConfig.description) } : null),
...(trackData.type === 'video' ? [
(trackData.track.source.metadata.frameRate ? { id: EBMLId.DefaultDuration, data: 1e9 / trackData.track.source.metadata.frameRate } : null),
(trackData.track.metadata.frameRate ? { id: EBMLId.DefaultDuration, data: 1e9 / trackData.track.metadata.frameRate } : null),
{ id: EBMLId.Video, data: [
{ id: EBMLId.PixelWidth, data: trackData.info.width },
{ id: EBMLId.PixelHeight, data: trackData.info.height },
@@ -474,7 +487,7 @@ export class MatroskaMuxer extends Muxer {
},
chunkQueue: [],
firstTimestamp: null,
lastTimestamp: null,
lastKeyFrameTimestamp: null,
lastWrittenTimestamp: null
};
@@ -504,7 +517,7 @@ export class MatroskaMuxer extends Muxer {
},
chunkQueue: [],
firstTimestamp: null,
lastTimestamp: null,
lastKeyFrameTimestamp: null,
lastWrittenTimestamp: null
};
@@ -514,14 +527,12 @@ export class MatroskaMuxer extends Muxer {
return newTrackData;
}
addEncodedVideoChunk(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata, compositionTimeOffset?: number) {
addEncodedVideoChunk(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata) {
const trackData = this.#getVideoTrackData(track, chunk, meta);
let videoChunk = this.#createInternalChunk(trackData, chunk);
if (track.source.codec === 'vp9') this.#fixVP9ColorSpace(trackData, videoChunk);
trackData.lastTimestamp = videoChunk.timestamp;
trackData.chunkQueue.push(videoChunk);
this.#interleaveChunks();
@@ -533,7 +544,6 @@ export class MatroskaMuxer extends Muxer {
const trackData = this.#getAudioTrackData(track, chunk, meta);
let audioChunk = this.#createInternalChunk(trackData, chunk);
trackData.lastTimestamp = audioChunk.timestamp;
trackData.chunkQueue.push(audioChunk);
this.#interleaveChunks();
@@ -683,7 +693,7 @@ export class MatroskaMuxer extends Muxer {
trackData: MatroskaTrackData,
chunk: EncodedVideoChunk | EncodedAudioChunk
) {
let adjustedTimestamp = this.#validateTimestamp(trackData, chunk.timestamp);
let adjustedTimestamp = this.#validateTimestamp(trackData, chunk);
let data = new Uint8Array(chunk.byteLength);
chunk.copyTo(data);
@@ -699,7 +709,9 @@ export class MatroskaMuxer extends Muxer {
return internalChunk;
}
#validateTimestamp(trackData: MatroskaTrackData, timestamp: number) {
#validateTimestamp(trackData: MatroskaTrackData, chunk: EncodedVideoChunk | EncodedAudioChunk) {
let timestamp = chunk.timestamp;
if (timestamp < 0) {
throw new Error(`Timestamps must be non-negative (got ${timestamp}s).`);
}
@@ -710,11 +722,12 @@ export class MatroskaMuxer extends Muxer {
timestamp -= trackData.firstTimestamp;
if (trackData.lastTimestamp !== null && timestamp < trackData.lastTimestamp) {
throw new Error(
`Timestamps must be monotonically increasing ` +
`(timestamp went from ${trackData.lastTimestamp}s to ${timestamp}s).`
);
if (trackData.lastKeyFrameTimestamp !== null && timestamp < trackData.lastKeyFrameTimestamp) {
throw new Error(`Timestamp cannot be before last key frame's timestamp (got ${timestamp / 1e6}s, last key frame at ${trackData.lastKeyFrameTimestamp / 1e6}s).`);
}
if (chunk.type === 'key') {
trackData.lastKeyFrameTimestamp = timestamp;
}
return timestamp;
@@ -787,7 +800,7 @@ export class MatroskaMuxer extends Muxer {
prelude,
chunk.data
] },
chunk.type === 'delta' ? { id: EBMLId.ReferenceBlock, data: trackData.lastWrittenTimestamp! - msTimestamp } : null,
chunk.type === 'delta' ? { id: EBMLId.ReferenceBlock, data: new EBMLSignedInt(trackData.lastWrittenTimestamp! - msTimestamp) } : null,
chunk.duration !== null ? { id: EBMLId.BlockDuration, data: msDuration } : null,
chunk.additions ? { id: EBMLId.BlockAdditions, data: chunk.additions } : null
] };