mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-10 09:13:47 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a97e9800d2 | ||
|
|
23f5e96afd | ||
|
|
9e203c0c8a | ||
|
|
dec669163f | ||
|
|
e16b01a4c6 | ||
|
|
398d172152 | ||
|
|
da18ceeb24 | ||
|
|
3c4c2a459c | ||
|
|
7d1eb5b063 | ||
|
|
42196e9f64 | ||
|
|
16f8889e14 | ||
|
|
d44253c7d3 | ||
|
|
c10f93ab8c | ||
|
|
14fb7d0119 | ||
|
|
0f9dc1f91b | ||
|
|
394eede178 | ||
|
|
6b5ae29252 | ||
|
|
656c7e70a8 | ||
|
|
3c4f4c27a3 | ||
|
|
166298df2c | ||
|
|
fdfab4660f | ||
|
|
1c4d1c70a8 |
+1
-1
@@ -2,7 +2,7 @@ node_modules
|
||||
/dist
|
||||
/dist-docs
|
||||
.DS_Store
|
||||
/docs/.vitepress/cache
|
||||
docs/.vitepress/cache
|
||||
/docs/api
|
||||
*.tsbuildinfo
|
||||
|
||||
|
||||
+3
-1
@@ -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()) {
|
||||
|
||||
Generated
+10
-10
@@ -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
@@ -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,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,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,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,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,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,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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -525,8 +525,8 @@ export class IsobmffMuxer extends Muxer {
|
||||
|
||||
decoderConfig.description = buildAacAudioSpecificConfig({
|
||||
objectType: adtsFrame.objectType,
|
||||
sampleRate,
|
||||
numberOfChannels,
|
||||
outputSampleRate: sampleRate,
|
||||
outputNumberOfChannels: numberOfChannels,
|
||||
});
|
||||
requiresAdtsStripping = true;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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.
Reference in New Issue
Block a user