mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-09 00:33:46 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d702409dd3 | ||
|
|
cf45c10ff2 | ||
|
|
e03bb21c8a | ||
|
|
0b330cb513 | ||
|
|
92c7831cf4 | ||
|
|
9e9240dfc3 | ||
|
|
fd39084e88 | ||
|
|
31d86331ec | ||
|
|
0d50526ff6 | ||
|
|
e693765d23 | ||
|
|
1da96f6d69 | ||
|
|
92ea645df4 | ||
|
|
35e16ca23e | ||
|
|
87ed3a1934 |
+3
-1
@@ -104,6 +104,8 @@
|
||||
},
|
||||
*/
|
||||
video: () => ({
|
||||
//width: 640,
|
||||
//rotate: 90,
|
||||
//forceTranscode: true,
|
||||
//forceTranscode: true,
|
||||
//width: 1280,
|
||||
@@ -189,7 +191,7 @@
|
||||
},
|
||||
trim: {
|
||||
//start: 0,
|
||||
//end: 10
|
||||
end: 2
|
||||
},
|
||||
});
|
||||
console.log(conversion);
|
||||
|
||||
+5
-1
@@ -21,7 +21,11 @@
|
||||
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
|
||||
|
||||
for await (const packet of sink.packets()) {
|
||||
console.log(packet.timestamp);
|
||||
console.log(packet.type, packet.timestamp);
|
||||
|
||||
if (packet.timestamp >= 5) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -303,9 +303,16 @@ const output = new Output({
|
||||
The following options are available:
|
||||
```ts
|
||||
type FlacOutputFormatOptions = {
|
||||
appendOnly?: boolean;
|
||||
|
||||
onFrame?: (data: Uint8Array, position: number) => unknown;
|
||||
};
|
||||
```
|
||||
- `appendOnly`\
|
||||
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.
|
||||
::: info
|
||||
This option ensures [append-only writing](#append-only-writing).
|
||||
:::
|
||||
- `onFrame`\
|
||||
Will be called for each FLAC frame that is written.
|
||||
|
||||
|
||||
@@ -109,9 +109,11 @@ const sponsors = {
|
||||
],
|
||||
individual: [
|
||||
{ image: '/sponsors/pintura-labs.png', name: 'Pintura Labs', url: 'https://pqina.nl/pintura/' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/11537072', name: 'Threema', url: 'https://threema.com/en' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/82552321', name: 'Polotno', url: 'https://github.com/polotno-project' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/489051', name: 'Roman Rädle', url: 'https://github.com/raedle' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/197597', name: 'Christopher Chedeau', url: 'https://github.com/vjeux' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/18653821', name: 'ZerGo0', url: 'https://github.com/ZerGo0' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/5913254', name: 'Brandon McConnell', url: 'https://github.com/brandonmcconnell' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/9549394', name: 'studnitz', url: 'https://github.com/studnitz' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/4714175', name: 'Phoomparin Mano', url: 'https://github.com/heypoom' },
|
||||
@@ -119,6 +121,7 @@ const sponsors = {
|
||||
{ image: 'https://avatars.githubusercontent.com/u/2698271', name: 'Matthew Gardner', url: 'https://github.com/spheric' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/5475819', name: 'AJ Funk', url: 'https://github.com/AJFunk' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/30229596', name: 'Pablo Bonilla', url: 'https://github.com/devPablo' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/536008', name: 'Satish Goda', url: 'https://github.com/satishgoda' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/139718', name: 'Anton Kosiakin', url: 'https://github.com/deil' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/56988069', name: 'SyhabouthAlex', url: 'https://github.com/SyhabouthAlex' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/38181164', name: 'wcw', url: 'https://github.com/asd55667' },
|
||||
@@ -135,6 +138,7 @@ const sponsors = {
|
||||
{ image: 'https://avatars.githubusercontent.com/u/31102694', name: 'Aiden Liu', url: 'https://github.com/aidenlx' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/41021374', name: 'arthco', url: 'https://github.com/arthtyagi' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/5907357', name: 'Harvey Zhao', url: 'https://github.com/zhw2590582' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/13042781?v=4', name: 'Anshul Bansal', url: 'https://github.com/akbansa' },
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
Generated
+9
-9
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"version": "1.39.1",
|
||||
"version": "1.40.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mediabunny",
|
||||
"version": "1.39.1",
|
||||
"version": "1.40.1",
|
||||
"license": "MPL-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
@@ -7751,9 +7751,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mediabunny": {
|
||||
"version": "1.39.0",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.39.0.tgz",
|
||||
"integrity": "sha512-NDeht2Sf7kYfrUu7b/9AhVK4KdQhy6UeTF/EPNXW+PdpIYvSv36ZQ/SRBoFAJN2LRl6XrGb8JYY4MCnhsGQ2yg==",
|
||||
"version": "1.40.0",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.40.0.tgz",
|
||||
"integrity": "sha512-UztWnjkA15yYxqq7AC8MSsU4U6FIFIBHvHV94pouvjxQa0y2pGvi1HPmrFcaYrRZAuSKTrdw45VXUIe3gEEpsA==",
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"workspaces": [
|
||||
@@ -12077,7 +12077,7 @@
|
||||
},
|
||||
"packages/aac-encoder": {
|
||||
"name": "@mediabunny/aac-encoder",
|
||||
"version": "1.39.1",
|
||||
"version": "1.40.1",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
@@ -12092,7 +12092,7 @@
|
||||
},
|
||||
"packages/ac3": {
|
||||
"name": "@mediabunny/ac3",
|
||||
"version": "1.39.1",
|
||||
"version": "1.40.1",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
@@ -12107,7 +12107,7 @@
|
||||
},
|
||||
"packages/flac-encoder": {
|
||||
"name": "@mediabunny/flac-encoder",
|
||||
"version": "1.39.1",
|
||||
"version": "1.40.1",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
@@ -12122,7 +12122,7 @@
|
||||
},
|
||||
"packages/mp3-encoder": {
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"version": "1.39.1",
|
||||
"version": "1.40.1",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.39.1",
|
||||
"version": "1.40.1",
|
||||
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/aac-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.39.1",
|
||||
"version": "1.40.1",
|
||||
"description": "AAC encoder extension for Mediabunny, based on FFmpeg.",
|
||||
"main": "./dist/bundles/mediabunny-aac-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-aac-encoder.mjs",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/ac3",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.39.1",
|
||||
"version": "1.40.1",
|
||||
"description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on FFmpeg.",
|
||||
"main": "./dist/bundles/mediabunny-ac3.mjs",
|
||||
"module": "./dist/bundles/mediabunny-ac3.mjs",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/flac-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.39.1",
|
||||
"version": "1.40.1",
|
||||
"description": "FLAC encoder extension for Mediabunny, based on libFLAC.",
|
||||
"main": "./dist/bundles/mediabunny-flac-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-flac-encoder.mjs",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.39.1",
|
||||
"version": "1.40.1",
|
||||
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
|
||||
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
|
||||
+2
-2
@@ -1086,6 +1086,8 @@ export class Conversion {
|
||||
}
|
||||
|
||||
if (needsRerender) {
|
||||
outputTrackRotation = 0; // Since the rotation is baked into the output
|
||||
|
||||
this._trackPromises.push((async () => {
|
||||
await this._started;
|
||||
|
||||
@@ -1101,8 +1103,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;
|
||||
|
||||
@@ -283,13 +283,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,
|
||||
);
|
||||
|
||||
@@ -312,7 +337,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
@@ -50,6 +50,10 @@ export class FlacMuxer extends Muxer {
|
||||
|
||||
this.writer = output._writer;
|
||||
this.format = format;
|
||||
|
||||
if (this.format._options.appendOnly) {
|
||||
this.writer.ensureMonotonicity = true;
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
@@ -165,7 +169,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;
|
||||
@@ -233,6 +239,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) {
|
||||
@@ -251,8 +281,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);
|
||||
@@ -274,43 +306,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();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -530,7 +530,7 @@ export class MpegTsInputFormat extends InputFormat {
|
||||
} else if (bytes[0] === 0x47 && bytes[TS_PACKET_SIZE + 16] === 0x47) {
|
||||
// MPEG-TS with Forward Error Correction
|
||||
return true;
|
||||
} else if (bytes[4] === 0x47 && bytes[4 + TS_PACKET_SIZE] === 0x47) {
|
||||
} else if (bytes[4] === 0x47 && bytes[4 + TS_PACKET_SIZE + 4] === 0x47) {
|
||||
// MPEG-2-TS (DVHS)
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2947,15 +2947,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;
|
||||
})();
|
||||
}
|
||||
}
|
||||
@@ -3169,22 +3177,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) => {
|
||||
|
||||
@@ -196,6 +196,7 @@ type InternalTrack = {
|
||||
defaultDurationNs: number | null;
|
||||
name: string | null;
|
||||
languageCode: string;
|
||||
hasLanguageBcp47: boolean;
|
||||
decodingInstructions: DecodingInstruction[];
|
||||
|
||||
info:
|
||||
@@ -1009,7 +1010,8 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
defaultDuration: null,
|
||||
defaultDurationNs: null,
|
||||
name: null,
|
||||
languageCode: UNDETERMINED_LANGUAGE,
|
||||
languageCode: 'eng', // The default in Matroska
|
||||
hasLanguageBcp47: false,
|
||||
decodingInstructions: [],
|
||||
|
||||
info: null,
|
||||
@@ -1086,11 +1088,7 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
const inputTrack = new InputVideoTrack(this.input, new MatroskaVideoTrackBacking(videoTrack));
|
||||
this.currentTrack.inputTrack = inputTrack;
|
||||
this.currentSegment.tracks.push(this.currentTrack);
|
||||
} else if (
|
||||
this.currentTrack.info.type === 'audio'
|
||||
&& this.currentTrack.info.numberOfChannels !== -1
|
||||
&& this.currentTrack.info.sampleRate !== -1
|
||||
) {
|
||||
} else if (this.currentTrack.info.type === 'audio') {
|
||||
if (codecIdWithoutSuffix === CODEC_STRING_MAP.aac) {
|
||||
this.currentTrack.info.codec = 'aac';
|
||||
this.currentTrack.info.aacCodecInfo = {
|
||||
@@ -1183,8 +1181,8 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
} else if (type === 2) {
|
||||
this.currentTrack.info = {
|
||||
type: 'audio',
|
||||
numberOfChannels: -1,
|
||||
sampleRate: -1,
|
||||
numberOfChannels: 1, // Default value
|
||||
sampleRate: 8000, // Default value
|
||||
bitDepth: -1,
|
||||
codec: null,
|
||||
codecDescription: null,
|
||||
@@ -1263,7 +1261,7 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
|
||||
case EBMLId.Language: {
|
||||
if (!this.currentTrack) break;
|
||||
if (this.currentTrack.languageCode !== UNDETERMINED_LANGUAGE) {
|
||||
if (this.currentTrack.hasLanguageBcp47) {
|
||||
// LanguageBCP47 was present, which takes precedence
|
||||
break;
|
||||
}
|
||||
@@ -1290,6 +1288,8 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
} else {
|
||||
this.currentTrack.languageCode = UNDETERMINED_LANGUAGE;
|
||||
}
|
||||
|
||||
this.currentTrack.hasLanguageBcp47 = true;
|
||||
}; break;
|
||||
|
||||
case EBMLId.Video: {
|
||||
@@ -2430,7 +2430,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,
|
||||
@@ -2453,11 +2453,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;
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,10 +161,10 @@ export class MpegTsDemuxer extends Demuxer {
|
||||
// MPEG-TS with Forward Error Correction
|
||||
this.packetOffset = 0;
|
||||
this.packetStride = TS_PACKET_SIZE + 16;
|
||||
} else if (startingBytes[4] === 0x47 && startingBytes[4 + TS_PACKET_SIZE] === 0x47) {
|
||||
} else if (startingBytes[4] === 0x47 && startingBytes[4 + TS_PACKET_SIZE + 4] === 0x47) {
|
||||
// MPEG-2-TS (DVHS)
|
||||
this.packetOffset = 4;
|
||||
this.packetStride = TS_PACKET_SIZE;
|
||||
this.packetStride = TS_PACKET_SIZE + 4;
|
||||
} else {
|
||||
throw new Error('Unreachable.');
|
||||
}
|
||||
@@ -236,12 +236,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1031,8 +1032,6 @@ export abstract class MpegTsTrackBacking implements InputTrackBacking {
|
||||
return null;
|
||||
}
|
||||
|
||||
// result.packet.randomAccessIndicator = 1; // Assume the first packet is always a key packet
|
||||
|
||||
const packet = this.createEncodedPacket(result.packet, result.duration, options);
|
||||
this.packetBuffers.set(packet, buffer);
|
||||
this.packetSectionStarts.set(packet, result.packet.sectionStartPos);
|
||||
@@ -1514,10 +1513,16 @@ class MpegTsVideoTrackBacking extends MpegTsTrackBacking implements InputVideoTr
|
||||
}),
|
||||
codedWidth: this.elementaryStream.info.width,
|
||||
codedHeight: this.elementaryStream.info.height,
|
||||
displayAspectWidth: this.elementaryStream.info.squarePixelWidth,
|
||||
displayAspectHeight: this.elementaryStream.info.squarePixelHeight,
|
||||
colorSpace: this.elementaryStream.info.colorSpace,
|
||||
};
|
||||
|
||||
if (
|
||||
this.elementaryStream.info.width !== this.elementaryStream.info.squarePixelWidth
|
||||
|| this.elementaryStream.info.height !== this.elementaryStream.info.squarePixelHeight
|
||||
) {
|
||||
this.decoderConfig.displayAspectWidth = this.elementaryStream.info.squarePixelWidth;
|
||||
this.decoderConfig.displayAspectHeight = this.elementaryStream.info.squarePixelHeight;
|
||||
}
|
||||
}
|
||||
|
||||
override getCodec(): VideoCodec {
|
||||
|
||||
+21
-18
@@ -46,12 +46,16 @@ export abstract class Muxer {
|
||||
|
||||
private trackTimestampInfo = new WeakMap<OutputTrack, {
|
||||
maxTimestamp: number;
|
||||
maxTimestampBeforeLastKeyPacket: number;
|
||||
maxTimestampBeforeLastKeyPacket: number | null;
|
||||
}>();
|
||||
|
||||
protected validateAndNormalizeTimestamp(track: OutputTrack, timestampInSeconds: number, isKeyPacket: boolean) {
|
||||
timestampInSeconds += track.source._timestampOffset;
|
||||
|
||||
if (timestampInSeconds < 0) {
|
||||
throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`);
|
||||
}
|
||||
|
||||
let timestampInfo = this.trackTimestampInfo.get(track);
|
||||
if (!timestampInfo) {
|
||||
if (!isKeyPacket) {
|
||||
@@ -60,29 +64,28 @@ export abstract class Muxer {
|
||||
|
||||
timestampInfo = {
|
||||
maxTimestamp: timestampInSeconds,
|
||||
maxTimestampBeforeLastKeyPacket: timestampInSeconds,
|
||||
maxTimestampBeforeLastKeyPacket: null,
|
||||
};
|
||||
this.trackTimestampInfo.set(track, timestampInfo);
|
||||
}
|
||||
} else {
|
||||
if (isKeyPacket) {
|
||||
timestampInfo.maxTimestampBeforeLastKeyPacket = timestampInfo.maxTimestamp;
|
||||
}
|
||||
|
||||
if (timestampInSeconds < 0) {
|
||||
throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`);
|
||||
}
|
||||
if (
|
||||
timestampInfo.maxTimestampBeforeLastKeyPacket !== null
|
||||
&& timestampInSeconds < timestampInfo.maxTimestampBeforeLastKeyPacket
|
||||
) {
|
||||
throw new Error(
|
||||
`Timestamps cannot be smaller than the largest timestamp of the previous GOP (a GOP begins with a`
|
||||
+ ` key packet and ends right before the next key packet). Got ${timestampInSeconds}s, but largest`
|
||||
+ ` timestamp is ${timestampInfo.maxTimestampBeforeLastKeyPacket}s.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (isKeyPacket) {
|
||||
timestampInfo.maxTimestampBeforeLastKeyPacket = timestampInfo.maxTimestamp;
|
||||
timestampInfo.maxTimestamp = Math.max(timestampInfo.maxTimestamp, timestampInSeconds);
|
||||
}
|
||||
|
||||
if (timestampInSeconds < timestampInfo.maxTimestampBeforeLastKeyPacket) {
|
||||
throw new Error(
|
||||
`Timestamps cannot be smaller than the largest timestamp of the previous GOP (a GOP begins with a key`
|
||||
+ ` packet and ends right before the next key packet). Got ${timestampInSeconds}s, but largest`
|
||||
+ ` timestamp is ${timestampInfo.maxTimestampBeforeLastKeyPacket}s.`,
|
||||
);
|
||||
}
|
||||
|
||||
timestampInfo.maxTimestamp = Math.max(timestampInfo.maxTimestamp, timestampInSeconds);
|
||||
|
||||
return timestampInSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -928,6 +928,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.
|
||||
*
|
||||
@@ -951,6 +958,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();
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ALL_FORMATS } from '../../src/input-format.js';
|
||||
import { Input } from '../../src/input.js';
|
||||
import { Mp4OutputFormat } from '../../src/output-format.js';
|
||||
import { Output } from '../../src/output.js';
|
||||
import { BufferSource, UrlSource } from '../../src/source.js';
|
||||
import { expect, test } from 'vitest';
|
||||
import { BufferTarget } from '../../src/target.js';
|
||||
import { Conversion } from '../../src/conversion.js';
|
||||
import { assert } from '../../src/misc.js';
|
||||
|
||||
test('Rotation is baked-in when rerendering', async () => {
|
||||
using input = new Input({
|
||||
source: new UrlSource('/rotate-buck-bunny.mp4'),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const ogTrack = await input.getPrimaryVideoTrack();
|
||||
assert(ogTrack);
|
||||
|
||||
expect(ogTrack.rotation).toBe(90);
|
||||
expect(ogTrack.codedWidth).toBe(1920);
|
||||
expect(ogTrack.codedHeight).toBe(1080);
|
||||
expect(ogTrack.displayWidth).toBe(1080);
|
||||
expect(ogTrack.displayHeight).toBe(1920);
|
||||
|
||||
const output = new Output({
|
||||
format: new Mp4OutputFormat(),
|
||||
target: new BufferTarget(),
|
||||
});
|
||||
|
||||
const conversion = await Conversion.init({ input, output, video: {
|
||||
width: 320,
|
||||
} });
|
||||
await conversion.execute();
|
||||
|
||||
using newInput = new Input({
|
||||
source: new BufferSource(output.target.buffer!),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const track = await newInput.getPrimaryVideoTrack();
|
||||
assert(track);
|
||||
|
||||
expect(track.codedWidth).toBe(320);
|
||||
expect(track.codedHeight).toBe(570);
|
||||
expect(track.displayWidth).toBe(320);
|
||||
expect(track.displayHeight).toBe(570);
|
||||
expect(track.rotation).toBe(0);
|
||||
});
|
||||
@@ -87,8 +87,8 @@ test('Pixel aspect ratio transcode conversion', async () => {
|
||||
expect(mp4.snapshot.squarePixelHeight).toBe(mp4.snapshot.codedHeight);
|
||||
expect(mp4.snapshot.displayWidth).toBe(mp4.snapshot.codedWidth);
|
||||
expect(mp4.snapshot.displayHeight).toBe(mp4.snapshot.codedHeight);
|
||||
expect(mp4.snapshot.decoderDisplayAspectWidth).toBe(mp4.snapshot.codedWidth);
|
||||
expect(mp4.snapshot.decoderDisplayAspectHeight).toBe(mp4.snapshot.codedHeight);
|
||||
expect(mp4.snapshot.decoderDisplayAspectWidth).toBeUndefined();
|
||||
expect(mp4.snapshot.decoderDisplayAspectHeight).toBeUndefined();
|
||||
|
||||
expect(mp4.snapshot.codedWidth).toBe(source.squarePixelWidth);
|
||||
expect(mp4.snapshot.codedHeight).toBe(source.squarePixelHeight);
|
||||
|
||||
@@ -254,3 +254,33 @@ test('can re-mux a .flac', async () => {
|
||||
|
||||
expect(otherInputDecoderConfig).toEqual(otherOutputDecoderConfig);
|
||||
});
|
||||
|
||||
test('appendOnly writes correct STREAMINFO header', async () => {
|
||||
const filePath = path.join(__dirname, '..', 'public/sample.flac');
|
||||
using input = new Input({
|
||||
source: new FilePathSource(filePath),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const target = new BufferTarget();
|
||||
const output = new Output({
|
||||
format: new FlacOutputFormat({ appendOnly: true }),
|
||||
target,
|
||||
});
|
||||
|
||||
const conversion = await Conversion.init({ input, output });
|
||||
await conversion.execute();
|
||||
|
||||
assert(target.buffer);
|
||||
const bytes = new Uint8Array(target.buffer);
|
||||
|
||||
// STREAMINFO: min_block=16, max_block=65535, min_frame=0, max_frame=0
|
||||
expect(bytes.slice(8, 18)).toEqual(new Uint8Array([
|
||||
// minimum_block_size=16
|
||||
0x00, 0x10,
|
||||
// maximum_block_size=65535
|
||||
0xFF, 0xFF,
|
||||
// minimum_frame_size=0
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]));
|
||||
});
|
||||
|
||||
@@ -46,8 +46,6 @@ test('MPEG-TS metadata reading', async () => {
|
||||
codec: 'avc1.640020',
|
||||
codedWidth: 720,
|
||||
codedHeight: 720,
|
||||
displayAspectWidth: 720,
|
||||
displayAspectHeight: 720,
|
||||
colorSpace: {
|
||||
primaries: 'bt2020',
|
||||
transfer: 'hlg',
|
||||
@@ -561,8 +559,6 @@ test('MPEG-TS with HEVC video', async () => {
|
||||
codec: 'hev1.1.6.L120.90',
|
||||
codedWidth: 1920,
|
||||
codedHeight: 1080,
|
||||
displayAspectWidth: 1920,
|
||||
displayAspectHeight: 1080,
|
||||
colorSpace: {
|
||||
primaries: 'bt709',
|
||||
transfer: 'bt709',
|
||||
|
||||
Binary file not shown.
@@ -6,7 +6,7 @@
|
||||
"composite": true,
|
||||
"noEmit": false,
|
||||
"paths": {
|
||||
"mediabunny": ["./src/index.ts"],
|
||||
//"mediabunny": ["./src/index.ts"], So that the direct source imports are preferred
|
||||
"@mediabunny/ac3": ["./packages/ac3/src/index.ts"],
|
||||
"@mediabunny/aac-encoder": ["./packages/aac-encoder/src/index.ts"],
|
||||
"@mediabunny/flac-encoder": ["./packages/flac-encoder/src/index.ts"],
|
||||
|
||||
Reference in New Issue
Block a user