diff --git a/dev/demux.html b/dev/demux.html
index 8d02ae1..8b06ec0 100644
--- a/dev/demux.html
+++ b/dev/demux.html
@@ -14,7 +14,18 @@
source: new Mediabunny.BlobSource(file),
});
- console.log(await input.getTracks());
+ const audioTrack = await input.getPrimaryAudioTrack();
+ const decoderConfig = await audioTrack.getDecoderConfig();
+ console.log(decoderConfig);
+
+ /*
+ const videoTrack = await input.getPrimaryVideoTrack();
+ const sink = new Mediabunny.EncodedPacketSink(videoTrack);
+
+ for await (const packet of sink.packets()) {
+ console.log(packet);
+ }
+ */
/*
return;
diff --git a/package-lock.json b/package-lock.json
index 2af63f6..2b55d39 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "mediabunny",
- "version": "1.25.3",
+ "version": "1.25.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mediabunny",
- "version": "1.25.3",
+ "version": "1.25.4",
"license": "MPL-2.0",
"workspaces": [
"packages/*"
@@ -7739,9 +7739,9 @@
}
},
"node_modules/mediabunny": {
- "version": "1.25.2",
- "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.25.2.tgz",
- "integrity": "sha512-bS+lFdHsvvWUGK8ARDzhHnjmzLDPLmRfwlqKiXSCk+cdlyjKpiSHdAaKmrI6+2hD9Tmeqr39OQJk67nG/kAJAQ==",
+ "version": "1.25.3",
+ "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.25.3.tgz",
+ "integrity": "sha512-+LDXv/kybsElRQCmMlrdbKmPNvegeIyouGeOUzW5DJ8+M56H4G6wyEL2AJ1A45UcOB+U62qJMV7tuFO9S7yA7g==",
"license": "MPL-2.0",
"peer": true,
"workspaces": [
@@ -12065,7 +12065,7 @@
},
"packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder",
- "version": "1.25.3",
+ "version": "1.25.4",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
diff --git a/package.json b/package.json
index 6d1861b..bcffe30 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "mediabunny",
"author": "Vanilagy",
- "version": "1.25.3",
+ "version": "1.25.4",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module",
"workspaces": [
diff --git a/packages/mp3-encoder/package.json b/packages/mp3-encoder/package.json
index a6b8f53..5e60596 100644
--- a/packages/mp3-encoder/package.json
+++ b/packages/mp3-encoder/package.json
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/mp3-encoder",
"author": "Vanilagy",
- "version": "1.25.3",
+ "version": "1.25.4",
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
diff --git a/src/codec-data.ts b/src/codec-data.ts
index eef04ec..03e1b24 100644
--- a/src/codec-data.ts
+++ b/src/codec-data.ts
@@ -590,9 +590,7 @@ export const extractNalUnitTypeForHevc = (data: Uint8Array) => {
};
/** Builds a HevcDecoderConfigurationRecord from an HEVC packet in Annex B format. */
-export const extractHevcDecoderConfigurationRecord = (
- packetData: Uint8Array,
-) => {
+export const extractHevcDecoderConfigurationRecord = (packetData: Uint8Array) => {
try {
const nalUnits = findNalUnitsInAnnexB(packetData);
@@ -1456,6 +1454,69 @@ export const extractAv1CodecInfoFromPacket = (
}
}
+ // Frame size
+ const frameWidthBitsMinus1 = bitstream.readBits(4);
+ const frameHeightBitsMinus1 = bitstream.readBits(4);
+ const n1 = frameWidthBitsMinus1 + 1;
+ bitstream.skipBits(n1); // max_frame_width_minus_1
+ const n2 = frameHeightBitsMinus1 + 1;
+ bitstream.skipBits(n2); // max_frame_height_minus_1
+
+ // Frame IDs
+ let frameIdNumbersPresentFlag = 0;
+ if (reducedStillPictureHeader) {
+ frameIdNumbersPresentFlag = 0;
+ } else {
+ frameIdNumbersPresentFlag = bitstream.readBits(1);
+ }
+
+ if (frameIdNumbersPresentFlag) {
+ bitstream.skipBits(4); // delta_frame_id_length_minus_2
+ bitstream.skipBits(3); // additional_frame_id_length_minus_1
+ }
+
+ bitstream.skipBits(1); // use_128x128_superblock
+ bitstream.skipBits(1); // enable_filter_intra
+ bitstream.skipBits(1); // enable_intra_edge_filter
+
+ if (!reducedStillPictureHeader) {
+ bitstream.skipBits(1); // enable_interintra_compound
+ bitstream.skipBits(1); // enable_masked_compound
+ bitstream.skipBits(1); // enable_warped_motion
+ bitstream.skipBits(1); // enable_dual_filter
+ const enableOrderHint = bitstream.readBits(1);
+
+ if (enableOrderHint) {
+ bitstream.skipBits(1); // enable_jnt_comp
+ bitstream.skipBits(1); // enable_ref_frame_mvs
+ }
+
+ const seqChooseScreenContentTools = bitstream.readBits(1);
+ let seqForceScreenContentTools = 0;
+
+ if (seqChooseScreenContentTools) {
+ seqForceScreenContentTools = 2; // SELECT_SCREEN_CONTENT_TOOLS
+ } else {
+ seqForceScreenContentTools = bitstream.readBits(1);
+ }
+
+ if (seqForceScreenContentTools > 0) {
+ const seqChooseIntegerMv = bitstream.readBits(1);
+ if (!seqChooseIntegerMv) {
+ bitstream.skipBits(1); // seq_force_integer_mv
+ }
+ }
+
+ if (enableOrderHint) {
+ bitstream.skipBits(3); // order_hint_bits_minus_1
+ }
+ }
+
+ bitstream.skipBits(1); // enable_superres
+ bitstream.skipBits(1); // enable_cdef
+ bitstream.skipBits(1); // enable_restoration
+
+ // color_config()
const highBitdepth = bitstream.readBits(1);
let bitDepth = 8;
diff --git a/src/conversion.ts b/src/conversion.ts
index 33bbd4d..40ca946 100644
--- a/src/conversion.ts
+++ b/src/conversion.ts
@@ -1509,7 +1509,10 @@ export class Conversion {
targetSampleRate,
startTime: this._startTimestamp,
endTime: this._endTimestamp,
- onSample: sample => this._registerAudioSample(track, trackOptions, source, sample),
+ onSample: async (sample) => {
+ await this._registerAudioSample(track, trackOptions, source, sample);
+ sample.close();
+ },
});
const sink = new AudioSampleSink(track);
@@ -1521,6 +1524,7 @@ export class Conversion {
}
await resampler.add(sample);
+ sample.close();
}
await resampler.finalize();
diff --git a/src/sample.ts b/src/sample.ts
index ddd4527..3c8ba36 100644
--- a/src/sample.ts
+++ b/src/sample.ts
@@ -21,6 +21,54 @@ import {
polyfillSymbolDispose();
+type FinalizationRegistryValue = {
+ type: 'video';
+ data: VideoFrame | OffscreenCanvas | Uint8Array;
+} | {
+ type: 'audio';
+ data: AudioData | Uint8Array;
+};
+
+// Let's manually handle logging the garbage collection errors that are typically logged by the browser. This way, they
+// also kick for audio samples (which is normally not the case), making sure any incorrect code is quickly caught.
+let lastVideoGcErrorLog = -Infinity;
+let lastAudioGcErrorLog = -Infinity;
+let finalizationRegistry: FinalizationRegistry | null = null;
+if (typeof FinalizationRegistry !== 'undefined') {
+ finalizationRegistry = new FinalizationRegistry((value) => {
+ const now = Date.now();
+
+ if (value.type === 'video') {
+ if (now - lastVideoGcErrorLog >= 1000) {
+ // This error is annoying but oh so important
+ console.error(
+ `A VideoSample was garbage collected without first being closed. For proper resource management,`
+ + ` make sure to call close() on all your VideoSamples as soon as you're done using them.`,
+ );
+
+ lastVideoGcErrorLog = now;
+ }
+
+ if (typeof VideoFrame !== 'undefined' && value.data instanceof VideoFrame) {
+ value.data.close(); // Prevent the browser error since we're logging our own
+ }
+ } else {
+ if (now - lastAudioGcErrorLog >= 1000) {
+ console.error(
+ `An AudioSample was garbage collected without first being closed. For proper resource management,`
+ + ` make sure to call close() on all your AudioSamples as soon as you're done using them.`,
+ );
+
+ lastAudioGcErrorLog = now;
+ }
+
+ if (typeof AudioData !== 'undefined' && value.data instanceof AudioData) {
+ value.data.close();
+ }
+ }
+ });
+}
+
/**
* Metadata used for VideoSample initialization.
* @group Samples
@@ -133,7 +181,11 @@ export class VideoSample implements Disposable {
data: VideoFrame | CanvasImageSource | AllowSharedBufferSource,
init?: VideoSampleInit,
) {
- if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
+ if (
+ data instanceof ArrayBuffer
+ || (typeof SharedArrayBuffer !== 'undefined' && data instanceof SharedArrayBuffer)
+ || ArrayBuffer.isView(data)
+ ) {
if (!init || typeof init !== 'object') {
throw new TypeError('init must be an object.');
}
@@ -265,6 +317,8 @@ export class VideoSample implements Disposable {
} else {
throw new TypeError('Invalid data type: Must be a BufferSource or CanvasImageSource.');
}
+
+ finalizationRegistry?.register(this, { type: 'video', data: this._data }, this);
}
/** Clones this video sample. */
@@ -313,6 +367,8 @@ export class VideoSample implements Disposable {
return;
}
+ finalizationRegistry?.unregister(this);
+
if (isVideoFrame(this._data)) {
this._data.close();
} else {
@@ -853,8 +909,8 @@ export type AudioSampleCopyToOptions = {
*/
export class AudioSample implements Disposable {
/** @internal */
- _data: AudioData | Uint8Array;
/** @internal */
+ _data: AudioData | Uint8Array;
_closed: boolean = false;
/**
@@ -955,6 +1011,8 @@ export class AudioSample implements Disposable {
this._data = dataBuffer;
}
+
+ finalizationRegistry?.register(this, { type: 'audio', data: this._data }, this);
}
/** Returns the number of bytes required to hold the audio sample's data as specified by the given options. */
@@ -1116,9 +1174,8 @@ export class AudioSample implements Disposable {
}
}
} else {
- // Branch for Uint8Array data (non-AudioData)
const uint8Data = this._data;
- const srcView = new DataView(uint8Data.buffer, uint8Data.byteOffset, uint8Data.byteLength);
+ const srcView = toDataView(uint8Data);
const srcFormat = this.format;
const readFn = getReadFunction(srcFormat);
@@ -1188,6 +1245,8 @@ export class AudioSample implements Disposable {
return;
}
+ finalizationRegistry?.unregister(this);
+
if (isAudioData(this._data)) {
this._data.close();
} else {