diff --git a/dev/demux.html b/dev/demux.html
index 5354400..6d93d39 100644
--- a/dev/demux.html
+++ b/dev/demux.html
@@ -17,8 +17,13 @@
source: new Mediabunny.BlobSource(file),
});
- const track = await input.getPrimaryVideoTrack();
- console.log(await track.getDecoderConfig());
+ const track = await input.getPrimaryAudioTrack();
+ const sink = new Mediabunny.AudioSampleSink(track);
+
+ for await (const sample of sink.samples()) {
+ console.log(sample);
+ sample.close()
+ }
/*
diff --git a/package-lock.json b/package-lock.json
index e0c691e..7cfc67e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "mediabunny",
- "version": "1.55.2",
+ "version": "1.55.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mediabunny",
- "version": "1.55.2",
+ "version": "1.55.3",
"license": "MPL-2.0",
"workspaces": [
".",
@@ -12958,7 +12958,7 @@
},
"packages/aac-encoder": {
"name": "@mediabunny/aac-encoder",
- "version": "1.55.2",
+ "version": "1.55.3",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
@@ -12973,7 +12973,7 @@
},
"packages/ac3": {
"name": "@mediabunny/ac3",
- "version": "1.55.2",
+ "version": "1.55.3",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
@@ -12988,7 +12988,7 @@
},
"packages/dts": {
"name": "@mediabunny/dts",
- "version": "1.55.2",
+ "version": "1.55.3",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
@@ -13003,7 +13003,7 @@
},
"packages/flac-encoder": {
"name": "@mediabunny/flac-encoder",
- "version": "1.55.2",
+ "version": "1.55.3",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
@@ -13018,7 +13018,7 @@
},
"packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder",
- "version": "1.55.2",
+ "version": "1.55.3",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
@@ -13033,7 +13033,7 @@
},
"packages/prores": {
"name": "@mediabunny/prores",
- "version": "1.55.2",
+ "version": "1.55.3",
"license": "MPL-2.0",
"dependencies": {
"turbores": "^1.2.2"
@@ -13048,10 +13048,10 @@
},
"packages/server": {
"name": "@mediabunny/server",
- "version": "1.55.2",
+ "version": "1.55.3",
"license": "MPL-2.0",
"dependencies": {
- "@mediabunny/prores": "^1.55.2",
+ "@mediabunny/prores": "^1.55.3",
"node-av": "^6.0.0"
},
"funding": {
diff --git a/package.json b/package.json
index bcec159..526d6a3 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "mediabunny",
"author": "Vanilagy",
- "version": "1.55.2",
+ "version": "1.55.3",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module",
"workspaces": [
diff --git a/packages/aac-encoder/package.json b/packages/aac-encoder/package.json
index cc781a5..9b6b8de 100644
--- a/packages/aac-encoder/package.json
+++ b/packages/aac-encoder/package.json
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/aac-encoder",
"author": "Vanilagy",
- "version": "1.55.2",
+ "version": "1.55.3",
"description": "AAC encoder extension for Mediabunny, based on libavcodec.",
"main": "./dist/bundles/mediabunny-aac-encoder.mjs",
"module": "./dist/bundles/mediabunny-aac-encoder.mjs",
diff --git a/packages/ac3/package.json b/packages/ac3/package.json
index a4980c8..d4ef024 100644
--- a/packages/ac3/package.json
+++ b/packages/ac3/package.json
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/ac3",
"author": "Vanilagy",
- "version": "1.55.2",
+ "version": "1.55.3",
"description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on libavcodec.",
"main": "./dist/bundles/mediabunny-ac3.mjs",
"module": "./dist/bundles/mediabunny-ac3.mjs",
diff --git a/packages/dts/package.json b/packages/dts/package.json
index 6e38903..ed17be8 100644
--- a/packages/dts/package.json
+++ b/packages/dts/package.json
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/dts",
"author": "Vanilagy",
- "version": "1.55.2",
+ "version": "1.55.3",
"description": "DTS decoder and encoder extension for Mediabunny, based on libavcodec.",
"main": "./dist/bundles/mediabunny-dts.mjs",
"module": "./dist/bundles/mediabunny-dts.mjs",
diff --git a/packages/flac-encoder/package.json b/packages/flac-encoder/package.json
index cec00fa..7b037e2 100644
--- a/packages/flac-encoder/package.json
+++ b/packages/flac-encoder/package.json
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/flac-encoder",
"author": "Vanilagy",
- "version": "1.55.2",
+ "version": "1.55.3",
"description": "FLAC encoder extension for Mediabunny, based on libFLAC.",
"main": "./dist/bundles/mediabunny-flac-encoder.mjs",
"module": "./dist/bundles/mediabunny-flac-encoder.mjs",
diff --git a/packages/mp3-encoder/package.json b/packages/mp3-encoder/package.json
index 5b29a83..27fe74b 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.55.2",
+ "version": "1.55.3",
"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/packages/prores/package.json b/packages/prores/package.json
index 40e789b..ba84e35 100644
--- a/packages/prores/package.json
+++ b/packages/prores/package.json
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/prores",
"author": "Vanilagy",
- "version": "1.55.2",
+ "version": "1.55.3",
"description": "Apple ProRes decoder extension for Mediabunny, based on TurboRes.",
"main": "./dist/bundles/mediabunny-prores.mjs",
"module": "./dist/bundles/mediabunny-prores.mjs",
diff --git a/packages/server/package.json b/packages/server/package.json
index 2cb58f1..772a3df 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/server",
"author": "Vanilagy",
- "version": "1.55.2",
+ "version": "1.55.3",
"description": "Adds full video and audio decoder and encoder support to Mediabunny for use in server-side environments (Node, Bun, Deno). Based on NodeAV.",
"main": "./dist/bundles/mediabunny-server.cjs",
"module": "./dist/bundles/mediabunny-server.mjs",
@@ -35,7 +35,7 @@
},
"dependencies": {
"node-av": "^6.0.0",
- "@mediabunny/prores": "^1.55.2"
+ "@mediabunny/prores": "^1.55.3"
},
"peerDependencies": {
"mediabunny": "^1.45.0"
diff --git a/shared/aac-misc.ts b/shared/aac-misc.ts
index 5c0fc71..e1ecaf7 100644
--- a/shared/aac-misc.ts
+++ b/shared/aac-misc.ts
@@ -10,10 +10,11 @@ import { Bitstream } from './bitstream';
export type AacAudioSpecificConfig = {
objectType: number;
+ coreObjectType: number;
frequencyIndex: number;
- sampleRate: number | null;
channelConfiguration: number;
- numberOfChannels: number | null;
+ outputSampleRate: number | null;
+ outputNumberOfChannels: number | null;
};
export const aacFrequencyTable = [
@@ -30,20 +31,8 @@ export const parseAacAudioSpecificConfig = (bytes: Uint8Array | null): AacAudioS
const bitstream = new Bitstream(bytes);
- let objectType = bitstream.readBits(5);
- if (objectType === 31) {
- objectType = 32 + bitstream.readBits(6);
- }
-
- const frequencyIndex = bitstream.readBits(4);
- let sampleRate: number | null = null;
- if (frequencyIndex === 15) {
- sampleRate = bitstream.readBits(24);
- } else {
- if (frequencyIndex < aacFrequencyTable.length) {
- sampleRate = aacFrequencyTable[frequencyIndex]!;
- }
- }
+ const objectType = readAacObjectType(bitstream);
+ const { frequencyIndex, sampleRate } = readAacSamplingFrequency(bitstream);
const channelConfiguration = bitstream.readBits(4);
let numberOfChannels: number | null = null;
@@ -51,61 +40,155 @@ export const parseAacAudioSpecificConfig = (bytes: Uint8Array | null): AacAudioS
numberOfChannels = aacChannelMap[channelConfiguration]!;
}
+ let coreObjectType = objectType;
+ let psPresent = false;
+ let outputSampleRate = sampleRate;
+
+ if (objectType === 5 || objectType === 29) {
+ // Explicit hierarchical signaling: everything read so far describes the core coder, and the rate the
+ // decoder actually outputs follows right here
+ psPresent = objectType === 29;
+
+ outputSampleRate = readAacSamplingFrequency(bitstream).sampleRate;
+ coreObjectType = readAacObjectType(bitstream);
+
+ if (coreObjectType === 22) {
+ bitstream.skipBits(4); // extensionChannelConfiguration
+ }
+ } else {
+ // There may be SBR/PS flags sitting behind a sync word after the config of the core coder. We find them by
+ // scanning for the sync word, just like FFmpeg does.
+ while (bitstream.getBitsLeft() > 15) {
+ const searchStart = bitstream.pos;
+
+ if (bitstream.readBits(11) !== 0x2b7) {
+ bitstream.pos = searchStart + 1;
+ continue;
+ }
+
+ if (readAacObjectType(bitstream) === 5 && bitstream.readBits(1)) {
+ outputSampleRate = readAacSamplingFrequency(bitstream).sampleRate;
+
+ if (bitstream.getBitsLeft() > 11 && bitstream.readBits(11) === 0x548) {
+ psPresent = !!bitstream.readBits(1);
+ }
+ }
+
+ break;
+ }
+ }
+
+ if (numberOfChannels !== null && numberOfChannels > 1) {
+ // PS only ever upmixes a mono core. A stereo signal means whatever we read was bogus
+ psPresent = false;
+ }
+
return {
objectType,
+ coreObjectType,
frequencyIndex,
- sampleRate,
channelConfiguration,
- numberOfChannels,
+ outputSampleRate,
+ outputNumberOfChannels: psPresent && numberOfChannels === 1
+ ? 2
+ : numberOfChannels,
+ };
+};
+
+const readAacObjectType = (bitstream: Bitstream) => {
+ const objectType = bitstream.readBits(5);
+ return objectType === 31 ? 32 + bitstream.readBits(6) : objectType;
+};
+
+const readAacSamplingFrequency = (bitstream: Bitstream) => {
+ const frequencyIndex = bitstream.readBits(4);
+
+ if (frequencyIndex === 15) {
+ return {
+ frequencyIndex,
+ sampleRate: bitstream.readBits(24),
+ };
+ }
+
+ return {
+ frequencyIndex,
+ sampleRate: frequencyIndex < aacFrequencyTable.length
+ ? aacFrequencyTable[frequencyIndex]!
+ : null,
};
};
export const buildAacAudioSpecificConfig = (config: {
objectType: number;
- sampleRate: number;
- numberOfChannels: number;
+ outputSampleRate: number;
+ outputNumberOfChannels: number;
}) => {
- let frequencyIndex = aacFrequencyTable.indexOf(config.sampleRate);
- let customSampleRate: number | null = null;
+ const usesSbr = config.objectType === 5 || config.objectType === 29;
+ const usesPs = config.objectType === 29;
- if (frequencyIndex === -1) {
- frequencyIndex = 15;
- customSampleRate = config.sampleRate;
- }
+ // SBR runs the core coder at half the output rate, and PS upmixes a mono core to stereo
+ const coreSampleRate = usesSbr ? config.outputSampleRate / 2 : config.outputSampleRate;
+ const coreNumberOfChannels = usesPs ? 1 : config.outputNumberOfChannels;
- const channelConfiguration = aacChannelMap.indexOf(config.numberOfChannels);
+ const channelConfiguration = aacChannelMap.indexOf(coreNumberOfChannels);
if (channelConfiguration === -1) {
- throw new TypeError(`Unsupported number of channels: ${config.numberOfChannels}`);
+ throw new TypeError(`Unsupported number of channels: ${config.outputNumberOfChannels}`);
}
- let bitCount = 5 + 4 + 4;
+ // Object type, sampling frequency, channel configuration, then a bare GASpecificConfig
+ let bitCount = 5 + 4 + 4 + 3;
if (config.objectType >= 32) {
bitCount += 6;
}
- if (frequencyIndex === 15) {
+ if (findAacFrequencyIndex(coreSampleRate) === 15) {
bitCount += 24;
}
+ if (usesSbr) {
+ bitCount += 4 + 5; // Extension sampling frequency and the object type of the core coder
+ if (findAacFrequencyIndex(config.outputSampleRate) === 15) {
+ bitCount += 24;
+ }
+ }
const byteCount = Math.ceil(bitCount / 8);
const bytes = new Uint8Array(byteCount);
const bitstream = new Bitstream(bytes);
- if (config.objectType < 32) {
- bitstream.writeBits(5, config.objectType);
- } else {
- bitstream.writeBits(5, 31);
- bitstream.writeBits(6, config.objectType - 32);
+ writeAacObjectType(bitstream, config.objectType);
+ writeAacSamplingFrequency(bitstream, coreSampleRate);
+ bitstream.writeBits(4, channelConfiguration);
+
+ if (usesSbr) {
+ writeAacSamplingFrequency(bitstream, config.outputSampleRate);
+ writeAacObjectType(bitstream, 2); // AAC-LC underneath
}
+ bitstream.writeBits(3, 0); // frameLengthFlag, dependsOnCoreCoder, extensionFlag
+
+ return bytes;
+};
+
+const writeAacObjectType = (bitstream: Bitstream, objectType: number) => {
+ if (objectType < 32) {
+ bitstream.writeBits(5, objectType);
+ } else {
+ bitstream.writeBits(5, 31);
+ bitstream.writeBits(6, objectType - 32);
+ }
+};
+
+const writeAacSamplingFrequency = (bitstream: Bitstream, sampleRate: number) => {
+ const frequencyIndex = findAacFrequencyIndex(sampleRate);
bitstream.writeBits(4, frequencyIndex);
if (frequencyIndex === 15) {
- bitstream.writeBits(24, customSampleRate!);
+ bitstream.writeBits(24, sampleRate);
}
+};
- bitstream.writeBits(4, channelConfiguration);
-
- return bytes;
+const findAacFrequencyIndex = (sampleRate: number) => {
+ const index = aacFrequencyTable.indexOf(sampleRate);
+ return index === -1 ? 15 : index;
};
export type AdtsHeaderTemplate = {
@@ -117,8 +200,8 @@ export const buildAdtsHeaderTemplate = (config: AacAudioSpecificConfig): AdtsHea
const header = new Uint8Array(7);
const bitstream = new Bitstream(header);
- const { objectType, frequencyIndex, channelConfiguration } = config;
- const profile = objectType - 1;
+ const { coreObjectType, frequencyIndex, channelConfiguration } = config;
+ const profile = coreObjectType - 1;
bitstream.writeBits(12, 0b1111_11111111); // Syncword
bitstream.writeBits(1, 0); // MPEG Version
diff --git a/src/adts/adts-demuxer.ts b/src/adts/adts-demuxer.ts
index d39427f..96423e2 100644
--- a/src/adts/adts-demuxer.ts
+++ b/src/adts/adts-demuxer.ts
@@ -23,6 +23,7 @@ import {
AsyncMutex,
binarySearchExact,
binarySearchLessOrEqual,
+ isThenable,
UNDETERMINED_LANGUAGE,
} from '../misc';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
@@ -84,7 +85,7 @@ export class AdtsDemuxer extends Demuxer {
// Skip all ID3v2 tags at the start of the file
while (true) {
let slice = this.reader.requestSlice(this.lastLoadedPos, ID3_V2_HEADER_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) {
this.lastSampleLoaded = true;
@@ -105,7 +106,7 @@ export class AdtsDemuxer extends Demuxer {
MIN_ADTS_FRAME_HEADER_SIZE,
MAX_ADTS_FRAME_HEADER_SIZE,
);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) {
this.lastSampleLoaded = true;
return;
@@ -167,7 +168,7 @@ export class AdtsDemuxer extends Demuxer {
while (true) {
let headerSlice = this.reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
- if (headerSlice instanceof Promise) headerSlice = await headerSlice;
+ if (isThenable(headerSlice)) headerSlice = await headerSlice;
if (!headerSlice) break;
const id3V2Header = readId3V2Header(headerSlice);
@@ -176,7 +177,7 @@ export class AdtsDemuxer extends Demuxer {
}
let contentSlice = this.reader.requestSlice(headerSlice.filePos, id3V2Header.size);
- if (contentSlice instanceof Promise) contentSlice = await contentSlice;
+ if (isThenable(contentSlice)) contentSlice = await contentSlice;
if (!contentSlice) break;
parseId3V2Tag(contentSlice, id3V2Header, this.metadataTags);
@@ -306,7 +307,7 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking {
data = PLACEHOLDER_DATA;
} else {
let slice = this.demuxer.reader.requestSlice(rawSample.dataStart, rawSample.dataSize);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) {
return null; // Data didn't fit into the rest of the file
diff --git a/src/aes.ts b/src/aes.ts
index 4a54618..50bf2b2 100644
--- a/src/aes.ts
+++ b/src/aes.ts
@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
-import { assert, MaybePromise } from './misc';
+import { assert, isThenable, MaybePromise } from './misc';
import { readBytes, Reader } from './reader';
// Inspired in part by https://github.com/halloweeks/AES-128-CBC/blob/main/AES_128_CBC.h
@@ -258,7 +258,7 @@ export const createAes128CbcDecryptStream = (
const requestedLength = CHUNK_SIZE + BLOCK_SIZE;
let nextSlice = reader.requestSliceRange(pos, 0, requestedLength);
- if (nextSlice instanceof Promise) nextSlice = await nextSlice;
+ if (isThenable(nextSlice)) nextSlice = await nextSlice;
if (!nextSlice || nextSlice.length === 0) {
// Due to padding, this should never happen
throw new Error('Invalid ciphertext.');
diff --git a/src/flac/flac-demuxer.ts b/src/flac/flac-demuxer.ts
index 2334ce5..7a581d0 100644
--- a/src/flac/flac-demuxer.ts
+++ b/src/flac/flac-demuxer.ts
@@ -15,6 +15,7 @@ import {
assert,
AsyncMutex,
binarySearchLessOrEqual,
+ isThenable,
textDecoder,
UNDETERMINED_LANGUAGE,
} from '../misc';
@@ -108,7 +109,7 @@ export class FlacDemuxer extends Demuxer {
let currentPos = 0;
while (true) {
let headerSlice = this.reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
- if (headerSlice instanceof Promise) headerSlice = await headerSlice;
+ if (isThenable(headerSlice)) headerSlice = await headerSlice;
if (!headerSlice) {
this.lastSampleLoaded = true;
@@ -121,7 +122,7 @@ export class FlacDemuxer extends Demuxer {
}
let contentSlice = this.reader.requestSlice(headerSlice.filePos, id3V2Header.size);
- if (contentSlice instanceof Promise) contentSlice = await contentSlice;
+ if (isThenable(contentSlice)) contentSlice = await contentSlice;
assert(contentSlice);
parseId3V2Tag(contentSlice, id3V2Header, this.metadataTags);
@@ -136,7 +137,7 @@ export class FlacDemuxer extends Demuxer {
|| currentPos < this.reader.fileSize
) {
let sizeSlice = this.reader.requestSlice(currentPos, 4);
- if (sizeSlice instanceof Promise) sizeSlice = await sizeSlice;
+ if (isThenable(sizeSlice)) sizeSlice = await sizeSlice;
currentPos += 4;
if (sizeSlice === null) {
@@ -160,7 +161,7 @@ export class FlacDemuxer extends Demuxer {
currentPos,
size,
);
- if (streamInfoBlock instanceof Promise) streamInfoBlock = await streamInfoBlock;
+ if (isThenable(streamInfoBlock)) streamInfoBlock = await streamInfoBlock;
assert(streamInfoBlock);
if (streamInfoBlock === null) {
@@ -219,7 +220,7 @@ export class FlacDemuxer extends Demuxer {
currentPos,
size,
);
- if (vorbisCommentBlock instanceof Promise) vorbisCommentBlock = await vorbisCommentBlock;
+ if (isThenable(vorbisCommentBlock)) vorbisCommentBlock = await vorbisCommentBlock;
assert(vorbisCommentBlock);
@@ -237,7 +238,7 @@ export class FlacDemuxer extends Demuxer {
currentPos,
size,
);
- if (pictureBlock instanceof Promise) pictureBlock = await pictureBlock;
+ if (isThenable(pictureBlock)) pictureBlock = await pictureBlock;
assert(pictureBlock);
const pictureType = readU32Be(pictureBlock);
@@ -775,7 +776,7 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
rawSample.byteOffset,
rawSample.byteSize,
);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) {
return null; // Data didn't fit into the rest of the file
diff --git a/src/input-format.ts b/src/input-format.ts
index b61adeb..7a420a3 100644
--- a/src/input-format.ts
+++ b/src/input-format.ts
@@ -29,14 +29,14 @@ import { OggDemuxer } from './ogg/ogg-demuxer';
import { WaveDemuxer } from './wave/wave-demuxer';
import { MAX_ADTS_FRAME_HEADER_SIZE, MIN_ADTS_FRAME_HEADER_SIZE, readAdtsFrameHeader } from './adts/adts-reader';
import { AdtsDemuxer } from './adts/adts-demuxer';
-import { readAscii, readBytes, readU32Be } from './reader';
+import { readAscii, readBytes, readU32Be, readU64Be } from './reader';
import { FlacDemuxer } from './flac/flac-demuxer';
import { MpegTsDemuxer } from './mpeg-ts/mpeg-ts-demuxer';
import { TS_PACKET_SIZE } from './mpeg-ts/mpeg-ts-misc';
import { HlsDemuxer } from './hls/hls-demuxer';
import { HLS_MIME_TYPE } from './hls/hls-misc';
import { PathedSource } from './source';
-import { MaybePromise } from './misc';
+import { isThenable, MaybePromise } from './misc';
/**
* Base class representing an input media file format.
@@ -75,7 +75,7 @@ export abstract class IsobmffInputFormat extends InputFormat {
/** @internal */
protected async _getMajorBrand(input: Input) {
let slice = input._reader.requestSlice(0, 12);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) return null;
slice.skip(4);
@@ -116,12 +116,41 @@ export class Mp4InputFormat extends IsobmffInputFormat {
return majorBrand !== 'qt ';
}
- let slice = input._reader.requestSlice(4, 4);
- if (slice instanceof Promise) slice = await slice;
- if (!slice) return false;
+ let pos = 0;
+ for (let iter = 0; iter < 10; iter++) { // Bounded loop
+ let slice = input._reader.requestSlice(pos, 8);
+ if (isThenable(slice)) slice = await slice;
+ if (!slice) return false;
- const fourCc = readAscii(slice, 4);
- return fourCc === 'moof' || fourCc === 'sidx'; // Seen in HLS for example
+ let size = readU32Be(slice);
+ let headerSize = 8;
+ if (size === 1) {
+ let sizeExtensionSlice = input._reader.requestSlice(pos + 8, 8);
+ if (isThenable(sizeExtensionSlice)) sizeExtensionSlice = await sizeExtensionSlice;
+ if (!sizeExtensionSlice) return false;
+
+ size = readU64Be(sizeExtensionSlice);
+ headerSize = 16;
+ }
+
+ if (size < headerSize) {
+ return false; // Hardly a box is it
+ }
+
+ const fourCc = readAscii(slice, 4);
+ if (fourCc === 'moof' || fourCc === 'sidx') { // Seen in HLS for example
+ return true;
+ } else if (fourCc === 'emsg' || fourCc === 'prft' || fourCc === 'free') {
+ // These are boxes that can appear before the first moof in DASH Media Segments. They're a strong signal
+ // that this is indeed an fMP4 segment, but just to be sure, let's walk the box structure to make sure
+ // it stays sane.
+ pos += size;
+ } else {
+ return false;
+ }
+ }
+
+ return false;
}
get name() {
@@ -169,7 +198,7 @@ export class MatroskaInputFormat extends InputFormat {
/** @internal */
protected async isSupportedEBMLOfDocType(input: Input, desiredDocType: string) {
let headerSlice = input._reader.requestSlice(0, MAX_HEADER_SIZE);
- if (headerSlice instanceof Promise) headerSlice = await headerSlice;
+ if (isThenable(headerSlice)) headerSlice = await headerSlice;
if (!headerSlice) return false;
const varIntSize = readVarIntSize(headerSlice);
@@ -192,7 +221,7 @@ export class MatroskaInputFormat extends InputFormat {
}
let dataSlice = input._reader.requestSlice(headerSlice.filePos, dataSize);
- if (dataSlice instanceof Promise) dataSlice = await dataSlice;
+ if (isThenable(dataSlice)) dataSlice = await dataSlice;
if (!dataSlice) return false;
const startPos = headerSlice.filePos;
@@ -295,7 +324,7 @@ export class Mp3InputFormat extends InputFormat {
while (true) {
let slice = input._reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) break;
const id3V2Header = readId3V2Header(slice);
@@ -315,7 +344,7 @@ export class Mp3InputFormat extends InputFormat {
const xingOffset = getXingOffset(firstHeader.mpegVersionId, firstHeader.channel);
let slice = input._reader.requestSlice(firstResult.startPos + xingOffset, 4);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) return false;
const word = readU32Be(slice);
@@ -376,7 +405,7 @@ export class WaveInputFormat extends InputFormat {
/** @internal */
async _canReadInput(input: Input) {
let slice = input._reader.requestSlice(0, 12);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) return false;
const riffType = readAscii(slice, 4);
@@ -416,7 +445,7 @@ export class OggInputFormat extends InputFormat {
/** @internal */
async _canReadInput(input: Input) {
let slice = input._reader.requestSlice(0, 4);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) return false;
return readAscii(slice, 4) === 'OggS';
@@ -451,7 +480,7 @@ export class FlacInputFormat extends InputFormat {
// There might be ID3v2 headers at the start, skip 'em
while (true) {
let slice = input._reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) break;
const id3V2Header = readId3V2Header(slice);
@@ -463,7 +492,7 @@ export class FlacInputFormat extends InputFormat {
}
let slice = input._reader.requestSlice(currentPos, 4);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) return false;
return readAscii(slice, 4) === 'fLaC';
@@ -498,7 +527,7 @@ export class AdtsInputFormat extends InputFormat {
while (true) {
let slice = input._reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) break;
const id3V2Header = readId3V2Header(slice);
@@ -514,7 +543,7 @@ export class AdtsInputFormat extends InputFormat {
MIN_ADTS_FRAME_HEADER_SIZE,
MAX_ADTS_FRAME_HEADER_SIZE,
);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) return false;
const firstHeader = readAdtsFrameHeader(slice);
@@ -529,7 +558,7 @@ export class AdtsInputFormat extends InputFormat {
MIN_ADTS_FRAME_HEADER_SIZE,
MAX_ADTS_FRAME_HEADER_SIZE,
);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) return false;
const secondHeader = readAdtsFrameHeader(slice);
@@ -573,7 +602,7 @@ export class MpegTsInputFormat extends InputFormat {
async _canReadInput(input: Input) {
const lengthToCheck = TS_PACKET_SIZE + 16 + 1;
let slice = input._reader.requestSlice(0, lengthToCheck);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) return false;
const bytes = readBytes(slice, lengthToCheck);
@@ -618,7 +647,7 @@ export class HlsInputFormat extends InputFormat {
/** @internal */
async _canReadInput(input: Input) {
let slice = input._reader.requestSlice(0, 7);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) return false;
const isM3u8 = readAscii(slice, 7) === '#EXTM3U';
diff --git a/src/input-track.ts b/src/input-track.ts
index 2a6400d..f1b6a16 100644
--- a/src/input-track.ts
+++ b/src/input-track.ts
@@ -12,7 +12,7 @@ import { customAudioDecoders, customVideoDecoders } from './custom-coder';
import { Input } from './input';
import { Logging } from './logging';
import { EncodedPacketSink, PacketRetrievalOptions } from './media-sink';
-import { assert, MaybePromise, Rational, Rotation, roundToDivisor, simplifyRational } from './misc';
+import { assert, isThenable, MaybePromise, Rational, Rotation, roundToDivisor, simplifyRational } from './misc';
import { TrackType } from './output';
import { EncodedPacket, PacketType } from './packet';
import { TrackDisposition } from './metadata';
@@ -528,7 +528,7 @@ export abstract class InputTrack {
}
const requireSync = (value: MaybePromise, getterName: string, asyncName: string): T => {
- if (value instanceof Promise) {
+ if (isThenable(value)) {
throw new Error(
`'${getterName}' is deprecated and not available synchronously for this track. Use the preferred`
+ ` '${asyncName}()' instead.`,
@@ -554,7 +554,7 @@ const toValidatedPredicate = (
};
const result = predicate(track);
- if (result instanceof Promise) {
+ if (isThenable(result)) {
return result.then(handle);
}
return handle(result);
@@ -1225,7 +1225,7 @@ export const toValidatedInputTrackQuery = (
};
const result = query.filter!(track);
- if (result instanceof Promise) {
+ if (isThenable(result)) {
return result.then(handle);
} else {
return handle(result);
@@ -1248,7 +1248,7 @@ export const toValidatedInputTrackQuery = (
};
const result = query.sortBy!(track);
- if (result instanceof Promise) {
+ if (isThenable(result)) {
return result.then(handle);
} else {
return handle(result);
@@ -1274,7 +1274,7 @@ export const mergeInputTrackQueries = (
return queryB?.filter?.(track) ?? true;
};
- if (resultA instanceof Promise) {
+ if (isThenable(resultA)) {
return resultA.then(handleResultA);
} else {
return handleResultA(resultA);
@@ -1294,7 +1294,7 @@ export const mergeInputTrackQueries = (
];
};
- if (resultA instanceof Promise || resultB instanceof Promise) {
+ if (isThenable(resultA) || isThenable(resultB)) {
return Promise.all([resultA, resultB]).then(([resultA, resultB]) => {
return join(resultA, resultB);
});
@@ -1313,7 +1313,7 @@ export const queryInputTracks = async (
let matched = tracks;
if (query?.filter) {
const filterMatches = tracks.map(t => query.filter!(t));
- const hasAsyncFilter = filterMatches.some(x => x instanceof Promise);
+ const hasAsyncFilter = filterMatches.some(x => isThenable(x));
if (hasAsyncFilter) {
// eslint-disable-next-line @typescript-eslint/await-thenable
const resolvedFilterMatches = await Promise.all(filterMatches);
@@ -1328,7 +1328,7 @@ export const queryInputTracks = async (
}
const sortValues = matched.map(t => query.sortBy!(t));
- const hasAsyncSort = sortValues.some(x => x instanceof Promise);
+ const hasAsyncSort = sortValues.some(x => isThenable(x));
const resolvedSortValues = hasAsyncSort
// eslint-disable-next-line @typescript-eslint/await-thenable
? await Promise.all(sortValues)
diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts
index 814dc8e..0e0fe60 100644
--- a/src/isobmff/isobmff-demuxer.ts
+++ b/src/isobmff/isobmff-demuxer.ts
@@ -58,6 +58,7 @@ import {
COLOR_PRIMARIES_MAP_INVERSE,
findLastIndex,
isIso639Dash2LanguageCode,
+ isThenable,
last,
MATRIX_COEFFICIENTS_MAP_INVERSE,
normalizeRotation,
@@ -364,7 +365,7 @@ export class IsobmffDemuxer extends Demuxer {
while (true) {
let slice = this.reader.requestSliceRange(currentPos, MIN_BOX_HEADER_SIZE, MAX_BOX_HEADER_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) break;
const startPos = currentPos;
@@ -380,7 +381,7 @@ export class IsobmffDemuxer extends Demuxer {
// Found moov, load it
let moovSlice = this.reader.requestSlice(slice.filePos, boxInfo.contentSize);
- if (moovSlice instanceof Promise) moovSlice = await moovSlice;
+ if (isThenable(moovSlice)) moovSlice = await moovSlice;
if (!moovSlice) break;
this.moovSlice = moovSlice;
@@ -431,7 +432,7 @@ export class IsobmffDemuxer extends Demuxer {
// The last 4 bytes may contain the size of the mfra box at the end of the file
let lastWordSlice = this.reader.requestSlice(this.reader.fileSize - 4, 4);
- if (lastWordSlice instanceof Promise) lastWordSlice = await lastWordSlice;
+ if (isThenable(lastWordSlice)) lastWordSlice = await lastWordSlice;
assert(lastWordSlice);
const lastWord = readU32Be(lastWordSlice);
@@ -443,7 +444,7 @@ export class IsobmffDemuxer extends Demuxer {
MIN_BOX_HEADER_SIZE,
MAX_BOX_HEADER_SIZE,
);
- if (mfraHeaderSlice instanceof Promise) mfraHeaderSlice = await mfraHeaderSlice;
+ if (isThenable(mfraHeaderSlice)) mfraHeaderSlice = await mfraHeaderSlice;
if (mfraHeaderSlice) {
const boxInfo = readBoxHeader(mfraHeaderSlice);
@@ -451,7 +452,7 @@ export class IsobmffDemuxer extends Demuxer {
if (boxInfo && boxInfo.name === 'mfra') {
// We found the mfra box, allowing for much better random access. Let's parse it.
let mfraSlice = this.reader.requestSlice(mfraHeaderSlice.filePos, boxInfo.contentSize);
- if (mfraSlice instanceof Promise) mfraSlice = await mfraSlice;
+ if (isThenable(mfraSlice)) mfraSlice = await mfraSlice;
if (mfraSlice) {
this.readContiguousBoxes(mfraSlice);
@@ -678,14 +679,14 @@ export class IsobmffDemuxer extends Demuxer {
}
let headerSlice = this.reader.requestSliceRange(startPos, MIN_BOX_HEADER_SIZE, MAX_BOX_HEADER_SIZE);
- if (headerSlice instanceof Promise) headerSlice = await headerSlice;
+ if (isThenable(headerSlice)) headerSlice = await headerSlice;
assert(headerSlice);
const moofBoxInfo = readBoxHeader(headerSlice);
assert(moofBoxInfo?.name === 'moof');
let entireSlice = this.reader.requestSlice(startPos, moofBoxInfo.totalSize);
- if (entireSlice instanceof Promise) entireSlice = await entireSlice;
+ if (isThenable(entireSlice)) entireSlice = await entireSlice;
assert(entireSlice);
this.traverseBox(entireSlice);
@@ -1608,11 +1609,11 @@ export class IsobmffDemuxer extends Demuxer {
if (track.info.codec === 'aac') {
// Let's try to deduce more accurate values directly from the AudioSpecificConfig:
const audioSpecificConfig = parseAacAudioSpecificConfig(track.info.codecDescription);
- if (audioSpecificConfig.numberOfChannels !== null) {
- track.info.numberOfChannels = audioSpecificConfig.numberOfChannels;
+ if (audioSpecificConfig.outputNumberOfChannels !== null) {
+ track.info.numberOfChannels = audioSpecificConfig.outputNumberOfChannels;
}
- if (audioSpecificConfig.sampleRate !== null) {
- track.info.sampleRate = audioSpecificConfig.sampleRate;
+ if (audioSpecificConfig.outputSampleRate !== null) {
+ track.info.sampleRate = audioSpecificConfig.outputSampleRate;
}
}
}
@@ -3144,24 +3145,31 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
sampleInfo.sampleOffset,
sampleInfo.sampleSize,
);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) {
return null; // Data is outside
}
data = readBytes(slice, sampleInfo.sampleSize);
- if (this.internalTrack.encryptionAuxInfo) {
- assert(this.internalTrack.encryptionInfo);
+ if (this.internalTrack.encryptionInfo) {
+ let sampleEncryption: SampleEncryptionInfo | null = null;
- const entries = await resolveEncryptionAuxInfo(
- this.internalTrack.demuxer.reader,
- this.internalTrack.encryptionInfo,
- this.internalTrack.encryptionAuxInfo,
- );
+ if (this.internalTrack.encryptionAuxInfo) {
+ const entries = await resolveEncryptionAuxInfo(
+ this.internalTrack.demuxer.reader,
+ this.internalTrack.encryptionInfo,
+ this.internalTrack.encryptionAuxInfo,
+ );
- if (sampleIndex < entries.length) {
- data = await decryptSample(this.internalTrack, entries[sampleIndex]!, data, null);
+ if (sampleIndex < entries.length) {
+ sampleEncryption = entries[sampleIndex]!;
+ }
+ }
+
+ sampleEncryption ??= getDefaultSampleEncryption(this.internalTrack.encryptionInfo);
+ if (sampleEncryption) {
+ data = await decryptSample(this.internalTrack, sampleEncryption, data, null);
}
}
}
@@ -3200,15 +3208,19 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
fragmentSample.byteOffset,
fragmentSample.byteSize,
);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) {
return null; // Data is outside
}
data = readBytes(slice, fragmentSample.byteSize);
- if (fragmentSample.encryption) {
- data = await decryptSample(this.internalTrack, fragmentSample.encryption, data, fragment);
+ if (this.internalTrack.encryptionInfo) {
+ const sampleEncryption = fragmentSample.encryption
+ ?? getDefaultSampleEncryption(this.internalTrack.encryptionInfo);
+ if (sampleEncryption) {
+ data = await decryptSample(this.internalTrack, sampleEncryption, data, fragment);
+ }
}
}
@@ -3310,7 +3322,7 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
// Load the header
let slice = demuxer.reader.requestSliceRange(currentPos, MIN_BOX_HEADER_SIZE, MAX_BOX_HEADER_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) break;
const boxStartPos = currentPos;
@@ -3737,7 +3749,7 @@ const resolveEncryptionAuxInfo = async (
}
let slice = reader.requestSlice(aux.offset, totalSize);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) {
throw new Error('Failed to read auxiliary encryption info.');
}
@@ -3779,6 +3791,17 @@ const resolveEncryptionAuxInfo = async (
return entries;
};
+const getDefaultSampleEncryption = (encryptionInfo: TrackEncryptionInfo): SampleEncryptionInfo | null => {
+ if (!encryptionInfo.defaultConstantIv) {
+ return null;
+ }
+
+ return {
+ iv: encryptionInfo.defaultConstantIv,
+ subsamples: null,
+ };
+};
+
const decryptSample = async (
track: InternalTrack,
sampleEncryption: SampleEncryptionInfo,
diff --git a/src/isobmff/isobmff-muxer.ts b/src/isobmff/isobmff-muxer.ts
index 4d6c8c5..1e3006c 100644
--- a/src/isobmff/isobmff-muxer.ts
+++ b/src/isobmff/isobmff-muxer.ts
@@ -525,8 +525,8 @@ export class IsobmffMuxer extends Muxer {
decoderConfig.description = buildAacAudioSpecificConfig({
objectType: adtsFrame.objectType,
- sampleRate,
- numberOfChannels,
+ outputSampleRate: sampleRate,
+ outputNumberOfChannels: numberOfChannels,
});
requiresAdtsStripping = true;
}
diff --git a/src/matroska/ebml.ts b/src/matroska/ebml.ts
index 6cc5a88..6c93e16 100644
--- a/src/matroska/ebml.ts
+++ b/src/matroska/ebml.ts
@@ -7,7 +7,7 @@
*/
import { MediaCodec } from '../codec';
-import { assert, assertNever, textDecoder, textEncoder } from '../misc';
+import { assert, assertNever, isThenable, textDecoder, textEncoder } from '../misc';
import { FileSlice, readBytes, Reader, readF32Be, readF64Be, readU8 } from '../reader';
import { Writer } from '../writer';
@@ -688,7 +688,7 @@ export const searchForNextElementId = async (
while (until === null || currentPos < until) {
let slice = reader.requestSliceRange(currentPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) break;
const elementHeader = readElementHeader(slice);
@@ -716,7 +716,7 @@ export const resync = async (reader: Reader, startPos: number, ids: EBMLId[], un
while (currentPos < until) {
let slice = reader.requestSliceRange(currentPos, 0, Math.min(CHUNK_SIZE, until - currentPos));
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) break;
if (slice.length < MAX_VAR_INT_SIZE) break;
diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts
index 9fbbc3f..8f397c1 100644
--- a/src/matroska/matroska-demuxer.ts
+++ b/src/matroska/matroska-demuxer.ts
@@ -42,6 +42,7 @@ import {
COLOR_PRIMARIES_MAP_INVERSE,
findLastIndex,
isIso639Dash2LanguageCode,
+ isThenable,
last,
MATRIX_COEFFICIENTS_MAP_INVERSE,
normalizeRotation,
@@ -334,7 +335,7 @@ export class MatroskaDemuxer extends Demuxer {
// Loop over all top-level elements in the file
while (true) {
let slice = this.reader.requestSliceRange(currentPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) break;
const header = readElementHeader(slice);
@@ -350,7 +351,7 @@ export class MatroskaDemuxer extends Demuxer {
assertDefinedSize(size);
let slice = this.reader.requestSlice(dataStartPos, size);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) break;
this.readContiguousElements(slice);
@@ -433,7 +434,7 @@ export class MatroskaDemuxer extends Demuxer {
while (this.currentSegment.elementEndPos === null || currentPos < this.currentSegment.elementEndPos) {
let slice = this.reader.requestSliceRange(currentPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) break;
const elementStartPos = currentPos;
@@ -468,7 +469,7 @@ export class MatroskaDemuxer extends Demuxer {
assertDefinedSize(size);
let slice = this.reader.requestSlice(dataStartPos, size);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (slice) {
this.readContiguousElements(slice);
@@ -484,7 +485,7 @@ export class MatroskaDemuxer extends Demuxer {
assertDefinedSize(size);
let slice = this.reader.requestSlice(dataStartPos, size);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (slice) {
this.readContiguousElements(slice);
@@ -519,7 +520,7 @@ export class MatroskaDemuxer extends Demuxer {
MIN_HEADER_SIZE,
MAX_HEADER_SIZE,
);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) continue;
const header = readElementHeader(slice);
@@ -533,7 +534,7 @@ export class MatroskaDemuxer extends Demuxer {
this.currentSegment[target.flag] = true;
let dataSlice = this.reader.requestSlice(slice.filePos, size);
- if (dataSlice instanceof Promise) dataSlice = await dataSlice;
+ if (isThenable(dataSlice)) dataSlice = await dataSlice;
if (!dataSlice) continue;
this.readContiguousElements(dataSlice);
@@ -608,7 +609,7 @@ export class MatroskaDemuxer extends Demuxer {
}
let headerSlice = this.reader.requestSliceRange(startPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE);
- if (headerSlice instanceof Promise) headerSlice = await headerSlice;
+ if (isThenable(headerSlice)) headerSlice = await headerSlice;
assert(headerSlice);
const elementStartPos = startPos;
@@ -637,7 +638,7 @@ export class MatroskaDemuxer extends Demuxer {
// Load the entire cluster
let dataSlice = this.reader.requestSlice(dataStartPos, size);
- if (dataSlice instanceof Promise) dataSlice = await dataSlice;
+ if (isThenable(dataSlice)) dataSlice = await dataSlice;
const cluster: Cluster = {
segment,
@@ -894,7 +895,7 @@ export class MatroskaDemuxer extends Demuxer {
MIN_HEADER_SIZE,
MAX_HEADER_SIZE,
);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) continue;
const header = readElementHeader(slice);
@@ -907,7 +908,7 @@ export class MatroskaDemuxer extends Demuxer {
this.currentSegment = segment;
let dataSlice = this.reader.requestSlice(slice.filePos, size);
- if (dataSlice instanceof Promise) dataSlice = await dataSlice;
+ if (isThenable(dataSlice)) dataSlice = await dataSlice;
if (dataSlice) {
this.readContiguousElements(dataSlice);
}
@@ -2347,7 +2348,7 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
// Load the header
let slice = demuxer.reader.requestSliceRange(currentPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) break;
const elementStartPos = currentPos;
@@ -2418,7 +2419,7 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
// the first segment.
let slice = demuxer.reader.requestSliceRange(endPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) break;
const elementId = readElementId(slice);
diff --git a/src/matroska/matroska-muxer.ts b/src/matroska/matroska-muxer.ts
index 7763df3..dc213a8 100644
--- a/src/matroska/matroska-muxer.ts
+++ b/src/matroska/matroska-muxer.ts
@@ -858,8 +858,8 @@ export class MatroskaMuxer extends Muxer {
decoderConfig.description = buildAacAudioSpecificConfig({
objectType: adtsFrame.objectType,
- sampleRate,
- numberOfChannels,
+ outputSampleRate: sampleRate,
+ outputNumberOfChannels: numberOfChannels,
});
requiresAdtsStripping = true;
}
diff --git a/src/media-source.ts b/src/media-source.ts
index c724b96..5260e12 100644
--- a/src/media-source.ts
+++ b/src/media-source.ts
@@ -28,6 +28,7 @@ import {
clamp,
clearIntervalUnthrottled,
floorToDivisor,
+ isThenable,
last,
missingWebCodecsClassMessage,
promiseWithResolvers,
@@ -426,7 +427,7 @@ class VideoEncoderWrapper {
// Apply the user-defined process function, if any
if (config.transform?.process) {
let processed = config.transform.process(videoSample);
- if (processed instanceof Promise) {
+ if (isThenable(processed)) {
processed = await processed;
}
@@ -1937,7 +1938,7 @@ class AudioEncoderWrapper {
if (config.transform?.process) {
try {
let processed = config.transform.process(audioSample);
- if (processed instanceof Promise) {
+ if (isThenable(processed)) {
processed = await processed;
}
@@ -2256,8 +2257,8 @@ class AudioEncoderWrapper {
meta.decoderConfig.description = buildAacAudioSpecificConfig({
objectType,
- numberOfChannels: meta.decoderConfig.numberOfChannels,
- sampleRate: meta.decoderConfig.sampleRate,
+ outputNumberOfChannels: meta.decoderConfig.numberOfChannels,
+ outputSampleRate: meta.decoderConfig.sampleRate,
});
}
}
diff --git a/src/misc.ts b/src/misc.ts
index 3b0d98a..3a247d3 100644
--- a/src/misc.ts
+++ b/src/misc.ts
@@ -738,6 +738,20 @@ export const missingWebCodecsClassMessage = (className: string) => {
*/
export type MaybePromise = T | Promise;
+const NativePromiseConstructor = /* #__PURE__ */ (async () => {})().constructor as PromiseConstructor;
+
+/**
+ * Needed to properly deal with custom Promise implementations and because this is closer to how the JS spec does it.
+ */
+export const isThenable = (value: MaybePromise): value is Promise => {
+ if (value instanceof NativePromiseConstructor || value instanceof Promise) {
+ return true;
+ }
+
+ // Fall back to a crude duck typing check
+ return typeof (value as PromiseLike | null | undefined)?.then === 'function';
+};
+
/** Acts like `??` except the condition is -1 and not null/undefined. */
export const coalesceIndex = (a: number, b: number) => {
return a !== -1 ? a : b;
diff --git a/src/mp3/mp3-demuxer.ts b/src/mp3/mp3-demuxer.ts
index 48b3dd0..a89d222 100644
--- a/src/mp3/mp3-demuxer.ts
+++ b/src/mp3/mp3-demuxer.ts
@@ -17,6 +17,7 @@ import {
AsyncMutex,
binarySearchExact,
binarySearchLessOrEqual,
+ isThenable,
toDataView,
UNDETERMINED_LANGUAGE,
} from '../misc';
@@ -102,7 +103,7 @@ export class Mp3Demuxer extends Demuxer {
// Let's skip all ID3v2 tags at the start of the file
while (true) {
let slice = this.reader.requestSlice(this.lastLoadedPos, ID3_V2_HEADER_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) {
this.lastSampleLoaded = true;
@@ -136,7 +137,7 @@ export class Mp3Demuxer extends Demuxer {
const xingOffset = getXingOffset(header.mpegVersionId, header.channel);
let slice = this.reader.requestSlice(result.startPos + xingOffset, 4);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (slice) {
const word = readU32Be(slice);
const isXing = word === XING || word === INFO;
@@ -151,7 +152,7 @@ export class Mp3Demuxer extends Demuxer {
if (!this.xingData) {
let xingDataSlice = this.reader.requestSlice(result.startPos + xingOffset + 4, 12);
- if (xingDataSlice instanceof Promise) xingDataSlice = await xingDataSlice;
+ if (isThenable(xingDataSlice)) xingDataSlice = await xingDataSlice;
if (xingDataSlice) {
const xingData = readBytes(xingDataSlice, 12);
const view = toDataView(xingData);
@@ -216,7 +217,7 @@ export class Mp3Demuxer extends Demuxer {
while (true) {
let headerSlice = this.reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
- if (headerSlice instanceof Promise) headerSlice = await headerSlice;
+ if (isThenable(headerSlice)) headerSlice = await headerSlice;
if (!headerSlice) break;
const id3V2Header = readId3V2Header(headerSlice);
@@ -227,7 +228,7 @@ export class Mp3Demuxer extends Demuxer {
id3V2HeaderFound = true;
let contentSlice = this.reader.requestSlice(headerSlice.filePos, id3V2Header.size);
- if (contentSlice instanceof Promise) contentSlice = await contentSlice;
+ if (isThenable(contentSlice)) contentSlice = await contentSlice;
if (!contentSlice) break;
parseId3V2Tag(contentSlice, id3V2Header, this.metadataTags);
@@ -238,7 +239,7 @@ export class Mp3Demuxer extends Demuxer {
if (!id3V2HeaderFound && this.reader.fileSize !== null && this.reader.fileSize >= ID3_V1_TAG_SIZE) {
// Try reading an ID3v1 tag at the end of the file
let slice = this.reader.requestSlice(this.reader.fileSize - ID3_V1_TAG_SIZE, ID3_V1_TAG_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
assert(slice);
const tag = readAscii(slice, 3);
@@ -388,7 +389,7 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
data = PLACEHOLDER_DATA;
} else {
let slice = this.demuxer.reader.requestSlice(rawSample.dataStart, rawSample.dataSize);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) {
return null; // Data didn't fit into the rest of the file
diff --git a/src/mp3/mp3-reader.ts b/src/mp3/mp3-reader.ts
index 4897dda..29c4689 100644
--- a/src/mp3/mp3-reader.ts
+++ b/src/mp3/mp3-reader.ts
@@ -7,6 +7,7 @@
*/
import { MP3_FRAME_HEADER_SIZE, getMp3ChannelCount, Mp3FrameHeader, readMp3FrameHeader } from '../../shared/mp3-misc';
+import { isThenable } from '../misc';
import { Reader, readU32Be } from '../reader';
export const readNextMp3FrameHeader = async (
@@ -27,7 +28,7 @@ export const readNextMp3FrameHeader = async (
: CHUNK_SIZE;
let slice = reader.requestSliceRange(currentPos, MP3_FRAME_HEADER_SIZE, maxLength);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice || slice.length < MP3_FRAME_HEADER_SIZE) break;
while (slice.remainingLength >= MP3_FRAME_HEADER_SIZE) {
diff --git a/src/mpeg-ts/mpeg-ts-demuxer.ts b/src/mpeg-ts/mpeg-ts-demuxer.ts
index 54635cd..7474a59 100644
--- a/src/mpeg-ts/mpeg-ts-demuxer.ts
+++ b/src/mpeg-ts/mpeg-ts-demuxer.ts
@@ -63,6 +63,7 @@ import {
COLOR_PRIMARIES_MAP_INVERSE,
findLastIndex,
floorToMultiple,
+ isThenable,
last,
MATRIX_COEFFICIENTS_MAP_INVERSE,
readExpGolomb,
@@ -190,7 +191,7 @@ export class MpegTsDemuxer extends Demuxer {
return this.metadataPromise ??= (async () => {
const lengthToCheck = TS_PACKET_SIZE + 16 + 1;
let startingSlice = this.reader.requestSlice(0, lengthToCheck);
- if (startingSlice instanceof Promise) startingSlice = await startingSlice;
+ if (isThenable(startingSlice)) startingSlice = await startingSlice;
assert(startingSlice);
const startingBytes = readBytes(startingSlice, lengthToCheck);
@@ -951,7 +952,7 @@ export class MpegTsDemuxer extends Demuxer {
async readPacketHeader(pos: number): Promise {
let slice = this.reader.requestSlice(pos, 4);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) {
return null;
@@ -987,7 +988,7 @@ export class MpegTsDemuxer extends Demuxer {
async readPacket(pos: number): Promise {
// Code in here is duplicated from readPacketHeader for performance reasons
let slice = this.reader.requestSlice(pos, TS_PACKET_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) {
return null;
@@ -2099,7 +2100,7 @@ class PacketReadingContext {
while (true) {
let remaining = this.ensureBuffered(CHUNK_SIZE);
- if (remaining instanceof Promise) remaining = await remaining;
+ if (isThenable(remaining)) remaining = await remaining;
if (remaining === 0) {
break;
@@ -2253,7 +2254,7 @@ class PacketReadingContext {
while (true) {
let remaining = this.ensureBuffered(CHUNK_SIZE);
- if (remaining instanceof Promise) remaining = await remaining;
+ if (isThenable(remaining)) remaining = await remaining;
const startPos = this.currentPos;
@@ -2269,7 +2270,7 @@ class PacketReadingContext {
const possibleHeaderStartPos = this.currentPos;
let remaining = this.ensureBuffered(MAX_ADTS_FRAME_HEADER_SIZE);
- if (remaining instanceof Promise) remaining = await remaining;
+ if (isThenable(remaining)) remaining = await remaining;
if (remaining < MAX_ADTS_FRAME_HEADER_SIZE) {
return;
@@ -2282,7 +2283,7 @@ class PacketReadingContext {
this.seekTo(possibleHeaderStartPos);
let remaining = this.ensureBuffered(header.frameLength);
- if (remaining instanceof Promise) remaining = await remaining;
+ if (isThenable(remaining)) remaining = await remaining;
return this.supplyPacket(
remaining,
@@ -2300,7 +2301,7 @@ class PacketReadingContext {
const possibleHeaderStartPos = this.currentPos;
let remaining = this.ensureBuffered(MP3_FRAME_HEADER_SIZE);
- if (remaining instanceof Promise) remaining = await remaining;
+ if (isThenable(remaining)) remaining = await remaining;
if (remaining < MP3_FRAME_HEADER_SIZE) {
return;
@@ -2314,7 +2315,7 @@ class PacketReadingContext {
this.seekTo(possibleHeaderStartPos);
let remaining = this.ensureBuffered(result.header.totalSize);
- if (remaining instanceof Promise) remaining = await remaining;
+ if (isThenable(remaining)) remaining = await remaining;
const duration = result.header.audioSamplesInFrame * TIMESCALE
/ elementaryStream.info.sampleRate;
@@ -2332,7 +2333,7 @@ class PacketReadingContext {
// Need at least 5 bytes for sync word + CRC + fscod/frmsizecod
let remaining = this.ensureBuffered(5);
- if (remaining instanceof Promise) remaining = await remaining;
+ if (isThenable(remaining)) remaining = await remaining;
if (remaining < 5) {
return;
@@ -2361,7 +2362,7 @@ class PacketReadingContext {
this.seekTo(possibleSyncPos);
remaining = this.ensureBuffered(frameSize);
- if (remaining instanceof Promise) remaining = await remaining;
+ if (isThenable(remaining)) remaining = await remaining;
const duration = Math.round(
AC3_SAMPLES_PER_FRAME * TIMESCALE / elementaryStream.info.sampleRate,
@@ -2377,7 +2378,7 @@ class PacketReadingContext {
// Need at least 5 bytes for E-AC-3 header parsing (sync word + frmsiz + fscod/numblkscod)
let remaining = this.ensureBuffered(5);
- if (remaining instanceof Promise) remaining = await remaining;
+ if (isThenable(remaining)) remaining = await remaining;
if (remaining < 5) {
return;
@@ -2399,7 +2400,7 @@ class PacketReadingContext {
this.seekTo(possibleSyncPos);
remaining = this.ensureBuffered(frameSize);
- if (remaining instanceof Promise) remaining = await remaining;
+ if (isThenable(remaining)) remaining = await remaining;
// Duration = numblks * 256 samples per block
const samplesPerFrame = numblks * 256;
@@ -2416,7 +2417,7 @@ class PacketReadingContext {
const possibleSyncPos = this.currentPos;
let remaining = this.ensureBuffered(DTS_CORE_FRAME_HEADER_SIZE);
- if (remaining instanceof Promise) remaining = await remaining;
+ if (isThenable(remaining)) remaining = await remaining;
if (remaining < DTS_CORE_FRAME_HEADER_SIZE) {
return;
@@ -2438,7 +2439,7 @@ class PacketReadingContext {
const headerBound = Math.min(leadingExss.frameSize, DTS_EXSS_MAX_HEADER_SIZE);
let remaining = this.ensureBuffered(headerBound);
- if (remaining instanceof Promise) remaining = await remaining;
+ if (isThenable(remaining)) remaining = await remaining;
leadingExss = parseDtsExssHeader(this.readBytes(remaining)) ?? leadingExss;
}
@@ -2456,7 +2457,7 @@ class PacketReadingContext {
const neededBytes = nextSubstreamPos + DTS_EXSS_HEADER_PREFIX_SIZE;
let remaining = this.ensureBuffered(neededBytes);
- if (remaining instanceof Promise) remaining = await remaining;
+ if (isThenable(remaining)) remaining = await remaining;
if (remaining < neededBytes) {
break;
@@ -2484,7 +2485,7 @@ class PacketReadingContext {
this.seekTo(possibleSyncPos);
remaining = this.ensureBuffered(frameSize);
- if (remaining instanceof Promise) remaining = await remaining;
+ if (isThenable(remaining)) remaining = await remaining;
const duration = Math.round(
sampleCount * TIMESCALE / elementaryStream.info.sampleRate,
diff --git a/src/ogg/ogg-demuxer.ts b/src/ogg/ogg-demuxer.ts
index b3fe01e..70ab3b8 100644
--- a/src/ogg/ogg-demuxer.ts
+++ b/src/ogg/ogg-demuxer.ts
@@ -18,6 +18,7 @@ import {
AsyncMutex,
binarySearchLessOrEqual,
findLast,
+ isThenable,
last,
roundIfAlmostInteger,
toDataView,
@@ -72,7 +73,7 @@ export class OggDemuxer extends Demuxer {
while (true) {
let slice = this.reader.requestSliceRange(currentPos, MIN_PAGE_HEADER_SIZE, MAX_PAGE_HEADER_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) break;
const page = readPageHeader(slice);
@@ -274,7 +275,7 @@ export class OggDemuxer extends Demuxer {
while (true) {
// Load the entire page data
let pageSlice = this.reader.requestSlice(currentPage.dataStartPos, currentPage.dataSize);
- if (pageSlice instanceof Promise) pageSlice = await pageSlice;
+ if (isThenable(pageSlice)) pageSlice = await pageSlice;
assert(pageSlice);
const pageData = readBytes(pageSlice, currentPage.dataSize);
@@ -299,7 +300,7 @@ export class OggDemuxer extends Demuxer {
let currentPos = currentPage.headerStartPos + currentPage.totalSize;
while (true) {
let headerSlice = this.reader.requestSliceRange(currentPos, MIN_PAGE_HEADER_SIZE, MAX_PAGE_HEADER_SIZE);
- if (headerSlice instanceof Promise) headerSlice = await headerSlice;
+ if (isThenable(headerSlice)) headerSlice = await headerSlice;
if (!headerSlice) {
return null;
}
@@ -358,7 +359,7 @@ export class OggDemuxer extends Demuxer {
let currentPos = lastPacket.endPage.headerStartPos + lastPacket.endPage.totalSize;
while (true) {
let slice = this.reader.requestSliceRange(currentPos, MIN_PAGE_HEADER_SIZE, MAX_PAGE_HEADER_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) {
return null;
}
@@ -658,7 +659,7 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
);
let searchSlice = this.demuxer.reader.requestSlice(searchStartPos, until - searchStartPos);
- if (searchSlice instanceof Promise) searchSlice = await searchSlice;
+ if (isThenable(searchSlice)) searchSlice = await searchSlice;
assert(searchSlice);
const found = findNextPageHeader(searchSlice, until);
@@ -672,7 +673,7 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
MIN_PAGE_HEADER_SIZE,
MAX_PAGE_HEADER_SIZE,
);
- if (headerSlice instanceof Promise) headerSlice = await headerSlice;
+ if (isThenable(headerSlice)) headerSlice = await headerSlice;
assert(headerSlice);
const page = readPageHeader(headerSlice);
@@ -685,7 +686,7 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
pageValid = true;
} else {
let pageSlice = this.demuxer.reader.requestSlice(page.headerStartPos, page.totalSize);
- if (pageSlice instanceof Promise) pageSlice = await pageSlice;
+ if (isThenable(pageSlice)) pageSlice = await pageSlice;
assert(pageSlice);
// Validate the page by checking checksum
@@ -759,7 +760,7 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
const nextPos = currentPage.headerStartPos + currentPage.totalSize;
let slice = this.demuxer.reader.requestSliceRange(nextPos, MIN_PAGE_HEADER_SIZE, MAX_PAGE_HEADER_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
assert(slice);
const nextPage = readPageHeader(slice);
diff --git a/src/output.ts b/src/output.ts
index 2c8329b..5a22d36 100644
--- a/src/output.ts
+++ b/src/output.ts
@@ -6,7 +6,16 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
-import { assert, AsyncMutex, EventEmitter, isIso639Dash2LanguageCode, MaybePromise, Rotation, toArray } from './misc';
+import {
+ assert,
+ AsyncMutex,
+ EventEmitter,
+ isIso639Dash2LanguageCode,
+ isThenable,
+ MaybePromise,
+ Rotation,
+ toArray,
+} from './misc';
import { MetadataTags, TrackDisposition, validateMetadataTags, validateTrackDisposition } from './metadata';
import { Muxer } from './muxer';
import { OutputFormat } from './output-format';
@@ -451,7 +460,7 @@ export class Output<
}
const rootTargetResult = this._getRootTarget();
- if (rootTargetResult instanceof Promise) {
+ if (isThenable(rootTargetResult)) {
throw new TypeError(errorMessage);
}
@@ -516,7 +525,7 @@ export class Output<
return result;
};
- if (result instanceof Promise) {
+ if (isThenable(result)) {
return result.then(handleResult);
} else {
return handleResult(result);
@@ -604,7 +613,7 @@ export class Output<
return target;
};
- if (result instanceof Promise) {
+ if (isThenable(result)) {
return this._rootTargetPromise = result.then(handleResult);
} else {
return handleResult(result);
diff --git a/src/reader.ts b/src/reader.ts
index 3e909c9..5865295 100644
--- a/src/reader.ts
+++ b/src/reader.ts
@@ -7,7 +7,7 @@
*/
import { InputDisposedError } from './input';
-import { assert, clamp, getUint24, MaybePromise, textDecoder, toDataView } from './misc';
+import { assert, clamp, getUint24, isThenable, MaybePromise, textDecoder, toDataView } from './misc';
import { DEFAULT_MAX_READ_POSITION, DEFAULT_MIN_READ_POSITION, Source } from './source';
export class Reader {
@@ -47,7 +47,7 @@ export class Reader {
const end = start + length;
const result = this.source._read(start, end, DEFAULT_MIN_READ_POSITION, DEFAULT_MAX_READ_POSITION);
- if (result instanceof Promise) {
+ if (isThenable(result)) {
return result.then((x) => {
if (!x) {
return null;
@@ -95,7 +95,7 @@ export class Reader {
);
};
- if (promisedAttempt instanceof Promise) {
+ if (isThenable(promisedAttempt)) {
return promisedAttempt.then(handleAttempt);
} else {
return handleAttempt(promisedAttempt);
@@ -121,7 +121,7 @@ export class Reader {
}
let slice = this.requestSliceRange(currentSize, 0, CHUNK_SIZE);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice || slice.length === 0) {
break;
diff --git a/src/sample.ts b/src/sample.ts
index 84bb869..7ecc9eb 100644
--- a/src/sample.ts
+++ b/src/sample.ts
@@ -11,6 +11,7 @@ import {
clamp,
COLOR_PRIMARIES_MAP,
isAllowSharedBufferSource,
+ isThenable,
MATRIX_COEFFICIENTS_MAP,
Rotation,
SECOND_TO_MICROSECOND_FACTOR,
@@ -864,7 +865,7 @@ export class VideoSample implements Disposable {
if (this._data instanceof VideoSampleResource) {
let result = this._data.getDataPlanes();
- if (result instanceof Promise) result = await result;
+ if (isThenable(result)) result = await result;
if (
!Array.isArray(result)
@@ -1022,7 +1023,7 @@ export class VideoSample implements Disposable {
}
const planes = this._data.getDataPlanes();
- if (planes instanceof Promise) {
+ if (isThenable(planes)) {
throw new Error(
'Cannot convert a VideoSampleResource-backed VideoSample to VideoFrame if getDataPlanes() returns'
+ ' a promise.',
@@ -1586,7 +1587,7 @@ export class VideoSample implements Disposable {
// Description's finalized; let's see if a registered transformer wants to handle it
for (const transformer of registeredVideoSampleTransformers) {
let result = transformer(this, description);
- if (result instanceof Promise) result = await result;
+ if (isThenable(result)) result = await result;
if (result !== null) {
return result;
diff --git a/src/source.ts b/src/source.ts
index 1cd4a45..6bab482 100644
--- a/src/source.ts
+++ b/src/source.ts
@@ -14,6 +14,7 @@ import {
closedIntervalsOverlap,
FilePath,
isNumber,
+ isThenable,
isWebKit,
MaybePromise,
mergeRequestInit,
@@ -328,7 +329,7 @@ export abstract class PathedSource extends Source {
return ref;
};
- if (result instanceof Promise) {
+ if (isThenable(result)) {
return result.then(handle);
} else {
return handle(result);
@@ -387,7 +388,7 @@ export class CustomPathedSource extends PathedSource {
return ref;
};
- if (result instanceof Promise) {
+ if (isThenable(result)) {
this._rootRequest = result.then(handle);
} else {
handle(result);
@@ -894,7 +895,7 @@ export class UrlSource extends PathedSource {
return result;
};
- if (result instanceof Promise) {
+ if (isThenable(result)) {
return result.then(processResult);
} else {
return processResult(result);
@@ -1198,7 +1199,7 @@ export class UrlSource extends PathedSource {
for (const slice of uniqueSlices) {
const result = backing._read(slice.start, slice.start + slice.bytes.length);
- if (result instanceof Promise) {
+ if (isThenable(result)) {
result.then((readResult) => {
if (readResult) {
// The backing's cache is empty at this point, so the read is guaranteed to produce
@@ -1459,7 +1460,7 @@ export class CustomSource extends Source {
const result = this._options.getSize();
- if (result instanceof Promise) {
+ if (isThenable(result)) {
return result.then((size) => {
if (!Number.isInteger(size) || size < 0) {
throw new TypeError('options.getSize must return or resolve to a non-negative integer.');
@@ -1485,7 +1486,7 @@ export class CustomSource extends Source {
const originalTargetPos = worker.targetPos;
let data = this._options.read(worker.currentPos, originalTargetPos);
- if (data instanceof Promise) data = await data;
+ if (isThenable(data)) data = await data;
if (worker.aborted) {
break;
@@ -2706,7 +2707,7 @@ export class RangedSource extends Source {
return result;
};
- if (result instanceof Promise) {
+ if (isThenable(result)) {
return result.then(processResult);
} else {
return processResult(result);
diff --git a/src/wave/wave-demuxer.ts b/src/wave/wave-demuxer.ts
index ddf750a..13e8892 100644
--- a/src/wave/wave-demuxer.ts
+++ b/src/wave/wave-demuxer.ts
@@ -12,7 +12,7 @@ import { Input } from '../input';
import { InputAudioTrackBacking } from '../input-track';
import { PacketRetrievalOptions } from '../media-sink';
import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
-import { assert, UNDETERMINED_LANGUAGE } from '../misc';
+import { assert, isThenable, UNDETERMINED_LANGUAGE } from '../misc';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
import { readAscii, readBytes, Reader, readU16, readU32, readU64 } from '../reader';
import { ID3_V2_HEADER_SIZE, parseId3V2Tag, readId3V2Header } from '../id3';
@@ -52,7 +52,7 @@ export class WaveDemuxer extends Demuxer {
async readMetadata() {
return this.metadataPromise ??= (async () => {
let slice = this.reader.requestSlice(0, 12);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
assert(slice);
const riffType = readAscii(slice, 4);
@@ -77,7 +77,7 @@ export class WaveDemuxer extends Demuxer {
while (totalFileSize === null || currentPos < totalFileSize) {
let slice = this.reader.requestSlice(currentPos, 8);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) break;
const chunkId = readAscii(slice, 4);
@@ -103,7 +103,7 @@ export class WaveDemuxer extends Demuxer {
// File and data chunk sizes are defined in here instead
let ds64Slice = this.reader.requestSlice(startPos, chunkSize);
- if (ds64Slice instanceof Promise) ds64Slice = await ds64Slice;
+ if (isThenable(ds64Slice)) ds64Slice = await ds64Slice;
if (!ds64Slice) break;
const riffChunkSize = readU64(ds64Slice, littleEndian);
@@ -136,7 +136,7 @@ export class WaveDemuxer extends Demuxer {
private async parseFmtChunk(startPos: number, size: number, littleEndian: boolean) {
let slice = this.reader.requestSlice(startPos, size);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) return; // File too short
let formatTag = readU16(slice, littleEndian);
@@ -206,7 +206,7 @@ export class WaveDemuxer extends Demuxer {
private async parseListChunk(startPos: number, size: number, littleEndian: boolean) {
let slice = this.reader.requestSlice(startPos, size);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) return; // File too short
const infoType = readAscii(slice, 4);
@@ -303,7 +303,7 @@ export class WaveDemuxer extends Demuxer {
private async parseId3Chunk(startPos: number, size: number) {
// Parse ID3 tag embedded in WAV file (non-default, but used a lot in practice anyway)
let slice = this.reader.requestSlice(startPos, size);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) return; // File too short
const id3V2Header = readId3V2Header(slice);
@@ -489,7 +489,7 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking {
// requested slice actually exists.
let slice = this.demuxer.reader.requestSlice(this.demuxer.dataStart + startOffset, sizeInBytes);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
if (!slice) {
return null;
@@ -501,7 +501,7 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking {
data = PLACEHOLDER_DATA;
} else {
let slice = this.demuxer.reader.requestSlice(this.demuxer.dataStart + startOffset, sizeInBytes);
- if (slice instanceof Promise) slice = await slice;
+ if (isThenable(slice)) slice = await slice;
assert(slice);
data = readBytes(slice, sizeInBytes);
diff --git a/test/browser/read-mp4.test.ts b/test/browser/read-mp4.test.ts
new file mode 100644
index 0000000..de0aa4a
--- /dev/null
+++ b/test/browser/read-mp4.test.ts
@@ -0,0 +1,27 @@
+import { expect, test } from 'vitest';
+import { Input } from '../../src/input.js';
+import { UrlSource } from '../../src/source.js';
+import { ALL_FORMATS } from '../../src/input-format.js';
+import { AudioSampleSink } from '../../src/media-sink.js';
+import { assert } from '../../src/misc.js';
+
+test('Encrypted MP4 without senc', async () => {
+ using input = new Input({
+ source: new UrlSource('/639955605-b752f7ad-7ce8-43c2-8ae9-88e74c6ae696.mp4'),
+ formats: ALL_FORMATS,
+ formatOptions: {
+ isobmff: {
+ resolveKeyId: () => 'aabbccddeeff00112233445566778899',
+ },
+ },
+ });
+
+ const audioTrack = await input.getPrimaryAudioTrack();
+ assert(audioTrack);
+
+ // Test that it can decode
+ const sink = new AudioSampleSink(audioTrack);
+ using firstSample = await sink.getSample(0);
+ assert(firstSample);
+ expect(firstSample.timestamp).toBe(0);
+});
diff --git a/test/node/read-mp4.test.ts b/test/node/read-mp4.test.ts
index 6b09ed5..12a2677 100644
--- a/test/node/read-mp4.test.ts
+++ b/test/node/read-mp4.test.ts
@@ -14,6 +14,7 @@ import {
Mp4OutputFormat,
Output,
} from '../../src/index.js';
+import { assert, toUint8Array } from '../../src/misc.js';
const __dirname = new URL('.', import.meta.url).pathname;
@@ -160,3 +161,23 @@ test('Annex B', async () => {
const firstPacket = (await sink.getFirstPacket())!;
expect([...firstPacket.data.slice(0, 4)]).toEqual([0, 0, 0, 1]);
});
+
+test('HE-AAC v2 audio config is parsed correctly', async () => {
+ const filePath = path.join(__dirname, '..', 'public/he-aac-v2.mp4');
+ using input = new Input({
+ source: new FilePathSource(filePath),
+ formats: ALL_FORMATS,
+ });
+
+ const track = await input.getPrimaryAudioTrack();
+ assert(track);
+
+ expect(await track.getSampleRate()).toBe(44100);
+ expect(await track.getNumberOfChannels()).toBe(2);
+
+ const decoderConfig = (await track.getDecoderConfig())!;
+ expect(decoderConfig.codec).toBe('mp4a.40.29');
+ expect(decoderConfig.sampleRate).toBe(44100);
+ expect(decoderConfig.numberOfChannels).toBe(2);
+ expect([...toUint8Array(decoderConfig.description!)]).toEqual([0xeb, 0x8a, 0x08, 0x00]);
+});
diff --git a/test/public/639955605-b752f7ad-7ce8-43c2-8ae9-88e74c6ae696.mp4 b/test/public/639955605-b752f7ad-7ce8-43c2-8ae9-88e74c6ae696.mp4
new file mode 100755
index 0000000..d5c3b2a
Binary files /dev/null and b/test/public/639955605-b752f7ad-7ce8-43c2-8ae9-88e74c6ae696.mp4 differ
diff --git a/test/public/he-aac-v2.mp4 b/test/public/he-aac-v2.mp4
new file mode 100644
index 0000000..5811026
Binary files /dev/null and b/test/public/he-aac-v2.mp4 differ