Compare commits

..
22 Commits
Author SHA1 Message Date
Vanilagy a97e9800d2 Bump patch 2026-08-31 14:11:25 +02:00
Vanilagy 23f5e96afd Rely not only on FinalizationRegistry 2026-08-31 14:05:33 +02:00
Vanilagy 9e203c0c8a Manually close file readers to avoid indefinite memory leak (fixes #144) 2026-08-31 13:44:58 +02:00
Vanilagy dec669163f Add reliable AudioSample.copyTo() fallback for unsupported destination formats (fixes #479) 2026-08-30 12:49:22 +02:00
Vanilagy e16b01a4c6 Fix Infinity not being allowed where it should be (fixes #478) 2026-08-30 12:19:25 +02:00
Vanilagy 398d172152 Bump patch 2026-08-29 20:11:58 +02:00
Vanilagy da18ceeb24 Add smpte170m vp9 test for CI stability, add missing VP9 color spaces in parser 2026-08-29 20:08:57 +02:00
Vanilagy 3c4c2a459c Remove Chromium version check 2026-08-29 19:53:42 +02:00
Vanilagy 7d1eb5b063 Fall back to color space information from bitstream, fill missing color space fields when passing it to Chromium's VideoDecoder, fix AV1 Sequence Header OBU parsing, fix vpcC box generation, still mux partial color space info into container (fixes #474, closes #475) 2026-08-29 19:48:20 +02:00
Vanilagy 42196e9f64 Add proper non-null check 2026-08-28 15:11:31 +02:00
Vanilagy 16f8889e14 Bump patch 2026-08-26 13:44:21 +02:00
Vanilagy d44253c7d3 Fix incomplete parsing and generation of AacAudioSpecificConfig (fixes #471) 2026-08-26 13:40:14 +02:00
Vanilagy c10f93ab8c Recognize fMP4 segments starting with an emsg box (or similar) (fixes #473) 2026-08-26 11:59:22 +02:00
Vanilagy 14fb7d0119 Add ducktyped Promise checks (fixes #472) 2026-08-26 11:24:24 +02:00
0f9dc1f91b Fix tenc fallback for encrypted ISOBMFF files (#470)
* fix tenc fallback

* Add test case

---------

Co-authored-by: Vanilagy <[email protected]>
2026-08-24 14:12:04 +02:00
Vanilagy 394eede178 Bump patch 2026-08-21 20:14:58 +02:00
Vanilagy 6b5ae29252 Add Annex B in MP4 test 2026-08-21 20:12:50 +02:00
Vanilagy 656c7e70a8 Merge branch 'main' of https://github.com/Vanilagy/mediabunny 2026-08-21 20:06:29 +02:00
Vanilagy 3c4f4c27a3 Slightly restructure code 2026-08-21 20:06:28 +02:00
Vanilagy 166298df2c Make WebCodecs missing error message more helpful (fixes #466) 2026-08-20 23:22:28 +02:00
Jasmine Minter fdfab4660f Support missing or empty avcC chunk 2026-08-20 13:28:50 +10:00
Vanilagy 1c4d1c70a8 Prevent warning from being emitted in CI 2026-08-17 18:55:15 +02:00
48 changed files with 1507 additions and 447 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ node_modules
/dist
/dist-docs
.DS_Store
/docs/.vitepress/cache
docs/.vitepress/cache
/docs/api
*.tsbuildinfo
+3 -1
View File
@@ -18,8 +18,10 @@
});
const track = await input.getPrimaryVideoTrack();
console.log(await track.computeFrameRateMetrics());
const sink = new Mediabunny.EncodedPacketSink(track);
const packet = await sink.getFirstPacket();
console.log(packet);
/*
for await (const packet of packetSink.packets()) {
+10 -10
View File
@@ -1,12 +1,12 @@
{
"name": "mediabunny",
"version": "1.55.1",
"version": "1.55.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mediabunny",
"version": "1.55.1",
"version": "1.55.5",
"license": "MPL-2.0",
"workspaces": [
".",
@@ -12958,7 +12958,7 @@
},
"packages/aac-encoder": {
"name": "@mediabunny/aac-encoder",
"version": "1.55.1",
"version": "1.55.5",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
@@ -12973,7 +12973,7 @@
},
"packages/ac3": {
"name": "@mediabunny/ac3",
"version": "1.55.1",
"version": "1.55.5",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
@@ -12988,7 +12988,7 @@
},
"packages/dts": {
"name": "@mediabunny/dts",
"version": "1.55.1",
"version": "1.55.5",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
@@ -13003,7 +13003,7 @@
},
"packages/flac-encoder": {
"name": "@mediabunny/flac-encoder",
"version": "1.55.1",
"version": "1.55.5",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
@@ -13018,7 +13018,7 @@
},
"packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder",
"version": "1.55.1",
"version": "1.55.5",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
@@ -13033,7 +13033,7 @@
},
"packages/prores": {
"name": "@mediabunny/prores",
"version": "1.55.1",
"version": "1.55.5",
"license": "MPL-2.0",
"dependencies": {
"turbores": "^1.2.2"
@@ -13048,10 +13048,10 @@
},
"packages/server": {
"name": "@mediabunny/server",
"version": "1.55.1",
"version": "1.55.5",
"license": "MPL-2.0",
"dependencies": {
"@mediabunny/prores": "^1.55.1",
"@mediabunny/prores": "^1.55.5",
"node-av": "^6.0.0"
},
"funding": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "mediabunny",
"author": "Vanilagy",
"version": "1.55.1",
"version": "1.55.5",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module",
"workspaces": [
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/aac-encoder",
"author": "Vanilagy",
"version": "1.55.1",
"version": "1.55.5",
"description": "AAC encoder extension for Mediabunny, based on libavcodec.",
"main": "./dist/bundles/mediabunny-aac-encoder.mjs",
"module": "./dist/bundles/mediabunny-aac-encoder.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/ac3",
"author": "Vanilagy",
"version": "1.55.1",
"version": "1.55.5",
"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",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/dts",
"author": "Vanilagy",
"version": "1.55.1",
"version": "1.55.5",
"description": "DTS decoder and encoder extension for Mediabunny, based on libavcodec.",
"main": "./dist/bundles/mediabunny-dts.mjs",
"module": "./dist/bundles/mediabunny-dts.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/flac-encoder",
"author": "Vanilagy",
"version": "1.55.1",
"version": "1.55.5",
"description": "FLAC encoder extension for Mediabunny, based on libFLAC.",
"main": "./dist/bundles/mediabunny-flac-encoder.mjs",
"module": "./dist/bundles/mediabunny-flac-encoder.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/mp3-encoder",
"author": "Vanilagy",
"version": "1.55.1",
"version": "1.55.5",
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/prores",
"author": "Vanilagy",
"version": "1.55.1",
"version": "1.55.5",
"description": "Apple ProRes decoder extension for Mediabunny, based on TurboRes.",
"main": "./dist/bundles/mediabunny-prores.mjs",
"module": "./dist/bundles/mediabunny-prores.mjs",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/server",
"author": "Vanilagy",
"version": "1.55.1",
"version": "1.55.5",
"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.1"
"@mediabunny/prores": "^1.55.5"
},
"peerDependencies": {
"mediabunny": "^1.45.0"
+124 -41
View File
@@ -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
+6 -5
View File
@@ -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
+2 -2
View File
@@ -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.');
+127 -59
View File
@@ -22,7 +22,6 @@ import {
textEncoder,
toDataView,
toUint8Array,
getChromiumVersion,
isChromium,
popcount,
setUint24,
@@ -1727,6 +1726,16 @@ export type Vp9CodecInfo = {
matrixCoefficients: number;
};
// The color spaces from section 7.2.2 of the VP9 spec The matrix column is taken from FFmpeg's libavcodec/vp9.c
const VP9_COLOR_SPACE_TABLE = {
1: { colourPrimaries: 5, transferCharacteristics: 6, matrixCoefficients: 5 }, // CS_BT_601
2: { colourPrimaries: 1, transferCharacteristics: 1, matrixCoefficients: 1 }, // CS_BT_709
3: { colourPrimaries: 6, transferCharacteristics: 6, matrixCoefficients: 6 }, // CS_SMPTE_170
4: { colourPrimaries: 7, transferCharacteristics: 7, matrixCoefficients: 7 }, // CS_SMPTE_240
5: { colourPrimaries: 9, transferCharacteristics: 14, matrixCoefficients: 9 }, // CS_BT_2020
7: { colourPrimaries: 1, transferCharacteristics: 13, matrixCoefficients: 0 }, // CS_RGB (sRGB)
};
export const extractVp9CodecInfoFromPacket = (
packet: Uint8Array,
): Vp9CodecInfo | null => {
@@ -1835,26 +1844,10 @@ export const extractVp9CodecInfoFromPacket = (
}
}
// Map color_space to standard values
const matrixCoefficients = colorSpace === 7
? 0
: colorSpace === 2
? 1
: colorSpace === 1
? 6
: 2;
const colourPrimaries = colorSpace === 2
? 1
: colorSpace === 1
? 6
: 2;
const transferCharacteristics = colorSpace === 2
? 1
: colorSpace === 1
? 6
: 2;
const colorSpaceValues = VP9_COLOR_SPACE_TABLE[colorSpace as keyof typeof VP9_COLOR_SPACE_TABLE];
const colourPrimaries = colorSpaceValues?.colourPrimaries ?? 2;
const transferCharacteristics = colorSpaceValues?.transferCharacteristics ?? 2;
const matrixCoefficients = colorSpaceValues?.matrixCoefficients ?? 2;
return {
profile,
@@ -1868,6 +1861,12 @@ export const extractVp9CodecInfoFromPacket = (
};
};
export const vp9CodecInfoHasColorInfo = (info: Vp9CodecInfo) => {
return info.colourPrimaries !== 2
|| info.transferCharacteristics !== 2
|| info.matrixCoefficients !== 2;
};
export type Av1CodecInfo = {
profile: number;
level: number;
@@ -1877,6 +1876,10 @@ export type Av1CodecInfo = {
chromaSubsamplingX: number;
chromaSubsamplingY: number;
chromaSamplePosition: number;
videoFullRangeFlag: number;
colourPrimaries: number;
transferCharacteristics: number;
matrixCoefficients: number;
};
/** Iterates over all OBUs in an AV1 packet bitstream. */
@@ -1891,7 +1894,8 @@ export const iterateAv1PacketObus = function* (packet: Uint8Array) {
for (let i = 0; i < 8; i++) {
const byte = bitstream.readAlignedByte();
value |= ((byte & 0x7f) << (i * 7));
// No bit shift since the value can get big
value += (byte & 0x7f) * 2 ** (i * 7);
if (!(byte & 0x80)) {
break;
@@ -1904,7 +1908,7 @@ export const iterateAv1PacketObus = function* (packet: Uint8Array) {
}
// Spec requirement
if (value >= 2 ** 32 - 1) {
if (value > 2 ** 32 - 1) {
return null;
}
@@ -1978,43 +1982,43 @@ export const extractAv1CodecInfoFromPacket = (
if (reducedStillPictureHeader) {
seqLevel = bitstream.readBits(5);
} else {
// Parse timing_info_present_flag
const timingInfoPresentFlag = bitstream.readBits(1);
let decoderModelInfoPresentFlag = 0;
if (timingInfoPresentFlag) {
// Skip timing info (num_units_in_display_tick, time_scale, equal_picture_interval)
bitstream.skipBits(32); // num_units_in_display_tick
bitstream.skipBits(32); // time_scale
const equalPictureInterval = bitstream.readBits(1);
if (equalPictureInterval) {
// Skip num_ticks_per_picture_minus_1 (uvlc)
// Since this is variable length, we'd need to implement uvlc reading
// For now, we'll return null as this is rare
return null;
// num_ticks_per_picture_minus_1 is uvlc-coded, so we need to seek past it manually
let leadingZeros = 0;
while (leadingZeros < 32 && !bitstream.readBits(1)) {
leadingZeros++;
}
if (leadingZeros < 32) {
bitstream.skipBits(leadingZeros);
}
}
decoderModelInfoPresentFlag = bitstream.readBits(1);
if (decoderModelInfoPresentFlag) {
bufferDelayLengthMinus1 = bitstream.readBits(5);
bitstream.skipBits(32); // num_units_in_decoding_tick
bitstream.skipBits(5); // buffer_removal_time_length_minus_1
bitstream.skipBits(5); // frame_presentation_time_length_minus_1
}
}
// Parse decoder_model_info_present_flag
const decoderModelInfoPresentFlag = bitstream.readBits(1);
if (decoderModelInfoPresentFlag) {
// Store buffer_delay_length_minus_1 instead of just skipping
bufferDelayLengthMinus1 = bitstream.readBits(5);
bitstream.skipBits(32); // num_units_in_decoding_tick
bitstream.skipBits(5); // buffer_removal_time_length_minus_1
bitstream.skipBits(5); // frame_presentation_time_length_minus_1
}
// Parse operating_points_cnt_minus_1
const initialDisplayDelayPresentFlag = bitstream.readBits(1);
const operatingPointsCntMinus1 = bitstream.readBits(5);
// For each operating point
for (let i = 0; i <= operatingPointsCntMinus1; i++) {
// operating_point_idc[i]
bitstream.skipBits(12);
bitstream.skipBits(12); // operating_point_idc[i]
// seq_level_idx[i]
const seqLevelIdx = bitstream.readBits(5);
if (i === 0) {
@@ -2022,7 +2026,6 @@ export const extractAv1CodecInfoFromPacket = (
}
if (seqLevelIdx > 7) {
// seq_tier[i]
const seqTierTemp = bitstream.readBits(1);
if (i === 0) {
seqTier = seqTierTemp;
@@ -2030,7 +2033,6 @@ export const extractAv1CodecInfoFromPacket = (
}
if (decoderModelInfoPresentFlag) {
// decoder_model_present_for_this_op[i]
const decoderModelPresentForThisOp = bitstream.readBits(1);
if (decoderModelPresentForThisOp) {
@@ -2041,12 +2043,12 @@ export const extractAv1CodecInfoFromPacket = (
}
}
// initial_display_delay_present_flag
const initialDisplayDelayPresentFlag = bitstream.readBits(1);
if (initialDisplayDelayPresentFlag) {
// initial_display_delay_minus_1[i]
bitstream.skipBits(4);
const initialDisplayDelayPresentForThisOp = bitstream.readBits(1);
if (initialDisplayDelayPresentForThisOp) {
bitstream.skipBits(4); // initial_display_delay_minus_1[i]
}
}
}
}
@@ -2129,11 +2131,32 @@ export const extractAv1CodecInfoFromPacket = (
monochrome = bitstream.readBits(1);
}
let colourPrimaries = 2; // CP_UNSPECIFIED
let transferCharacteristics = 2; // TC_UNSPECIFIED
let matrixCoefficients = 2; // MC_UNSPECIFIED
const colorDescriptionPresentFlag = bitstream.readBits(1);
if (colorDescriptionPresentFlag) {
colourPrimaries = bitstream.readBits(8);
transferCharacteristics = bitstream.readBits(8);
matrixCoefficients = bitstream.readBits(8);
}
let videoFullRangeFlag = 0;
let chromaSubsamplingX = 1;
let chromaSubsamplingY = 1;
let chromaSamplePosition = 0;
let chromaSamplePosition = 0; // CSP_UNKNOWN
if (monochrome) {
videoFullRangeFlag = bitstream.readBits(1);
} else if (colourPrimaries === 1 && transferCharacteristics === 13 && matrixCoefficients === 0) {
// sRGB with an identity matrix, which is always full range 4:4:4
videoFullRangeFlag = 1;
chromaSubsamplingX = 0;
chromaSubsamplingY = 0;
} else {
videoFullRangeFlag = bitstream.readBits(1);
if (!monochrome) {
if (seqProfile === 0) {
chromaSubsamplingX = 1;
chromaSubsamplingY = 1;
@@ -2143,9 +2166,10 @@ export const extractAv1CodecInfoFromPacket = (
} else {
if (bitDepth === 12) {
chromaSubsamplingX = bitstream.readBits(1);
if (chromaSubsamplingX) {
chromaSubsamplingY = bitstream.readBits(1);
}
chromaSubsamplingY = chromaSubsamplingX ? bitstream.readBits(1) : 0;
} else {
chromaSubsamplingX = 1;
chromaSubsamplingY = 0;
}
}
@@ -2163,12 +2187,56 @@ export const extractAv1CodecInfoFromPacket = (
chromaSubsamplingX,
chromaSubsamplingY,
chromaSamplePosition,
videoFullRangeFlag,
colourPrimaries,
transferCharacteristics,
matrixCoefficients,
};
}
return null;
};
export const av1CodecInfoHasColorInfo = (info: Av1CodecInfo) => {
return info.colourPrimaries !== 2
|| info.transferCharacteristics !== 2
|| info.matrixCoefficients !== 2;
};
export type ProresCodecInfo = {
colourPrimaries: number;
transferCharacteristics: number;
matrixCoefficients: number;
fullRange: boolean;
};
export const extractProresCodecInfoFromPacket = (packet: Uint8Array): ProresCodecInfo | null => {
// https://wiki.multimedia.cx/index.php/Apple_ProRes
const frameHeaderStart = 8;
if (packet.length < frameHeaderStart + 28) {
return null;
}
const view = toDataView(packet);
if (view.getUint32(4) !== 0x69637066) { // 'icpf'
return null;
}
const headerSize = view.getUint16(frameHeaderStart);
if (headerSize < 28) {
return null;
}
return {
fullRange: false, // ProRes is always limited range
colourPrimaries: view.getUint8(frameHeaderStart + 14),
transferCharacteristics: view.getUint8(frameHeaderStart + 15),
matrixCoefficients: view.getUint8(frameHeaderStart + 16),
};
};
export const parseOpusIdentificationHeader = (bytes: Uint8Array) => {
const view = toDataView(bytes);
@@ -2337,8 +2405,8 @@ export const determineVideoPacketType = (
// In addition to IDR, Recovery Point SEI also counts as a valid H.264 keyframe by current consensus.
// See https://github.com/w3c/webcodecs/issues/650 for the relevant discussion. WebKit and Firefox have
// always supported them, but Chromium hasn't, therefore the (admittedly dirty) version check.
if (type === AvcNalUnitType.SEI && (!isChromium() || getChromiumVersion()! >= 144)) {
// always supported them, but Chromium hasn't.
if (type === AvcNalUnitType.SEI && !isChromium()) {
const nalUnit = packetData.subarray(loc.offset, loc.offset + loc.length);
const bytes = removeEmulationPreventionBytes(nalUnit);
let pos = 1; // Skip NALU header
+136 -1
View File
@@ -10,13 +10,22 @@ import { parseAacAudioSpecificConfig } from '../shared/aac-misc';
import {
Av1CodecInfo,
AvcDecoderConfigurationRecord,
deserializeAvcDecoderConfigurationRecord,
deserializeHevcDecoderConfigurationRecord,
HevcDecoderConfigurationRecord,
HevcNalUnitType,
parseAvcSps,
parseHevcSps,
ProresCodecInfo,
Vp9CodecInfo,
} from './codec-data';
import {
COLOR_PRIMARIES_MAP,
COLOR_PRIMARIES_MAP_INVERSE,
MATRIX_COEFFICIENTS_MAP,
MATRIX_COEFFICIENTS_MAP_INVERSE,
TRANSFER_CHARACTERISTICS_MAP,
TRANSFER_CHARACTERISTICS_MAP_INVERSE,
assert,
assertNever,
base64ToBytes,
@@ -370,7 +379,7 @@ export const generateAv1CodecConfigurationFromCodecString = (codecString: string
const tier = levelAndTier.slice(-1) === 'H' ? 1 : 0;
const bitDepth = Number(parts[3]);
const highBitDepth = bitDepth === 8 ? 0 : 1;
const twelveBit = 0;
const twelveBit = bitDepth === 12 ? 1 : 0;
const monochrome = parts[4] ? Number(parts[4]) : 0;
const chromaSubsamplingX = parts[5] ? Number(parts[5][0]) : 1;
const chromaSubsamplingY = parts[5] ? Number(parts[5][1]) : 1;
@@ -578,6 +587,132 @@ export const extractVideoCodecString = (trackInfo: {
throw new TypeError(`Unhandled codec '${codec}'.`);
};
export const extractColorSpace = (info: {
codec: VideoCodec | null;
codecDescription: Uint8Array | null;
avcCodecInfo: AvcDecoderConfigurationRecord | null;
hevcCodecInfo: HevcDecoderConfigurationRecord | null;
vp9CodecInfo: Vp9CodecInfo | null;
av1CodecInfo: Av1CodecInfo | null;
proresCodecInfo: ProresCodecInfo | null;
}): VideoColorSpaceInit => {
switch (info.codec) {
case 'avc': {
let spsData = info.avcCodecInfo?.sequenceParameterSets[0];
if (!spsData && info.codecDescription) {
spsData = deserializeAvcDecoderConfigurationRecord(info.codecDescription)?.sequenceParameterSets[0];
}
if (spsData) {
const spsInfo = parseAvcSps(spsData);
if (spsInfo) {
return {
primaries:
COLOR_PRIMARIES_MAP_INVERSE[spsInfo.colourPrimaries] as
VideoColorPrimaries | undefined,
transfer:
TRANSFER_CHARACTERISTICS_MAP_INVERSE[spsInfo.transferCharacteristics] as
VideoTransferCharacteristics | undefined,
matrix:
MATRIX_COEFFICIENTS_MAP_INVERSE[spsInfo.matrixCoefficients] as
VideoMatrixCoefficients | undefined,
fullRange: !!spsInfo.fullRangeFlag,
};
}
}
}; break;
case 'hevc': {
let spsData = info.hevcCodecInfo?.arrays
.find(x => x.nalUnitType === HevcNalUnitType.SPS_NUT)?.nalUnits[0];
if (!spsData && info.codecDescription) {
spsData = deserializeHevcDecoderConfigurationRecord(info.codecDescription)?.arrays
.find(x => x.nalUnitType === HevcNalUnitType.SPS_NUT)?.nalUnits[0];
}
if (spsData) {
const spsInfo = parseHevcSps(spsData);
if (spsInfo) {
return {
primaries:
COLOR_PRIMARIES_MAP_INVERSE[spsInfo.colourPrimaries] as
VideoColorPrimaries | undefined,
transfer:
TRANSFER_CHARACTERISTICS_MAP_INVERSE[spsInfo.transferCharacteristics] as
VideoTransferCharacteristics | undefined,
matrix:
MATRIX_COEFFICIENTS_MAP_INVERSE[spsInfo.matrixCoefficients] as
VideoMatrixCoefficients | undefined,
fullRange: !!spsInfo.fullRangeFlag,
};
}
}
}; break;
case 'vp8': {
// The situation is fucky; do nothing for now.
}; break;
case 'vp9': {
if (info.vp9CodecInfo) {
return {
primaries:
COLOR_PRIMARIES_MAP_INVERSE[info.vp9CodecInfo.colourPrimaries] as
VideoColorPrimaries | undefined,
transfer:
TRANSFER_CHARACTERISTICS_MAP_INVERSE[info.vp9CodecInfo.transferCharacteristics] as
VideoTransferCharacteristics | undefined,
matrix:
MATRIX_COEFFICIENTS_MAP_INVERSE[info.vp9CodecInfo.matrixCoefficients] as
VideoMatrixCoefficients | undefined,
fullRange: !!info.vp9CodecInfo.videoFullRangeFlag,
};
}
}; break;
case 'av1': {
if (info.av1CodecInfo) {
return {
primaries:
COLOR_PRIMARIES_MAP_INVERSE[info.av1CodecInfo.colourPrimaries] as
VideoColorPrimaries | undefined,
transfer:
TRANSFER_CHARACTERISTICS_MAP_INVERSE[info.av1CodecInfo.transferCharacteristics] as
VideoTransferCharacteristics | undefined,
matrix:
MATRIX_COEFFICIENTS_MAP_INVERSE[info.av1CodecInfo.matrixCoefficients] as
VideoMatrixCoefficients | undefined,
fullRange: !!info.av1CodecInfo.videoFullRangeFlag,
};
}
}; break;
case 'prores': {
if (info.proresCodecInfo) {
return {
primaries:
COLOR_PRIMARIES_MAP_INVERSE[info.proresCodecInfo.colourPrimaries] as
VideoColorPrimaries | undefined,
transfer:
TRANSFER_CHARACTERISTICS_MAP_INVERSE[info.proresCodecInfo.transferCharacteristics] as
VideoTransferCharacteristics | undefined,
matrix:
MATRIX_COEFFICIENTS_MAP_INVERSE[info.proresCodecInfo.matrixCoefficients] as
VideoMatrixCoefficients | undefined,
fullRange: info.proresCodecInfo.fullRange,
};
}
}; break;
}
return {
primaries: undefined,
transfer: undefined,
matrix: undefined,
fullRange: undefined,
};
};
export const buildAudioCodecString = (codec: AudioCodec, numberOfChannels: number, sampleRate: number) => {
if (codec === 'aac') {
// If stereo or higher channels and lower sample rate, likely using HE-AAC v2 with PS
+8 -7
View File
@@ -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
+50 -21
View File
@@ -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';
+19 -10
View File
@@ -12,7 +12,16 @@ 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,
isNumber,
isThenable,
MaybePromise,
Rational,
Rotation,
roundToDivisor,
simplifyRational,
} from './misc';
import { TrackType } from './output';
import { EncodedPacket, PacketType } from './packet';
import { TrackDisposition } from './metadata';
@@ -528,7 +537,7 @@ export abstract class InputTrack {
}
const requireSync = <T>(value: MaybePromise<T>, 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 +563,7 @@ const toValidatedPredicate = <T extends InputTrack>(
};
const result = predicate(track);
if (result instanceof Promise) {
if (isThenable(result)) {
return result.then(handle);
}
return handle(result);
@@ -869,7 +878,7 @@ export class InputVideoTrack extends InputTrack {
}
if (
options.targetPacketCount !== undefined
&& (!Number.isFinite(options.targetPacketCount) || options.targetPacketCount < 0)
&& (!isNumber(options.targetPacketCount) || options.targetPacketCount < 0)
) {
throw new TypeError('options.targetPacketCount must be a non-negative number.');
}
@@ -1225,7 +1234,7 @@ export const toValidatedInputTrackQuery = <T extends InputTrack>(
};
const result = query.filter!(track);
if (result instanceof Promise) {
if (isThenable(result)) {
return result.then(handle);
} else {
return handle(result);
@@ -1248,7 +1257,7 @@ export const toValidatedInputTrackQuery = <T extends InputTrack>(
};
const result = query.sortBy!(track);
if (result instanceof Promise) {
if (isThenable(result)) {
return result.then(handle);
} else {
return handle(result);
@@ -1274,7 +1283,7 @@ export const mergeInputTrackQueries = <T extends InputTrack>(
return queryB?.filter?.(track) ?? true;
};
if (resultA instanceof Promise) {
if (isThenable(resultA)) {
return resultA.then(handleResultA);
} else {
return handleResultA(resultA);
@@ -1294,7 +1303,7 @@ export const mergeInputTrackQueries = <T extends InputTrack>(
];
};
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 +1322,7 @@ export const queryInputTracks = async <T extends InputTrack>(
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 +1337,7 @@ export const queryInputTracks = async <T extends InputTrack>(
}
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)
+29 -14
View File
@@ -16,7 +16,7 @@ import {
COLOR_PRIMARIES_MAP,
TRANSFER_CHARACTERISTICS_MAP,
MATRIX_COEFFICIENTS_MAP,
colorSpaceIsComplete,
colorSpaceIsEmpty,
UNDETERMINED_LANGUAGE,
assertNever,
keyValueIterator,
@@ -750,7 +750,9 @@ export const videoSampleDescription = (
], [
VIDEO_CODEC_TO_CONFIGURATION_BOX[trackData.track.source._codec]?.(trackData) ?? null,
pasp(trackData),
colorSpaceIsComplete(trackData.info.decoderConfig.colorSpace) ? colr(trackData) : null,
colorSpaceIsEmpty(trackData.info.decoderConfig.colorSpace)
? null
: colr(trackData),
]);
/** Pixel Aspect Ratio Box: Specifies pixel width:height spacing for non-square pixels. */
@@ -766,15 +768,28 @@ export const pasp = (trackData: IsobmffVideoTrackData) => {
};
/** Colour Information Box: Specifies the color space of the video. */
export const colr = (trackData: IsobmffVideoTrackData) => box('colr', [
ascii(trackData.muxer.isQuickTime ? 'nclc' : 'nclx'), // Colour type
u16(COLOR_PRIMARIES_MAP[trackData.info.decoderConfig.colorSpace!.primaries!]), // Colour primaries
u16(TRANSFER_CHARACTERISTICS_MAP[trackData.info.decoderConfig.colorSpace!.transfer!]), // Transfer characteristics
u16(MATRIX_COEFFICIENTS_MAP[trackData.info.decoderConfig.colorSpace!.matrix!]), // Matrix coefficients
trackData.muxer.isQuickTime
? [] // Doesn't have it
: u8((trackData.info.decoderConfig.colorSpace!.fullRange ? 1 : 0) << 7), // Full range flag
]);
export const colr = (trackData: IsobmffVideoTrackData) => {
const colorSpace = trackData.info.decoderConfig.colorSpace;
return box('colr', [
// Colour type
ascii(trackData.muxer.isQuickTime ? 'nclc' : 'nclx'),
// Colour primaries
u16(colorSpace?.primaries != null ? COLOR_PRIMARIES_MAP[colorSpace.primaries] : 2),
// Transfer characteristics
u16(colorSpace?.transfer != null ? TRANSFER_CHARACTERISTICS_MAP[colorSpace.transfer] : 2),
// Matrix coefficients
u16(colorSpace?.matrix != null ? MATRIX_COEFFICIENTS_MAP[colorSpace.matrix] : 2),
// Full range flag
trackData.muxer.isQuickTime
? [] // Doesn't have it
: u8((colorSpace?.fullRange ? 1 : 0) << 7),
]);
};
/** AVC Configuration Box: Provides additional information to the decoder. */
export const avcC = (trackData: IsobmffVideoTrackData) => trackData.info.decoderConfig && box('avcC', [
@@ -811,17 +826,17 @@ export const vpcC = (trackData: IsobmffVideoTrackData) => {
? Number(parts[5])
: decoderConfig.colorSpace?.primaries
? COLOR_PRIMARIES_MAP[decoderConfig.colorSpace.primaries]
: 2; // Default to undetermined
: 1;
const transferCharacteristics = parts[6]
? Number(parts[6])
: decoderConfig.colorSpace?.transfer
? TRANSFER_CHARACTERISTICS_MAP[decoderConfig.colorSpace.transfer]
: 2;
: 1;
const matrixCoefficients = parts[7]
? Number(parts[7])
: decoderConfig.colorSpace?.matrix
? MATRIX_COEFFICIENTS_MAP[decoderConfig.colorSpace.matrix]
: 2;
: 1;
return fullBox('vpcC', 1, 0, [
u8(profile), // Profile
+148 -37
View File
@@ -14,6 +14,7 @@ import {
DTS_FOURCCS,
DtsFourCc,
extractAudioCodecString,
extractColorSpace,
extractVideoCodecString,
MediaCodec,
OPUS_SAMPLE_RATE,
@@ -26,12 +27,16 @@ import {
} from '../codec';
import {
Av1CodecInfo,
av1CodecInfoHasColorInfo,
AvcDecoderConfigurationRecord,
extractAv1CodecInfoFromPacket,
extractProresCodecInfoFromPacket,
extractVp9CodecInfoFromPacket,
FlacBlockType,
HevcDecoderConfigurationRecord,
ProresCodecInfo,
Vp9CodecInfo,
vp9CodecInfoHasColorInfo,
parseEac3Config,
getEac3SampleRate,
getEac3ChannelCount,
@@ -39,6 +44,8 @@ import {
parseDtsSpecificBox,
DTS_SPECIFIC_BOX_SIZE,
AC3_ACMOD_CHANNEL_COUNTS,
extractAvcDecoderConfigurationRecord,
extractHevcDecoderConfigurationRecord,
} from '../codec-data';
import { Demuxer } from '../demuxer';
import { Input } from '../input';
@@ -56,6 +63,7 @@ import {
COLOR_PRIMARIES_MAP_INVERSE,
findLastIndex,
isIso639Dash2LanguageCode,
isThenable,
last,
MATRIX_COEFFICIENTS_MAP_INVERSE,
normalizeRotation,
@@ -69,6 +77,8 @@ import {
roundIfAlmostInteger,
hexStringToBytes,
HEX_STRING_REGEX,
EMPTY_COLOR_SPACE,
colorSpaceIsComplete,
} from '../misc';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
import { buildIsobmffMimeType, parsePsshBoxContents, psshBoxesAreEqual, PsshBox } from './isobmff-misc';
@@ -148,12 +158,13 @@ type InternalTrack = {
squarePixelHeight: number;
codec: VideoCodec | null;
codecDescription: Uint8Array | null;
colorSpace: VideoColorSpaceInit | null;
colorSpace: VideoColorSpaceInit;
avcType: 1 | 3 | null;
avcCodecInfo: AvcDecoderConfigurationRecord | null;
hevcCodecInfo: HevcDecoderConfigurationRecord | null;
vp9CodecInfo: Vp9CodecInfo | null;
av1CodecInfo: Av1CodecInfo | null;
proresCodecInfo: ProresCodecInfo | null;
proresFormat: ProresFourCc | null;
};
} | {
@@ -362,7 +373,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;
@@ -378,7 +389,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;
@@ -429,7 +440,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);
@@ -441,7 +452,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);
@@ -449,7 +460,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);
@@ -676,14 +687,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);
@@ -1024,12 +1035,13 @@ export class IsobmffDemuxer extends Demuxer {
squarePixelHeight: -1,
codec: null,
codecDescription: null,
colorSpace: null,
colorSpace: { ...EMPTY_COLOR_SPACE },
avcType: null,
avcCodecInfo: null,
hevcCodecInfo: null,
vp9CodecInfo: null,
av1CodecInfo: null,
proresCodecInfo: null,
proresFormat: null,
};
} else if (handlerType === 'soun') {
@@ -1390,6 +1402,11 @@ export class IsobmffDemuxer extends Demuxer {
}
assert(track.info);
if (boxInfo.contentSize === 0) {
// avcC box is empty, let's treat this like an Annex B stream
break;
}
track.info.codecDescription = readBytes(slice, boxInfo.contentSize);
}; break;
@@ -1400,6 +1417,11 @@ export class IsobmffDemuxer extends Demuxer {
}
assert(track.info);
if (boxInfo.contentSize === 0) {
// hvcC box is empty, let's treat this like an Annex B stream
break;
}
track.info.codecDescription = readBytes(slice, boxInfo.contentSize);
}; break;
@@ -1459,6 +1481,12 @@ export class IsobmffDemuxer extends Demuxer {
// Logic from https://aomediacodec.github.io/av1-spec/av1-spec.pdf
const bitDepth = profile === 2 && highBitDepth ? (twelveBit ? 12 : 10) : (highBitDepth ? 10 : 8);
slice.skip(1); // Reserved bits + initial presentation delay
// Parse config OBUs if there are any
const configObus = readBytes(slice, boxInfo.contentSize - 4);
const configObuInfo = extractAv1CodecInfoFromPacket(configObus);
track.info.av1CodecInfo = {
profile,
level,
@@ -1468,6 +1496,10 @@ export class IsobmffDemuxer extends Demuxer {
chromaSubsamplingX,
chromaSubsamplingY,
chromaSamplePosition,
videoFullRangeFlag: configObuInfo?.videoFullRangeFlag ?? 0,
colourPrimaries: configObuInfo?.colourPrimaries ?? 2,
transferCharacteristics: configObuInfo?.transferCharacteristics ?? 2,
matrixCoefficients: configObuInfo?.matrixCoefficients ?? 2,
};
}; break;
@@ -1596,11 +1628,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;
}
}
}
@@ -3132,24 +3164,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);
}
}
}
@@ -3188,15 +3227,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);
}
}
}
@@ -3298,7 +3341,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;
@@ -3386,11 +3429,16 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
}
async getColorSpace(): Promise<VideoColorSpaceInit> {
const decoderConfig = await this.getDecoderConfig();
if (!decoderConfig) {
return this.internalTrack.info.colorSpace;
}
return {
primaries: this.internalTrack.info.colorSpace?.primaries,
transfer: this.internalTrack.info.colorSpace?.transfer,
matrix: this.internalTrack.info.colorSpace?.matrix,
fullRange: this.internalTrack.info.colorSpace?.fullRange,
primaries: decoderConfig.colorSpace?.primaries,
transfer: decoderConfig.colorSpace?.transfer,
matrix: decoderConfig.colorSpace?.matrix,
fullRange: decoderConfig.colorSpace?.fullRange,
};
}
@@ -3407,12 +3455,64 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
}
return this.decoderConfigPromise ??= (async (): Promise<VideoDecoderConfig> => {
if (this.internalTrack.info.codec === 'vp9' && !this.internalTrack.info.vp9CodecInfo) {
if (this.internalTrack.info.codec === 'avc' && !this.internalTrack.info.codecDescription) {
const firstPacket = await this.getFirstPacket({});
this.internalTrack.info.vp9CodecInfo = firstPacket && extractVp9CodecInfoFromPacket(firstPacket.data);
} else if (this.internalTrack.info.codec === 'av1' && !this.internalTrack.info.av1CodecInfo) {
this.internalTrack.info.avcCodecInfo
= firstPacket && extractAvcDecoderConfigurationRecord(firstPacket.data);
} else if (this.internalTrack.info.codec === 'hevc' && !this.internalTrack.info.codecDescription) {
const firstPacket = await this.getFirstPacket({});
this.internalTrack.info.av1CodecInfo = firstPacket && extractAv1CodecInfoFromPacket(firstPacket.data);
this.internalTrack.info.hevcCodecInfo
= firstPacket && extractHevcDecoderConfigurationRecord(firstPacket.data);
} else if (
this.internalTrack.info.codec === 'vp9'
&& (
!this.internalTrack.info.vp9CodecInfo
// The codec info extracted from vpcC may claim the color space is "undefined"
|| !vp9CodecInfoHasColorInfo(this.internalTrack.info.vp9CodecInfo)
)
) {
const firstPacket = await this.getFirstPacket({});
const packetInfo = firstPacket && extractVp9CodecInfoFromPacket(firstPacket.data);
if (packetInfo) {
this.internalTrack.info.vp9CodecInfo = {
...(this.internalTrack.info.vp9CodecInfo ?? packetInfo),
videoFullRangeFlag: packetInfo.videoFullRangeFlag,
colourPrimaries: packetInfo.colourPrimaries,
transferCharacteristics: packetInfo.transferCharacteristics,
matrixCoefficients: packetInfo.matrixCoefficients,
};
}
} else if (
this.internalTrack.info.codec === 'av1'
&& (
!this.internalTrack.info.av1CodecInfo
// The codec info extracted from av1C may not contain color space information
|| !av1CodecInfoHasColorInfo(
this.internalTrack.info.av1CodecInfo,
)
)
) {
const firstPacket = await this.getFirstPacket({});
const packetInfo = firstPacket && extractAv1CodecInfoFromPacket(firstPacket.data);
if (packetInfo) {
this.internalTrack.info.av1CodecInfo = packetInfo;
}
} else if (this.internalTrack.info.codec === 'prores' && !this.internalTrack.info.proresCodecInfo) {
const firstPacket = await this.getFirstPacket({});
this.internalTrack.info.proresCodecInfo
= firstPacket && extractProresCodecInfoFromPacket(firstPacket.data);
}
if (!colorSpaceIsComplete(this.internalTrack.info.colorSpace)) {
const colorSpace = extractColorSpace(this.internalTrack.info);
// Fill the missing values
this.internalTrack.info.colorSpace.primaries ??= colorSpace.primaries;
this.internalTrack.info.colorSpace.transfer ??= colorSpace.transfer;
this.internalTrack.info.colorSpace.matrix ??= colorSpace.matrix;
this.internalTrack.info.colorSpace.fullRange ??= colorSpace.fullRange;
}
const config: VideoDecoderConfig = {
@@ -3420,7 +3520,7 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
codedWidth: this.internalTrack.info.width,
codedHeight: this.internalTrack.info.height,
description: this.internalTrack.info.codecDescription ?? undefined,
colorSpace: this.internalTrack.info.colorSpace ?? undefined,
colorSpace: this.internalTrack.info.colorSpace,
};
if (
@@ -3717,7 +3817,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.');
}
@@ -3759,6 +3859,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,
+2 -2
View File
@@ -525,8 +525,8 @@ export class IsobmffMuxer extends Muxer {
decoderConfig.description = buildAacAudioSpecificConfig({
objectType: adtsFrame.objectType,
sampleRate,
numberOfChannels,
outputSampleRate: sampleRate,
outputNumberOfChannels: numberOfChannels,
});
requiresAdtsStripping = true;
}
+3 -3
View File
@@ -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;
+78 -50
View File
@@ -12,6 +12,7 @@ import {
extractDtsFourCcFromPacket,
extractAvcDecoderConfigurationRecord,
extractHevcDecoderConfigurationRecord,
extractProresCodecInfoFromPacket,
extractVp9CodecInfoFromPacket,
} from '../codec-data';
import {
@@ -19,6 +20,7 @@ import {
AudioCodec,
DtsFourCc,
extractAudioCodecString,
extractColorSpace,
extractVideoCodecString,
MediaCodec,
OPUS_SAMPLE_RATE,
@@ -40,8 +42,11 @@ import {
assert,
binarySearchLessOrEqual,
COLOR_PRIMARIES_MAP_INVERSE,
colorSpaceIsComplete,
EMPTY_COLOR_SPACE,
findLastIndex,
isIso639Dash2LanguageCode,
isThenable,
last,
MATRIX_COEFFICIENTS_MAP_INVERSE,
normalizeRotation,
@@ -219,7 +224,7 @@ type InternalTrack = {
rotation: Rotation;
codec: VideoCodec | null;
codecDescription: Uint8Array | null;
colorSpace: VideoColorSpaceInit | null;
colorSpace: VideoColorSpaceInit;
alphaMode: boolean;
proresFormat: ProresFourCc | null;
}
@@ -334,7 +339,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 +355,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 +438,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 +473,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 +489,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 +524,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 +538,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 +613,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 +642,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 +899,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 +912,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);
}
@@ -1198,7 +1203,7 @@ export class MatroskaDemuxer extends Demuxer {
rotation: 0,
codec: null,
codecDescription: null,
colorSpace: null,
colorSpace: { ...EMPTY_COLOR_SPACE },
alphaMode: false,
proresFormat: null,
};
@@ -1362,37 +1367,39 @@ export class MatroskaDemuxer extends Demuxer {
case EBMLId.Colour: {
if (this.currentTrack?.info?.type !== 'video') break;
this.currentTrack.info.colorSpace = {};
this.readContiguousElements(slice.slice(dataStartPos, size));
}; break;
case EBMLId.MatrixCoefficients: {
if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace) break;
if (this.currentTrack?.info?.type !== 'video') break;
const matrixCoefficients = readUnsignedInt(slice, size);
const mapped = MATRIX_COEFFICIENTS_MAP_INVERSE[matrixCoefficients] ?? null;
const mapped = MATRIX_COEFFICIENTS_MAP_INVERSE[matrixCoefficients];
this.currentTrack.info.colorSpace.matrix = mapped as VideoColorSpaceInit['matrix'];
}; break;
case EBMLId.Range: {
if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace) break;
if (this.currentTrack?.info?.type !== 'video') break;
this.currentTrack.info.colorSpace.fullRange = readUnsignedInt(slice, size) === 2;
const range = readUnsignedInt(slice, size);
this.currentTrack.info.colorSpace.fullRange = range === 1 || range === 2
? range === 2
: undefined;
}; break;
case EBMLId.TransferCharacteristics: {
if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace) break;
if (this.currentTrack?.info?.type !== 'video') break;
const transferCharacteristics = readUnsignedInt(slice, size);
const mapped = TRANSFER_CHARACTERISTICS_MAP_INVERSE[transferCharacteristics] ?? null;
const mapped = TRANSFER_CHARACTERISTICS_MAP_INVERSE[transferCharacteristics];
this.currentTrack.info.colorSpace.transfer = mapped as VideoColorSpaceInit['transfer'];
}; break;
case EBMLId.Primaries: {
if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace) break;
if (this.currentTrack?.info?.type !== 'video') break;
const primaries = readUnsignedInt(slice, size);
const mapped = COLOR_PRIMARIES_MAP_INVERSE[primaries] ?? null;
const mapped = COLOR_PRIMARIES_MAP_INVERSE[primaries];
this.currentTrack.info.colorSpace.primaries = mapped as VideoColorSpaceInit['primaries'];
}; break;
@@ -2347,7 +2354,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 +2425,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);
@@ -2489,11 +2496,16 @@ class MatroskaVideoTrackBacking extends MatroskaTrackBacking implements InputVid
}
async getColorSpace(): Promise<VideoColorSpaceInit> {
const decoderConfig = await this.getDecoderConfig();
if (!decoderConfig) {
return this.internalTrack.info.colorSpace;
}
return {
primaries: this.internalTrack.info.colorSpace?.primaries,
transfer: this.internalTrack.info.colorSpace?.transfer,
matrix: this.internalTrack.info.colorSpace?.matrix,
fullRange: this.internalTrack.info.colorSpace?.fullRange,
primaries: decoderConfig.colorSpace?.primaries,
transfer: decoderConfig.colorSpace?.transfer,
matrix: decoderConfig.colorSpace?.matrix,
fullRange: decoderConfig.colorSpace?.fullRange,
};
}
@@ -2516,6 +2528,7 @@ class MatroskaVideoTrackBacking extends MatroskaTrackBacking implements InputVid
const needsPacketForAdditionalInfo
= this.internalTrack.info.codec === 'vp9'
|| this.internalTrack.info.codec === 'av1'
|| this.internalTrack.info.codec === 'prores'
// Packets are in Annex B format:
|| (this.internalTrack.info.codec === 'avc' && !this.internalTrack.info.codecDescription)
// Packets are in Annex B format:
@@ -2525,32 +2538,47 @@ class MatroskaVideoTrackBacking extends MatroskaTrackBacking implements InputVid
firstPacket = await this.getFirstPacket({});
}
const codecInfo = {
width: this.internalTrack.info.width,
height: this.internalTrack.info.height,
codec: this.internalTrack.info.codec,
codecDescription: this.internalTrack.info.codecDescription,
colorSpace: this.internalTrack.info.colorSpace,
avcType: 1 as const, // We don't know better (or do we?) so just assume 'avc1'
avcCodecInfo: this.internalTrack.info.codec === 'avc' && firstPacket
? extractAvcDecoderConfigurationRecord(firstPacket.data)
: null,
hevcCodecInfo: this.internalTrack.info.codec === 'hevc' && firstPacket
? extractHevcDecoderConfigurationRecord(firstPacket.data)
: null,
vp9CodecInfo: this.internalTrack.info.codec === 'vp9' && firstPacket
? extractVp9CodecInfoFromPacket(firstPacket.data)
: null,
av1CodecInfo: this.internalTrack.info.codec === 'av1' && firstPacket
? extractAv1CodecInfoFromPacket(firstPacket.data)
: null,
proresCodecInfo: this.internalTrack.info.codec === 'prores' && firstPacket
? extractProresCodecInfoFromPacket(firstPacket.data)
: null,
proresFormat: this.internalTrack.info.proresFormat,
};
if (!colorSpaceIsComplete(this.internalTrack.info.colorSpace)) {
const colorSpace = extractColorSpace(codecInfo);
// Fill the missing values
this.internalTrack.info.colorSpace.primaries ??= colorSpace.primaries;
this.internalTrack.info.colorSpace.transfer ??= colorSpace.transfer;
this.internalTrack.info.colorSpace.matrix ??= colorSpace.matrix;
this.internalTrack.info.colorSpace.fullRange ??= colorSpace.fullRange;
}
const config: VideoDecoderConfig = {
codec: extractVideoCodecString({
width: this.internalTrack.info.width,
height: this.internalTrack.info.height,
codec: this.internalTrack.info.codec,
codecDescription: this.internalTrack.info.codecDescription,
colorSpace: this.internalTrack.info.colorSpace,
avcType: 1, // We don't know better (or do we?) so just assume 'avc1'
avcCodecInfo: this.internalTrack.info.codec === 'avc' && firstPacket
? extractAvcDecoderConfigurationRecord(firstPacket.data)
: null,
hevcCodecInfo: this.internalTrack.info.codec === 'hevc' && firstPacket
? extractHevcDecoderConfigurationRecord(firstPacket.data)
: null,
vp9CodecInfo: this.internalTrack.info.codec === 'vp9' && firstPacket
? extractVp9CodecInfoFromPacket(firstPacket.data)
: null,
av1CodecInfo: this.internalTrack.info.codec === 'av1' && firstPacket
? extractAv1CodecInfoFromPacket(firstPacket.data)
: null,
proresFormat: this.internalTrack.info.proresFormat,
}),
codec: extractVideoCodecString(codecInfo),
codedWidth: this.internalTrack.info.width,
codedHeight: this.internalTrack.info.height,
description: this.internalTrack.info.codecDescription ?? undefined,
colorSpace: this.internalTrack.info.colorSpace ?? undefined,
colorSpace: this.internalTrack.info.colorSpace,
};
if (
+13 -11
View File
@@ -14,7 +14,7 @@ import {
UNDETERMINED_LANGUAGE,
assert,
assertNever,
colorSpaceIsComplete,
colorSpaceIsEmpty,
imageMimeTypeToExtension,
keyValueIterator,
normalizeRotation,
@@ -405,29 +405,31 @@ export class MatroskaMuxer extends Muxer {
(hasNonSquarePixelAspectRatio ? { id: EBMLId.DisplayHeight, data: trackData.info.aspectRatio!.den } : null),
(hasNonSquarePixelAspectRatio ? { id: EBMLId.DisplayUnit, data: 3 } : null), // 3 = display aspect ratio
trackData.info.alphaMode ? { id: EBMLId.AlphaMode, data: 1 } : null,
(colorSpaceIsComplete(colorSpace)
? {
(colorSpaceIsEmpty(colorSpace)
? null
: {
id: EBMLId.Colour,
data: [
{
id: EBMLId.MatrixCoefficients,
data: MATRIX_COEFFICIENTS_MAP[colorSpace.matrix],
data: colorSpace?.matrix != null ? MATRIX_COEFFICIENTS_MAP[colorSpace.matrix] : 2,
},
{
id: EBMLId.TransferCharacteristics,
data: TRANSFER_CHARACTERISTICS_MAP[colorSpace.transfer],
data: colorSpace?.transfer != null
? TRANSFER_CHARACTERISTICS_MAP[colorSpace.transfer]
: 2,
},
{
id: EBMLId.Primaries,
data: COLOR_PRIMARIES_MAP[colorSpace.primaries],
data: colorSpace?.primaries != null ? COLOR_PRIMARIES_MAP[colorSpace.primaries] : 2,
},
{
id: EBMLId.Range,
data: colorSpace.fullRange ? 2 : 1,
data: colorSpace?.fullRange != null ? (colorSpace.fullRange ? 2 : 1) : 0,
},
],
}
: null),
}),
(flippedRotation
? {
id: EBMLId.Projection,
@@ -858,8 +860,8 @@ export class MatroskaMuxer extends Muxer {
decoderConfig.description = buildAacAudioSpecificConfig({
objectType: adtsFrame.objectType,
sampleRate,
numberOfChannels,
outputSampleRate: sampleRate,
outputNumberOfChannels: numberOfChannels,
});
requiresAdtsStripping = true;
}
+47 -15
View File
@@ -29,6 +29,7 @@ import {
assertNever,
CallSerializer,
clamp,
colorSpaceIsComplete,
getInt24,
getUint24,
insertSorted,
@@ -38,6 +39,7 @@ import {
isWebKit,
last,
mapAsyncGenerator,
missingWebCodecsClassMessage,
promiseWithResolvers,
removeItem,
Rotation,
@@ -943,20 +945,42 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
}
};
if (codec === 'avc' && this.decoderConfig.description && isChromium()) {
// Chromium has/had a bug with playing interlaced AVC (https://issues.chromium.org/issues/456919096)
// which can be worked around by requesting that software decoding be used. So, here we peek into the
// AVC description, if present, and switch to software decoding if we find interlaced content.
const record = deserializeAvcDecoderConfigurationRecord(toUint8Array(this.decoderConfig.description));
if (record && record.sequenceParameterSets.length > 0) {
const sps = parseAvcSps(record.sequenceParameterSets[0]!);
if (sps && sps.frameMbsOnlyFlag === 0) {
this.decoderConfig = {
...this.decoderConfig,
hardwareAcceleration: 'prefer-software',
};
if (isChromium()) {
if (codec === 'avc' && this.decoderConfig.description) {
// Chromium has/had a bug with playing interlaced AVC (https://issues.chromium.org/issues/456919096)
// which can be worked around by requesting that software decoding be used. So, here we peek into
// the AVC description, if present, and switch to software decoding if we find interlaced content.
const record = deserializeAvcDecoderConfigurationRecord(
toUint8Array(this.decoderConfig.description),
);
if (record && record.sequenceParameterSets.length > 0) {
const sps = parseAvcSps(record.sequenceParameterSets[0]!);
if (sps && sps.frameMbsOnlyFlag === 0) {
this.decoderConfig = {
...this.decoderConfig,
hardwareAcceleration: 'prefer-software',
};
}
}
}
if (!colorSpaceIsComplete(this.decoderConfig.colorSpace)) {
// Found via https://github.com/remotion-dev/remotion/issues/10841.
// If the color space is incomplete (which is often that it's just partially filled), Chromium has
// some nasty logic where it doesn't pass that information along to the GPU at all. The result is
// that information is genuinely lost, like the color matrix for example. Chromium has other code
// paths where it just fills the missing values with a hardcoded default, so we do the exact same
// thing here, with the same hardcoded defaults:
this.decoderConfig = {
...this.decoderConfig,
colorSpace: {
primaries: this.decoderConfig.colorSpace?.primaries ?? 'bt709',
matrix: this.decoderConfig.colorSpace?.matrix ?? 'bt709',
transfer: this.decoderConfig.colorSpace?.transfer ?? 'bt709',
fullRange: this.decoderConfig.colorSpace?.fullRange ?? false,
},
};
}
}
const stack = new Error('Decoding error').stack;
@@ -1691,8 +1715,12 @@ export class VideoSampleSink extends BaseMediaSampleSink<VideoSample> {
onError: (error: unknown) => unknown,
) {
if (!(await this._track.canDecode())) {
if (typeof VideoDecoder === 'undefined') {
throw new Error(missingWebCodecsClassMessage('VideoDecoder'));
}
throw new Error(
'This video track cannot be decoded by this browser. Make sure to check decodability before using'
'This video track cannot be decoded in this environment. Make sure to check decodability before using'
+ ' a track.',
);
}
@@ -2090,7 +2118,7 @@ class AudioDecoderWrapper extends DecoderWrapper<AudioSample> {
const sampleHandler = (sample: AudioSample) => {
let sampleTimestamp = sample.timestamp;
if (this.expectedFirstTimestamp && this.currentTimestamp === null) {
if (this.expectedFirstTimestamp !== null && this.currentTimestamp === null) {
this.timestampOffset = this.expectedFirstTimestamp - sampleTimestamp; ;
}
@@ -2426,8 +2454,12 @@ export class AudioSampleSink extends BaseMediaSampleSink<AudioSample> {
onError: (error: unknown) => unknown,
) {
if (!(await this._track.canDecode())) {
if (typeof AudioDecoder === 'undefined') {
throw new Error(missingWebCodecsClassMessage('AudioDecoder'));
}
throw new Error(
'This audio track cannot be decoded by this browser. Make sure to check decodability before using'
'This audio track cannot be decoded in this environment. Make sure to check decodability before using'
+ ' a track.',
);
}
+13 -10
View File
@@ -28,7 +28,9 @@ import {
clamp,
clearIntervalUnthrottled,
floorToDivisor,
isThenable,
last,
missingWebCodecsClassMessage,
promiseWithResolvers,
roundToDivisor,
setInt24,
@@ -425,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;
}
@@ -718,7 +720,7 @@ class VideoEncoderWrapper {
if (!selected) {
if (typeof VideoEncoder === 'undefined') {
throw new Error('VideoEncoder is not supported by this browser.');
throw new Error(missingWebCodecsClassMessage('VideoEncoder'));
}
// The candidates only differ in their rate control, so we describe them as one config with a
@@ -731,7 +733,7 @@ class VideoEncoderWrapper {
throw new Error(
`This specific encoder configuration (${firstConfig.codec}, ${rateControls.join(' / ')},`
+ ` ${firstConfig.width}x${firstConfig.height}, hardware acceleration:`
+ ` ${firstConfig.hardwareAcceleration ?? 'no-preference'}) is not supported by this browser.`
+ ` ${firstConfig.hardwareAcceleration ?? 'no-preference'}) is not supported in this environment.`
+ ` Consider using another codec or changing your video parameters.`,
);
}
@@ -1694,8 +1696,8 @@ export class MediaStreamVideoTrackSource extends VideoSource {
});
} else {
throw new Error(
'When no explicit frame rate is set, MediaStreamTrackProcessor is required; but it\'s not supported'
+ ' by this browser.',
'When no explicit frame rate is set, MediaStreamTrackProcessor is required; but it\'s not available'
+ ' in this environment.',
);
}
}
@@ -1936,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;
}
@@ -2210,7 +2212,7 @@ class AudioEncoderWrapper {
this.initPcmEncoder();
} else {
if (typeof AudioEncoder === 'undefined') {
throw new Error('AudioEncoder is not supported by this browser.');
throw new Error(missingWebCodecsClassMessage('AudioEncoder'));
}
let supported: boolean;
@@ -2226,7 +2228,8 @@ class AudioEncoderWrapper {
throw new Error(
`This specific encoder configuration (${encoderConfig.codec}, ${encoderConfig.bitrate} bps,`
+ ` ${encoderConfig.numberOfChannels} channels, ${encoderConfig.sampleRate} Hz) is not`
+ ` supported by this browser. Consider using another codec or changing your audio parameters.`,
+ ` supported in this environment. Consider using another codec or changing your`
+ ` audio parameters.`,
);
}
@@ -2254,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,
});
}
}
+43
View File
@@ -162,6 +162,25 @@ export const colorSpaceIsComplete = (
);
};
export const colorSpaceIsEmpty = (colorSpace: VideoColorSpaceInit | undefined) => {
return (
!colorSpace
|| (
colorSpace.primaries == null
&& colorSpace.transfer == null
&& colorSpace.matrix == null
&& colorSpace.fullRange == null
)
);
};
export const EMPTY_COLOR_SPACE: VideoColorSpaceInit = {
primaries: undefined,
transfer: undefined,
matrix: undefined,
fullRange: undefined,
};
export const isAllowSharedBufferSource = (x: unknown) => {
return (
x instanceof ArrayBuffer
@@ -721,6 +740,16 @@ export const getChromiumVersion = () => {
return chromiumVersionCache = Number(match[1]!);
};
export const missingWebCodecsClassMessage = (className: string) => {
if (typeof globalThis.isSecureContext !== 'undefined' && !globalThis.isSecureContext) {
// WebCodecs is not exposed in insecure contexts
return `${className} is not available in this environment; this may be because this page is running in an`
+ ` insecure context. Try serving your page over HTTPS or use localhost.`;
}
return `${className} is not available in this environment.`;
};
/**
* T or a promise that resolves to T.
* @group Miscellaneous
@@ -728,6 +757,20 @@ export const getChromiumVersion = () => {
*/
export type MaybePromise<T> = T | Promise<T>;
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 = <T>(value: MaybePromise<T>): value is Promise<T> => {
if (value instanceof NativePromiseConstructor || value instanceof Promise) {
return true;
}
// Fall back to a crude duck typing check
return typeof (value as PromiseLike<T> | 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;
+8 -7
View File
@@ -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
+2 -1
View File
@@ -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) {
+18 -17
View File
@@ -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<TsPacketHeader | null> {
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<TsPacket | null> {
// 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,
+9 -8
View File
@@ -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);
+4 -4
View File
@@ -30,7 +30,7 @@ import { MpegTsMuxer } from './mpeg-ts/mpeg-ts-muxer';
import { WaveMuxer } from './wave/wave-muxer';
import { HlsMuxer } from './hls/hls-muxer';
import { HLS_MIME_TYPE } from './hls/hls-misc';
import { MaybePromise, FilePath, toArray } from './misc';
import { MaybePromise, FilePath, isNumber, toArray } from './misc';
import { Target } from './target';
/**
@@ -229,7 +229,7 @@ export abstract class IsobmffOutputFormat extends OutputFormat {
}
if (
options.minimumFragmentDuration !== undefined
&& (!Number.isFinite(options.minimumFragmentDuration) || options.minimumFragmentDuration < 0)
&& (!isNumber(options.minimumFragmentDuration) || options.minimumFragmentDuration < 0)
) {
throw new TypeError('options.minimumFragmentDuration, when provided, must be a non-negative number.');
}
@@ -514,7 +514,7 @@ export class MkvOutputFormat extends OutputFormat {
}
if (
options.minimumClusterDuration !== undefined
&& (!Number.isFinite(options.minimumClusterDuration) || options.minimumClusterDuration < 0)
&& (!isNumber(options.minimumClusterDuration) || options.minimumClusterDuration < 0)
) {
throw new TypeError('options.minimumClusterDuration, when provided, must be a non-negative number.');
}
@@ -861,7 +861,7 @@ export class OggOutputFormat extends OutputFormat {
}
if (
options.maximumPageDuration !== undefined
&& (!Number.isFinite(options.maximumPageDuration) || options.maximumPageDuration <= 0)
&& (!isNumber(options.maximumPageDuration) || options.maximumPageDuration <= 0)
) {
throw new TypeError('options.maximumPageDuration, when provided, must be a positive number.');
}
+13 -4
View File
@@ -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);
+3 -3
View File
@@ -155,7 +155,7 @@ export class EncodedPacket {
throw new TypeError('Metadata-only packets cannot be converted to a video chunk.');
}
if (typeof EncodedVideoChunk === 'undefined') {
throw new Error('Your browser does not support EncodedVideoChunk.');
throw new Error('EncodedVideoChunk is not available in this environment.');
}
return new EncodedVideoChunk({
@@ -179,7 +179,7 @@ export class EncodedPacket {
throw new TypeError('Metadata-only packets cannot be converted to a video chunk.');
}
if (typeof EncodedVideoChunk === 'undefined') {
throw new Error('Your browser does not support EncodedVideoChunk.');
throw new Error('EncodedVideoChunk is not available in this environment.');
}
return new EncodedVideoChunk({
@@ -198,7 +198,7 @@ export class EncodedPacket {
throw new TypeError('Metadata-only packets cannot be converted to an audio chunk.');
}
if (typeof EncodedAudioChunk === 'undefined') {
throw new Error('Your browser does not support EncodedAudioChunk.');
throw new Error('EncodedAudioChunk is not available in this environment.');
}
return new EncodedAudioChunk({
+4 -4
View File
@@ -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;
+116 -70
View File
@@ -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;
@@ -2657,7 +2658,7 @@ export class AudioSample implements Disposable {
const { format, frameCount: optFrameCount, frameOffset: optFrameOffset } = options;
let { planeIndex } = options;
const srcFormat = this.format;
let srcFormat = this.format;
const destFormat = format ?? this.format;
if (!destFormat) throw new Error('Destination format not determined');
@@ -2704,88 +2705,133 @@ export class AudioSample implements Disposable {
frameOffset,
copyFrameCount,
);
return;
} else {
// Per spec, only f32-planar conversion must be supported, but in practice, all browsers support all
// destination formats, so let's just delegate here:
this._data.copyTo(destination, {
planeIndex,
frameOffset,
frameCount: copyFrameCount,
format: destFormat,
});
try {
// The spec only requires f32-planar to work here, but most of the time, all formats work. Sometimes
// they don't, and that's why we try-catch it.
this._data.copyTo(destination, {
planeIndex,
frameOffset,
frameCount: copyFrameCount,
format: destFormat,
});
return;
} catch (error) {
if (destFormat === 'f32-planar') {
throw error;
}
// Format conversion likely wasn't supported, so let's fall back to the manual conversion path
srcFormat = 'f32-planar';
}
}
} else {
const readFn = getReadFunction(srcFormat);
const srcBytesPerSample = getBytesPerSample(srcFormat);
const srcIsPlanar = formatIsPlanar(srcFormat);
}
let uint8Data: Uint8Array;
if (this._data instanceof AudioSampleResource) {
const getDataPlaneValidated = (index: number) => {
const result = (this._data as AudioSampleResource).getDataPlane(index);
if (!(result instanceof Uint8Array)) {
throw new TypeError('getDataPlane() must return a Uint8Array.');
}
const readFn = getReadFunction(srcFormat);
const srcBytesPerSample = getBytesPerSample(srcFormat);
const srcIsPlanar = formatIsPlanar(srcFormat);
const expectedSize = numFrames * srcBytesPerSample * (srcIsPlanar ? 1 : numChannels);
if (result.byteLength !== expectedSize) {
throw new TypeError(
`Data plane ${index} has invalid size. Expected exactly ${expectedSize} bytes, got`
+ ` ${result.byteLength} bytes.`,
);
}
let uint8Data: Uint8Array;
if (this._data instanceof AudioSampleResource) {
const getDataPlaneValidated = (index: number) => {
const result = (this._data as AudioSampleResource).getDataPlane(index);
if (!(result instanceof Uint8Array)) {
throw new TypeError('getDataPlane() must return a Uint8Array.');
}
return result;
};
const expectedSize = numFrames * srcBytesPerSample * (srcIsPlanar ? 1 : numChannels);
if (result.byteLength !== expectedSize) {
throw new TypeError(
`Data plane ${index} has invalid size. Expected exactly ${expectedSize} bytes, got`
+ ` ${result.byteLength} bytes.`,
);
}
if (srcIsPlanar) {
if (destIsPlanar) {
// Only one source plane will be extracted, so let's fetch only that one
uint8Data = getDataPlaneValidated(planeIndex);
planeIndex = 0; // To fix the subsequent access
} else {
// Pack all planes tightly together
uint8Data = new Uint8Array(numFrames * srcBytesPerSample * numChannels);
for (let ch = 0; ch < numChannels; ch++) {
const planeData = getDataPlaneValidated(ch);
uint8Data.set(planeData, ch * numFrames * srcBytesPerSample);
}
}
return result;
};
if (srcIsPlanar) {
if (destIsPlanar) {
// Only one source plane will be extracted, so let's fetch only that one
uint8Data = getDataPlaneValidated(planeIndex);
planeIndex = 0; // To fix the subsequent access
} else {
uint8Data = getDataPlaneValidated(0); // That's the only plane there is
// Pack all planes tightly together
uint8Data = new Uint8Array(numFrames * srcBytesPerSample * numChannels);
for (let ch = 0; ch < numChannels; ch++) {
const planeData = getDataPlaneValidated(ch);
uint8Data.set(planeData, ch * numFrames * srcBytesPerSample);
}
}
} else {
uint8Data = this._data;
uint8Data = getDataPlaneValidated(0); // That's the only plane there is
}
} else if (this._data instanceof Uint8Array) {
uint8Data = this._data;
} else {
assert(srcFormat === 'f32-planar');
const srcView = toDataView(uint8Data);
if (destIsPlanar) {
// Only one source plane will be read, so let's copy only that one
uint8Data = new Uint8Array(this._data.allocationSize({
format: 'f32-planar',
planeIndex,
}));
this._data.copyTo(uint8Data, {
format: 'f32-planar',
planeIndex,
});
planeIndex = 0; // To fix the subsequent access
} else {
// All planes will be read, so fetch 'em all
uint8Data = new Uint8Array(this._data.allocationSize({
format: 'f32-planar',
planeIndex: 0,
}) * numChannels);
for (let i = 0; i < copyFrameCount; i++) {
if (destIsPlanar) {
const destOffset = i * destBytesPerSample;
for (let ch = 0; ch < numChannels; ch++) {
this._data.copyTo(
uint8Data.subarray(
ch * numFrames * srcBytesPerSample,
(ch + 1) * numFrames * srcBytesPerSample,
),
{
format: 'f32-planar',
planeIndex: ch,
},
);
}
}
}
const srcView = toDataView(uint8Data);
for (let i = 0; i < copyFrameCount; i++) {
if (destIsPlanar) {
const destOffset = i * destBytesPerSample;
let srcOffset: number;
if (srcIsPlanar) {
srcOffset = (planeIndex * numFrames + (i + frameOffset)) * srcBytesPerSample;
} else {
srcOffset = (((i + frameOffset) * numChannels) + planeIndex) * srcBytesPerSample;
}
const normalized = readFn(srcView, srcOffset);
writeFn(destView, destOffset, normalized);
} else {
for (let ch = 0; ch < numChannels; ch++) {
const destIndex = i * numChannels + ch;
const destOffset = destIndex * destBytesPerSample;
let srcOffset: number;
if (srcIsPlanar) {
srcOffset = (planeIndex * numFrames + (i + frameOffset)) * srcBytesPerSample;
srcOffset = (ch * numFrames + (i + frameOffset)) * srcBytesPerSample;
} else {
srcOffset = (((i + frameOffset) * numChannels) + planeIndex) * srcBytesPerSample;
srcOffset = (((i + frameOffset) * numChannels) + ch) * srcBytesPerSample;
}
const normalized = readFn(srcView, srcOffset);
writeFn(destView, destOffset, normalized);
} else {
for (let ch = 0; ch < numChannels; ch++) {
const destIndex = i * numChannels + ch;
const destOffset = destIndex * destBytesPerSample;
let srcOffset: number;
if (srcIsPlanar) {
srcOffset = (ch * numFrames + (i + frameOffset)) * srcBytesPerSample;
} else {
srcOffset = (((i + frameOffset) * numChannels) + ch) * srcBytesPerSample;
}
const normalized = readFn(srcView, srcOffset);
writeFn(destView, destOffset, normalized);
}
}
}
}
@@ -3200,9 +3246,9 @@ export const toInterleavedAudioFormat = (format: AudioSampleFormat): 'u8' | 's16
};
/**
* WebKit has a bug where calling AudioData.copyTo with a format different from the source format
* crashes the tab when there are more than 2 channels. This function works around that by always
* copying with the source format and then manually converting to the destination format.
* WebKit has a bug where calling AudioData.copyTo with a format different from the source format crashes the tab when
* there are more than 2 channels. This function works around that by always copying with the source format and then
* manually converting to the destination format.
*
* See https://bugs.webkit.org/show_bug.cgi?id=302521.
*/
+44 -11
View File
@@ -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);
@@ -495,6 +496,14 @@ export type BlobSourceOptions = {
useStreamReader?: boolean;
};
const blobReaderRegistry = typeof FinalizationRegistry !== 'undefined'
? new FinalizationRegistry<ReadableStreamDefaultReader<Uint8Array>>((reader) => {
// Browsers don't GC readers that aren't "done", which creates indefinite memory leaks,
// see https://github.com/Vanilagy/mediabunny/issues/144#issuecomment-5465467062. So, we need to do it instead.
void reader.cancel().catch(() => {});
})
: null;
/**
* A source backed by a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob). Since a
* [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) is also a `Blob`, this is the source to use when
@@ -509,6 +518,8 @@ export class BlobSource extends Source {
_options: BlobSourceOptions;
/** @internal */
_orchestrator: ReadOrchestrator;
/** @internal */
_readers = new WeakMap<ReadWorker, ReadableStreamDefaultReader<Uint8Array> | null>();
/**
* Creates a new {@link BlobSource} backed by the specified
@@ -540,6 +551,17 @@ export class BlobSource extends Source {
maxCacheSize: options.maxCacheSize ?? (8 * 2 ** 20 /* 8 MiB */),
maxWorkerCount: 4,
runWorker: this._runWorker.bind(this),
onIdleWorkerRemoved: (worker) => {
const reader = this._readers.get(worker);
if (reader) {
this._readers.delete(worker);
blobReaderRegistry?.unregister(worker);
// If we don't do this, memory leaks indefinitely
void reader.cancel().catch(() => {});
}
},
prefetchProfile: PREFETCH_PROFILES.fileSystem,
});
@@ -561,9 +583,6 @@ export class BlobSource extends Source {
return this._orchestrator.read(start, end, minReadPosition, maxReadPosition);
}
/** @internal */
_readers = new WeakMap<ReadWorker, ReadableStreamDefaultReader<Uint8Array> | null>();
/** @internal */
private async _runWorker(worker: ReadWorker) {
assert(worker.strictTarget);
@@ -577,10 +596,15 @@ export class BlobSource extends Source {
// - ReadableStream stalls under backpressure (especially video)
// Affects Safari and all iOS browsers (Chrome, Firefox, etc.).
// Use arrayBuffer() fallback for WebKit browsers.
if ('stream' in this._blob && !isWebKit() && this._options.useStreamReader !== false) {
if (
'stream' in this._blob && !isWebKit()
&& this._options.useStreamReader !== false
) {
// Get a reader of the blob starting at the required offset, and then keep it around
const slice = this._blob.slice(worker.currentPos);
reader = slice.stream().getReader();
blobReaderRegistry?.register(worker, reader, worker);
} else {
// We'll need to use more primitive ways
reader = null;
@@ -894,7 +918,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 +1222,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 +1483,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 +1509,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;
@@ -1969,6 +1993,7 @@ class ReadOrchestrator {
runWorker: (worker: ReadWorker) => Promise<void>;
prefetchProfile: PrefetchProfile;
maxWorkerCount: number;
onIdleWorkerRemoved?: (worker: ReadWorker) => void;
}) {}
read(
@@ -2262,6 +2287,7 @@ class ReadOrchestrator {
assert(oldestIndex !== null);
assert(oldestWorker.pendingSlices.length === 0);
this.workers.splice(oldestIndex, 1);
this.options.onIdleWorkerRemoved?.(oldestWorker);
} else {
return null; // All workers are still running, we can't create a new one
}
@@ -2409,6 +2435,7 @@ class ReadOrchestrator {
otherWorker.currentPos, otherWorker.targetPos, // These should typically be equal when the worker's idle
)) {
this.workers.splice(i, 1);
this.options.onIdleWorkerRemoved?.(otherWorker);
i--;
}
}
@@ -2485,6 +2512,7 @@ class ReadOrchestrator {
worker.running = false;
this.workers.splice(index, 1);
this.options.onIdleWorkerRemoved?.(worker);
if (this.fileSize === null) {
// We can now deduce the file size!
@@ -2596,6 +2624,11 @@ class ReadOrchestrator {
worker.pendingSlices.length = 0;
worker.aborted = true;
if (!worker.running) {
// Running workers clean up after themselves when they notice the abort
this.options.onIdleWorkerRemoved?.(worker);
}
}
for (const queuedRead of this.queuedReads) {
@@ -2706,7 +2739,7 @@ export class RangedSource extends Source {
return result;
};
if (result instanceof Promise) {
if (isThenable(result)) {
return result.then(processResult);
} else {
return processResult(result);
+9 -9
View File
@@ -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);
+105
View File
@@ -137,3 +137,108 @@ test('trim, throws on a closed sample', () => {
sample.close();
expect(() => sample.trim(0, 5)).toThrow('AudioSample is closed.');
});
test('copyTo, AudioData-backed, planar destination falls back to manual conversion', () => {
withMockedNativeCopyTo((calls) => {
using sample = makeAudioDataBackedSample();
for (let c = 0; c < NUM_CHANNELS; c++) {
const out = new Int16Array(NUM_FRAMES);
sample.copyTo(out, { planeIndex: c, format: 's16-planar' });
for (let f = 0; f < NUM_FRAMES; f++) {
expect(out[f]).toBe(cellValue(f, c));
}
}
// With a frame offset
const out = new Int16Array(NUM_FRAMES - 3);
sample.copyTo(out, { planeIndex: 1, format: 's16-planar', frameOffset: 3 });
for (let f = 0; f < NUM_FRAMES - 3; f++) {
expect(out[f]).toBe(cellValue(f + 3, 1));
}
// The native conversion must have been attempted (and thrown)
expect(calls.some(options => options.format === 's16-planar')).toBe(true);
});
});
test('copyTo, AudioData-backed, interleaved destination falls back to manual conversion', () => {
withMockedNativeCopyTo((calls) => {
using sample = makeAudioDataBackedSample();
const out = new Float32Array(NUM_FRAMES * NUM_CHANNELS);
sample.copyTo(out, { planeIndex: 0, format: 'f32' });
for (let f = 0; f < NUM_FRAMES; f++) {
for (let c = 0; c < NUM_CHANNELS; c++) {
expect(out[f * NUM_CHANNELS + c]).toBe(Math.fround(cellValue(f, c) / 32767));
}
}
// The native conversion must have been attempted (and thrown)
expect(calls.some(options => options.format === 'f32')).toBe(true);
});
});
test('copyTo, AudioData-backed, control case with native format conversion', () => {
using sample = makeAudioDataBackedSample();
for (let c = 0; c < NUM_CHANNELS; c++) {
const out = new Int16Array(NUM_FRAMES);
sample.copyTo(out, { planeIndex: c, format: 's16-planar' });
for (let f = 0; f < NUM_FRAMES; f++) {
// Allow off-by-one
expect(Math.abs(out[f]! - cellValue(f, c))).toBeLessThanOrEqual(1);
}
}
const out = new Float32Array(NUM_FRAMES * NUM_CHANNELS);
sample.copyTo(out, { planeIndex: 0, format: 'f32' });
for (let f = 0; f < NUM_FRAMES; f++) {
for (let c = 0; c < NUM_CHANNELS; c++) {
expect(out[f * NUM_CHANNELS + c]).toBe(Math.fround(cellValue(f, c) / 32767));
}
}
});
const withMockedNativeCopyTo = (fn: (calls: AudioDataCopyToOptions[]) => void) => {
// eslint-disable-next-line @typescript-eslint/unbound-method
const original = AudioData.prototype.copyTo;
const calls: AudioDataCopyToOptions[] = [];
AudioData.prototype.copyTo = function (this: AudioData, destination, options) {
calls.push(options);
if (options.format !== undefined && options.format !== 'f32-planar') {
throw new DOMException('Format conversion not supported.', 'NotSupportedError');
}
return original.call(this, destination, options);
};
try {
fn(calls);
} finally {
AudioData.prototype.copyTo = original;
}
};
const makeAudioDataBackedSample = () => {
const data = new Float32Array(NUM_FRAMES * NUM_CHANNELS);
for (let f = 0; f < NUM_FRAMES; f++) {
for (let c = 0; c < NUM_CHANNELS; c++) {
data[f * NUM_CHANNELS + c] = cellValue(f, c) / 32767;
}
}
return new AudioSample(new AudioData({
format: 'f32',
sampleRate: SAMPLE_RATE,
numberOfFrames: NUM_FRAMES,
numberOfChannels: NUM_CHANNELS,
timestamp: TIMESTAMP * 1e6,
data,
}));
};
+232
View File
@@ -0,0 +1,232 @@
import { expect, test } from 'vitest';
import { VideoCodec } from '../../src/codec.js';
import { ALL_FORMATS } from '../../src/input-format.js';
import { Input } from '../../src/input.js';
import { EncodedPacketSink } from '../../src/media-sink.js';
import { EncodedVideoPacketSource } from '../../src/media-source.js';
import { assert, colorSpaceIsComplete } from '../../src/misc.js';
import { Output } from '../../src/output.js';
import { MkvOutputFormat, Mp4OutputFormat } from '../../src/output-format.js';
import { EncodedPacket } from '../../src/packet.js';
import { BufferSource, Source, UrlSource } from '../../src/source.js';
import { BufferTarget } from '../../src/target.js';
test('Color space extraction, AVC in MP4', async () => {
const source = await readPackets('/video.mp4');
const buffer = await remuxWithoutColorSpace(new Mp4OutputFormat(), source);
await expectColorSpace(new BufferSource(buffer), {
primaries: undefined,
transfer: undefined,
matrix: 'bt470bg',
fullRange: true,
});
});
test('Color space extraction, HEVC in MP4', async () => {
const source = await readPackets('/video-h265.mp4');
const buffer = await remuxWithoutColorSpace(new Mp4OutputFormat(), source);
await expectColorSpace(new BufferSource(buffer), {
primaries: undefined,
transfer: undefined,
matrix: 'bt470bg',
fullRange: true,
});
});
test('Color space extraction, VP9 in MP4', async () => {
const source = await readPackets('/vp9-smpte170m.webm');
const buffer = await remuxWithoutColorSpace(new Mp4OutputFormat(), source);
await expectColorSpace(new BufferSource(buffer), {
primaries: 'smpte170m',
transfer: 'smpte170m',
matrix: 'smpte170m',
fullRange: false,
});
});
test('Color space extraction, AV1 in MP4', async () => {
const source = await encodePackets('av1', 'av01.0.04M.08');
const buffer = await remuxWithoutColorSpace(new Mp4OutputFormat(), source);
await expectCompleteColorSpace(new BufferSource(buffer));
});
test('Color space extraction, AVC in Matroska', async () => {
const source = await readPackets('/video.mp4');
const buffer = await remuxWithoutColorSpace(new MkvOutputFormat(), source);
await expectColorSpace(new BufferSource(buffer), {
primaries: undefined,
transfer: undefined,
matrix: 'bt470bg',
fullRange: true,
});
});
test('Color space extraction, HEVC in Matroska', async () => {
const source = await readPackets('/video-h265.mp4');
const buffer = await remuxWithoutColorSpace(new MkvOutputFormat(), source);
await expectColorSpace(new BufferSource(buffer), {
// The SPS only signals the matrix and the range, leaving the rest unspecified
primaries: undefined,
transfer: undefined,
matrix: 'bt470bg',
fullRange: true,
});
});
test('Color space extraction, VP9 in Matroska', async () => {
const source = await readPackets('/vp9-smpte170m.webm');
const buffer = await remuxWithoutColorSpace(new MkvOutputFormat(), source);
await expectColorSpace(new BufferSource(buffer), {
primaries: 'smpte170m',
transfer: 'smpte170m',
matrix: 'smpte170m',
fullRange: false,
});
});
test('Color space extraction, AV1 in Matroska', async () => {
const source = await encodePackets('av1', 'av01.0.04M.08');
const buffer = await remuxWithoutColorSpace(new MkvOutputFormat(), source);
await expectCompleteColorSpace(new BufferSource(buffer));
});
const readPackets = async (path: string) => {
using input = new Input({
source: new UrlSource(path),
formats: ALL_FORMATS,
});
const track = await input.getPrimaryVideoTrack();
assert(track);
const codec = await track.getCodec();
assert(codec);
const decoderConfig = await track.getDecoderConfig();
assert(decoderConfig);
const sink = new EncodedPacketSink(track);
const packets: EncodedPacket[] = [];
for await (const packet of sink.packets()) {
packets.push(packet);
if (packets.length === 10) {
break;
}
}
return { codec, decoderConfig, packets };
};
const encodePackets = async (codec: VideoCodec, codecString: string) => {
const width = 320;
const height = 240;
const canvas = new OffscreenCanvas(width, height);
const context = canvas.getContext('2d')!;
let decoderConfig: VideoDecoderConfig | null = null;
const packets: EncodedPacket[] = [];
// We manually go through WebCodecs here so we can intercept the decoder config
const encoder = new VideoEncoder({
output: (chunk, metadata) => {
decoderConfig ??= metadata?.decoderConfig ?? null;
packets.push(EncodedPacket.fromEncodedChunk(chunk));
},
error: (error) => {
throw error;
},
});
encoder.configure({ codec: codecString, width, height });
for (let i = 0; i < 10; i++) {
context.fillStyle = `hsl(${36 * i}, 100%, 50%)`;
context.fillRect(0, 0, width, height);
context.fillStyle = 'black';
context.fillRect(20 * i, 10 * i, 80, 80);
const frame = new VideoFrame(canvas, { timestamp: i * 1e6 / 30, duration: 1e6 / 30 });
encoder.encode(frame, { keyFrame: i === 0 });
frame.close();
}
await encoder.flush();
encoder.close();
assert(decoderConfig);
return { codec, decoderConfig, packets };
};
const remuxWithoutColorSpace = async (
format: Mp4OutputFormat | MkvOutputFormat,
source: { codec: VideoCodec; decoderConfig: VideoDecoderConfig; packets: EncodedPacket[] },
) => {
const output = new Output({
format,
target: new BufferTarget(),
});
const videoSource = new EncodedVideoPacketSource(source.codec);
output.addVideoTrack(videoSource);
await output.start();
const decoderConfig = { ...source.decoderConfig };
delete decoderConfig.colorSpace;
let isFirstPacket = true;
for (const packet of source.packets) {
await videoSource.add(packet, isFirstPacket ? { decoderConfig } : undefined);
isFirstPacket = false;
}
await output.finalize();
const buffer = output.target.buffer;
assert(buffer);
return buffer;
};
const expectColorSpace = async (source: Source, expected: VideoColorSpaceInit) => {
using input = new Input({
source,
formats: ALL_FORMATS,
});
const track = await input.getPrimaryVideoTrack();
assert(track);
const decoderConfig = await track.getDecoderConfig();
assert(decoderConfig);
expect(await track.getColorSpace()).toEqual(expected);
expect({ ...decoderConfig.colorSpace }).toEqual(expected);
};
const expectCompleteColorSpace = async (source: Source) => {
using input = new Input({
source,
formats: ALL_FORMATS,
});
const track = await input.getPrimaryVideoTrack();
assert(track);
const decoderConfig = await track.getDecoderConfig();
assert(decoderConfig);
expect(colorSpaceIsComplete(await track.getColorSpace())).toBe(true);
expect(colorSpaceIsComplete(decoderConfig.colorSpace)).toBe(true);
};
+1
View File
@@ -76,6 +76,7 @@ const convertVideo = async (quality: Quality, codec: VideoCodec, allowMissingQua
trim: { start: 0, end: 2 },
video: { codec, quality },
audio: { discard: true },
showWarnings: false,
});
if (allowMissingQuantizerSupport && !conversion.isValid) {
+27
View File
@@ -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);
});
+41
View File
@@ -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;
@@ -140,3 +141,43 @@ test('QuickTime nclc color information', async () => {
});
expect(await track.hasHighDynamicRange()).toBe(true);
});
// Annex B isn't supposed to exist in MP4, but some files have it anyway, with an empty avcC box
test('Annex B', async () => {
const filePath = path.join(__dirname, '..', 'public/annex-b.mp4');
using input = new Input({
source: new FilePathSource(filePath),
formats: ALL_FORMATS,
});
const track = await input.getPrimaryVideoTrack();
if (!track) throw new Error('No video track found');
const decoderConfig = (await track.getDecoderConfig())!;
expect(decoderConfig.codec).toBe('avc1.424028');
expect(decoderConfig.description).toBeUndefined();
const sink = new EncodedPacketSink(track);
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]);
});
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.