mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 19:03:46 +02:00
Merge branch 'main' into hls
This commit is contained in:
+2
-2
@@ -1096,6 +1096,8 @@ export class Conversion {
|
||||
}
|
||||
|
||||
if (needsRerender) {
|
||||
outputTrackRotation = 0; // Since the rotation is baked into the output
|
||||
|
||||
this._trackPromises.push((async () => {
|
||||
await this._started;
|
||||
|
||||
@@ -1111,8 +1113,6 @@ export class Conversion {
|
||||
const iterator = sink.canvases(this._startTimestamp, this._endTimestamp);
|
||||
const frameRate = trackOptions.frameRate;
|
||||
|
||||
outputTrackRotation = 0; // Since the rotation is baked into the output
|
||||
|
||||
let lastCanvas: HTMLCanvasElement | OffscreenCanvas | null = null;
|
||||
let lastCanvasTimestamp: number | null = null;
|
||||
let lastCanvasEndTimestamp: number | null = null;
|
||||
|
||||
@@ -292,13 +292,38 @@ export class FlacDemuxer extends Demuxer {
|
||||
// --> 6 bytes
|
||||
const minimumHeaderLength = 6;
|
||||
// If we read everything in readFlacFrameHeader, we read 16 bytes
|
||||
const maximumHeaderSize = 16;
|
||||
const maximumHeaderLength = 16;
|
||||
|
||||
// The shortest valid FLAC frame per RFC 9639:
|
||||
// 6 bytes header (see minimumHeaderLength above)
|
||||
// 2 bytes subframe (constant subframe with minimum bit depth,
|
||||
// padded to byte boundary)
|
||||
// 2 bytes footer (CRC-16)
|
||||
// --> 10 bytes
|
||||
const minimumFrameLength = 10;
|
||||
|
||||
// The longest valid FLAC frame per RFC 9639:
|
||||
// https://www.rfc-editor.org/rfc/rfc9639.html#name-prediction
|
||||
// https://www.rfc-editor.org/rfc/rfc9639.html#name-frame-structure
|
||||
// maximumBlockSize * numberOfChannels * 4 bytes (max 32 bps verbatim)
|
||||
// + 16 bytes header (see maximumHeaderSize above)
|
||||
// + 2 bytes footer (CRC-16)
|
||||
const maximumFrameLength = this.audioInfo.maximumBlockSize
|
||||
* this.audioInfo.numberOfChannels
|
||||
* 4
|
||||
+ maximumHeaderLength
|
||||
+ 2;
|
||||
|
||||
// Per RFC 9639, a value of 0 means "unknown" for frame sizes.
|
||||
const effectiveMinFrameSize = this.audioInfo.minimumFrameSize || minimumFrameLength;
|
||||
const effectiveMaxFrameSize = this.audioInfo.maximumFrameSize || maximumFrameLength;
|
||||
|
||||
const maximumSliceLength
|
||||
= this.audioInfo.maximumFrameSize + maximumHeaderSize;
|
||||
= effectiveMaxFrameSize + maximumHeaderLength;
|
||||
|
||||
const slice = await this.reader.requestSliceRange(
|
||||
startPos,
|
||||
this.audioInfo.minimumFrameSize,
|
||||
maximumHeaderLength,
|
||||
maximumSliceLength,
|
||||
);
|
||||
|
||||
@@ -321,7 +346,7 @@ export class FlacDemuxer extends Demuxer {
|
||||
|
||||
// The next sync word is expected at earliest when `minimumFrameSize` is reached,
|
||||
// we can skip over anything before that
|
||||
slice.filePos = startPos + this.audioInfo.minimumFrameSize;
|
||||
slice.filePos = startPos + effectiveMinFrameSize;
|
||||
|
||||
while (true) {
|
||||
// Reached end of the file, packet is over
|
||||
|
||||
+70
-36
@@ -49,6 +49,10 @@ export class FlacMuxer extends Muxer {
|
||||
super(output);
|
||||
|
||||
this.format = format;
|
||||
|
||||
if (this.format._options.appendOnly) {
|
||||
this.writer.ensureMonotonicity = true;
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
@@ -169,7 +173,9 @@ export class FlacMuxer extends Muxer {
|
||||
}
|
||||
|
||||
writeVorbisCommentAndPictureBlock() {
|
||||
this.writer.seek(STREAMINFO_SIZE + FLAC_HEADER.byteLength);
|
||||
if (!this.format._options.appendOnly) {
|
||||
this.writer.seek(STREAMINFO_SIZE + FLAC_HEADER.byteLength);
|
||||
}
|
||||
if (metadataTagsAreEmpty(this.output._metadataTags)) {
|
||||
this.metadataWritten = true;
|
||||
return;
|
||||
@@ -237,6 +243,30 @@ export class FlacMuxer extends Muxer {
|
||||
descriptionBitstream.skipBits(103 + 64);
|
||||
const bitsPerSample = descriptionBitstream.readBits(5) + 1;
|
||||
this.bitsPerSample = bitsPerSample;
|
||||
|
||||
if (this.format._options.appendOnly) {
|
||||
// Write STREAMINFO immediately since we can't seek back later.
|
||||
this.writeHeader({
|
||||
// https://www.rfc-editor.org/rfc/rfc9639.html#name-streaminfo
|
||||
// Per RFC 9639, min/max block sizes can be looser than
|
||||
// actual values, so we use the full valid range (16–65535).
|
||||
// "The actual max block size MAY be smaller than what's
|
||||
// listed, and the actual min (excluding last block) MAY be
|
||||
// larger. This is because the encoder has to write these
|
||||
// fields before receiving any input audio data and cannot
|
||||
// know beforehand what block sizes it will use."
|
||||
minimumBlockSize: 16,
|
||||
maximumBlockSize: 65535,
|
||||
// https://www.rfc-editor.org/rfc/rfc9639.html#name-streaminfo
|
||||
// "A value of 0 signifies that the value is not known."
|
||||
minimumFrameSize: 0,
|
||||
maximumFrameSize: 0,
|
||||
sampleRate: this.sampleRate,
|
||||
channels: this.channels,
|
||||
bitsPerSample: this.bitsPerSample,
|
||||
totalSamples: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.metadataWritten) {
|
||||
@@ -255,8 +285,10 @@ export class FlacMuxer extends Muxer {
|
||||
readCodedNumber(slice); // num
|
||||
const blockSize = readBlockSize(slice, blockSizeOrUncommon);
|
||||
|
||||
this.blockSizes.push(blockSize);
|
||||
this.frameSizes.push(packet.data.length);
|
||||
if (!this.format._options.appendOnly) {
|
||||
this.blockSizes.push(blockSize);
|
||||
this.frameSizes.push(packet.data.length);
|
||||
}
|
||||
|
||||
const startPos = this.writer.getPos();
|
||||
this.writer.write(packet.data);
|
||||
@@ -278,43 +310,45 @@ export class FlacMuxer extends Muxer {
|
||||
async finalize(): Promise<void> {
|
||||
const release = await this.mutex.acquire();
|
||||
|
||||
let minimumBlockSize = Infinity;
|
||||
let maximumBlockSize = 0;
|
||||
let minimumFrameSize = Infinity;
|
||||
let maximumFrameSize = 0;
|
||||
let totalSamples = 0;
|
||||
for (let i = 0; i < this.blockSizes.length; i++) {
|
||||
minimumFrameSize = Math.min(minimumFrameSize, this.frameSizes[i]!);
|
||||
maximumFrameSize = Math.max(maximumFrameSize, this.frameSizes[i]!);
|
||||
maximumBlockSize = Math.max(maximumBlockSize, this.blockSizes[i]!);
|
||||
totalSamples += this.blockSizes[i]!;
|
||||
if (!this.format._options.appendOnly) {
|
||||
let minimumBlockSize = Infinity;
|
||||
let maximumBlockSize = 0;
|
||||
let minimumFrameSize = Infinity;
|
||||
let maximumFrameSize = 0;
|
||||
let totalSamples = 0;
|
||||
for (let i = 0; i < this.blockSizes.length; i++) {
|
||||
minimumFrameSize = Math.min(minimumFrameSize, this.frameSizes[i]!);
|
||||
maximumFrameSize = Math.max(maximumFrameSize, this.frameSizes[i]!);
|
||||
maximumBlockSize = Math.max(maximumBlockSize, this.blockSizes[i]!);
|
||||
totalSamples += this.blockSizes[i]!;
|
||||
|
||||
// Excluding the last frame from block size calculation
|
||||
// https://www.rfc-editor.org/rfc/rfc9639.html#name-streaminfo
|
||||
// "The minimum block size (in samples) used in the stream, excluding the last block."
|
||||
const isLastFrame = i === this.blockSizes.length - 1;
|
||||
if (isLastFrame) {
|
||||
continue;
|
||||
// Excluding the last frame from block size calculation
|
||||
// https://www.rfc-editor.org/rfc/rfc9639.html#name-streaminfo
|
||||
// "The minimum block size (in samples) used in the stream, excluding the last block."
|
||||
const isLastFrame = i === this.blockSizes.length - 1;
|
||||
if (isLastFrame) {
|
||||
continue;
|
||||
}
|
||||
minimumBlockSize = Math.min(minimumBlockSize, this.blockSizes[i]!);
|
||||
}
|
||||
minimumBlockSize = Math.min(minimumBlockSize, this.blockSizes[i]!);
|
||||
|
||||
assert(this.sampleRate !== null);
|
||||
assert(this.channels !== null);
|
||||
assert(this.bitsPerSample !== null);
|
||||
|
||||
this.writer.seek(4);
|
||||
this.writeHeader({
|
||||
minimumBlockSize,
|
||||
maximumBlockSize,
|
||||
minimumFrameSize,
|
||||
maximumFrameSize,
|
||||
sampleRate: this.sampleRate,
|
||||
channels: this.channels,
|
||||
bitsPerSample: this.bitsPerSample,
|
||||
totalSamples,
|
||||
});
|
||||
}
|
||||
|
||||
assert(this.sampleRate !== null);
|
||||
assert(this.channels !== null);
|
||||
assert(this.bitsPerSample !== null);
|
||||
|
||||
this.writer.seek(4);
|
||||
this.writeHeader({
|
||||
minimumBlockSize,
|
||||
maximumBlockSize,
|
||||
minimumFrameSize,
|
||||
maximumFrameSize,
|
||||
sampleRate: this.sampleRate,
|
||||
channels: this.channels,
|
||||
bitsPerSample: this.bitsPerSample,
|
||||
totalSamples,
|
||||
});
|
||||
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3077,15 +3077,23 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
|
||||
this.internalTrack.info.av1CodecInfo = firstPacket && extractAv1CodecInfoFromPacket(firstPacket.data);
|
||||
}
|
||||
|
||||
return {
|
||||
const config: VideoDecoderConfig = {
|
||||
codec: extractVideoCodecString(this.internalTrack.info),
|
||||
codedWidth: this.internalTrack.info.width,
|
||||
codedHeight: this.internalTrack.info.height,
|
||||
displayAspectWidth: this.internalTrack.info.squarePixelWidth,
|
||||
displayAspectHeight: this.internalTrack.info.squarePixelHeight,
|
||||
description: this.internalTrack.info.codecDescription ?? undefined,
|
||||
colorSpace: this.internalTrack.info.colorSpace ?? undefined,
|
||||
};
|
||||
|
||||
if (
|
||||
this.internalTrack.info.width !== this.internalTrack.info.squarePixelWidth
|
||||
|| this.internalTrack.info.height !== this.internalTrack.info.squarePixelHeight
|
||||
) {
|
||||
config.displayAspectWidth = this.internalTrack.info.squarePixelWidth;
|
||||
config.displayAspectHeight = this.internalTrack.info.squarePixelHeight;
|
||||
}
|
||||
|
||||
return config;
|
||||
})();
|
||||
}
|
||||
}
|
||||
@@ -3299,22 +3307,16 @@ const offsetFragmentTrackDataByTimestamp = (trackData: FragmentTrackData, timest
|
||||
|
||||
/** Extracts the rotation component from a transformation matrix, in degrees. */
|
||||
const extractRotationFromMatrix = (matrix: TransformationMatrix) => {
|
||||
const [m11, , , m21] = matrix;
|
||||
const [a, b] = matrix; // (1, 0) projects onto (a, b), so that's all we need
|
||||
|
||||
const scaleX = Math.hypot(m11, m21);
|
||||
const radians = Math.atan2(b, a);
|
||||
|
||||
const cosTheta = m11 / scaleX;
|
||||
const sinTheta = m21 / scaleX;
|
||||
|
||||
// Invert the rotation because matrices are post-multiplied in ISOBMFF
|
||||
const result = -Math.atan2(sinTheta, cosTheta) * (180 / Math.PI);
|
||||
|
||||
if (!Number.isFinite(result)) {
|
||||
if (!Number.isFinite(radians)) {
|
||||
// Can happen if the entire matrix is 0, for example
|
||||
return 0;
|
||||
}
|
||||
|
||||
return result;
|
||||
return radians * (180 / Math.PI);
|
||||
};
|
||||
|
||||
const sampleTableIsEmpty = (sampleTable: SampleTable) => {
|
||||
|
||||
@@ -2467,7 +2467,7 @@ class MatroskaVideoTrackBacking extends MatroskaTrackBacking implements InputVid
|
||||
firstPacket = await this.getFirstPacket({});
|
||||
}
|
||||
|
||||
return {
|
||||
const config: VideoDecoderConfig = {
|
||||
codec: extractVideoCodecString({
|
||||
width: this.internalTrack.info.width,
|
||||
height: this.internalTrack.info.height,
|
||||
@@ -2490,11 +2490,19 @@ class MatroskaVideoTrackBacking extends MatroskaTrackBacking implements InputVid
|
||||
}),
|
||||
codedWidth: this.internalTrack.info.width,
|
||||
codedHeight: this.internalTrack.info.height,
|
||||
displayAspectWidth: this.internalTrack.info.squarePixelWidth,
|
||||
displayAspectHeight: this.internalTrack.info.squarePixelHeight,
|
||||
description: this.internalTrack.info.codecDescription ?? undefined,
|
||||
colorSpace: this.internalTrack.info.colorSpace ?? undefined,
|
||||
};
|
||||
|
||||
if (
|
||||
this.internalTrack.info.width !== this.internalTrack.info.squarePixelWidth
|
||||
|| this.internalTrack.info.height !== this.internalTrack.info.squarePixelHeight
|
||||
) {
|
||||
config.displayAspectWidth = this.internalTrack.info.squarePixelWidth;
|
||||
config.displayAspectHeight = this.internalTrack.info.squarePixelHeight;
|
||||
}
|
||||
|
||||
return config;
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,12 +250,13 @@ export class MpegTsDemuxer extends Demuxer {
|
||||
while (8 * (sectionLength + BYTES_BEFORE_SECTION_LENGTH) - bitstream.pos > BITS_IN_CRC_32) {
|
||||
const programNumber = bitstream.readBits(16);
|
||||
bitstream.skipBits(3); // Reserved
|
||||
const id = bitstream.readBits(13);
|
||||
|
||||
if (programNumber !== 0) {
|
||||
if (programMapPid !== null) {
|
||||
throw new Error('Only files with a single program are supported.');
|
||||
} else {
|
||||
programMapPid = bitstream.readBits(13);
|
||||
programMapPid = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -577,10 +578,19 @@ export class MpegTsDemuxer extends Demuxer {
|
||||
}),
|
||||
codedWidth: elementaryStream.info.width,
|
||||
codedHeight: elementaryStream.info.height,
|
||||
displayAspectWidth: elementaryStream.info.squarePixelWidth,
|
||||
displayAspectHeight: elementaryStream.info.squarePixelHeight,
|
||||
colorSpace: elementaryStream.info.colorSpace,
|
||||
};
|
||||
|
||||
if (
|
||||
elementaryStream.info.width !== elementaryStream.info.squarePixelWidth
|
||||
|| elementaryStream.info.height !== elementaryStream.info.squarePixelHeight
|
||||
) {
|
||||
elementaryStream.info.decoderConfig.displayAspectWidth
|
||||
= elementaryStream.info.squarePixelWidth;
|
||||
elementaryStream.info.decoderConfig.displayAspectHeight
|
||||
= elementaryStream.info.squarePixelHeight;
|
||||
}
|
||||
|
||||
elementaryStream.initialized = true;
|
||||
} else {
|
||||
await context.markNextPacket();
|
||||
|
||||
@@ -929,6 +929,13 @@ export class AdtsOutputFormat extends OutputFormat {
|
||||
* @public
|
||||
*/
|
||||
export type FlacOutputFormatOptions = {
|
||||
/**
|
||||
* Configures the output to only append new data at the end, useful for live-streaming the file as it's being
|
||||
* created. When enabled, the STREAMINFO block will not be finalized with accurate min/max block sizes, frame sizes,
|
||||
* or total sample count, so don't use this option when you want to write out a clean file for later use.
|
||||
*/
|
||||
appendOnly?: boolean;
|
||||
|
||||
/**
|
||||
* Will be called for each FLAC frame that is written.
|
||||
*
|
||||
@@ -952,6 +959,9 @@ export class FlacOutputFormat extends OutputFormat {
|
||||
if (!options || typeof options !== 'object') {
|
||||
throw new TypeError('options must be an object.');
|
||||
}
|
||||
if (options.appendOnly !== undefined && typeof options.appendOnly !== 'boolean') {
|
||||
throw new TypeError('options.appendOnly, when provided, must be a boolean.');
|
||||
}
|
||||
|
||||
super();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user