Add Matroska resync logic for malformed files, add RVE sponsor

This commit is contained in:
Vanilagy
2025-08-20 13:38:54 +02:00
parent b265826b7e
commit 5782219941
9 changed files with 104 additions and 19 deletions
+23 -2
View File
@@ -122,9 +122,8 @@ export const LEVEL_0_EBML_IDS: EBMLId[] = [
EBMLId.Segment,
];
// All the stuff that can appear in a segment, basically
export const LEVEL_1_EBML_IDS: EBMLId[] = [
EBMLId.EBMLMaxIDLength,
EBMLId.EBMLMaxSizeLength,
EBMLId.SeekHead,
EBMLId.Info,
EBMLId.Cluster,
@@ -585,6 +584,28 @@ export class EBMLReader {
return null;
}
/** Searches for the next occurrence of an element ID using a naive byte-wise search. */
async resync(ids: EBMLId[], until: number) {
const loadChunkSize = 2 ** 20; // 1 MiB
const idsSet = new Set(ids);
while (this.pos <= until - MIN_HEADER_SIZE) {
if (!this.reader.rangeIsLoaded(this.pos, Math.min(this.pos + MAX_HEADER_SIZE, until))) {
await this.reader.loadRange(this.pos, Math.min(this.pos + loadChunkSize, until));
}
const elementStartPos = this.pos;
const elementId = this.readElementId();
if (elementId !== null && idsSet.has(elementId)) {
return elementStartPos;
}
this.pos = elementStartPos + 1;
}
return null;
}
}
export const CODEC_STRING_MAP: Partial<Record<MediaCodec, string>> = {
+47 -6
View File
@@ -56,6 +56,7 @@ import {
EBMLId,
EBMLReader,
LEVEL_0_AND_1_EBML_IDS,
LEVEL_1_EBML_IDS,
MAX_HEADER_SIZE,
MIN_HEADER_SIZE,
readVarInt,
@@ -175,6 +176,7 @@ const METADATA_ELEMENTS = [
{ id: EBMLId.Tracks, flag: 'tracksSeen' },
{ id: EBMLId.Cues, flag: 'cuesSeen' },
] as const;
const MAX_RESYNC_LENGTH = 10 * 2 ** 20; // 10 MiB
export class MatroskaDemuxer extends Demuxer {
metadataReader: EBMLReader;
@@ -330,8 +332,22 @@ export class MatroskaDemuxer extends Demuxer {
const elementStartPos = this.metadataReader.pos;
const header = this.metadataReader.readElementHeader();
if (!header) {
break;
if (!header || !LEVEL_1_EBML_IDS.includes(header.id)) {
// Potential junk. Let's try to resync
this.metadataReader.pos = elementStartPos;
const nextPos = await this.metadataReader.resync(
LEVEL_1_EBML_IDS,
Math.min(this.currentSegment.elementEndPos, this.metadataReader.pos + MAX_RESYNC_LENGTH),
);
if (nextPos) {
this.metadataReader.pos = nextPos;
continue;
} else {
break; // Resync failed
}
}
const { id, size } = header;
@@ -352,6 +368,10 @@ export class MatroskaDemuxer extends Demuxer {
}
}
if (size !== null) {
this.metadataReader.pos = dataStartPos + size;
}
if (this.currentSegment.infoSeen && this.currentSegment.tracksSeen && this.currentSegment.cuesSeen) {
// No need to search anymore, we have everything
break;
@@ -381,10 +401,15 @@ export class MatroskaDemuxer extends Demuxer {
if (size === null) {
break;
}
}
this.metadataReader.pos = dataStartPos + size;
if (!clusterEncountered) {
const seekEntry = this.currentSegment.seekEntries.find(entry => entry.id === EBMLId.Cluster);
if (!clusterEncountered) {
if (seekEntry) {
// The seek head points us to the first cluster, nice
this.currentSegment.clusterSeekStartPos = segmentDataStart + seekEntry.segmentPosition;
} else {
this.currentSegment.clusterSeekStartPos = this.metadataReader.pos;
}
}
@@ -1609,8 +1634,24 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
await metadataReader.reader.loadRange(metadataReader.pos, metadataReader.pos + MAX_HEADER_SIZE);
const elementStartPos = metadataReader.pos;
const elementHeader = metadataReader.readElementHeader();
if (!elementHeader) {
break;
if (!elementHeader || !LEVEL_1_EBML_IDS.includes(elementHeader.id)) {
// There's an element here that shouldn't be here (or Void). Might be garbage. In this case, let's
// try and resync to the next valid element.
metadataReader.pos = elementStartPos;
const nextPos = await metadataReader.resync(
LEVEL_1_EBML_IDS,
Math.min(segment.elementEndPos, metadataReader.pos + MAX_RESYNC_LENGTH),
);
if (nextPos) {
metadataReader.pos = nextPos;
continue;
} else {
break; // Resync failed
}
}
const id = elementHeader.id;