mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-28 03:13:51 +02:00
Merge main into release for tag v1.55.4
This commit is contained in:
+4
-7
@@ -17,14 +17,11 @@
|
||||
source: new Mediabunny.BlobSource(file),
|
||||
});
|
||||
|
||||
const track = await input.getPrimaryAudioTrack();
|
||||
const sink = new Mediabunny.AudioSampleSink(track);
|
||||
|
||||
for await (const sample of sink.samples()) {
|
||||
console.log(sample);
|
||||
sample.close()
|
||||
}
|
||||
const track = await input.getPrimaryVideoTrack();
|
||||
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.3",
|
||||
"version": "1.55.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mediabunny",
|
||||
"version": "1.55.3",
|
||||
"version": "1.55.4",
|
||||
"license": "MPL-2.0",
|
||||
"workspaces": [
|
||||
".",
|
||||
@@ -12958,7 +12958,7 @@
|
||||
},
|
||||
"packages/aac-encoder": {
|
||||
"name": "@mediabunny/aac-encoder",
|
||||
"version": "1.55.3",
|
||||
"version": "1.55.4",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
@@ -12973,7 +12973,7 @@
|
||||
},
|
||||
"packages/ac3": {
|
||||
"name": "@mediabunny/ac3",
|
||||
"version": "1.55.3",
|
||||
"version": "1.55.4",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
@@ -12988,7 +12988,7 @@
|
||||
},
|
||||
"packages/dts": {
|
||||
"name": "@mediabunny/dts",
|
||||
"version": "1.55.3",
|
||||
"version": "1.55.4",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
@@ -13003,7 +13003,7 @@
|
||||
},
|
||||
"packages/flac-encoder": {
|
||||
"name": "@mediabunny/flac-encoder",
|
||||
"version": "1.55.3",
|
||||
"version": "1.55.4",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
@@ -13018,7 +13018,7 @@
|
||||
},
|
||||
"packages/mp3-encoder": {
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"version": "1.55.3",
|
||||
"version": "1.55.4",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
@@ -13033,7 +13033,7 @@
|
||||
},
|
||||
"packages/prores": {
|
||||
"name": "@mediabunny/prores",
|
||||
"version": "1.55.3",
|
||||
"version": "1.55.4",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"turbores": "^1.2.2"
|
||||
@@ -13048,10 +13048,10 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@mediabunny/server",
|
||||
"version": "1.55.3",
|
||||
"version": "1.55.4",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"@mediabunny/prores": "^1.55.3",
|
||||
"@mediabunny/prores": "^1.55.4",
|
||||
"node-av": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.55.3",
|
||||
"version": "1.55.4",
|
||||
"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.3",
|
||||
"version": "1.55.4",
|
||||
"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.3",
|
||||
"version": "1.55.4",
|
||||
"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.3",
|
||||
"version": "1.55.4",
|
||||
"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.3",
|
||||
"version": "1.55.4",
|
||||
"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.3",
|
||||
"version": "1.55.4",
|
||||
"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.3",
|
||||
"version": "1.55.4",
|
||||
"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.3",
|
||||
"version": "1.55.4",
|
||||
"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.3"
|
||||
"@mediabunny/prores": "^1.55.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mediabunny": "^1.45.0"
|
||||
|
||||
+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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -72,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';
|
||||
@@ -151,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;
|
||||
};
|
||||
} | {
|
||||
@@ -1027,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') {
|
||||
@@ -1472,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,
|
||||
@@ -1481,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;
|
||||
|
||||
@@ -3410,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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3439,12 +3463,56 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
|
||||
const firstPacket = await this.getFirstPacket({});
|
||||
this.internalTrack.info.hevcCodecInfo
|
||||
= firstPacket && extractHevcDecoderConfigurationRecord(firstPacket.data);
|
||||
} else if (this.internalTrack.info.codec === 'vp9' && !this.internalTrack.info.vp9CodecInfo) {
|
||||
} 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({});
|
||||
this.internalTrack.info.vp9CodecInfo = firstPacket && extractVp9CodecInfoFromPacket(firstPacket.data);
|
||||
} else if (this.internalTrack.info.codec === 'av1' && !this.internalTrack.info.av1CodecInfo) {
|
||||
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({});
|
||||
this.internalTrack.info.av1CodecInfo = firstPacket && extractAv1CodecInfoFromPacket(firstPacket.data);
|
||||
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 = {
|
||||
@@ -3452,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 (
|
||||
|
||||
@@ -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,6 +42,8 @@ import {
|
||||
assert,
|
||||
binarySearchLessOrEqual,
|
||||
COLOR_PRIMARIES_MAP_INVERSE,
|
||||
colorSpaceIsComplete,
|
||||
EMPTY_COLOR_SPACE,
|
||||
findLastIndex,
|
||||
isIso639Dash2LanguageCode,
|
||||
isThenable,
|
||||
@@ -220,7 +224,7 @@ type InternalTrack = {
|
||||
rotation: Rotation;
|
||||
codec: VideoCodec | null;
|
||||
codecDescription: Uint8Array | null;
|
||||
colorSpace: VideoColorSpaceInit | null;
|
||||
colorSpace: VideoColorSpaceInit;
|
||||
alphaMode: boolean;
|
||||
proresFormat: ProresFourCc | null;
|
||||
}
|
||||
@@ -1199,7 +1203,7 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
rotation: 0,
|
||||
codec: null,
|
||||
codecDescription: null,
|
||||
colorSpace: null,
|
||||
colorSpace: { ...EMPTY_COLOR_SPACE },
|
||||
alphaMode: false,
|
||||
proresFormat: null,
|
||||
};
|
||||
@@ -1363,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;
|
||||
|
||||
@@ -2490,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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2517,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:
|
||||
@@ -2526,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,
|
||||
|
||||
+36
-13
@@ -29,6 +29,7 @@ import {
|
||||
assertNever,
|
||||
CallSerializer,
|
||||
clamp,
|
||||
colorSpaceIsComplete,
|
||||
getInt24,
|
||||
getUint24,
|
||||
insertSorted,
|
||||
@@ -944,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;
|
||||
@@ -2095,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; ;
|
||||
}
|
||||
|
||||
|
||||
+19
@@ -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
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
Binary file not shown.
Reference in New Issue
Block a user