mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 19:03:46 +02:00
Add Matroska demuxer
This commit is contained in:
+63
-4
@@ -2,6 +2,8 @@ import { Demuxer } from './demuxer';
|
||||
import { Input } from './input';
|
||||
import { IsobmffDemuxer } from './isobmff/isobmff-demuxer';
|
||||
import { IsobmffReader } from './isobmff/isobmff-reader';
|
||||
import { EBMLId, EBMLReader } from './matroska/ebml';
|
||||
import { MatroskaDemuxer } from './matroska/matroska-demuxer';
|
||||
import { RiffReader } from './wave/riff-reader';
|
||||
import { WaveDemuxer } from './wave/wave-demuxer';
|
||||
|
||||
@@ -79,13 +81,70 @@ export class QuickTimeInputFormat extends IsobmffInputFormat {
|
||||
/** @public */
|
||||
export class MatroskaInputFormat extends InputFormat {
|
||||
/** @internal */
|
||||
async _canReadInput() {
|
||||
return false; // TODO
|
||||
async _canReadInput(input: Input) {
|
||||
const sourceSize = await input._mainReader.source._getSize();
|
||||
if (sourceSize < 8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ebmlReader = new EBMLReader(input._mainReader);
|
||||
const varIntSize = ebmlReader.readVarIntSize();
|
||||
if (varIntSize < 1 || varIntSize > 6) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const id = ebmlReader.readUnsignedInt(varIntSize);
|
||||
if (id !== EBMLId.EBML) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dataSize = ebmlReader.readElementSize();
|
||||
if (dataSize === -1) {
|
||||
return false; // Miss me with that shit
|
||||
}
|
||||
|
||||
const startPos = ebmlReader.pos;
|
||||
while (ebmlReader.pos < startPos + dataSize) {
|
||||
const { id, size } = ebmlReader.readElementHeader();
|
||||
const dataStartPos = ebmlReader.pos;
|
||||
if (size === -1) return false;
|
||||
|
||||
switch (id) {
|
||||
case EBMLId.EBMLVersion: {
|
||||
const ebmlVersion = ebmlReader.readUnsignedInt(size);
|
||||
if (ebmlVersion !== 1) {
|
||||
return false;
|
||||
}
|
||||
}; break;
|
||||
case EBMLId.EBMLReadVersion: {
|
||||
const ebmlReadVersion = ebmlReader.readUnsignedInt(size);
|
||||
if (ebmlReadVersion !== 1) {
|
||||
return false;
|
||||
}
|
||||
}; break;
|
||||
case EBMLId.DocType: {
|
||||
const docType = ebmlReader.readString(size);
|
||||
if (docType !== 'matroska' && docType !== 'webm') {
|
||||
return false;
|
||||
}
|
||||
}; break;
|
||||
case EBMLId.DocTypeVersion: {
|
||||
const docTypeVersion = ebmlReader.readUnsignedInt(size);
|
||||
if (docTypeVersion > 4) { // Support up to Matroska v4
|
||||
return false;
|
||||
}
|
||||
}; break;
|
||||
}
|
||||
|
||||
ebmlReader.pos = dataStartPos + size;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_createDemuxer(): never {
|
||||
throw new Error('Not implemented');
|
||||
_createDemuxer(input: Input) {
|
||||
return new MatroskaDemuxer(input);
|
||||
}
|
||||
|
||||
getName() {
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ export class Input {
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Input has an unrecognizable format.');
|
||||
throw new Error('Input has an unsupported or unrecognizable format.');
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
} from '../misc';
|
||||
import { Reader } from '../reader';
|
||||
import { EncodedAudioSample, EncodedVideoSample, PLACEHOLDER_DATA, SampleType } from '../sample';
|
||||
import { IsobmffReader } from './isobmff-reader';
|
||||
import { IsobmffReader, MAX_BOX_HEADER_SIZE } from './isobmff-reader';
|
||||
|
||||
type InternalTrack = {
|
||||
id: number;
|
||||
@@ -221,7 +221,10 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
const sourceSize = await this.isobmffReader.reader.source._getSize();
|
||||
|
||||
while (this.isobmffReader.pos < sourceSize) {
|
||||
await this.isobmffReader.reader.loadRange(this.isobmffReader.pos, this.isobmffReader.pos + 16);
|
||||
await this.isobmffReader.reader.loadRange(
|
||||
this.isobmffReader.pos,
|
||||
this.isobmffReader.pos + MAX_BOX_HEADER_SIZE,
|
||||
);
|
||||
const startPos = this.isobmffReader.pos;
|
||||
const boxInfo = this.isobmffReader.readBoxHeader();
|
||||
|
||||
@@ -409,7 +412,10 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
async readFragment(): Promise<Fragment> {
|
||||
const startPos = this.isobmffReader.pos;
|
||||
|
||||
await this.isobmffReader.reader.loadRange(this.isobmffReader.pos, this.isobmffReader.pos + 16);
|
||||
await this.isobmffReader.reader.loadRange(
|
||||
this.isobmffReader.pos,
|
||||
this.isobmffReader.pos + MAX_BOX_HEADER_SIZE,
|
||||
);
|
||||
|
||||
const moofBoxInfo = this.isobmffReader.readBoxHeader();
|
||||
assert(moofBoxInfo.name === 'moof');
|
||||
@@ -458,7 +464,10 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
currentFragment = currentFragment.nextFragment;
|
||||
this.isobmffReader.pos = currentFragment.moofOffset + currentFragment.moofSize;
|
||||
} else {
|
||||
await this.isobmffReader.reader.loadRange(this.isobmffReader.pos, this.isobmffReader.pos + 16);
|
||||
await this.isobmffReader.reader.loadRange(
|
||||
this.isobmffReader.pos,
|
||||
this.isobmffReader.pos + MAX_BOX_HEADER_SIZE,
|
||||
);
|
||||
const startPos = this.isobmffReader.pos;
|
||||
const boxInfo = this.isobmffReader.readBoxHeader();
|
||||
|
||||
@@ -1674,16 +1683,16 @@ abstract class IsobmffTrackBacking<
|
||||
throw new Error('Not implemented on base class.');
|
||||
}
|
||||
|
||||
async getFirstTimestamp() {
|
||||
const firstSample = await this.getFirstSample({ metadataOnly: true });
|
||||
return firstSample?.timestamp ?? 0;
|
||||
}
|
||||
|
||||
async computeDuration() {
|
||||
const lastSample = await this.getSample(Infinity, { metadataOnly: true });
|
||||
return (lastSample?.timestamp ?? 0) + (lastSample?.duration ?? 0);
|
||||
}
|
||||
|
||||
async getFirstTimestamp() {
|
||||
const firstSample = await this.getFirstSample({ metadataOnly: true });
|
||||
return firstSample?.timestamp ?? 0;
|
||||
}
|
||||
|
||||
abstract createSample(
|
||||
data: Uint8Array,
|
||||
byteLength: number,
|
||||
@@ -2116,7 +2125,7 @@ abstract class IsobmffTrackBacking<
|
||||
}
|
||||
|
||||
// Load the header
|
||||
await isobmffReader.reader.loadRange(isobmffReader.pos, isobmffReader.pos + 16);
|
||||
await isobmffReader.reader.loadRange(isobmffReader.pos, isobmffReader.pos + MAX_BOX_HEADER_SIZE);
|
||||
const startPos = isobmffReader.pos;
|
||||
const boxInfo = isobmffReader.readBoxHeader();
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Reader } from '../reader';
|
||||
|
||||
export const MAX_BOX_HEADER_SIZE = 16;
|
||||
|
||||
export class IsobmffReader {
|
||||
pos = 0;
|
||||
|
||||
|
||||
+371
-2
@@ -1,3 +1,7 @@
|
||||
import { MediaCodec } from '../codec';
|
||||
import { Reader } from '../reader';
|
||||
import { Writer } from '../writer';
|
||||
|
||||
export interface EBMLElement {
|
||||
id: number;
|
||||
size?: number;
|
||||
@@ -57,6 +61,10 @@ export enum EBMLId {
|
||||
TrackNumber = 0xd7,
|
||||
TrackUID = 0x73c5,
|
||||
TrackType = 0x83,
|
||||
FlagEnabled = 0xb9,
|
||||
FlagDefault = 0x88,
|
||||
FlagForced = 0x55aa,
|
||||
FlagLacing = 0x9c,
|
||||
CodecID = 0x86,
|
||||
CodecPrivate = 0x63a2,
|
||||
DefaultDuration = 0x23e383,
|
||||
@@ -90,6 +98,8 @@ export enum EBMLId {
|
||||
TransferCharacteristics = 0x55ba,
|
||||
Primaries = 0x55bb,
|
||||
Range = 0x55b9,
|
||||
Projection = 0x7670,
|
||||
ProjectionPoseRoll = 0x7675,
|
||||
}
|
||||
|
||||
export const measureUnsignedInt = (value: number) => {
|
||||
@@ -124,7 +134,7 @@ export const measureSignedInt = (value: number) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const measureEBMLVarInt = (value: number) => {
|
||||
export const measureVarInt = (value: number) => {
|
||||
if (value < (1 << 7) - 1) {
|
||||
/** Top bit is set, leaving 7 bits to hold the integer, but we can't store
|
||||
* 127 because "all bits set to one" is a reserved value. Same thing for the
|
||||
@@ -142,6 +152,365 @@ export const measureEBMLVarInt = (value: number) => {
|
||||
} else if (value < 2 ** 42 - 1) {
|
||||
return 6;
|
||||
} else {
|
||||
throw new Error('EBML VINT size not supported ' + value);
|
||||
throw new Error('EBML varint size not supported ' + value);
|
||||
}
|
||||
};
|
||||
|
||||
export class EBMLWriter {
|
||||
helper = new Uint8Array(8);
|
||||
helperView = new DataView(this.helper.buffer);
|
||||
|
||||
/**
|
||||
* Stores the position from the start of the file to where EBML elements have been written. This is used to
|
||||
* rewrite/edit elements that were already added before, and to measure sizes of things.
|
||||
*/
|
||||
offsets = new WeakMap<EBML, number>();
|
||||
/** Same as offsets, but stores position where the element's data starts (after ID and size fields). */
|
||||
dataOffsets = new WeakMap<EBML, number>();
|
||||
|
||||
constructor(private writer: Writer) {}
|
||||
|
||||
writeByte(value: number) {
|
||||
this.helperView.setUint8(0, value);
|
||||
this.writer.write(this.helper.subarray(0, 1));
|
||||
}
|
||||
|
||||
writeFloat32(value: number) {
|
||||
this.helperView.setFloat32(0, value, false);
|
||||
this.writer.write(this.helper.subarray(0, 4));
|
||||
}
|
||||
|
||||
writeFloat64(value: number) {
|
||||
this.helperView.setFloat64(0, value, false);
|
||||
this.writer.write(this.helper);
|
||||
}
|
||||
|
||||
writeUnsignedInt(value: number, width = measureUnsignedInt(value)) {
|
||||
let pos = 0;
|
||||
|
||||
// Each case falls through:
|
||||
switch (width) {
|
||||
case 6:
|
||||
// Need to use division to access >32 bits of floating point var
|
||||
this.helperView.setUint8(pos++, (value / 2 ** 40) | 0);
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 5:
|
||||
this.helperView.setUint8(pos++, (value / 2 ** 32) | 0);
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 4:
|
||||
this.helperView.setUint8(pos++, value >> 24);
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 3:
|
||||
this.helperView.setUint8(pos++, value >> 16);
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 2:
|
||||
this.helperView.setUint8(pos++, value >> 8);
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 1:
|
||||
this.helperView.setUint8(pos++, value);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Bad unsigned int size ' + width);
|
||||
}
|
||||
|
||||
this.writer.write(this.helper.subarray(0, pos));
|
||||
}
|
||||
|
||||
writeSignedInt(value: number, width = measureSignedInt(value)) {
|
||||
if (value < 0) {
|
||||
// Two's complement stuff
|
||||
value += 2 ** (width * 8);
|
||||
}
|
||||
|
||||
this.writeUnsignedInt(value, width);
|
||||
}
|
||||
|
||||
writeVarInt(value: number, width = measureVarInt(value)) {
|
||||
let pos = 0;
|
||||
|
||||
switch (width) {
|
||||
case 1:
|
||||
this.helperView.setUint8(pos++, (1 << 7) | value);
|
||||
break;
|
||||
case 2:
|
||||
this.helperView.setUint8(pos++, (1 << 6) | (value >> 8));
|
||||
this.helperView.setUint8(pos++, value);
|
||||
break;
|
||||
case 3:
|
||||
this.helperView.setUint8(pos++, (1 << 5) | (value >> 16));
|
||||
this.helperView.setUint8(pos++, value >> 8);
|
||||
this.helperView.setUint8(pos++, value);
|
||||
break;
|
||||
case 4:
|
||||
this.helperView.setUint8(pos++, (1 << 4) | (value >> 24));
|
||||
this.helperView.setUint8(pos++, value >> 16);
|
||||
this.helperView.setUint8(pos++, value >> 8);
|
||||
this.helperView.setUint8(pos++, value);
|
||||
break;
|
||||
case 5:
|
||||
/**
|
||||
* JavaScript converts its doubles to 32-bit integers for bitwise
|
||||
* operations, so we need to do a division by 2^32 instead of a
|
||||
* right-shift of 32 to retain those top 3 bits
|
||||
*/
|
||||
this.helperView.setUint8(pos++, (1 << 3) | ((value / 2 ** 32) & 0x7));
|
||||
this.helperView.setUint8(pos++, value >> 24);
|
||||
this.helperView.setUint8(pos++, value >> 16);
|
||||
this.helperView.setUint8(pos++, value >> 8);
|
||||
this.helperView.setUint8(pos++, value);
|
||||
break;
|
||||
case 6:
|
||||
this.helperView.setUint8(pos++, (1 << 2) | ((value / 2 ** 40) & 0x3));
|
||||
this.helperView.setUint8(pos++, (value / 2 ** 32) | 0);
|
||||
this.helperView.setUint8(pos++, value >> 24);
|
||||
this.helperView.setUint8(pos++, value >> 16);
|
||||
this.helperView.setUint8(pos++, value >> 8);
|
||||
this.helperView.setUint8(pos++, value);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Bad EBML varint size ' + width);
|
||||
}
|
||||
|
||||
this.writer.write(this.helper.subarray(0, pos));
|
||||
}
|
||||
|
||||
// Assumes the string is ASCII
|
||||
writeString(str: string) {
|
||||
this.writer.write(new Uint8Array(str.split('').map(x => x.charCodeAt(0))));
|
||||
}
|
||||
|
||||
writeEBML(data: EBML | null) {
|
||||
if (data === null) return;
|
||||
|
||||
if (data instanceof Uint8Array) {
|
||||
this.writer.write(data);
|
||||
} else if (Array.isArray(data)) {
|
||||
for (const elem of data) {
|
||||
this.writeEBML(elem);
|
||||
}
|
||||
} else {
|
||||
this.offsets.set(data, this.writer.getPos());
|
||||
|
||||
this.writeUnsignedInt(data.id); // ID field
|
||||
|
||||
if (Array.isArray(data.data)) {
|
||||
const sizePos = this.writer.getPos();
|
||||
const sizeSize = data.size === -1 ? 1 : (data.size ?? 4);
|
||||
|
||||
if (data.size === -1) {
|
||||
// Write the reserved all-one-bits marker for unknown/unbounded size.
|
||||
this.writeByte(0xff);
|
||||
} else {
|
||||
this.writer.seek(this.writer.getPos() + sizeSize);
|
||||
}
|
||||
|
||||
const startPos = this.writer.getPos();
|
||||
this.dataOffsets.set(data, startPos);
|
||||
this.writeEBML(data.data);
|
||||
|
||||
if (data.size !== -1) {
|
||||
const size = this.writer.getPos() - startPos;
|
||||
const endPos = this.writer.getPos();
|
||||
this.writer.seek(sizePos);
|
||||
this.writeVarInt(size, sizeSize);
|
||||
this.writer.seek(endPos);
|
||||
}
|
||||
} else if (typeof data.data === 'number') {
|
||||
const size = data.size ?? measureUnsignedInt(data.data);
|
||||
this.writeVarInt(size);
|
||||
this.writeUnsignedInt(data.data, size);
|
||||
} else if (typeof data.data === 'string') {
|
||||
this.writeVarInt(data.data.length);
|
||||
this.writeString(data.data);
|
||||
} else if (data.data instanceof Uint8Array) {
|
||||
this.writeVarInt(data.data.byteLength, data.size);
|
||||
this.writer.write(data.data);
|
||||
} else if (data.data instanceof EBMLFloat32) {
|
||||
this.writeVarInt(4);
|
||||
this.writeFloat32(data.data.value);
|
||||
} else if (data.data instanceof EBMLFloat64) {
|
||||
this.writeVarInt(8);
|
||||
this.writeFloat64(data.data.value);
|
||||
} else if (data.data instanceof EBMLSignedInt) {
|
||||
const size = data.size ?? measureSignedInt(data.data.value);
|
||||
this.writeVarInt(size);
|
||||
this.writeSignedInt(data.data.value, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_VAR_INT_SIZE = 8;
|
||||
export const MIN_HEADER_SIZE = 2; // 1-byte ID and 1-byte size
|
||||
export const MAX_HEADER_SIZE = 4 + MAX_VAR_INT_SIZE; // 4-byte ID and 8-byte size
|
||||
|
||||
export class EBMLReader {
|
||||
pos = 0;
|
||||
|
||||
constructor(public reader: Reader) {}
|
||||
|
||||
readBytes(length: number) {
|
||||
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length);
|
||||
this.pos += length;
|
||||
|
||||
return new Uint8Array(view.buffer, offset, length);
|
||||
}
|
||||
|
||||
readU8() {
|
||||
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 1);
|
||||
this.pos++;
|
||||
|
||||
return view.getUint8(offset);
|
||||
}
|
||||
|
||||
readS16() {
|
||||
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 2);
|
||||
this.pos += 2;
|
||||
|
||||
return view.getInt16(offset, false);
|
||||
}
|
||||
|
||||
readVarIntSize() {
|
||||
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 1);
|
||||
const firstByte = view.getUint8(offset);
|
||||
|
||||
let width = 1;
|
||||
let mask = 0x80;
|
||||
while ((firstByte & mask) === 0 && width < 8) {
|
||||
width++;
|
||||
mask >>= 1;
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
readVarInt() {
|
||||
// Read the first byte to determine the width of the variable-length integer
|
||||
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 1);
|
||||
const firstByte = view.getUint8(offset);
|
||||
|
||||
// Find the position of the first set bit, which determines the width
|
||||
let width = 1;
|
||||
let mask = 0x80;
|
||||
while ((firstByte & mask) === 0 && width < MAX_VAR_INT_SIZE) {
|
||||
width++;
|
||||
mask >>= 1;
|
||||
}
|
||||
|
||||
// Read all bytes
|
||||
const { view: fullView, offset: fullOffset } = this.reader.getViewAndOffset(this.pos, this.pos + width);
|
||||
|
||||
// First byte's value needs the marker bit cleared
|
||||
let value = firstByte & (mask - 1);
|
||||
|
||||
// Read remaining bytes
|
||||
for (let i = 1; i < width; i++) {
|
||||
value = (value << 8) | fullView.getUint8(fullOffset + i);
|
||||
}
|
||||
|
||||
this.pos += width;
|
||||
return value;
|
||||
}
|
||||
|
||||
readUnsignedInt(width: number) {
|
||||
if (width < 1 || width > 8) {
|
||||
throw new Error('Bad unsigned int size ' + width);
|
||||
}
|
||||
|
||||
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + width);
|
||||
let value = 0;
|
||||
|
||||
// Read bytes from most significant to least significant
|
||||
for (let i = 0; i < width; i++) {
|
||||
value = (value << 8) | view.getUint8(offset + i);
|
||||
}
|
||||
|
||||
this.pos += width;
|
||||
return value;
|
||||
}
|
||||
|
||||
readSignedInt(width: number) {
|
||||
let value = this.readUnsignedInt(width);
|
||||
|
||||
// If the highest bit is set, convert from two's complement
|
||||
if (value & (1 << (width * 8 - 1))) {
|
||||
value -= 2 ** (width * 8);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
readFloat(width: number) {
|
||||
if (width === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (width !== 4 && width !== 8) {
|
||||
throw new Error('Bad FLOAT size ' + width);
|
||||
}
|
||||
|
||||
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + width);
|
||||
const value = width === 4 ? view.getFloat32(offset, false) : view.getFloat64(offset, false);
|
||||
|
||||
this.pos += width;
|
||||
return value;
|
||||
}
|
||||
|
||||
readString(length: number) {
|
||||
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length);
|
||||
this.pos += length;
|
||||
|
||||
return String.fromCharCode(...new Uint8Array(view.buffer, offset, length));
|
||||
}
|
||||
|
||||
readElementId() {
|
||||
const size = this.readVarIntSize();
|
||||
const id = this.readUnsignedInt(size);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
readElementSize() {
|
||||
let size = this.readU8();
|
||||
|
||||
if (size === 0xff) {
|
||||
size = -1;
|
||||
} else {
|
||||
this.pos--;
|
||||
size = this.readVarInt();
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
readElementHeader() {
|
||||
const id = this.readElementId();
|
||||
const size = this.readElementSize();
|
||||
|
||||
return { id, size };
|
||||
}
|
||||
}
|
||||
|
||||
export const CODEC_STRING_MAP: Partial<Record<MediaCodec, string>> = {
|
||||
'avc': 'V_MPEG4/ISO/AVC',
|
||||
'hevc': 'V_MPEGH/ISO/HEVC',
|
||||
'vp8': 'V_VP8',
|
||||
'vp9': 'V_VP9',
|
||||
'av1': 'V_AV1',
|
||||
|
||||
'aac': 'A_AAC',
|
||||
'mp3': 'A_MPEG/L3',
|
||||
'opus': 'A_OPUS',
|
||||
'vorbis': 'A_VORBIS',
|
||||
'flac': 'A_FLAC',
|
||||
'pcm-u8': 'A_PCM/INT/LIT',
|
||||
'pcm-s16': 'A_PCM/INT/LIT',
|
||||
'pcm-s16be': 'A_PCM/INT/BIG',
|
||||
'pcm-s24': 'A_PCM/INT/LIT',
|
||||
'pcm-s24be': 'A_PCM/INT/BIG',
|
||||
'pcm-s32': 'A_PCM/INT/LIT',
|
||||
'pcm-s32be': 'A_PCM/INT/BIG',
|
||||
'pcm-f32': 'A_PCM/FLOAT/IEEE',
|
||||
|
||||
'webvtt': 'S_TEXT/WEBVTT',
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+25
-230
@@ -10,15 +10,14 @@ import {
|
||||
writeBits,
|
||||
} from '../misc';
|
||||
import {
|
||||
CODEC_STRING_MAP,
|
||||
EBML,
|
||||
EBMLElement,
|
||||
EBMLFloat32,
|
||||
EBMLFloat64,
|
||||
EBMLId,
|
||||
EBMLSignedInt,
|
||||
measureEBMLVarInt,
|
||||
measureSignedInt,
|
||||
measureUnsignedInt,
|
||||
EBMLWriter,
|
||||
} from './ebml';
|
||||
import { MkvOutputFormat, WebMOutputFormat } from '../output-format';
|
||||
import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from '../output';
|
||||
@@ -31,11 +30,8 @@ import {
|
||||
parseSubtitleTimestamp,
|
||||
} from '../subtitles';
|
||||
import {
|
||||
AudioCodec,
|
||||
PCM_CODECS,
|
||||
PcmAudioCodec,
|
||||
SubtitleCodec,
|
||||
VideoCodec,
|
||||
parsePcmCodec,
|
||||
validateAudioChunkMetadata,
|
||||
validateSubtitleMetadata,
|
||||
@@ -105,30 +101,6 @@ type MatroskaVideoTrackData = MatroskaTrackData & { type: 'video' };
|
||||
type MatroskaAudioTrackData = MatroskaTrackData & { type: 'audio' };
|
||||
type MatroskaSubtitleTrackData = MatroskaTrackData & { type: 'subtitle' };
|
||||
|
||||
const CODEC_STRING_MAP: Partial<Record<VideoCodec | AudioCodec | SubtitleCodec, string>> = {
|
||||
'avc': 'V_MPEG4/ISO/AVC',
|
||||
'hevc': 'V_MPEGH/ISO/HEVC',
|
||||
'vp8': 'V_VP8',
|
||||
'vp9': 'V_VP9',
|
||||
'av1': 'V_AV1',
|
||||
|
||||
'aac': 'A_AAC',
|
||||
'mp3': 'A_MPEG/L3',
|
||||
'opus': 'A_OPUS',
|
||||
'vorbis': 'A_VORBIS',
|
||||
'flac': 'A_FLAC',
|
||||
'pcm-u8': 'A_PCM/INT/LIT',
|
||||
'pcm-s16': 'A_PCM/INT/LIT',
|
||||
'pcm-s16be': 'A_PCM/INT/BIG',
|
||||
'pcm-s24': 'A_PCM/INT/LIT',
|
||||
'pcm-s24be': 'A_PCM/INT/BIG',
|
||||
'pcm-s32': 'A_PCM/INT/LIT',
|
||||
'pcm-s32be': 'A_PCM/INT/BIG',
|
||||
'pcm-f32': 'A_PCM/FLOAT/IEEE',
|
||||
|
||||
'webvtt': 'S_TEXT/WEBVTT',
|
||||
};
|
||||
|
||||
const TRACK_TYPE_MAP: Record<OutputTrack['type'], number> = {
|
||||
video: 1,
|
||||
audio: 2,
|
||||
@@ -137,19 +109,9 @@ const TRACK_TYPE_MAP: Record<OutputTrack['type'], number> = {
|
||||
|
||||
export class MatroskaMuxer extends Muxer {
|
||||
private writer: Writer;
|
||||
private ebmlWriter: EBMLWriter;
|
||||
private format: WebMOutputFormat | MkvOutputFormat;
|
||||
|
||||
private helper = new Uint8Array(8);
|
||||
private helperView = new DataView(this.helper.buffer);
|
||||
|
||||
/**
|
||||
* Stores the position from the start of the file to where EBML elements have been written. This is used to
|
||||
* rewrite/edit elements that were already added before, and to measure sizes of things.
|
||||
*/
|
||||
private offsets = new WeakMap<EBML, number>();
|
||||
/** Same as offsets, but stores position where the element's data starts (after ID and size fields). */
|
||||
private dataOffsets = new WeakMap<EBML, number>();
|
||||
|
||||
private trackDatas: MatroskaTrackData[] = [];
|
||||
|
||||
private segment: EBMLElement | null = null;
|
||||
@@ -171,180 +133,13 @@ export class MatroskaMuxer extends Muxer {
|
||||
this.writer = output._writer;
|
||||
this.format = format;
|
||||
|
||||
this.ebmlWriter = new EBMLWriter(this.writer);
|
||||
|
||||
if (this.format._options.streamable) {
|
||||
this.writer.ensureMonotonicity = true;
|
||||
}
|
||||
}
|
||||
|
||||
private writeByte(value: number) {
|
||||
this.helperView.setUint8(0, value);
|
||||
this.writer.write(this.helper.subarray(0, 1));
|
||||
}
|
||||
|
||||
private writeFloat32(value: number) {
|
||||
this.helperView.setFloat32(0, value, false);
|
||||
this.writer.write(this.helper.subarray(0, 4));
|
||||
}
|
||||
|
||||
private writeFloat64(value: number) {
|
||||
this.helperView.setFloat64(0, value, false);
|
||||
this.writer.write(this.helper);
|
||||
}
|
||||
|
||||
private writeUnsignedInt(value: number, width = measureUnsignedInt(value)) {
|
||||
let pos = 0;
|
||||
|
||||
// Each case falls through:
|
||||
switch (width) {
|
||||
case 6:
|
||||
// Need to use division to access >32 bits of floating point var
|
||||
this.helperView.setUint8(pos++, (value / 2 ** 40) | 0);
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 5:
|
||||
this.helperView.setUint8(pos++, (value / 2 ** 32) | 0);
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 4:
|
||||
this.helperView.setUint8(pos++, value >> 24);
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 3:
|
||||
this.helperView.setUint8(pos++, value >> 16);
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 2:
|
||||
this.helperView.setUint8(pos++, value >> 8);
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 1:
|
||||
this.helperView.setUint8(pos++, value);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Bad UINT size ' + width);
|
||||
}
|
||||
|
||||
this.writer.write(this.helper.subarray(0, pos));
|
||||
}
|
||||
|
||||
private writeSignedInt(value: number, width = measureSignedInt(value)) {
|
||||
if (value < 0) {
|
||||
// Two's complement stuff
|
||||
value += 2 ** (width * 8);
|
||||
}
|
||||
|
||||
this.writeUnsignedInt(value, width);
|
||||
}
|
||||
|
||||
writeEBMLVarInt(value: number, width = measureEBMLVarInt(value)) {
|
||||
let pos = 0;
|
||||
|
||||
switch (width) {
|
||||
case 1:
|
||||
this.helperView.setUint8(pos++, (1 << 7) | value);
|
||||
break;
|
||||
case 2:
|
||||
this.helperView.setUint8(pos++, (1 << 6) | (value >> 8));
|
||||
this.helperView.setUint8(pos++, value);
|
||||
break;
|
||||
case 3:
|
||||
this.helperView.setUint8(pos++, (1 << 5) | (value >> 16));
|
||||
this.helperView.setUint8(pos++, value >> 8);
|
||||
this.helperView.setUint8(pos++, value);
|
||||
break;
|
||||
case 4:
|
||||
this.helperView.setUint8(pos++, (1 << 4) | (value >> 24));
|
||||
this.helperView.setUint8(pos++, value >> 16);
|
||||
this.helperView.setUint8(pos++, value >> 8);
|
||||
this.helperView.setUint8(pos++, value);
|
||||
break;
|
||||
case 5:
|
||||
/**
|
||||
* JavaScript converts its doubles to 32-bit integers for bitwise
|
||||
* operations, so we need to do a division by 2^32 instead of a
|
||||
* right-shift of 32 to retain those top 3 bits
|
||||
*/
|
||||
this.helperView.setUint8(pos++, (1 << 3) | ((value / 2 ** 32) & 0x7));
|
||||
this.helperView.setUint8(pos++, value >> 24);
|
||||
this.helperView.setUint8(pos++, value >> 16);
|
||||
this.helperView.setUint8(pos++, value >> 8);
|
||||
this.helperView.setUint8(pos++, value);
|
||||
break;
|
||||
case 6:
|
||||
this.helperView.setUint8(pos++, (1 << 2) | ((value / 2 ** 40) & 0x3));
|
||||
this.helperView.setUint8(pos++, (value / 2 ** 32) | 0);
|
||||
this.helperView.setUint8(pos++, value >> 24);
|
||||
this.helperView.setUint8(pos++, value >> 16);
|
||||
this.helperView.setUint8(pos++, value >> 8);
|
||||
this.helperView.setUint8(pos++, value);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Bad EBML VINT size ' + width);
|
||||
}
|
||||
|
||||
this.writer.write(this.helper.subarray(0, pos));
|
||||
}
|
||||
|
||||
// Assumes the string is ASCII
|
||||
private writeString(str: string) {
|
||||
this.writer.write(new Uint8Array(str.split('').map(x => x.charCodeAt(0))));
|
||||
}
|
||||
|
||||
private writeEBML(data: EBML | null) {
|
||||
if (data === null) return;
|
||||
|
||||
if (data instanceof Uint8Array) {
|
||||
this.writer.write(data);
|
||||
} else if (Array.isArray(data)) {
|
||||
for (const elem of data) {
|
||||
this.writeEBML(elem);
|
||||
}
|
||||
} else {
|
||||
this.offsets.set(data, this.writer.getPos());
|
||||
|
||||
this.writeUnsignedInt(data.id); // ID field
|
||||
|
||||
if (Array.isArray(data.data)) {
|
||||
const sizePos = this.writer.getPos();
|
||||
const sizeSize = data.size === -1 ? 1 : (data.size ?? 4);
|
||||
|
||||
if (data.size === -1) {
|
||||
// Write the reserved all-one-bits marker for unknown/unbounded size.
|
||||
this.writeByte(0xff);
|
||||
} else {
|
||||
this.writer.seek(this.writer.getPos() + sizeSize);
|
||||
}
|
||||
|
||||
const startPos = this.writer.getPos();
|
||||
this.dataOffsets.set(data, startPos);
|
||||
this.writeEBML(data.data);
|
||||
|
||||
if (data.size !== -1) {
|
||||
const size = this.writer.getPos() - startPos;
|
||||
const endPos = this.writer.getPos();
|
||||
this.writer.seek(sizePos);
|
||||
this.writeEBMLVarInt(size, sizeSize);
|
||||
this.writer.seek(endPos);
|
||||
}
|
||||
} else if (typeof data.data === 'number') {
|
||||
const size = data.size ?? measureUnsignedInt(data.data);
|
||||
this.writeEBMLVarInt(size);
|
||||
this.writeUnsignedInt(data.data, size);
|
||||
} else if (typeof data.data === 'string') {
|
||||
this.writeEBMLVarInt(data.data.length);
|
||||
this.writeString(data.data);
|
||||
} else if (data.data instanceof Uint8Array) {
|
||||
this.writeEBMLVarInt(data.data.byteLength, data.size);
|
||||
this.writer.write(data.data);
|
||||
} else if (data.data instanceof EBMLFloat32) {
|
||||
this.writeEBMLVarInt(4);
|
||||
this.writeFloat32(data.data.value);
|
||||
} else if (data.data instanceof EBMLFloat64) {
|
||||
this.writeEBMLVarInt(8);
|
||||
this.writeFloat64(data.data.value);
|
||||
} else if (data.data instanceof EBMLSignedInt) {
|
||||
const size = data.size ?? measureSignedInt(data.data.value);
|
||||
this.writeEBMLVarInt(size);
|
||||
this.writeSignedInt(data.data.value, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
const release = await this.mutex.acquire();
|
||||
|
||||
@@ -372,7 +167,7 @@ export class MatroskaMuxer extends Muxer {
|
||||
{ id: EBMLId.DocTypeVersion, data: 2 },
|
||||
{ id: EBMLId.DocTypeReadVersion, data: 2 },
|
||||
] };
|
||||
this.writeEBML(ebmlHeader);
|
||||
this.ebmlWriter.writeEBML(ebmlHeader);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -522,7 +317,7 @@ export class MatroskaMuxer extends Muxer {
|
||||
};
|
||||
this.segment = segment;
|
||||
|
||||
this.writeEBML(segment);
|
||||
this.ebmlWriter.writeEBML(segment);
|
||||
|
||||
/*
|
||||
if (this.#writer instanceof BaseStreamTargetWriter && this.#writer.target.options.onHeader) {
|
||||
@@ -538,7 +333,7 @@ export class MatroskaMuxer extends Muxer {
|
||||
|
||||
private get segmentDataOffset() {
|
||||
assert(this.segment);
|
||||
return this.dataOffsets.get(this.segment)!;
|
||||
return this.ebmlWriter.dataOffsets.get(this.segment)!;
|
||||
}
|
||||
|
||||
private getVideoTrackData(track: OutputVideoTrack, meta?: EncodedVideoChunkMetadata) {
|
||||
@@ -857,7 +652,7 @@ export class MatroskaMuxer extends Muxer {
|
||||
prelude,
|
||||
chunk.data,
|
||||
] };
|
||||
this.writeEBML(simpleBlock);
|
||||
this.ebmlWriter.writeEBML(simpleBlock);
|
||||
} else {
|
||||
const blockGroup = { id: EBMLId.BlockGroup, data: [
|
||||
{ id: EBMLId.Block, data: [
|
||||
@@ -880,7 +675,7 @@ export class MatroskaMuxer extends Muxer {
|
||||
: null,
|
||||
msDuration > 0 ? { id: EBMLId.BlockDuration, data: msDuration } : null,
|
||||
] };
|
||||
this.writeEBML(blockGroup);
|
||||
this.ebmlWriter.writeEBML(blockGroup);
|
||||
}
|
||||
|
||||
this.duration = Math.max(this.duration, msTimestamp + msDuration);
|
||||
@@ -908,7 +703,7 @@ export class MatroskaMuxer extends Muxer {
|
||||
{ id: EBMLId.Timestamp, data: msTimestamp },
|
||||
],
|
||||
};
|
||||
this.writeEBML(this.currentCluster);
|
||||
this.ebmlWriter.writeEBML(this.currentCluster);
|
||||
|
||||
this.currentClusterMsTimestamp = msTimestamp;
|
||||
this.trackDatasInCurrentCluster.clear();
|
||||
@@ -916,12 +711,12 @@ export class MatroskaMuxer extends Muxer {
|
||||
|
||||
private finalizeCurrentCluster() {
|
||||
assert(this.currentCluster);
|
||||
const clusterSize = this.writer.getPos() - this.dataOffsets.get(this.currentCluster)!;
|
||||
const clusterSize = this.writer.getPos() - this.ebmlWriter.dataOffsets.get(this.currentCluster)!;
|
||||
const endPos = this.writer.getPos();
|
||||
|
||||
// Write the size now that we know it
|
||||
this.writer.seek(this.offsets.get(this.currentCluster)! + 4);
|
||||
this.writeEBMLVarInt(clusterSize, CLUSTER_SIZE_BYTES);
|
||||
this.writer.seek(this.ebmlWriter.offsets.get(this.currentCluster)! + 4);
|
||||
this.ebmlWriter.writeVarInt(clusterSize, CLUSTER_SIZE_BYTES);
|
||||
this.writer.seek(endPos);
|
||||
|
||||
/*
|
||||
@@ -932,7 +727,7 @@ export class MatroskaMuxer extends Muxer {
|
||||
*/
|
||||
|
||||
const clusterOffsetFromSegment
|
||||
= this.offsets.get(this.currentCluster)! - this.segmentDataOffset;
|
||||
= this.ebmlWriter.offsets.get(this.currentCluster)! - this.segmentDataOffset;
|
||||
|
||||
assert(this.cues);
|
||||
|
||||
@@ -980,31 +775,31 @@ export class MatroskaMuxer extends Muxer {
|
||||
}
|
||||
|
||||
assert(this.cues);
|
||||
this.writeEBML(this.cues);
|
||||
this.ebmlWriter.writeEBML(this.cues);
|
||||
|
||||
if (!this.format._options.streamable) {
|
||||
const endPos = this.writer.getPos();
|
||||
|
||||
// Write the Segment size
|
||||
const segmentSize = this.writer.getPos() - this.segmentDataOffset;
|
||||
this.writer.seek(this.offsets.get(this.segment!)! + 4);
|
||||
this.writeEBMLVarInt(segmentSize, SEGMENT_SIZE_BYTES);
|
||||
this.writer.seek(this.ebmlWriter.offsets.get(this.segment!)! + 4);
|
||||
this.ebmlWriter.writeVarInt(segmentSize, SEGMENT_SIZE_BYTES);
|
||||
|
||||
// Write the duration of the media to the Segment
|
||||
this.segmentDuration!.data = new EBMLFloat64(this.duration);
|
||||
this.writer.seek(this.offsets.get(this.segmentDuration!)!);
|
||||
this.writeEBML(this.segmentDuration);
|
||||
this.writer.seek(this.ebmlWriter.offsets.get(this.segmentDuration!)!);
|
||||
this.ebmlWriter.writeEBML(this.segmentDuration);
|
||||
|
||||
// Fill in SeekHead position data and write it again
|
||||
this.seekHead!.data[0]!.data[1]!.data
|
||||
= this.offsets.get(this.cues)! - this.segmentDataOffset;
|
||||
= this.ebmlWriter.offsets.get(this.cues)! - this.segmentDataOffset;
|
||||
this.seekHead!.data[1]!.data[1]!.data
|
||||
= this.offsets.get(this.segmentInfo!)! - this.segmentDataOffset;
|
||||
= this.ebmlWriter.offsets.get(this.segmentInfo!)! - this.segmentDataOffset;
|
||||
this.seekHead!.data[2]!.data[1]!.data
|
||||
= this.offsets.get(this.tracksElement!)! - this.segmentDataOffset;
|
||||
= this.ebmlWriter.offsets.get(this.tracksElement!)! - this.segmentDataOffset;
|
||||
|
||||
this.writer.seek(this.offsets.get(this.seekHead!)!);
|
||||
this.writeEBML(this.seekHead);
|
||||
this.writer.seek(this.ebmlWriter.offsets.get(this.seekHead!)!);
|
||||
this.ebmlWriter.writeEBML(this.seekHead);
|
||||
|
||||
this.writer.seek(endPos);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user