Implement optimized fragmented MP4 demuxing

This commit is contained in:
David Payr
2024-12-14 21:50:51 +01:00
parent 5978110de9
commit 07689fbd8d
14 changed files with 2543 additions and 163 deletions
+2
View File
@@ -39,3 +39,5 @@ export { Source, ArrayBufferSource, BlobSource } from './source';
export { ALL_FORMATS, ISOBMFF, MP4, MOV, MATROSKA, MKV, WEBM } from './input-format';
export { Input, InputOptions } from './input';
export { EncodedVideoChunkDrain, VideoFrameDrain } from './media-drain';
// 🐡🦔
-2
View File
@@ -20,8 +20,6 @@ class IsobmffInputFormat extends InputFormat {
return false;
}
await input._mainReader.loadRange(4, 8);
const isobmffReader = new IsobmffReader(input._mainReader);
isobmffReader.pos = 4;
const fourCc = isobmffReader.readAscii(4);
+2 -1
View File
@@ -1,4 +1,5 @@
import { AudioCodec, MediaCodec, VideoCodec } from './codec';
import { Rotation } from './misc';
export interface InputTrackBacking {
getCodec(): Promise<MediaCodec>;
@@ -34,7 +35,7 @@ export interface InputVideoTrackBacking extends InputTrackBacking {
getCodec(): Promise<VideoCodec>;
getWidth(): Promise<number>;
getHeight(): Promise<number>;
getRotation(): Promise<number>;
getRotation(): Promise<Rotation>;
getDecoderConfig(): Promise<VideoDecoderConfig>;
getFirstChunk(): Promise<EncodedVideoChunk | null>;
getChunk(timestamp: number): Promise<EncodedVideoChunk | null>;
+2
View File
@@ -30,6 +30,8 @@ export class Input {
/** @internal */
_getDemuxer() {
return this._demuxerPromise ??= (async () => {
await this._mainReader.loadRange(0, 4096); // Load the first 4 kiB so we can determine the format
for (const format of this._formats) {
const canRead = await format._canReadInput(this);
if (canRead) {
+1 -1
View File
@@ -989,7 +989,7 @@ export const tfra = (trackData: IsobmffTrackData, trackIndex: number) => {
u32(0b111111), // This specifies that traf number, trun number and sample number are 32-bit ints
u32(trackData.finalizedChunks.length), // Number of entries
trackData.finalizedChunks.map(chunk => [
u64(intoTimescale(chunk.startTimestamp, trackData.timescale)), // Time
u64(intoTimescale(chunk.samples[0]!.timestamp, trackData.timescale)), // Time (in presentation time)
u64(chunk.moofOffset!), // moof offset
u32(trackIndex + 1), // traf number
u32(1), // trun number
File diff suppressed because it is too large Load Diff
+10 -8
View File
@@ -701,20 +701,22 @@ export class IsobmffMuxer extends Muxer {
this.boxWriter.writeBox(movieBox);
}
// Not all tracks need to be present in every fragment
const tracksInFragment = this.trackDatas.filter(x => x.currentChunk);
// Write out an initial moof box; will be overwritten later once actual chunk offsets are known
const moofOffset = this.writer.getPos();
const moofBox = moof(fragmentNumber, this.trackDatas);
const moofBox = moof(fragmentNumber, tracksInFragment);
this.boxWriter.writeBox(moofBox);
// Create the mdat box
{
const mdatBox = mdat(false); // Initially assume no fragment is larger than 4 GiB
const mdatBox = mdat(false); // Initially assume the fragment is not larger than 4 GiB
let totalTrackSampleSize = 0;
// Compute the size of the mdat box
for (const trackData of this.trackDatas) {
assert(trackData.currentChunk);
for (const sample of trackData.currentChunk.samples) {
for (const trackData of tracksInFragment) {
for (const sample of trackData.currentChunk!.samples) {
totalTrackSampleSize += sample.size;
}
}
@@ -731,7 +733,7 @@ export class IsobmffMuxer extends Muxer {
}
// Write sample data
for (const trackData of this.trackDatas) {
for (const trackData of tracksInFragment) {
trackData.currentChunk!.offset = this.writer.getPos();
trackData.currentChunk!.moofOffset = moofOffset;
@@ -744,11 +746,11 @@ export class IsobmffMuxer extends Muxer {
// Now that we set the actual chunk offsets, fix the moof box
const endPos = this.writer.getPos();
this.writer.seek(this.boxWriter.offsets.get(moofBox)!);
const newMoofBox = moof(fragmentNumber, this.trackDatas);
const newMoofBox = moof(fragmentNumber, tracksInFragment);
this.boxWriter.writeBox(newMoofBox);
this.writer.seek(endPos);
for (const trackData of this.trackDatas) {
for (const trackData of tracksInFragment) {
trackData.finalizedChunks.push(trackData.currentChunk!);
this.finalizedChunks.push(trackData.currentChunk!);
trackData.currentChunk = null;
+2
View File
@@ -4,6 +4,8 @@ export function assert(x: unknown): asserts x {
}
}
export type Rotation = 0 | 90 | 180 | 270;
/** @public */
export type TransformationMatrix = [number, number, number, number, number, number, number, number, number];
+25 -11
View File
@@ -1,8 +1,6 @@
import { assert, binarySearchLessOrEqual, removeItem } from './misc';
import { Source } from './source';
const PAGE_SIZE = 4096;
type ReadSegment = {
start: number;
end: number;
@@ -27,32 +25,29 @@ export class Reader {
constructor(public source: Source, public maxStorableBytes = Infinity) {}
async loadRange(start: number, end: number) {
// Read rounded to the nearest page
const alignedStart = Math.floor(start / PAGE_SIZE) * PAGE_SIZE;
let alignedEnd = Math.ceil(end / PAGE_SIZE) * PAGE_SIZE;
alignedEnd = Math.min(alignedEnd, await this.source._getSize());
end = Math.min(end, await this.source._getSize());
const matchingLoadingSegment = this.loadingSegments.find(x => x.start <= alignedStart && x.end >= alignedEnd);
const matchingLoadingSegment = this.loadingSegments.find(x => x.start <= start && x.end >= end);
if (matchingLoadingSegment) {
// Simply wait for the existing promise to finish to avoid loading the same range twice
await matchingLoadingSegment.promise;
return;
}
const encasingSegmentExists = this.loadedSegments.some(x => x.start <= alignedStart && x.end >= alignedEnd);
const encasingSegmentExists = this.loadedSegments.some(x => x.start <= start && x.end >= end);
if (encasingSegmentExists) {
// Nothing to load
return;
}
const bytesPromise = this.source._read(alignedStart, alignedEnd);
const loadingSegment: LoadingSegment = { start: alignedStart, end: alignedEnd, promise: bytesPromise };
const bytesPromise = this.source._read(start, end);
const loadingSegment: LoadingSegment = { start, end, promise: bytesPromise };
this.loadingSegments.push(loadingSegment);
const bytes = await bytesPromise;
removeItem(this.loadingSegments, loadingSegment);
this.insertIntoLoadedSegments(alignedStart, bytes);
this.insertIntoLoadedSegments(start, bytes);
}
private insertIntoLoadedSegments(start: number, bytes: Uint8Array) {
@@ -136,4 +131,23 @@ export class Reader {
offset: segment.bytes.byteOffset + start - segment.start,
};
}
forgetRange(start: number, end: number) {
if (end <= start) {
return;
}
const startIndex = binarySearchLessOrEqual(this.loadedSegments, start, x => x.start);
if (startIndex === -1) {
return;
}
const segment = this.loadedSegments[startIndex]!;
if (segment.start !== start || segment.end !== end) {
return;
}
this.loadedSegments.splice(startIndex, 1);
this.totalStoredBytes -= segment.bytes.byteLength;
}
}