Allow all Matroska AttachedFiles to be read & written via metadata tags

This commit is contained in:
Vanilagy
2025-09-19 20:12:29 +02:00
parent e28c34cb7a
commit a3f4f7f4f7
8 changed files with 220 additions and 21 deletions
+54 -4
View File
@@ -16,6 +16,7 @@ export interface EBMLElement {
size?: number;
data:
| number
| bigint
| string
| Uint8Array
| EBMLFloat32
@@ -198,6 +199,26 @@ export const measureUnsignedInt = (value: number) => {
}
};
export const measureUnsignedBigInt = (value: bigint) => {
if (value < (1n << 8n)) {
return 1;
} else if (value < (1n << 16n)) {
return 2;
} else if (value < (1n << 24n)) {
return 3;
} else if (value < (1n << 32n)) {
return 4;
} else if (value < (1n << 40n)) {
return 5;
} else if (value < (1n << 48n)) {
return 6;
} else if (value < (1n << 56n)) {
return 7;
} else {
return 8;
}
};
export const measureSignedInt = (value: number) => {
if (value >= -(1 << 6) && value < (1 << 6)) {
return 1;
@@ -276,16 +297,16 @@ export class EBMLWriter {
// eslint-disable-next-line no-fallthrough
case 5:
this.helperView.setUint8(pos++, (value / 2 ** 32) | 0);
// eslint-disable-next-line no-fallthrough
// eslint-disable-next-line no-fallthrough
case 4:
this.helperView.setUint8(pos++, value >> 24);
// eslint-disable-next-line no-fallthrough
// eslint-disable-next-line no-fallthrough
case 3:
this.helperView.setUint8(pos++, value >> 16);
// eslint-disable-next-line no-fallthrough
// eslint-disable-next-line no-fallthrough
case 2:
this.helperView.setUint8(pos++, value >> 8);
// eslint-disable-next-line no-fallthrough
// eslint-disable-next-line no-fallthrough
case 1:
this.helperView.setUint8(pos++, value);
break;
@@ -296,6 +317,16 @@ export class EBMLWriter {
this.writer.write(this.helper.subarray(0, pos));
}
writeUnsignedBigInt(value: bigint, width = measureUnsignedBigInt(value)) {
let pos = 0;
for (let i = width - 1; i >= 0; i--) {
this.helperView.setUint8(pos++, Number((value >> BigInt(i * 8)) & 0xffn));
}
this.writer.write(this.helper.subarray(0, pos));
}
writeSignedInt(value: number, width = measureSignedInt(value)) {
if (value < 0) {
// Two's complement stuff
@@ -398,6 +429,10 @@ export class EBMLWriter {
const size = data.size ?? measureUnsignedInt(data.data);
this.writeVarInt(size);
this.writeUnsignedInt(data.data, size);
} else if (typeof data.data === 'bigint') {
const size = data.size ?? measureUnsignedBigInt(data.data);
this.writeVarInt(size);
this.writeUnsignedBigInt(data.data, size);
} else if (typeof data.data === 'string') {
this.writeVarInt(data.data.length);
this.writeAsciiString(data.data);
@@ -491,6 +526,21 @@ export const readUnsignedInt = (slice: FileSlice, width: number) => {
return value;
};
export const readUnsignedBigInt = (slice: FileSlice, width: number) => {
if (width < 1) {
throw new Error('Bad unsigned int size ' + width);
}
let value = 0n;
for (let i = 0; i < width; i++) {
value <<= 8n;
value += BigInt(readU8(slice));
}
return value;
};
export const readSignedInt = (slice: FileSlice, width: number) => {
let value = readUnsignedInt(slice, width);
+25 -3
View File
@@ -31,7 +31,7 @@ import {
InputVideoTrack,
InputVideoTrackBacking,
} from '../input-track';
import { MetadataTags } from '../tags';
import { AttachedFile, MetadataTags } from '../tags';
import { PacketRetrievalOptions } from '../media-sink';
import {
assert,
@@ -69,6 +69,7 @@ import {
readVarInt,
resync,
searchForNextElementId,
readUnsignedBigInt,
} from './ebml';
import { buildMatroskaMimeType } from './matroska-misc';
import { FileSlice, readBytes, Reader, readI16Be, readU8 } from '../reader';
@@ -240,6 +241,7 @@ export class MatroskaDemuxer extends Demuxer {
currentTagTargetIsMovie: boolean = true;
currentSimpleTagName: string | null = null;
currentAttachedFile: {
fileUid: bigint | null;
fileName: string | null;
fileMediaType: string | null;
fileData: Uint8Array | null;
@@ -1478,6 +1480,7 @@ export class MatroskaDemuxer extends Demuxer {
if (!this.currentSegment) break;
this.currentAttachedFile = {
fileUid: null,
fileName: null,
fileMediaType: null,
fileData: null,
@@ -1486,6 +1489,19 @@ export class MatroskaDemuxer extends Demuxer {
this.readContiguousElements(slice.slice(dataStartPos, size));
const tags = this.currentSegment.metadataTags;
if (this.currentAttachedFile.fileUid && this.currentAttachedFile.fileData) {
// All attached files get surfaced in the `raw` metadata tags
tags.raw ??= {};
tags.raw[this.currentAttachedFile.fileUid.toString()] = new AttachedFile(
this.currentAttachedFile.fileData,
this.currentAttachedFile.fileMediaType ?? undefined,
this.currentAttachedFile.fileName ?? undefined,
this.currentAttachedFile.fileDescription ?? undefined,
);
}
// Only process image attachments
if (this.currentAttachedFile.fileMediaType?.startsWith('image/') && this.currentAttachedFile.fileData) {
const fileName = this.currentAttachedFile.fileName;
@@ -1500,8 +1516,8 @@ export class MatroskaDemuxer extends Demuxer {
}
}
this.currentSegment.metadataTags.images ??= [];
this.currentSegment.metadataTags.images.push({
tags.images ??= [];
tags.images.push({
data: this.currentAttachedFile.fileData,
mimeType: this.currentAttachedFile.fileMediaType,
kind,
@@ -1513,6 +1529,12 @@ export class MatroskaDemuxer extends Demuxer {
this.currentAttachedFile = null;
}; break;
case EBMLId.FileUID: {
if (!this.currentAttachedFile) break;
this.currentAttachedFile.fileUid = readUnsignedBigInt(slice, size);
}; break;
case EBMLId.FileName: {
if (!this.currentAttachedFile) break;
+45 -7
View File
@@ -22,6 +22,7 @@ import {
roundToMultiple,
textEncoder,
toUint8Array,
uint8ArraysAreEqual,
writeBits,
} from '../misc';
import {
@@ -62,6 +63,7 @@ import { Muxer } from '../muxer';
import { Writer } from '../writer';
import { EncodedPacket } from '../packet';
import { parseOpusIdentificationHeader } from '../codec-data';
import { AttachedFile } from '../tags';
const MIN_CLUSTER_TIMESTAMP_MS = -(2 ** 15);
const MAX_CLUSTER_TIMESTAMP_MS = 2 ** 15 - 1;
@@ -537,13 +539,12 @@ export class MatroskaMuxer extends Muxer {
private maybeCreateAttachments() {
const metadataTags = this.output._metadataTags;
if (!metadataTags.images || metadataTags.images.length === 0) {
return;
}
const elements: EBMLElement[] = [];
const existingFileUids = new Set<number>();
const images = metadataTags.images ?? [];
this.attachmentsElement = { id: EBMLId.Attachments, data: metadataTags.images.map((image): EBMLElement => {
for (const image of images) {
let imageName = image.name;
if (imageName === undefined) {
const baseName = image.kind === 'coverFront' ? 'cover' : image.kind === 'coverBack' ? 'back' : 'image';
@@ -561,7 +562,7 @@ export class MatroskaMuxer extends Muxer {
existingFileUids.add(fileUid);
return {
elements.push({
id: EBMLId.AttachedFile,
data: [
image.description !== undefined
@@ -572,8 +573,45 @@ export class MatroskaMuxer extends Muxer {
{ id: EBMLId.FileData, data: image.data },
{ id: EBMLId.FileUID, data: fileUid },
],
};
}) };
});
}
// Add all AttachedFiles from the raw metadata
for (const [key, value] of Object.entries(metadataTags.raw ?? {})) {
if (!(value instanceof AttachedFile)) {
continue;
}
const keyIsNumeric = /^\d+$/.test(key);
if (!keyIsNumeric) {
continue;
}
if (images.find(x => x.mimeType === value.mimeType && uint8ArraysAreEqual(x.data, value.data))) {
// This attached file has very likely already been added as an image above
// (happens when remuxing Matroska)
continue;
}
elements.push({
id: EBMLId.AttachedFile,
data: [
value.description !== undefined
? { id: EBMLId.FileDescription, data: new EBMLUnicodeString(value.description) }
: null,
{ id: EBMLId.FileName, data: new EBMLUnicodeString(value.name ?? '') },
{ id: EBMLId.FileMediaType, data: value.mimeType ?? '' },
{ id: EBMLId.FileData, data: value.data },
{ id: EBMLId.FileUID, data: BigInt(key) },
],
});
}
if (elements.length === 0) {
return;
}
this.attachmentsElement = { id: EBMLId.Attachments, data: elements };
}
private createSegment() {