mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-02 21:33:52 +02:00
Allow all Matroska AttachedFiles to be read & written via metadata tags
This commit is contained in:
+54
-4
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user