mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-09 08:43:49 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5d1efc18d | ||
|
|
9464adf22f | ||
|
|
17dfd2c68a | ||
|
|
abe6185ddd | ||
|
|
217b383f12 | ||
|
|
2c96ec0f1b | ||
|
|
89d48d1bf9 | ||
|
|
6684984e7e | ||
|
|
0f030dc2a6 | ||
|
|
02b08e036b | ||
|
|
327696666b | ||
|
|
07b2e70863 | ||
|
|
3cb6ed82ce | ||
|
|
12216ae29e | ||
|
|
198b3d2eae | ||
|
|
e33e9f13fe |
@@ -39,6 +39,10 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi
|
||||
<a href="https://pqina.nl/pintura/" target="_blank" rel="sponsored">
|
||||
<img src="./docs/public/sponsors/pintura-labs.png" width="50" height="50" alt="Pintura Labs">
|
||||
</a>
|
||||
|
||||
<a href="https://ponder.ai/" target="_blank" rel="sponsored">
|
||||
<img src="./docs/public/sponsors/ponder.png" width="50" height="50" alt="Ponder">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### Bronze sponsors
|
||||
|
||||
+7
-4
@@ -57,7 +57,7 @@
|
||||
output,
|
||||
audio: (_, n) => ({
|
||||
discard: n > 1,
|
||||
codec: 'aac',
|
||||
//codec: 'opus',
|
||||
//codec: 'opus',
|
||||
/*
|
||||
process: (sample) => {
|
||||
@@ -73,7 +73,7 @@
|
||||
//numberOfChannels: 1,
|
||||
//sampleRate: 4000
|
||||
//discard: true
|
||||
forceTranscode: true,
|
||||
//forceTranscode: true,
|
||||
}),
|
||||
/*
|
||||
video: {
|
||||
@@ -100,6 +100,9 @@
|
||||
},
|
||||
*/
|
||||
video: () => ({
|
||||
width: 720,
|
||||
frameRate: 30,
|
||||
bitrate: Mediabunny.QUALITY_VERY_LOW,
|
||||
//discard: true,
|
||||
/*
|
||||
process: (sample) => {
|
||||
@@ -176,8 +179,8 @@
|
||||
}
|
||||
},
|
||||
trim: {
|
||||
//start: 0,
|
||||
//end: 10
|
||||
start: 0,
|
||||
end: 20
|
||||
},
|
||||
});
|
||||
console.log(conversion);
|
||||
|
||||
@@ -14,6 +14,20 @@
|
||||
source: new Mediabunny.BlobSource(file),
|
||||
});
|
||||
|
||||
console.log(await input.getTracks());
|
||||
|
||||
/*
|
||||
return;
|
||||
|
||||
const audioTrack = await input.getPrimaryAudioTrack();
|
||||
const sink = new Mediabunny.EncodedPacketSink(audioTrack);
|
||||
|
||||
for await (const packet of sink.packets()) {
|
||||
console.log(packet.timestamp, packet.duration, packet.type)
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
const audioTrack = await input.getPrimaryAudioTrack();
|
||||
const sink = new Mediabunny.AudioSampleSink(audioTrack);
|
||||
|
||||
@@ -27,6 +41,7 @@
|
||||
lastEnd = sample.timestamp + sample.duration;
|
||||
sample.close();
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
|
||||
|
||||
@@ -106,6 +106,10 @@ track.languageCode; // => string
|
||||
|
||||
// A user-defined name for this track.
|
||||
track.name; // => string
|
||||
|
||||
// Information about the intended usage of the track
|
||||
// (default, commentary, hearing-impaired, visually-impaired, etc.)
|
||||
track.disposition; // TrackDisposition
|
||||
```
|
||||
|
||||
#### Codec information
|
||||
|
||||
@@ -56,6 +56,7 @@ output.addVideoTrack(videoSource, {
|
||||
output.addAudioTrack(audioSourceEng, {
|
||||
language: 'eng', // ISO 639-2/T language code
|
||||
name: 'Developer Commentary', // Sets a user-defined track name
|
||||
disposition: { commentary: true }, // Sets additional flags in the file
|
||||
});
|
||||
output.addAudioTrack(audioSourceGer, {
|
||||
language: 'ger',
|
||||
|
||||
@@ -98,6 +98,7 @@ const sponsors = {
|
||||
],
|
||||
silver: [
|
||||
{ image: '/sponsors/pintura-labs.png', name: 'Pintura Labs', url: 'https://pqina.nl/pintura/' },
|
||||
{ image: '/sponsors/ponder.png', name: 'Ponder', url: 'https://ponder.ai/' },
|
||||
],
|
||||
bronze: [
|
||||
{ image: '/sponsors/rve.png', name: 'React Video Editor', url: 'https://www.reactvideoeditor.com/' },
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Generated
+6
-6
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"version": "1.24.6",
|
||||
"version": "1.25.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mediabunny",
|
||||
"version": "1.24.6",
|
||||
"version": "1.25.1",
|
||||
"license": "MPL-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
@@ -7749,9 +7749,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mediabunny": {
|
||||
"version": "1.24.5",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.24.5.tgz",
|
||||
"integrity": "sha512-4Sd5x8WXQiuDKZcJDAiQjyD0Lg/cg9p+dBShCM02F1pMvKdB9R6+WHZ9NFCFSqViVPX2fYSu95ZIpBUg9+TwTQ==",
|
||||
"version": "1.25.0",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.25.0.tgz",
|
||||
"integrity": "sha512-ozaqk6zS2Vbf3+3+OoxKfnCVeZRcv5PO8DgQtBrM5vpWIbpEK+kMVV6pgfo4mC3XtMwvQEMbhj3zEf0LNklh9w==",
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"workspaces": [
|
||||
@@ -12242,7 +12242,7 @@
|
||||
},
|
||||
"packages/mp3-encoder": {
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"version": "1.24.6",
|
||||
"version": "1.25.1",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.24.6",
|
||||
"version": "1.25.1",
|
||||
"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/mp3-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.24.6",
|
||||
"version": "1.25.1",
|
||||
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
|
||||
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from '../misc';
|
||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
||||
import { readBytes, Reader } from '../reader';
|
||||
import { DEFAULT_TRACK_DISPOSITION } from '../metadata';
|
||||
import { FrameHeader, MAX_FRAME_HEADER_SIZE, MIN_FRAME_HEADER_SIZE, readFrameHeader } from './adts-reader';
|
||||
|
||||
const SAMPLES_PER_AAC_FRAME = 1024;
|
||||
@@ -188,6 +189,12 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return sampleRate;
|
||||
}
|
||||
|
||||
getDisposition() {
|
||||
return {
|
||||
...DEFAULT_TRACK_DISPOSITION,
|
||||
};
|
||||
}
|
||||
|
||||
async getDecoderConfig(): Promise<AudioDecoderConfig> {
|
||||
assert(this.demuxer.firstFrameHeader);
|
||||
|
||||
|
||||
+216
-49
@@ -24,7 +24,7 @@ import {
|
||||
toUint8Array,
|
||||
} from './misc';
|
||||
import { PacketType } from './packet';
|
||||
import { MetadataTags } from './tags';
|
||||
import { MetadataTags } from './metadata';
|
||||
|
||||
// References for AVC/HEVC code:
|
||||
// ISO 14496-15
|
||||
@@ -229,7 +229,7 @@ const extractNalUnitTypeForAvc = (data: Uint8Array) => {
|
||||
};
|
||||
|
||||
/** Builds an AvcDecoderConfigurationRecord from an AVC packet in Annex B format. */
|
||||
export const extractAvcDecoderConfigurationRecord = (packetData: Uint8Array) => {
|
||||
export const extractAvcDecoderConfigurationRecord = (packetData: Uint8Array): AvcDecoderConfigurationRecord | null => {
|
||||
try {
|
||||
const nalUnits = findNalUnitsInAnnexB(packetData);
|
||||
|
||||
@@ -247,60 +247,27 @@ export const extractAvcDecoderConfigurationRecord = (packetData: Uint8Array) =>
|
||||
|
||||
// Let's get the first SPS for profile and level information
|
||||
const spsData = spsUnits[0]!;
|
||||
const bitstream = new Bitstream(removeEmulationPreventionBytes(spsData));
|
||||
const spsInfo = parseAvcSps(spsData);
|
||||
assert(spsInfo !== null);
|
||||
|
||||
bitstream.skipBits(1); // forbidden_zero_bit
|
||||
bitstream.skipBits(2); // nal_ref_idc
|
||||
const nal_unit_type = bitstream.readBits(5);
|
||||
const hasExtendedData = spsInfo.profileIdc === 100
|
||||
|| spsInfo.profileIdc === 110
|
||||
|| spsInfo.profileIdc === 122
|
||||
|| spsInfo.profileIdc === 144;
|
||||
|
||||
if (nal_unit_type !== 7) { // SPS NAL unit type is 7
|
||||
console.error('Invalid SPS NAL unit type');
|
||||
return null;
|
||||
}
|
||||
|
||||
const profile_idc = bitstream.readAlignedByte();
|
||||
const constraint_flags = bitstream.readAlignedByte();
|
||||
const level_idc = bitstream.readAlignedByte();
|
||||
|
||||
const record: AvcDecoderConfigurationRecord = {
|
||||
return {
|
||||
configurationVersion: 1,
|
||||
avcProfileIndication: profile_idc,
|
||||
profileCompatibility: constraint_flags,
|
||||
avcLevelIndication: level_idc,
|
||||
avcProfileIndication: spsInfo.profileIdc,
|
||||
profileCompatibility: spsInfo.constraintFlags,
|
||||
avcLevelIndication: spsInfo.levelIdc,
|
||||
lengthSizeMinusOne: 3, // Typically 4 bytes for length field
|
||||
sequenceParameterSets: spsUnits,
|
||||
pictureParameterSets: ppsUnits,
|
||||
chromaFormat: null,
|
||||
bitDepthLumaMinus8: null,
|
||||
bitDepthChromaMinus8: null,
|
||||
sequenceParameterSetExt: null,
|
||||
chromaFormat: hasExtendedData ? spsInfo.chromaFormatIdc : null,
|
||||
bitDepthLumaMinus8: hasExtendedData ? spsInfo.bitDepthLumaMinus8 : null,
|
||||
bitDepthChromaMinus8: hasExtendedData ? spsInfo.bitDepthChromaMinus8 : null,
|
||||
sequenceParameterSetExt: hasExtendedData ? spsExtUnits : null,
|
||||
};
|
||||
|
||||
if (
|
||||
profile_idc === 100
|
||||
|| profile_idc === 110
|
||||
|| profile_idc === 122
|
||||
|| profile_idc === 144
|
||||
) {
|
||||
readExpGolomb(bitstream); // seq_parameter_set_id
|
||||
|
||||
const chroma_format_idc = readExpGolomb(bitstream);
|
||||
|
||||
if (chroma_format_idc === 3) {
|
||||
bitstream.skipBits(1); // separate_colour_plane_flag
|
||||
}
|
||||
|
||||
const bit_depth_luma_minus8 = readExpGolomb(bitstream);
|
||||
|
||||
const bit_depth_chroma_minus8 = readExpGolomb(bitstream);
|
||||
|
||||
record.chromaFormat = chroma_format_idc;
|
||||
record.bitDepthLumaMinus8 = bit_depth_luma_minus8;
|
||||
record.bitDepthChromaMinus8 = bit_depth_chroma_minus8;
|
||||
record.sequenceParameterSetExt = spsExtUnits;
|
||||
}
|
||||
|
||||
return record;
|
||||
} catch (error) {
|
||||
console.error('Error building AVC Decoder Configuration Record:', error);
|
||||
return null;
|
||||
@@ -377,6 +344,206 @@ export const serializeAvcDecoderConfigurationRecord = (record: AvcDecoderConfigu
|
||||
return new Uint8Array(bytes);
|
||||
};
|
||||
|
||||
/** Deserializes an AvcDecoderConfigurationRecord from the format specified in Section 5.3.3.1 of ISO 14496-15. */
|
||||
export const deserializeAvcDecoderConfigurationRecord = (data: Uint8Array): AvcDecoderConfigurationRecord | null => {
|
||||
try {
|
||||
const view = toDataView(data);
|
||||
let offset = 0;
|
||||
|
||||
// Read header
|
||||
const configurationVersion = view.getUint8(offset++);
|
||||
const avcProfileIndication = view.getUint8(offset++);
|
||||
const profileCompatibility = view.getUint8(offset++);
|
||||
const avcLevelIndication = view.getUint8(offset++);
|
||||
const lengthSizeMinusOne = view.getUint8(offset++) & 0x03;
|
||||
|
||||
const numOfSequenceParameterSets = view.getUint8(offset++) & 0x1F;
|
||||
|
||||
// Read SPS
|
||||
const sequenceParameterSets: Uint8Array[] = [];
|
||||
for (let i = 0; i < numOfSequenceParameterSets; i++) {
|
||||
const length = view.getUint16(offset, false);
|
||||
offset += 2;
|
||||
|
||||
sequenceParameterSets.push(data.subarray(offset, offset + length));
|
||||
offset += length;
|
||||
}
|
||||
|
||||
const numOfPictureParameterSets = view.getUint8(offset++);
|
||||
|
||||
// Read PPS
|
||||
const pictureParameterSets: Uint8Array[] = [];
|
||||
for (let i = 0; i < numOfPictureParameterSets; i++) {
|
||||
const length = view.getUint16(offset, false);
|
||||
offset += 2;
|
||||
|
||||
pictureParameterSets.push(data.subarray(offset, offset + length));
|
||||
offset += length;
|
||||
}
|
||||
|
||||
const record: AvcDecoderConfigurationRecord = {
|
||||
configurationVersion,
|
||||
avcProfileIndication,
|
||||
profileCompatibility,
|
||||
avcLevelIndication,
|
||||
lengthSizeMinusOne,
|
||||
sequenceParameterSets,
|
||||
pictureParameterSets,
|
||||
chromaFormat: null,
|
||||
bitDepthLumaMinus8: null,
|
||||
bitDepthChromaMinus8: null,
|
||||
sequenceParameterSetExt: null,
|
||||
};
|
||||
|
||||
// Check if there are extended profile fields
|
||||
if (
|
||||
(
|
||||
avcProfileIndication === 100
|
||||
|| avcProfileIndication === 110
|
||||
|| avcProfileIndication === 122
|
||||
|| avcProfileIndication === 144
|
||||
)
|
||||
&& offset + 4 <= data.length
|
||||
) {
|
||||
const chromaFormat = view.getUint8(offset++) & 0x03;
|
||||
const bitDepthLumaMinus8 = view.getUint8(offset++) & 0x07;
|
||||
const bitDepthChromaMinus8 = view.getUint8(offset++) & 0x07;
|
||||
const numOfSequenceParameterSetExt = view.getUint8(offset++);
|
||||
|
||||
record.chromaFormat = chromaFormat;
|
||||
record.bitDepthLumaMinus8 = bitDepthLumaMinus8;
|
||||
record.bitDepthChromaMinus8 = bitDepthChromaMinus8;
|
||||
|
||||
// Read SPS Ext
|
||||
const sequenceParameterSetExt: Uint8Array[] = [];
|
||||
for (let i = 0; i < numOfSequenceParameterSetExt; i++) {
|
||||
const length = view.getUint16(offset, false);
|
||||
offset += 2;
|
||||
|
||||
sequenceParameterSetExt.push(data.subarray(offset, offset + length));
|
||||
offset += length;
|
||||
}
|
||||
|
||||
record.sequenceParameterSetExt = sequenceParameterSetExt;
|
||||
}
|
||||
|
||||
return record;
|
||||
} catch (error) {
|
||||
console.error('Error deserializing AVC Decoder Configuration Record:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export type AvcSpsInfo = {
|
||||
profileIdc: number;
|
||||
constraintFlags: number;
|
||||
levelIdc: number;
|
||||
frameMbsOnlyFlag: number;
|
||||
chromaFormatIdc: number | null;
|
||||
bitDepthLumaMinus8: number | null;
|
||||
bitDepthChromaMinus8: number | null;
|
||||
};
|
||||
|
||||
/** Parses an AVC SPS (Sequence Parameter Set) to extract basic information. */
|
||||
export const parseAvcSps = (sps: Uint8Array): AvcSpsInfo | null => {
|
||||
try {
|
||||
const bitstream = new Bitstream(removeEmulationPreventionBytes(sps));
|
||||
|
||||
bitstream.skipBits(1); // forbidden_zero_bit
|
||||
bitstream.skipBits(2); // nal_ref_idc
|
||||
const nalUnitType = bitstream.readBits(5);
|
||||
|
||||
if (nalUnitType !== 7) { // SPS NAL unit type is 7
|
||||
return null;
|
||||
}
|
||||
|
||||
const profileIdc = bitstream.readAlignedByte();
|
||||
const constraintFlags = bitstream.readAlignedByte();
|
||||
const levelIdc = bitstream.readAlignedByte();
|
||||
|
||||
readExpGolomb(bitstream); // seq_parameter_set_id
|
||||
|
||||
let chromaFormatIdc: number | null = null;
|
||||
let bitDepthLumaMinus8: number | null = null;
|
||||
let bitDepthChromaMinus8: number | null = null;
|
||||
|
||||
// Handle high profile chroma_format_idc
|
||||
if (
|
||||
profileIdc === 100
|
||||
|| profileIdc === 110
|
||||
|| profileIdc === 122
|
||||
|| profileIdc === 244
|
||||
|| profileIdc === 44
|
||||
|| profileIdc === 83
|
||||
|| profileIdc === 86
|
||||
|| profileIdc === 118
|
||||
|| profileIdc === 128
|
||||
) {
|
||||
chromaFormatIdc = readExpGolomb(bitstream);
|
||||
if (chromaFormatIdc === 3) {
|
||||
bitstream.skipBits(1); // separate_colour_plane_flag
|
||||
}
|
||||
bitDepthLumaMinus8 = readExpGolomb(bitstream);
|
||||
bitDepthChromaMinus8 = readExpGolomb(bitstream);
|
||||
bitstream.skipBits(1); // qpprime_y_zero_transform_bypass_flag
|
||||
const seqScalingMatrixPresentFlag = bitstream.readBits(1);
|
||||
if (seqScalingMatrixPresentFlag) {
|
||||
for (let i = 0; i < (chromaFormatIdc !== 3 ? 8 : 12); i++) {
|
||||
const seqScalingListPresentFlag = bitstream.readBits(1);
|
||||
if (seqScalingListPresentFlag) {
|
||||
const sizeOfScalingList = i < 6 ? 16 : 64;
|
||||
let lastScale = 8;
|
||||
let nextScale = 8;
|
||||
for (let j = 0; j < sizeOfScalingList; j++) {
|
||||
if (nextScale !== 0) {
|
||||
const deltaScale = readSignedExpGolomb(bitstream);
|
||||
nextScale = (lastScale + deltaScale + 256) % 256;
|
||||
}
|
||||
lastScale = nextScale === 0 ? lastScale : nextScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readExpGolomb(bitstream); // log2_max_frame_num_minus4
|
||||
|
||||
const picOrderCntType = readExpGolomb(bitstream);
|
||||
if (picOrderCntType === 0) {
|
||||
readExpGolomb(bitstream); // log2_max_pic_order_cnt_lsb_minus4
|
||||
} else if (picOrderCntType === 1) {
|
||||
bitstream.skipBits(1); // delta_pic_order_always_zero_flag
|
||||
readSignedExpGolomb(bitstream); // offset_for_non_ref_pic
|
||||
readSignedExpGolomb(bitstream); // offset_for_top_to_bottom_field
|
||||
const numRefFramesInPicOrderCntCycle = readExpGolomb(bitstream);
|
||||
for (let i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
|
||||
readSignedExpGolomb(bitstream); // offset_for_ref_frame[i]
|
||||
}
|
||||
}
|
||||
|
||||
readExpGolomb(bitstream); // max_num_ref_frames
|
||||
bitstream.skipBits(1); // gaps_in_frame_num_value_allowed_flag
|
||||
|
||||
readExpGolomb(bitstream); // pic_width_in_mbs_minus1
|
||||
readExpGolomb(bitstream); // pic_height_in_map_units_minus1
|
||||
|
||||
const frameMbsOnlyFlag = bitstream.readBits(1);
|
||||
|
||||
return {
|
||||
profileIdc,
|
||||
constraintFlags,
|
||||
levelIdc,
|
||||
frameMbsOnlyFlag,
|
||||
chromaFormatIdc,
|
||||
bitDepthLumaMinus8,
|
||||
bitDepthChromaMinus8,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error parsing AVC SPS:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Data specified in ISO 14496-15
|
||||
export type HevcDecoderConfigurationRecord = {
|
||||
configurationVersion: number;
|
||||
|
||||
+7
-5
@@ -48,7 +48,7 @@ import {
|
||||
import { Output, TrackType } from './output';
|
||||
import { Mp4OutputFormat } from './output-format';
|
||||
import { AudioSample, clampCropRectangle, validateCropRectangle, VideoSample } from './sample';
|
||||
import { MetadataTags, validateMetadataTags } from './tags';
|
||||
import { MetadataTags, validateMetadataTags } from './metadata';
|
||||
import { NullTarget } from './target';
|
||||
|
||||
/**
|
||||
@@ -1043,6 +1043,7 @@ export class Conversion {
|
||||
duration: 1 / frameRate,
|
||||
});
|
||||
await this._registerVideoSample(track, trackOptions, source, sample);
|
||||
sample.close();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1079,12 +1080,11 @@ export class Conversion {
|
||||
duration: frameRate !== undefined ? 1 / frameRate : duration,
|
||||
});
|
||||
await this._registerVideoSample(track, trackOptions, source, sample);
|
||||
sample.close();
|
||||
|
||||
if (frameRate !== undefined) {
|
||||
lastCanvas = canvas;
|
||||
lastCanvasTimestamp = adjustedSampleTimestamp;
|
||||
} else {
|
||||
sample.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1184,9 +1184,10 @@ export class Conversion {
|
||||
|
||||
this.output.addVideoTrack(videoSource, {
|
||||
frameRate: trackOptions.frameRate,
|
||||
// TEMP: This condition can be removed when all demuxers properly homogenize to BCP47 in v2
|
||||
// TODO: This condition can be removed when all demuxers properly homogenize to BCP47 in v2
|
||||
languageCode: isIso639Dash2LanguageCode(track.languageCode) ? track.languageCode : undefined,
|
||||
name: track.name ?? undefined,
|
||||
disposition: track.disposition,
|
||||
rotation: needsRerender ? 0 : totalRotation, // Rerendering will bake the rotation into the output
|
||||
});
|
||||
this._addedCounts.video++;
|
||||
@@ -1423,9 +1424,10 @@ export class Conversion {
|
||||
}
|
||||
|
||||
this.output.addAudioTrack(audioSource, {
|
||||
// TEMP: This condition can be removed when all demuxers properly homogenize to BCP47 in v2
|
||||
// TODO: This condition can be removed when all demuxers properly homogenize to BCP47 in v2
|
||||
languageCode: isIso639Dash2LanguageCode(track.languageCode) ? track.languageCode : undefined,
|
||||
name: track.name ?? undefined,
|
||||
disposition: track.disposition,
|
||||
});
|
||||
this._addedCounts.audio++;
|
||||
this._totalTrackCount++;
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
import { Input } from './input';
|
||||
import { InputTrack } from './input-track';
|
||||
import { MetadataTags } from './tags';
|
||||
import { MetadataTags } from './metadata';
|
||||
|
||||
export abstract class Demuxer {
|
||||
input: Input;
|
||||
|
||||
+15
-8
@@ -115,9 +115,15 @@ export type VideoEncodingAdditionalOptions = {
|
||||
* format that supports transparency (such as WebM or Matroska).
|
||||
*/
|
||||
alpha?: 'discard' | 'keep';
|
||||
/** Configures the bitrate mode. */
|
||||
/** Configures the bitrate mode; defaults to `'variable'`. */
|
||||
bitrateMode?: 'constant' | 'variable';
|
||||
/** The latency mode used by the encoder; controls the performance-quality tradeoff. */
|
||||
/**
|
||||
* The latency mode used by the encoder; controls the performance-quality tradeoff.
|
||||
*
|
||||
* - `'quality'` (default): The encoder prioritizes quality over latency, and no frames can be dropped.
|
||||
* - `'realtime'`: The encoder prioritizes low latency over quality, and may drop frames if the encoder becomes
|
||||
* overloaded to keep up with real-time requirements.
|
||||
*/
|
||||
latencyMode?: 'quality' | 'realtime';
|
||||
/**
|
||||
* The full codec string as specified in the WebCodecs Codec Registry. This string must match the codec
|
||||
@@ -125,7 +131,8 @@ export type VideoEncodingAdditionalOptions = {
|
||||
*/
|
||||
fullCodecString?: string;
|
||||
/**
|
||||
* A hint that configures the hardware acceleration method of this codec. This is best left on `'no-preference'`.
|
||||
* A hint that configures the hardware acceleration method of this codec. This is best left on `'no-preference'`,
|
||||
* the default.
|
||||
*/
|
||||
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
|
||||
/**
|
||||
@@ -403,31 +410,31 @@ export class Quality {
|
||||
* @group Encoding
|
||||
* @public
|
||||
*/
|
||||
export const QUALITY_VERY_LOW = new Quality(0.3);
|
||||
export const QUALITY_VERY_LOW = /* #__PURE__ */ new Quality(0.3);
|
||||
/**
|
||||
* Represents a low media quality.
|
||||
* @group Encoding
|
||||
* @public
|
||||
*/
|
||||
export const QUALITY_LOW = new Quality(0.6);
|
||||
export const QUALITY_LOW = /* #__PURE__ */ new Quality(0.6);
|
||||
/**
|
||||
* Represents a medium media quality.
|
||||
* @group Encoding
|
||||
* @public
|
||||
*/
|
||||
export const QUALITY_MEDIUM = new Quality(1);
|
||||
export const QUALITY_MEDIUM = /* #__PURE__ */ new Quality(1);
|
||||
/**
|
||||
* Represents a high media quality.
|
||||
* @group Encoding
|
||||
* @public
|
||||
*/
|
||||
export const QUALITY_HIGH = new Quality(2);
|
||||
export const QUALITY_HIGH = /* #__PURE__ */ new Quality(2);
|
||||
/**
|
||||
* Represents a very high media quality.
|
||||
* @group Encoding
|
||||
* @public
|
||||
*/
|
||||
export const QUALITY_VERY_HIGH = new Quality(4);
|
||||
export const QUALITY_VERY_HIGH = /* #__PURE__ */ new Quality(4);
|
||||
|
||||
/**
|
||||
* Checks if the browser is able to encode the given codec.
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
readU32Be,
|
||||
readU8,
|
||||
} from '../reader';
|
||||
import { MetadataTags } from '../tags';
|
||||
import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
|
||||
import {
|
||||
calculateCrc8,
|
||||
readBlockSize,
|
||||
@@ -565,6 +565,12 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return this.demuxer.audioInfo.sampleRate;
|
||||
}
|
||||
|
||||
getDisposition() {
|
||||
return {
|
||||
...DEFAULT_TRACK_DISPOSITION,
|
||||
};
|
||||
}
|
||||
|
||||
async getFirstTimestamp() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Output, OutputAudioTrack } from '../output';
|
||||
import { FlacOutputFormat } from '../output-format';
|
||||
import { EncodedPacket } from '../packet';
|
||||
import { FileSlice, readBytes } from '../reader';
|
||||
import { AttachedImage, metadataTagsAreEmpty } from '../tags';
|
||||
import { AttachedImage, metadataTagsAreEmpty } from '../metadata';
|
||||
import { Writer } from '../writer';
|
||||
import {
|
||||
readBlockSize,
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
readCodedNumber,
|
||||
} from './flac-misc';
|
||||
|
||||
const FLAC_HEADER = new Uint8Array([0x66, 0x4c, 0x61, 0x43]); // 'fLaC'
|
||||
const FLAC_HEADER = /* #__PURE__ */ new Uint8Array([0x66, 0x4c, 0x61, 0x43]); // 'fLaC'
|
||||
const STREAMINFO_SIZE = 38;
|
||||
const STREAMINFO_BLOCK_SIZE = 34;
|
||||
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { decodeSynchsafe, encodeSynchsafe } from '../shared/mp3-misc';
|
||||
import { MetadataTags } from './tags';
|
||||
import { MetadataTags } from './metadata';
|
||||
import {
|
||||
coalesceIndex,
|
||||
textDecoder,
|
||||
|
||||
+2
-1
@@ -202,6 +202,7 @@ export {
|
||||
AttachedImage,
|
||||
RichImageData,
|
||||
AttachedFile,
|
||||
} from './tags';
|
||||
TrackDisposition,
|
||||
} from './metadata';
|
||||
|
||||
// 🐡🦔
|
||||
|
||||
+9
-9
@@ -481,56 +481,56 @@ export class AdtsInputFormat extends InputFormat {
|
||||
* @group Input formats
|
||||
* @public
|
||||
*/
|
||||
export const MP4 = new Mp4InputFormat();
|
||||
export const MP4 = /* #__PURE__ */ new Mp4InputFormat();
|
||||
/**
|
||||
* QuickTime File Format input format singleton.
|
||||
* @group Input formats
|
||||
* @public
|
||||
*/
|
||||
export const QTFF = new QuickTimeInputFormat();
|
||||
export const QTFF = /* #__PURE__ */ new QuickTimeInputFormat();
|
||||
/**
|
||||
* Matroska input format singleton.
|
||||
* @group Input formats
|
||||
* @public
|
||||
*/
|
||||
export const MATROSKA = new MatroskaInputFormat();
|
||||
export const MATROSKA = /* #__PURE__ */ new MatroskaInputFormat();
|
||||
/**
|
||||
* WebM input format singleton.
|
||||
* @group Input formats
|
||||
* @public
|
||||
*/
|
||||
export const WEBM = new WebMInputFormat();
|
||||
export const WEBM = /* #__PURE__ */ new WebMInputFormat();
|
||||
/**
|
||||
* MP3 input format singleton.
|
||||
* @group Input formats
|
||||
* @public
|
||||
*/
|
||||
export const MP3 = new Mp3InputFormat();
|
||||
export const MP3 = /* #__PURE__ */ new Mp3InputFormat();
|
||||
/**
|
||||
* WAVE input format singleton.
|
||||
* @group Input formats
|
||||
* @public
|
||||
*/
|
||||
export const WAVE = new WaveInputFormat();
|
||||
export const WAVE = /* #__PURE__ */ new WaveInputFormat();
|
||||
/**
|
||||
* Ogg input format singleton.
|
||||
* @group Input formats
|
||||
* @public
|
||||
*/
|
||||
export const OGG = new OggInputFormat();
|
||||
export const OGG = /* #__PURE__ */ new OggInputFormat();
|
||||
/**
|
||||
* ADTS input format singleton.
|
||||
* @group Input formats
|
||||
* @public
|
||||
*/
|
||||
export const ADTS = new AdtsInputFormat();
|
||||
export const ADTS = /* #__PURE__ */ new AdtsInputFormat();
|
||||
|
||||
/**
|
||||
* FLAC input format singleton.
|
||||
* @group Input formats
|
||||
* @public
|
||||
*/
|
||||
export const FLAC = new FlacInputFormat();
|
||||
export const FLAC = /* #__PURE__ */ new FlacInputFormat();
|
||||
|
||||
/**
|
||||
* List of all input format singletons. If you don't need to support all input formats, you should specify the
|
||||
|
||||
@@ -14,6 +14,7 @@ import { EncodedPacketSink, PacketRetrievalOptions } from './media-sink';
|
||||
import { assert, Rotation } from './misc';
|
||||
import { TrackType } from './output';
|
||||
import { EncodedPacket, PacketType } from './packet';
|
||||
import { TrackDisposition } from './metadata';
|
||||
|
||||
/**
|
||||
* Contains aggregate statistics about the encoded packets of a track.
|
||||
@@ -36,6 +37,7 @@ export interface InputTrackBacking {
|
||||
getName(): string | null;
|
||||
getLanguageCode(): string;
|
||||
getTimeResolution(): number;
|
||||
getDisposition(): TrackDisposition;
|
||||
getFirstTimestamp(): Promise<number>;
|
||||
computeDuration(): Promise<number>;
|
||||
|
||||
@@ -128,6 +130,11 @@ export abstract class InputTrack {
|
||||
return this._backing.getTimeResolution();
|
||||
}
|
||||
|
||||
/** The track's disposition, i.e. information about its intended usage. */
|
||||
get disposition() {
|
||||
return this._backing.getDisposition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the start timestamp of the first packet of this track, in seconds. While often near zero, this value
|
||||
* may be positive or even negative. A negative starting timestamp means the track's timing has been offset. Samples
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
Sample,
|
||||
} from './isobmff-muxer';
|
||||
import { parseOpusIdentificationHeader } from '../codec-data';
|
||||
import { MetadataTags, RichImageData } from '../tags';
|
||||
import { MetadataTags, RichImageData } from '../metadata';
|
||||
|
||||
export class IsobmffBoxWriter {
|
||||
private helper = new Uint8Array(8);
|
||||
@@ -135,8 +135,8 @@ export class IsobmffBoxWriter {
|
||||
}
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(8);
|
||||
const view = new DataView(bytes.buffer);
|
||||
const bytes = /* #__PURE__ */ new Uint8Array(8);
|
||||
const view = /* #__PURE__ */ new DataView(bytes.buffer);
|
||||
|
||||
const u8 = (value: number) => {
|
||||
return [(value % 0x100 + 0x100) % 0x100];
|
||||
@@ -243,7 +243,7 @@ const rotationMatrix = (rotationInDegrees: number): TransformationMatrix => {
|
||||
0, 0, 1,
|
||||
];
|
||||
};
|
||||
const IDENTITY_MATRIX = rotationMatrix(0);
|
||||
const IDENTITY_MATRIX = /* #__PURE__ */ rotationMatrix(0);
|
||||
|
||||
const matrixToBytes = (matrix: TransformationMatrix) => {
|
||||
return [
|
||||
@@ -421,7 +421,12 @@ export const tkhd = (
|
||||
matrix = IDENTITY_MATRIX;
|
||||
}
|
||||
|
||||
return fullBox('tkhd', +needsU64, 3, [
|
||||
let flags = 0x2; // Track in movie
|
||||
if (trackData.track.metadata.disposition?.default !== false) {
|
||||
flags |= 0x1; // Track enabled
|
||||
}
|
||||
|
||||
return fullBox('tkhd', +needsU64, flags, [
|
||||
u32OrU64(creationTime), // Creation time
|
||||
u32OrU64(creationTime), // Modification time
|
||||
u32(trackData.track.id), // Track ID
|
||||
|
||||
@@ -51,13 +51,13 @@ import {
|
||||
MATRIX_COEFFICIENTS_MAP_INVERSE,
|
||||
normalizeRotation,
|
||||
roundToMultiple,
|
||||
roundToPrecision,
|
||||
Rotation,
|
||||
textDecoder,
|
||||
TransformationMatrix,
|
||||
TRANSFER_CHARACTERISTICS_MAP_INVERSE,
|
||||
UNDETERMINED_LANGUAGE,
|
||||
toDataView,
|
||||
roundIfAlmostInteger,
|
||||
} from '../misc';
|
||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
||||
import { buildIsobmffMimeType } from './isobmff-misc';
|
||||
@@ -86,12 +86,13 @@ import {
|
||||
readU8,
|
||||
readAscii,
|
||||
} from '../reader';
|
||||
import { MetadataTags, RichImageData } from '../tags';
|
||||
import { DEFAULT_TRACK_DISPOSITION, MetadataTags, RichImageData, TrackDisposition } from '../metadata';
|
||||
|
||||
type InternalTrack = {
|
||||
id: number;
|
||||
demuxer: IsobmffDemuxer;
|
||||
inputTrack: InputTrack | null;
|
||||
disposition: TrackDisposition;
|
||||
timescale: number;
|
||||
durationInMovieTimescale: number;
|
||||
durationInMediaTimescale: number;
|
||||
@@ -315,6 +316,9 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
this.moovSlice = moovSlice;
|
||||
this.readContiguousBoxes(this.moovSlice);
|
||||
|
||||
// Put default tracks first
|
||||
this.tracks.sort((a, b) => Number(b.disposition.default) - Number(a.disposition.default));
|
||||
|
||||
for (const track of this.tracks) {
|
||||
// Modify the edit list offset based on the previous segment durations. They are in different
|
||||
// timescales, so we first convert to seconds and then into the track timescale.
|
||||
@@ -651,6 +655,9 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
id: -1,
|
||||
demuxer: this,
|
||||
inputTrack: null,
|
||||
disposition: {
|
||||
...DEFAULT_TRACK_DISPOSITION,
|
||||
},
|
||||
info: null,
|
||||
timescale: -1,
|
||||
durationInMovieTimescale: -1,
|
||||
@@ -695,10 +702,10 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
const version = readU8(slice);
|
||||
const flags = readU24Be(slice);
|
||||
|
||||
const trackEnabled = (flags & 0x1) !== 0;
|
||||
if (!trackEnabled) {
|
||||
break;
|
||||
}
|
||||
// Spec says disabled tracks are to be treated like they don't exist, but in practice, they are treated
|
||||
// more like non-default tracks.
|
||||
const trackEnabled = !!(flags & 0x1);
|
||||
track.disposition.default = trackEnabled;
|
||||
|
||||
// Skip over creation & modification time to reach the track ID
|
||||
if (version === 0) {
|
||||
@@ -2345,6 +2352,10 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
||||
return this.internalTrack.timescale;
|
||||
}
|
||||
|
||||
getDisposition() {
|
||||
return this.internalTrack.disposition;
|
||||
}
|
||||
|
||||
async computeDuration() {
|
||||
const lastPacket = await this.getPacket(Infinity, { metadataOnly: true });
|
||||
return (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0);
|
||||
@@ -2388,7 +2399,7 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
||||
// Do a little rounding to catch cases where the result is very close to an integer. If it is, it's likely
|
||||
// that the number was originally an integer divided by the timescale. For stability, it's best
|
||||
// to return the integer in this case.
|
||||
return roundToPrecision(timestamp * this.internalTrack.timescale, 14) + this.internalTrack.editListOffset;
|
||||
return roundIfAlmostInteger(timestamp * this.internalTrack.timescale) + this.internalTrack.editListOffset;
|
||||
}
|
||||
|
||||
async getPacket(timestamp: number, options: PacketRetrievalOptions) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { RichImageData } from '../tags';
|
||||
import { RichImageData } from '../metadata';
|
||||
import { textDecoder } from '../misc';
|
||||
import { FileSlice, readAscii, readBytes, readI32Be, readU16Be, readU32Be, readU64Be, readU8 } from '../reader';
|
||||
|
||||
|
||||
@@ -88,6 +88,10 @@ export enum EBMLId {
|
||||
FlagEnabled = 0xb9,
|
||||
FlagDefault = 0x88,
|
||||
FlagForced = 0x55aa,
|
||||
FlagOriginal = 0x55ae,
|
||||
FlagHearingImpaired = 0x55ab,
|
||||
FlagVisualImpaired = 0x55ac,
|
||||
FlagCommentary = 0x55af,
|
||||
FlagLacing = 0x9c,
|
||||
Name = 0x536e,
|
||||
Language = 0x22b59c,
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
InputVideoTrack,
|
||||
InputVideoTrackBacking,
|
||||
} from '../input-track';
|
||||
import { AttachedFile, MetadataTags } from '../tags';
|
||||
import { AttachedFile, DEFAULT_TRACK_DISPOSITION, MetadataTags, TrackDisposition } from '../metadata';
|
||||
import { PacketRetrievalOptions } from '../media-sink';
|
||||
import {
|
||||
assert,
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
MATRIX_COEFFICIENTS_MAP_INVERSE,
|
||||
normalizeRotation,
|
||||
Rotation,
|
||||
roundToPrecision,
|
||||
roundIfAlmostInteger,
|
||||
TRANSFER_CHARACTERISTICS_MAP_INVERSE,
|
||||
UNDETERMINED_LANGUAGE,
|
||||
} from '../misc';
|
||||
@@ -61,7 +61,6 @@ import {
|
||||
readElementHeader,
|
||||
readElementId,
|
||||
readFloat,
|
||||
readSignedInt,
|
||||
readUnsignedInt,
|
||||
readVarInt,
|
||||
resync,
|
||||
@@ -137,7 +136,6 @@ type ClusterBlock = {
|
||||
timestamp: number;
|
||||
duration: number;
|
||||
isKeyFrame: boolean;
|
||||
referencedTimestamps: number[];
|
||||
data: Uint8Array;
|
||||
lacing: BlockLacing;
|
||||
decoded: boolean;
|
||||
@@ -190,7 +188,7 @@ type InternalTrack = {
|
||||
}[];
|
||||
cuePoints: CuePoint[];
|
||||
|
||||
isDefault: boolean;
|
||||
disposition: TrackDisposition;
|
||||
inputTrack: InputTrack | null;
|
||||
codecId: string | null;
|
||||
codecPrivate: Uint8Array | null;
|
||||
@@ -539,7 +537,7 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
// Put default tracks first
|
||||
this.currentSegment.tracks.sort((a, b) => Number(b.isDefault) - Number(a.isDefault));
|
||||
this.currentSegment.tracks.sort((a, b) => Number(b.disposition.default) - Number(a.disposition.default));
|
||||
|
||||
// Now, let's distribute the cue points to the tracks
|
||||
const idToTrack = new Map(this.currentSegment.tracks.map(x => [x.id, x]));
|
||||
@@ -649,21 +647,15 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
// This must hold, as track datas only get created if a block for that track is encountered
|
||||
assert(trackData.blocks.length > 0);
|
||||
|
||||
let blockReferencesExist = false;
|
||||
let hasLacedBlocks = false;
|
||||
|
||||
for (let i = 0; i < trackData.blocks.length; i++) {
|
||||
const block = trackData.blocks[i]!;
|
||||
block.timestamp += cluster.timestamp;
|
||||
|
||||
blockReferencesExist ||= block.referencedTimestamps.length > 0;
|
||||
hasLacedBlocks ||= block.lacing !== BlockLacing.None;
|
||||
}
|
||||
|
||||
if (blockReferencesExist) {
|
||||
trackData.blocks = sortBlocksByReferences(trackData.blocks);
|
||||
}
|
||||
|
||||
trackData.presentationTimestamps = trackData.blocks
|
||||
.map((block, i) => ({ timestamp: block.timestamp, blockIndex: i }))
|
||||
.sort((a, b) => a.timestamp - b.timestamp);
|
||||
@@ -859,7 +851,6 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
timestamp: frameTimestamp,
|
||||
duration: frameDuration,
|
||||
isKeyFrame: originalBlock.isKeyFrame,
|
||||
referencedTimestamps: originalBlock.referencedTimestamps,
|
||||
data: frameData,
|
||||
lacing: BlockLacing.None,
|
||||
decoded: true,
|
||||
@@ -998,7 +989,9 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
clusterPositionCache: [],
|
||||
cuePoints: [],
|
||||
|
||||
isDefault: false,
|
||||
disposition: {
|
||||
...DEFAULT_TRACK_DISPOSITION,
|
||||
},
|
||||
inputTrack: null,
|
||||
codecId: null,
|
||||
codecPrivate: null,
|
||||
@@ -1163,7 +1156,37 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
case EBMLId.FlagDefault: {
|
||||
if (!this.currentTrack) break;
|
||||
|
||||
this.currentTrack.isDefault = !!readUnsignedInt(slice, size);
|
||||
this.currentTrack.disposition.default = !!readUnsignedInt(slice, size);
|
||||
}; break;
|
||||
|
||||
case EBMLId.FlagForced: {
|
||||
if (!this.currentTrack) break;
|
||||
|
||||
this.currentTrack.disposition.forced = !!readUnsignedInt(slice, size);
|
||||
}; break;
|
||||
|
||||
case EBMLId.FlagOriginal: {
|
||||
if (!this.currentTrack) break;
|
||||
|
||||
this.currentTrack.disposition.original = !!readUnsignedInt(slice, size);
|
||||
}; break;
|
||||
|
||||
case EBMLId.FlagHearingImpaired: {
|
||||
if (!this.currentTrack) break;
|
||||
|
||||
this.currentTrack.disposition.hearingImpaired = !!readUnsignedInt(slice, size);
|
||||
}; break;
|
||||
|
||||
case EBMLId.FlagVisualImpaired: {
|
||||
if (!this.currentTrack) break;
|
||||
|
||||
this.currentTrack.disposition.visuallyImpaired = !!readUnsignedInt(slice, size);
|
||||
}; break;
|
||||
|
||||
case EBMLId.FlagCommentary: {
|
||||
if (!this.currentTrack) break;
|
||||
|
||||
this.currentTrack.disposition.commentary = !!readUnsignedInt(slice, size);
|
||||
}; break;
|
||||
|
||||
case EBMLId.CodecID: {
|
||||
@@ -1383,9 +1406,17 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
const relativeTimestamp = readI16Be(slice);
|
||||
|
||||
const flags = readU8(slice);
|
||||
const isKeyFrame = !!(flags & 0x80);
|
||||
const lacing = (flags >> 1) & 0x3 as BlockLacing; // If the block is laced, we'll expand it later
|
||||
|
||||
let isKeyFrame = !!(flags & 0x80);
|
||||
if (trackData.track.info?.type === 'audio' && trackData.track.info.codec) {
|
||||
// Some files don't mark their audio packets as key packets (I'm looking at you, Firefox). But, we
|
||||
// can fix this in most cases: if we recognize the codec of the track, then we know every packet is
|
||||
// necessarily a key packet, no matter what the container says.
|
||||
// https://github.com/Vanilagy/mediabunny/issues/192
|
||||
isKeyFrame = true;
|
||||
}
|
||||
|
||||
const blockData = readBytes(slice, size - (slice.filePos - dataStartPos));
|
||||
const hasDecodingInstructions = trackData.track.decodingInstructions.length > 0;
|
||||
|
||||
@@ -1393,7 +1424,6 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
timestamp: relativeTimestamp, // We'll add the cluster's timestamp to this later
|
||||
duration: 0, // Will set later
|
||||
isKeyFrame,
|
||||
referencedTimestamps: [],
|
||||
data: blockData,
|
||||
lacing,
|
||||
decoded: !hasDecodingInstructions,
|
||||
@@ -1406,13 +1436,7 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
|
||||
this.readContiguousElements(slice.slice(dataStartPos, size));
|
||||
|
||||
if (this.currentBlock) {
|
||||
for (let i = 0; i < this.currentBlock.referencedTimestamps.length; i++) {
|
||||
this.currentBlock.referencedTimestamps[i]! += this.currentBlock.timestamp;
|
||||
}
|
||||
|
||||
this.currentBlock = null;
|
||||
}
|
||||
this.currentBlock = null;
|
||||
}; break;
|
||||
|
||||
case EBMLId.Block: {
|
||||
@@ -1436,7 +1460,6 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
timestamp: relativeTimestamp, // We'll add the cluster's timestamp to this later
|
||||
duration: 0, // Will set later
|
||||
isKeyFrame: true,
|
||||
referencedTimestamps: [],
|
||||
data: blockData,
|
||||
lacing,
|
||||
decoded: !hasDecodingInstructions,
|
||||
@@ -1487,11 +1510,8 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
if (!this.currentBlock) break;
|
||||
|
||||
this.currentBlock.isKeyFrame = false;
|
||||
|
||||
const relativeTimestamp = readSignedInt(slice, size);
|
||||
|
||||
// We'll offset this by the block's timestamp later
|
||||
this.currentBlock.referencedTimestamps.push(relativeTimestamp);
|
||||
// We ignore the actual value here, we just use the reference as an indicator for "not a key frame".
|
||||
// This is in line with FFmpeg's behavior.
|
||||
}; break;
|
||||
|
||||
case EBMLId.Tag: {
|
||||
@@ -1859,6 +1879,10 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
|
||||
return this.internalTrack.segment.timestampFactor;
|
||||
}
|
||||
|
||||
getDisposition() {
|
||||
return this.internalTrack.disposition;
|
||||
}
|
||||
|
||||
async getFirstPacket(options: PacketRetrievalOptions) {
|
||||
return this.performClusterLookup(
|
||||
null,
|
||||
@@ -1886,7 +1910,7 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
|
||||
// Do a little rounding to catch cases where the result is very close to an integer. If it is, it's likely
|
||||
// that the number was originally an integer divided by the timescale. For stability, it's best
|
||||
// to return the integer in this case.
|
||||
return roundToPrecision(timestamp * this.internalTrack.segment.timestampFactor, 14);
|
||||
return roundIfAlmostInteger(timestamp * this.internalTrack.segment.timestampFactor);
|
||||
}
|
||||
|
||||
async getPacket(timestamp: number, options: PacketRetrievalOptions) {
|
||||
@@ -2380,43 +2404,3 @@ class MatroskaAudioTrackBacking extends MatroskaTrackBacking implements InputAud
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Sorts blocks such that referenced blocks come before the blocks that reference them. */
|
||||
const sortBlocksByReferences = (blocks: ClusterBlock[]) => {
|
||||
const timestampToBlock = new Map<number, ClusterBlock>();
|
||||
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
const block = blocks[i]!;
|
||||
timestampToBlock.set(block.timestamp, block);
|
||||
}
|
||||
|
||||
const processedBlocks = new Set<ClusterBlock>();
|
||||
const result: ClusterBlock[] = [];
|
||||
|
||||
const processBlock = (block: ClusterBlock) => {
|
||||
if (processedBlocks.has(block)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Marking the block as processed here already; prevents this algorithm from dying on cycles
|
||||
processedBlocks.add(block);
|
||||
|
||||
for (let j = 0; j < block.referencedTimestamps.length; j++) {
|
||||
const timestamp = block.referencedTimestamps[j]!;
|
||||
const otherBlock = timestampToBlock.get(timestamp);
|
||||
if (!otherBlock) {
|
||||
continue;
|
||||
}
|
||||
|
||||
processBlock(otherBlock);
|
||||
}
|
||||
|
||||
result.push(block);
|
||||
};
|
||||
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
processBlock(blocks[i]!);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -63,7 +63,7 @@ import { Muxer } from '../muxer';
|
||||
import { Writer } from '../writer';
|
||||
import { EncodedPacket } from '../packet';
|
||||
import { parseOpusIdentificationHeader } from '../codec-data';
|
||||
import { AttachedFile } from '../tags';
|
||||
import { AttachedFile } from '../metadata';
|
||||
|
||||
const MIN_CLUSTER_TIMESTAMP_MS = -(2 ** 15);
|
||||
const MAX_CLUSTER_TIMESTAMP_MS = 2 ** 15 - 1;
|
||||
@@ -304,6 +304,24 @@ export class MatroskaMuxer extends Muxer {
|
||||
{ id: EBMLId.TrackNumber, data: trackData.track.id },
|
||||
{ id: EBMLId.TrackUID, data: trackData.track.id },
|
||||
{ id: EBMLId.TrackType, data: TRACK_TYPE_MAP[trackData.type] },
|
||||
trackData.track.metadata.disposition?.default === false
|
||||
? { id: EBMLId.FlagDefault, data: 0 }
|
||||
: null,
|
||||
trackData.track.metadata.disposition?.forced
|
||||
? { id: EBMLId.FlagForced, data: 1 }
|
||||
: null,
|
||||
trackData.track.metadata.disposition?.hearingImpaired
|
||||
? { id: EBMLId.FlagHearingImpaired, data: 1 }
|
||||
: null,
|
||||
trackData.track.metadata.disposition?.visuallyImpaired
|
||||
? { id: EBMLId.FlagVisualImpaired, data: 1 }
|
||||
: null,
|
||||
trackData.track.metadata.disposition?.original
|
||||
? { id: EBMLId.FlagOriginal, data: 1 }
|
||||
: null,
|
||||
trackData.track.metadata.disposition?.commentary
|
||||
? { id: EBMLId.FlagCommentary, data: 1 }
|
||||
: null,
|
||||
{ id: EBMLId.FlagLacing, data: 0 },
|
||||
{ id: EBMLId.Language, data: trackData.track.metadata.languageCode ?? UNDETERMINED_LANGUAGE },
|
||||
{ id: EBMLId.CodecID, data: codecId },
|
||||
|
||||
+21
-1
@@ -8,10 +8,12 @@
|
||||
|
||||
import { parsePcmCodec, PCM_AUDIO_CODECS, PcmAudioCodec, VideoCodec, AudioCodec } from './codec';
|
||||
import {
|
||||
deserializeAvcDecoderConfigurationRecord,
|
||||
determineVideoPacketType,
|
||||
extractHevcNalUnits,
|
||||
extractNalUnitTypeForHevc,
|
||||
HevcNalUnitType,
|
||||
parseAvcSps,
|
||||
} from './codec-data';
|
||||
import { CustomVideoDecoder, customVideoDecoders, CustomAudioDecoder, customAudioDecoders } from './custom-coder';
|
||||
import { InputDisposedError } from './input';
|
||||
@@ -24,6 +26,7 @@ import {
|
||||
getInt24,
|
||||
getUint24,
|
||||
insertSorted,
|
||||
isChromium,
|
||||
isFirefox,
|
||||
isNumber,
|
||||
isWebKit,
|
||||
@@ -33,6 +36,7 @@ import {
|
||||
Rotation,
|
||||
toAsyncIterator,
|
||||
toDataView,
|
||||
toUint8Array,
|
||||
validateAnyIterable,
|
||||
} from './misc';
|
||||
import { EncodedPacket } from './packet';
|
||||
@@ -872,6 +876,22 @@ 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',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.decoder = new VideoDecoder({
|
||||
output: (frame) => {
|
||||
try {
|
||||
@@ -882,7 +902,7 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
|
||||
},
|
||||
error: onError,
|
||||
});
|
||||
this.decoder.configure(decoderConfig);
|
||||
this.decoder.configure(this.decoderConfig);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -259,3 +259,62 @@ export const metadataTagsAreEmpty = (tags: MetadataTags) => {
|
||||
&& tags.comment === undefined
|
||||
&& (tags.raw === undefined || Object.keys(tags.raw).length === 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Specifies a track's disposition, i.e. information about its intended usage.
|
||||
* @public
|
||||
* @group Miscellaneous
|
||||
*/
|
||||
export type TrackDisposition = {
|
||||
/**
|
||||
* Indicates that this track is eligible for automatic selection by a player; that it is the main track among other,
|
||||
* non-default tracks of the same type.
|
||||
*/
|
||||
default: boolean;
|
||||
/**
|
||||
* Indicates that players should always display this track by default, even if it goes against the user's default
|
||||
* preferences. For example, a subtitle track only containing translations of foreign-language audio.
|
||||
*/
|
||||
forced: boolean;
|
||||
/** Indicates that this track is in the content's original language. */
|
||||
original: boolean;
|
||||
/** Indicates that this track contains commentary. */
|
||||
commentary: boolean;
|
||||
/** Indicates that this track is intended for hearing-impaired users. */
|
||||
hearingImpaired: boolean;
|
||||
/** Indicates that this track is intended for visually-impaired users. */
|
||||
visuallyImpaired: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_TRACK_DISPOSITION: TrackDisposition = {
|
||||
default: true,
|
||||
forced: false,
|
||||
original: false,
|
||||
commentary: false,
|
||||
hearingImpaired: false,
|
||||
visuallyImpaired: false,
|
||||
};
|
||||
|
||||
export const validateTrackDisposition = (disposition: Partial<TrackDisposition>) => {
|
||||
if (!disposition || typeof disposition !== 'object') {
|
||||
throw new TypeError('disposition must be an object.');
|
||||
}
|
||||
if (disposition.default !== undefined && typeof disposition.default !== 'boolean') {
|
||||
throw new TypeError('disposition.default must be a boolean.');
|
||||
}
|
||||
if (disposition.forced !== undefined && typeof disposition.forced !== 'boolean') {
|
||||
throw new TypeError('disposition.forced must be a boolean.');
|
||||
}
|
||||
if (disposition.original !== undefined && typeof disposition.original !== 'boolean') {
|
||||
throw new TypeError('disposition.original must be a boolean.');
|
||||
}
|
||||
if (disposition.commentary !== undefined && typeof disposition.commentary !== 'boolean') {
|
||||
throw new TypeError('disposition.commentary must be a boolean.');
|
||||
}
|
||||
if (disposition.hearingImpaired !== undefined && typeof disposition.hearingImpaired !== 'boolean') {
|
||||
throw new TypeError('disposition.hearingImpaired must be a boolean.');
|
||||
}
|
||||
if (disposition.visuallyImpaired !== undefined && typeof disposition.visuallyImpaired !== 'boolean') {
|
||||
throw new TypeError('disposition.visuallyImpaired must be a boolean.');
|
||||
}
|
||||
};
|
||||
+23
-12
@@ -174,8 +174,8 @@ export const toDataView = (source: AllowSharedBufferSource) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const textDecoder = new TextDecoder();
|
||||
export const textEncoder = new TextEncoder();
|
||||
export const textDecoder = /* #__PURE__ */ new TextDecoder();
|
||||
export const textEncoder = /* #__PURE__ */ new TextEncoder();
|
||||
|
||||
export const isIso88591Compatible = (text: string) => {
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
@@ -201,7 +201,7 @@ export const COLOR_PRIMARIES_MAP = {
|
||||
bt2020: 9, // ITU-R BT.202
|
||||
smpte432: 12, // SMPTE EG 432-1
|
||||
};
|
||||
export const COLOR_PRIMARIES_MAP_INVERSE = invertObject(COLOR_PRIMARIES_MAP);
|
||||
export const COLOR_PRIMARIES_MAP_INVERSE = /* #__PURE__ */ invertObject(COLOR_PRIMARIES_MAP);
|
||||
|
||||
export const TRANSFER_CHARACTERISTICS_MAP = {
|
||||
'bt709': 1, // ITU-R BT.709
|
||||
@@ -211,7 +211,7 @@ export const TRANSFER_CHARACTERISTICS_MAP = {
|
||||
'pq': 16, // Rec. ITU-R BT.2100-2 perceptual quantization (PQ) system
|
||||
'hlg': 18, // Rec. ITU-R BT.2100-2 hybrid loggamma (HLG) system
|
||||
};
|
||||
export const TRANSFER_CHARACTERISTICS_MAP_INVERSE = invertObject(TRANSFER_CHARACTERISTICS_MAP);
|
||||
export const TRANSFER_CHARACTERISTICS_MAP_INVERSE = /* #__PURE__ */ invertObject(TRANSFER_CHARACTERISTICS_MAP);
|
||||
|
||||
export const MATRIX_COEFFICIENTS_MAP = {
|
||||
'rgb': 0, // Identity
|
||||
@@ -220,7 +220,7 @@ export const MATRIX_COEFFICIENTS_MAP = {
|
||||
'smpte170m': 6, // SMPTE 170M
|
||||
'bt2020-ncl': 9, // ITU-R BT.2020-2 (non-constant luminance)
|
||||
};
|
||||
export const MATRIX_COEFFICIENTS_MAP_INVERSE = invertObject(MATRIX_COEFFICIENTS_MAP);
|
||||
export const MATRIX_COEFFICIENTS_MAP_INVERSE = /* #__PURE__ */ invertObject(MATRIX_COEFFICIENTS_MAP);
|
||||
|
||||
export type RequiredNonNull<T> = {
|
||||
[K in keyof T]-?: NonNullable<T[K]>;
|
||||
@@ -486,9 +486,14 @@ export const clamp = (value: number, min: number, max: number) => {
|
||||
|
||||
export const UNDETERMINED_LANGUAGE = 'und';
|
||||
|
||||
export const roundToPrecision = (value: number, digits: number) => {
|
||||
const factor = 10 ** digits;
|
||||
return Math.round(value * factor) / factor;
|
||||
export const roundIfAlmostInteger = (value: number) => {
|
||||
const rounded = Math.round(value);
|
||||
|
||||
if (Math.abs(value / rounded - 1) < 10 * Number.EPSILON) {
|
||||
return rounded;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
export const roundToMultiple = (value: number, multiple: number) => {
|
||||
@@ -665,10 +670,7 @@ export const isWebKit = () => {
|
||||
}
|
||||
|
||||
// This even returns true for WebKit-wrapping browsers such as Chrome on iOS
|
||||
const result = !!(typeof navigator !== 'undefined' && navigator.vendor?.match(/apple/i));
|
||||
|
||||
isWebKitCache = result;
|
||||
return result;
|
||||
return isWebKitCache = !!(typeof navigator !== 'undefined' && navigator.vendor?.match(/apple/i));
|
||||
};
|
||||
|
||||
let isFirefoxCache: boolean | null = null;
|
||||
@@ -680,6 +682,15 @@ export const isFirefox = () => {
|
||||
return isFirefoxCache = typeof navigator !== 'undefined' && navigator.userAgent?.includes('Firefox');
|
||||
};
|
||||
|
||||
let isChromiumCache: boolean | null = null;
|
||||
export const isChromium = () => {
|
||||
if (isChromiumCache !== null) {
|
||||
return isChromiumCache;
|
||||
}
|
||||
|
||||
return isChromiumCache = !!(typeof navigator !== 'undefined' && navigator.vendor?.includes('Google Inc'));
|
||||
};
|
||||
|
||||
/**
|
||||
* T or a promise that resolves to T.
|
||||
* @group Miscellaneous
|
||||
|
||||
@@ -10,7 +10,7 @@ import { AudioCodec } from '../codec';
|
||||
import { Demuxer } from '../demuxer';
|
||||
import { Input } from '../input';
|
||||
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
|
||||
import { MetadataTags } from '../tags';
|
||||
import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
|
||||
import { PacketRetrievalOptions } from '../media-sink';
|
||||
import { assert, AsyncMutex, binarySearchExact, binarySearchLessOrEqual, UNDETERMINED_LANGUAGE } from '../misc';
|
||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
||||
@@ -257,6 +257,12 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
|
||||
return this.demuxer.firstFrameHeader.sampleRate;
|
||||
}
|
||||
|
||||
getDisposition() {
|
||||
return {
|
||||
...DEFAULT_TRACK_DISPOSITION,
|
||||
};
|
||||
}
|
||||
|
||||
async getDecoderConfig(): Promise<AudioDecoderConfig> {
|
||||
assert(this.demuxer.firstFrameHeader);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { assert, toDataView } from '../misc';
|
||||
import { metadataTagsAreEmpty } from '../tags';
|
||||
import { metadataTagsAreEmpty } from '../metadata';
|
||||
import { Muxer } from '../muxer';
|
||||
import { Output, OutputAudioTrack } from '../output';
|
||||
import { Mp3OutputFormat } from '../output-format';
|
||||
|
||||
+11
-11
@@ -46,21 +46,21 @@ export abstract class Muxer {
|
||||
|
||||
private trackTimestampInfo = new WeakMap<OutputTrack, {
|
||||
maxTimestamp: number;
|
||||
maxTimestampBeforeLastKeyFrame: number;
|
||||
maxTimestampBeforeLastKeyPacket: number;
|
||||
}>();
|
||||
|
||||
protected validateAndNormalizeTimestamp(track: OutputTrack, timestampInSeconds: number, isKeyFrame: boolean) {
|
||||
protected validateAndNormalizeTimestamp(track: OutputTrack, timestampInSeconds: number, isKeyPacket: boolean) {
|
||||
timestampInSeconds += track.source._timestampOffset;
|
||||
|
||||
let timestampInfo = this.trackTimestampInfo.get(track);
|
||||
if (!timestampInfo) {
|
||||
if (!isKeyFrame) {
|
||||
throw new Error('First frame must be a key frame.');
|
||||
if (!isKeyPacket) {
|
||||
throw new Error('First packet must be a key packet.');
|
||||
}
|
||||
|
||||
timestampInfo = {
|
||||
maxTimestamp: timestampInSeconds,
|
||||
maxTimestampBeforeLastKeyFrame: timestampInSeconds,
|
||||
maxTimestampBeforeLastKeyPacket: timestampInSeconds,
|
||||
};
|
||||
this.trackTimestampInfo.set(track, timestampInfo);
|
||||
}
|
||||
@@ -69,15 +69,15 @@ export abstract class Muxer {
|
||||
throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`);
|
||||
}
|
||||
|
||||
if (isKeyFrame) {
|
||||
timestampInfo.maxTimestampBeforeLastKeyFrame = timestampInfo.maxTimestamp;
|
||||
if (isKeyPacket) {
|
||||
timestampInfo.maxTimestampBeforeLastKeyPacket = timestampInfo.maxTimestamp;
|
||||
}
|
||||
|
||||
if (timestampInSeconds < timestampInfo.maxTimestampBeforeLastKeyFrame) {
|
||||
if (timestampInSeconds < timestampInfo.maxTimestampBeforeLastKeyPacket) {
|
||||
throw new Error(
|
||||
`Timestamps cannot be smaller than the highest timestamp of the previous GOP (a GOP begins with a key`
|
||||
+ ` frame and ends right before the next key frame). Got ${timestampInSeconds}s, but highest timestamp`
|
||||
+ ` is ${timestampInfo.maxTimestampBeforeLastKeyFrame}s.`,
|
||||
`Timestamps cannot be smaller than the largest timestamp of the previous GOP (a GOP begins with a key`
|
||||
+ ` packet and ends right before the next key packet). Got ${timestampInSeconds}s, but largest`
|
||||
+ ` timestamp is ${timestampInfo.maxTimestampBeforeLastKeyPacket}s.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+10
-4
@@ -12,14 +12,14 @@ import { Demuxer } from '../demuxer';
|
||||
import { Input } from '../input';
|
||||
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
|
||||
import { PacketRetrievalOptions } from '../media-sink';
|
||||
import { MetadataTags } from '../tags';
|
||||
import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
|
||||
import {
|
||||
assert,
|
||||
AsyncMutex,
|
||||
binarySearchLessOrEqual,
|
||||
findLast,
|
||||
last,
|
||||
roundToPrecision,
|
||||
roundIfAlmostInteger,
|
||||
toDataView,
|
||||
UNDETERMINED_LANGUAGE,
|
||||
} from '../misc';
|
||||
@@ -463,6 +463,12 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return UNDETERMINED_LANGUAGE;
|
||||
}
|
||||
|
||||
getDisposition() {
|
||||
return {
|
||||
...DEFAULT_TRACK_DISPOSITION,
|
||||
};
|
||||
}
|
||||
|
||||
async getFirstTimestamp() {
|
||||
return 0;
|
||||
}
|
||||
@@ -577,7 +583,7 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return this.getPacketSequential(timestamp, options);
|
||||
}
|
||||
|
||||
const timestampInSamples = roundToPrecision(timestamp * this.internalSampleRate, 14);
|
||||
const timestampInSamples = roundIfAlmostInteger(timestamp * this.internalSampleRate);
|
||||
if (timestampInSamples === 0) {
|
||||
// Fast path for timestamp 0 - avoids binary search when playing back from the start
|
||||
return this.getFirstPacket(options);
|
||||
@@ -910,7 +916,7 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
||||
const release = await this.sequentialScanMutex.acquire(); // Requires exclusivity because we write to a cache
|
||||
|
||||
try {
|
||||
const timestampInSamples = roundToPrecision(timestamp * this.internalSampleRate, 14);
|
||||
const timestampInSamples = roundIfAlmostInteger(timestamp * this.internalSampleRate);
|
||||
timestamp = timestampInSamples / this.internalSampleRate;
|
||||
|
||||
const index = binarySearchLessOrEqual(
|
||||
|
||||
+6
-1
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { AsyncMutex, isIso639Dash2LanguageCode, Rotation } from './misc';
|
||||
import { MetadataTags, validateMetadataTags } from './tags';
|
||||
import { MetadataTags, TrackDisposition, validateMetadataTags, validateTrackDisposition } from './metadata';
|
||||
import { Muxer } from './muxer';
|
||||
import { OutputFormat } from './output-format';
|
||||
import { AudioSource, MediaSource, SubtitleSource, VideoSource } from './media-source';
|
||||
@@ -74,6 +74,8 @@ export type BaseTrackMetadata = {
|
||||
languageCode?: string;
|
||||
/** A user-defined name for this track, like "English" or "Director Commentary". */
|
||||
name?: string;
|
||||
/** The track's disposition, i.e. information about its intended usage. */
|
||||
disposition?: Partial<TrackDisposition>;
|
||||
/**
|
||||
* The maximum amount of encoded packets that will be added to this track. Setting this field provides the muxer
|
||||
* with an additional signal that it can use to preallocate space in the file.
|
||||
@@ -129,6 +131,9 @@ const validateBaseTrackMetadata = (metadata: BaseTrackMetadata) => {
|
||||
if (metadata.name !== undefined && typeof metadata.name !== 'string') {
|
||||
throw new TypeError('metadata.name, when provided, must be a string.');
|
||||
}
|
||||
if (metadata.disposition !== undefined) {
|
||||
validateTrackDisposition(metadata.disposition);
|
||||
}
|
||||
if (
|
||||
metadata.maximumPacketCount !== undefined
|
||||
&& (!Number.isInteger(metadata.maximumPacketCount) || metadata.maximumPacketCount < 0)
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
import { SECOND_TO_MICROSECOND_FACTOR } from './misc';
|
||||
|
||||
export const PLACEHOLDER_DATA = new Uint8Array(0);
|
||||
export const PLACEHOLDER_DATA = /* #__PURE__ */ new Uint8Array(0);
|
||||
|
||||
/**
|
||||
* The type of a packet. Key packets can be decoded without previous packets, while delta packets depend on previous
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Demuxer } from '../demuxer';
|
||||
import { Input } from '../input';
|
||||
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
|
||||
import { PacketRetrievalOptions } from '../media-sink';
|
||||
import { MetadataTags } from '../tags';
|
||||
import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
|
||||
import { assert, UNDETERMINED_LANGUAGE } from '../misc';
|
||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
||||
import { readAscii, readBytes, Reader, readU16, readU32, readU64 } from '../reader';
|
||||
@@ -407,6 +407,12 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return UNDETERMINED_LANGUAGE;
|
||||
}
|
||||
|
||||
getDisposition() {
|
||||
return {
|
||||
...DEFAULT_TRACK_DISPOSITION,
|
||||
};
|
||||
}
|
||||
|
||||
async getFirstTimestamp() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Writer } from '../writer';
|
||||
import { EncodedPacket } from '../packet';
|
||||
import { WavOutputFormat } from '../output-format';
|
||||
import { assert, assertNever, isIso88591Compatible, keyValueIterator } from '../misc';
|
||||
import { MetadataTags, metadataTagsAreEmpty } from '../tags';
|
||||
import { MetadataTags, metadataTagsAreEmpty } from '../metadata';
|
||||
import { Id3V2Writer } from '../id3';
|
||||
|
||||
export class WaveMuxer extends Muxer {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { Output } from '../../src/output.js';
|
||||
import { MkvOutputFormat } from '../../src/output-format.js';
|
||||
import { BufferTarget } from '../../src/target.js';
|
||||
import { EncodedVideoPacketSource } from '../../src/media-source.js';
|
||||
import { EncodedPacket } from '../../src/packet.js';
|
||||
import { Input } from '../../src/input.js';
|
||||
import { BufferSource } from '../../src/source.js';
|
||||
import { ALL_FORMATS } from '../../src/input-format.js';
|
||||
|
||||
test('Default track disposition', async () => {
|
||||
const output = new Output({
|
||||
format: new MkvOutputFormat(),
|
||||
target: new BufferTarget(),
|
||||
});
|
||||
|
||||
const source = new EncodedVideoPacketSource('avc');
|
||||
output.addVideoTrack(source);
|
||||
|
||||
await output.start();
|
||||
await source.add(new EncodedPacket(new Uint8Array(1024), 'key', 0, 1), {
|
||||
decoderConfig: {
|
||||
codec: 'avc1.123456',
|
||||
codedWidth: 1920,
|
||||
codedHeight: 1080,
|
||||
},
|
||||
});
|
||||
|
||||
await output.finalize();
|
||||
|
||||
using input = new Input({
|
||||
source: new BufferSource(output.target.buffer!),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const track = (await input.getPrimaryVideoTrack())!;
|
||||
|
||||
expect(track.disposition).toEqual({
|
||||
default: true,
|
||||
forced: false,
|
||||
original: false,
|
||||
hearingImpaired: false,
|
||||
visuallyImpaired: false,
|
||||
commentary: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('Customized track disposition', async () => {
|
||||
const output = new Output({
|
||||
format: new MkvOutputFormat(),
|
||||
target: new BufferTarget(),
|
||||
});
|
||||
|
||||
const source = new EncodedVideoPacketSource('avc');
|
||||
output.addVideoTrack(source, {
|
||||
disposition: {
|
||||
default: false,
|
||||
forced: true,
|
||||
original: true,
|
||||
hearingImpaired: true,
|
||||
visuallyImpaired: true,
|
||||
commentary: true,
|
||||
},
|
||||
});
|
||||
|
||||
await output.start();
|
||||
await source.add(new EncodedPacket(new Uint8Array(1024), 'key', 0, 1), {
|
||||
decoderConfig: {
|
||||
codec: 'avc1.123456',
|
||||
codedWidth: 1920,
|
||||
codedHeight: 1080,
|
||||
},
|
||||
});
|
||||
|
||||
await output.finalize();
|
||||
|
||||
using input = new Input({
|
||||
source: new BufferSource(output.target.buffer!),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const track = (await input.getPrimaryVideoTrack())!;
|
||||
|
||||
expect(track.disposition).toEqual({
|
||||
default: false,
|
||||
forced: true,
|
||||
original: true,
|
||||
hearingImpaired: true,
|
||||
visuallyImpaired: true,
|
||||
commentary: true,
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,7 @@ import { EncodedPacket } from '../../src/packet.js';
|
||||
import { Input } from '../../src/input.js';
|
||||
import { BufferSource, FilePathSource } from '../../src/source.js';
|
||||
import { ALL_FORMATS } from '../../src/input-format.js';
|
||||
import { AttachedFile, MetadataTags } from '../../src/tags.js';
|
||||
import { AttachedFile, MetadataTags } from '../../src/metadata.js';
|
||||
import path from 'node:path';
|
||||
import { AudioCodec, buildAudioCodecString } from '../../src/codec.js';
|
||||
import { Conversion } from '../../src/conversion.js';
|
||||
|
||||
Reference in New Issue
Block a user