From 578221994158571796c3aa0acb4fe8d041f03a7e Mon Sep 17 00:00:00 2001
From: Vanilagy <1696106+Vanilagy@users.noreply.github.com>
Date: Wed, 20 Aug 2025 13:38:54 +0200
Subject: [PATCH] Add Matroska resync logic for malformed files, add RVE
sponsor
---
README.md | 8 +++++
dev/demux.html | 3 +-
docs/index.md | 14 +++++++-
docs/public/sponsors/rve.svg | 4 +++
package-lock.json | 12 +++----
package.json | 2 +-
packages/mp3-encoder/package.json | 2 +-
src/matroska/ebml.ts | 25 +++++++++++++--
src/matroska/matroska-demuxer.ts | 53 +++++++++++++++++++++++++++----
9 files changed, 104 insertions(+), 19 deletions(-)
create mode 100644 docs/public/sponsors/rve.svg
diff --git a/README.md b/README.md
index 2c8a726..66a35a4 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,14 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi
+### Bronze sponsors
+
+
+
[Sponsor Mediabunny's development](https://github.com/sponsors/Vanilagy)
## Features
diff --git a/dev/demux.html b/dev/demux.html
index 04d1e43..eb8fce3 100644
--- a/dev/demux.html
+++ b/dev/demux.html
@@ -19,9 +19,8 @@
const videoTrack = await input.getPrimaryVideoTrack();
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
- for await (const packet of sink.packets(undefined, undefined, { verifyKeyPackets: false })) {
+ for await (const packet of sink.packets()) {
console.log(packet);
- if (packet.timestamp >= 2.4) break;
}
/*
diff --git a/docs/index.md b/docs/index.md
index e3c7672..b4c2bd4 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -92,6 +92,9 @@ const sponsors = {
{ image: '/sponsors/diffusionstudio.png', name: 'Diffusion Studio', url: 'https://diffusion.studio/' },
{ image: '/sponsors/kino.jpg', name: 'Kino', url: 'https://kino.ai/' },
],
+ bronze: [
+ { image: '/sponsors/rve.svg', name: 'React Video Editor', url: 'https://www.reactvideoeditor.com/' },
+ ],
individual: [
{ image: 'https://avatars.githubusercontent.com/u/84167135', name: 'Memenome', url: 'https://github.com/memenome' },
{ image: 'https://avatars.githubusercontent.com/u/5913254', name: 'Brandon McConnell', url: 'https://github.com/brandonmcconnell' },
@@ -336,8 +339,17 @@ await conversion.execute();
+
+ Bronze sponsors
+
+
- Individual sponsors
+ Individual sponsors
diff --git a/docs/public/sponsors/rve.svg b/docs/public/sponsors/rve.svg
new file mode 100644
index 0000000..388894f
--- /dev/null
+++ b/docs/public/sponsors/rve.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 700dfb5..dd43508 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "mediabunny",
- "version": "1.9.0",
+ "version": "1.9.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mediabunny",
- "version": "1.9.0",
+ "version": "1.9.1",
"license": "MPL-2.0",
"workspaces": [
"packages/*"
@@ -5900,9 +5900,9 @@
}
},
"node_modules/mediabunny": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.8.0.tgz",
- "integrity": "sha512-+JjE+A2NxtjxnBsEy4yCw9CYx8yDyJzivAx28xU5vWqEy4Xu9gE8qiIklKWfLEAoiyudJ+wn0gn3rl1HtK8vcw==",
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.9.0.tgz",
+ "integrity": "sha512-pNviaYemNRjrIbx1K1+Q1xcCbD1PRVp6i1+0GXJf5NPVeHUVJfJY2xkhtBjbd4LpbqCTqRoSPYxcrOmIRt14bw==",
"license": "MPL-2.0",
"peer": true,
"workspaces": [
@@ -9017,7 +9017,7 @@
},
"packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder",
- "version": "1.9.0",
+ "version": "1.9.1",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
diff --git a/package.json b/package.json
index 92cdef9..f958375 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "mediabunny",
"author": "Vanilagy",
- "version": "1.9.0",
+ "version": "1.9.1",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module",
"workspaces": [
diff --git a/packages/mp3-encoder/package.json b/packages/mp3-encoder/package.json
index a56cdc3..24c8fc3 100644
--- a/packages/mp3-encoder/package.json
+++ b/packages/mp3-encoder/package.json
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/mp3-encoder",
"author": "Vanilagy",
- "version": "1.9.0",
+ "version": "1.9.1",
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
diff --git a/src/matroska/ebml.ts b/src/matroska/ebml.ts
index b8b7829..4a2d560 100644
--- a/src/matroska/ebml.ts
+++ b/src/matroska/ebml.ts
@@ -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> = {
diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts
index 9d3bfa5..bb6f7ae 100644
--- a/src/matroska/matroska-demuxer.ts
+++ b/src/matroska/matroska-demuxer.ts
@@ -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;