mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-04 06:13:46 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89d48d1bf9 | ||
|
|
6684984e7e | ||
|
|
0f030dc2a6 | ||
|
|
02b08e036b | ||
|
|
327696666b | ||
|
|
07b2e70863 | ||
|
|
3cb6ed82ce | ||
|
|
12216ae29e | ||
|
|
198b3d2eae | ||
|
|
e33e9f13fe | ||
|
|
fa1f44cf92 | ||
|
|
966ac133e9 | ||
|
|
23f814679e | ||
|
|
f3a2d57156 | ||
|
|
9d477dfb14 | ||
|
|
5b45c51d40 | ||
|
|
71dd2c565b | ||
|
|
3db4aa8b91 | ||
|
|
8b39eb0d43 | ||
|
|
6022781e8c | ||
|
|
c0da28fb6c | ||
|
|
fd8c74d4db | ||
|
|
6fb3103c01 | ||
|
|
d969266662 | ||
|
|
e0dda582b1 | ||
|
|
5aabf6350a |
@@ -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
|
||||
|
||||
+12
-3
@@ -48,6 +48,7 @@
|
||||
target
|
||||
});
|
||||
|
||||
let ctx = null;
|
||||
const conversion = await Mediabunny.Conversion.init({
|
||||
input: new Mediabunny.Input({
|
||||
formats: Mediabunny.ALL_FORMATS,
|
||||
@@ -56,6 +57,8 @@
|
||||
output,
|
||||
audio: (_, n) => ({
|
||||
discard: n > 1,
|
||||
//codec: 'opus',
|
||||
//codec: 'opus',
|
||||
/*
|
||||
process: (sample) => {
|
||||
return sample;
|
||||
@@ -97,6 +100,10 @@
|
||||
},
|
||||
*/
|
||||
video: () => ({
|
||||
width: 720,
|
||||
frameRate: 30,
|
||||
bitrate: Mediabunny.QUALITY_VERY_LOW,
|
||||
//discard: true,
|
||||
/*
|
||||
process: (sample) => {
|
||||
if (!ctx) {
|
||||
@@ -108,9 +115,11 @@
|
||||
ctx = canvas.getContext('2d');
|
||||
}
|
||||
|
||||
console.log(ctx.canvas.width, ctx.canvas.height);
|
||||
|
||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
sample.draw(ctx, 0, 0);
|
||||
ctx.drawImage(watermark, 32, 32);
|
||||
sample.drawWithFit(ctx, { fit: 'fill' });
|
||||
//ctx.drawImage(watermark, 32, 32);
|
||||
|
||||
return ctx.canvas;
|
||||
},
|
||||
@@ -171,7 +180,7 @@
|
||||
},
|
||||
trim: {
|
||||
start: 0,
|
||||
end: 10
|
||||
end: 20
|
||||
},
|
||||
});
|
||||
console.log(conversion);
|
||||
|
||||
+53
-1
@@ -14,7 +14,58 @@
|
||||
source: new Mediabunny.BlobSource(file),
|
||||
});
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
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);
|
||||
|
||||
let lastEnd = 0;
|
||||
for await (const sample of sink.samples()) {
|
||||
if (sample.timestamp - lastEnd > 0) {
|
||||
console.warn(sample.timestamp - lastEnd)
|
||||
console.log(sample.timestamp, sample.duration, "diffie", sample.timestamp - lastEnd);
|
||||
}
|
||||
|
||||
lastEnd = sample.timestamp + sample.duration;
|
||||
sample.close();
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
|
||||
|
||||
for await (const packet of sink.packets()) {
|
||||
console.log(packet.timestamp, packet.duration, packet.timestamp + packet.duration);
|
||||
|
||||
if (packet.timestamp > 10) break;
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
const sink = new Mediabunny.VideoSampleSink(videoTrack);
|
||||
|
||||
for await (const sample of sink.samples()) {
|
||||
console.log(sample.timestamp, sample.duration);
|
||||
sample.close();
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
|
||||
|
||||
for await (const packet of sink.packets()) {
|
||||
console.log(packet.timestamp, packet.timestamp + packet.duration);
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
const sink = new Mediabunny.VideoSampleSink(videoTrack);
|
||||
|
||||
console.log(await videoTrack.getDecoderConfig())
|
||||
@@ -30,6 +81,7 @@
|
||||
for await (const sample of sink.samples()) {
|
||||
console.log(sample);
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
let total = 0;
|
||||
|
||||
@@ -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.4",
|
||||
"version": "1.24.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mediabunny",
|
||||
"version": "1.24.4",
|
||||
"version": "1.24.7",
|
||||
"license": "MPL-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
@@ -7749,9 +7749,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mediabunny": {
|
||||
"version": "1.24.3",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.24.3.tgz",
|
||||
"integrity": "sha512-uQu5lXg6c3z2YUKLimAsem1qYPdGfGziQ5CKaAMJAs0C0NNw5T/Zkn0foPML0gWqQJ5ZeC0wTgCxpiBQY8m1Tw==",
|
||||
"version": "1.24.6",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.24.6.tgz",
|
||||
"integrity": "sha512-GODsNvKeig2Zhj2NCABBskVgJS1IMltP0uX/I0ltFksPCHpshreZhJEMwc903YbgdsW3RT7e6uvLX6UtlmeIkQ==",
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"workspaces": [
|
||||
@@ -12242,7 +12242,7 @@
|
||||
},
|
||||
"packages/mp3-encoder": {
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"version": "1.24.4",
|
||||
"version": "1.24.7",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.24.4",
|
||||
"version": "1.24.7",
|
||||
"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.4",
|
||||
"version": "1.24.7",
|
||||
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
|
||||
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
|
||||
+215
-48
@@ -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;
|
||||
|
||||
+53
-2
@@ -337,6 +337,7 @@ export const extractVideoCodecString = (trackInfo: {
|
||||
codec: VideoCodec | null;
|
||||
codecDescription: Uint8Array | null;
|
||||
colorSpace: VideoColorSpaceInit | null;
|
||||
avcType: 1 | 3 | null;
|
||||
avcCodecInfo: AvcDecoderConfigurationRecord | null;
|
||||
hevcCodecInfo: HevcDecoderConfigurationRecord | null;
|
||||
vp9CodecInfo: Vp9CodecInfo | null;
|
||||
@@ -345,6 +346,8 @@ export const extractVideoCodecString = (trackInfo: {
|
||||
const { codec, codecDescription, colorSpace, avcCodecInfo, hevcCodecInfo, vp9CodecInfo, av1CodecInfo } = trackInfo;
|
||||
|
||||
if (codec === 'avc') {
|
||||
assert(trackInfo.avcType !== null);
|
||||
|
||||
if (avcCodecInfo) {
|
||||
const bytes = new Uint8Array([
|
||||
avcCodecInfo.avcProfileIndication,
|
||||
@@ -352,14 +355,14 @@ export const extractVideoCodecString = (trackInfo: {
|
||||
avcCodecInfo.avcLevelIndication,
|
||||
]);
|
||||
|
||||
return `avc1.${bytesToHexString(bytes)}`;
|
||||
return `avc${trackInfo.avcType}.${bytesToHexString(bytes)}`;
|
||||
}
|
||||
|
||||
if (!codecDescription || codecDescription.byteLength < 4) {
|
||||
throw new TypeError('AVC decoder description is not provided or is not at least 4 bytes long.');
|
||||
}
|
||||
|
||||
return `avc1.${bytesToHexString(codecDescription.subarray(1, 4))}`;
|
||||
return `avc${trackInfo.avcType}.${bytesToHexString(codecDescription.subarray(1, 4))}`;
|
||||
} else if (codec === 'hevc') {
|
||||
let generalProfileSpace: number;
|
||||
let generalProfileIdc: number;
|
||||
@@ -621,6 +624,54 @@ export const parseAacAudioSpecificConfig = (bytes: Uint8Array | null): AacAudioS
|
||||
};
|
||||
};
|
||||
|
||||
export const buildAacAudioSpecificConfig = (config: {
|
||||
objectType: number;
|
||||
sampleRate: number;
|
||||
numberOfChannels: number;
|
||||
}) => {
|
||||
let frequencyIndex = aacFrequencyTable.indexOf(config.sampleRate);
|
||||
let customSampleRate: number | null = null;
|
||||
|
||||
if (frequencyIndex === -1) {
|
||||
frequencyIndex = 15;
|
||||
customSampleRate = config.sampleRate;
|
||||
}
|
||||
|
||||
const channelConfiguration = aacChannelMap.indexOf(config.numberOfChannels);
|
||||
if (channelConfiguration === -1) {
|
||||
throw new TypeError(`Unsupported number of channels: ${config.numberOfChannels}`);
|
||||
}
|
||||
|
||||
let bitCount = 5 + 4 + 4;
|
||||
if (config.objectType >= 32) {
|
||||
bitCount += 6;
|
||||
}
|
||||
if (frequencyIndex === 15) {
|
||||
bitCount += 24;
|
||||
}
|
||||
|
||||
const byteCount = Math.ceil(bitCount / 8);
|
||||
const bytes = new Uint8Array(byteCount);
|
||||
const bitstream = new Bitstream(bytes);
|
||||
|
||||
if (config.objectType < 32) {
|
||||
bitstream.writeBits(5, config.objectType);
|
||||
} else {
|
||||
bitstream.writeBits(5, 31);
|
||||
bitstream.writeBits(6, config.objectType - 32);
|
||||
}
|
||||
|
||||
bitstream.writeBits(4, frequencyIndex);
|
||||
|
||||
if (frequencyIndex === 15) {
|
||||
bitstream.writeBits(24, customSampleRate!);
|
||||
}
|
||||
|
||||
bitstream.writeBits(4, channelConfiguration);
|
||||
|
||||
return bytes;
|
||||
};
|
||||
|
||||
export const OPUS_SAMPLE_RATE = 48_000;
|
||||
|
||||
const PCM_CODEC_REGEX = /^pcm-([usf])(\d+)+(be)?$/;
|
||||
|
||||
+8
-4
@@ -182,7 +182,8 @@ export type ConversionVideoOptions = {
|
||||
* corrections.
|
||||
*
|
||||
* Must return a {@link VideoSample} or a `CanvasImageSource`, an array of them, or `null` for dropping the frame.
|
||||
* When non-timestamped data is returned, the timestamp and duration from the source sample will be used.
|
||||
* When non-timestamped data is returned, the timestamp and duration from the source sample will be used. Rotation
|
||||
* metadata of the returned sample will be ignored.
|
||||
*
|
||||
* This function can also be used to manually resize frames. When doing so, you should signal the post-process
|
||||
* dimensions using the `processedWidth` and `processedHeight` fields, which enables the encoder to better know what
|
||||
@@ -873,7 +874,10 @@ export class Conversion {
|
||||
|| trackOptions.process !== undefined;
|
||||
let needsRerender = width !== originalWidth
|
||||
|| height !== originalHeight
|
||||
|| (totalRotation !== 0 && !outputSupportsRotation)
|
||||
// TODO This is suboptimal: Forcing a rerender when both rotation and process are set is not
|
||||
// performance-optimal, but right now there's no other way because we can't change the track rotation
|
||||
// metadata after the output has already started. Should be possible with API changes in v2, though!
|
||||
|| (totalRotation !== 0 && (!outputSupportsRotation || trackOptions.process !== undefined))
|
||||
|| !!crop;
|
||||
|
||||
const alpha = trackOptions.alpha ?? 'discard';
|
||||
@@ -1039,6 +1043,7 @@ export class Conversion {
|
||||
duration: 1 / frameRate,
|
||||
});
|
||||
await this._registerVideoSample(track, trackOptions, source, sample);
|
||||
sample.close();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1075,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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+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.
|
||||
|
||||
@@ -328,11 +328,13 @@ export class FlacDemuxer extends Demuxer {
|
||||
|
||||
const nextByte = readU8(slice);
|
||||
if (nextByte === 0xff) {
|
||||
const positionBeforeReading = slice.filePos;
|
||||
|
||||
const byteAfterNextByte = readU8(slice);
|
||||
|
||||
const expected = this.blockingBit === 1 ? 0b1111_1001 : 0b1111_1000;
|
||||
if (byteAfterNextByte !== expected) {
|
||||
slice.skip(-1);
|
||||
slice.filePos = positionBeforeReading;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -345,7 +347,7 @@ export class FlacDemuxer extends Demuxer {
|
||||
});
|
||||
|
||||
if (!nextFrameHeader) {
|
||||
slice.skip(-1);
|
||||
slice.filePos = positionBeforeReading;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -355,14 +357,14 @@ export class FlacDemuxer extends Demuxer {
|
||||
if (this.blockingBit === 0) {
|
||||
// Case A: If the stream is fixed block size, this is the frame number, which increments by 1
|
||||
if (nextFrameHeader.num - frameHeader.num !== 1) {
|
||||
slice.skip(-1);
|
||||
slice.filePos = positionBeforeReading;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Case B: If the stream is variable block size, this is the sample number, which increments by
|
||||
// amount of samples in a frame.
|
||||
if (nextFrameHeader.num - frameHeader.num !== frameHeader.blockSize) {
|
||||
slice.skip(-1);
|
||||
slice.filePos = positionBeforeReading;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+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
|
||||
|
||||
@@ -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 [
|
||||
@@ -577,7 +577,7 @@ export const stsd = (trackData: IsobmffTrackData) => {
|
||||
|
||||
if (trackData.type === 'video') {
|
||||
sampleDescription = videoSampleDescription(
|
||||
VIDEO_CODEC_TO_BOX_NAME[trackData.track.source._codec],
|
||||
videoCodecToBoxName(trackData.track.source._codec, trackData.info.decoderConfig.codec),
|
||||
trackData,
|
||||
);
|
||||
} else if (trackData.type === 'audio') {
|
||||
@@ -1572,12 +1572,14 @@ const dataStringBoxLong = (value: string) => {
|
||||
]);
|
||||
};
|
||||
|
||||
const VIDEO_CODEC_TO_BOX_NAME: Record<VideoCodec, string> = {
|
||||
avc: 'avc1',
|
||||
hevc: 'hvc1',
|
||||
vp8: 'vp08',
|
||||
vp9: 'vp09',
|
||||
av1: 'av01',
|
||||
const videoCodecToBoxName = (codec: VideoCodec, fullCodecString: string) => {
|
||||
switch (codec) {
|
||||
case 'avc': return fullCodecString.startsWith('avc3') ? 'avc3' : 'avc1';
|
||||
case 'hevc': return 'hvc1';
|
||||
case 'vp8': return 'vp08';
|
||||
case 'vp9': return 'vp09';
|
||||
case 'av1': return 'av01';
|
||||
}
|
||||
};
|
||||
|
||||
const VIDEO_CODEC_TO_CONFIGURATION_BOX: Record<VideoCodec, (trackData: IsobmffVideoTrackData) => Box | null> = {
|
||||
|
||||
@@ -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';
|
||||
@@ -126,6 +126,7 @@ type InternalTrack = {
|
||||
codec: VideoCodec | null;
|
||||
codecDescription: Uint8Array | null;
|
||||
colorSpace: VideoColorSpaceInit | null;
|
||||
avcType: 1 | 3 | null;
|
||||
avcCodecInfo: AvcDecoderConfigurationRecord | null;
|
||||
hevcCodecInfo: HevcDecoderConfigurationRecord | null;
|
||||
vp9CodecInfo: Vp9CodecInfo | null;
|
||||
@@ -836,6 +837,7 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
codec: null,
|
||||
codecDescription: null,
|
||||
colorSpace: null,
|
||||
avcType: null,
|
||||
avcCodecInfo: null,
|
||||
hevcCodecInfo: null,
|
||||
vp9CodecInfo: null,
|
||||
@@ -890,8 +892,9 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
const lowercaseBoxName = sampleBoxInfo.name.toLowerCase();
|
||||
|
||||
if (track.info.type === 'video') {
|
||||
if (lowercaseBoxName === 'avc1') {
|
||||
if (lowercaseBoxName === 'avc1' || lowercaseBoxName === 'avc3') {
|
||||
track.info.codec = 'avc';
|
||||
track.info.avcType = lowercaseBoxName === 'avc1' ? 1 : 3;
|
||||
} else if (lowercaseBoxName === 'hvc1' || lowercaseBoxName === 'hev1') {
|
||||
track.info.codec = 'hevc';
|
||||
} else if (lowercaseBoxName === 'vp08') {
|
||||
@@ -2385,7 +2388,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) {
|
||||
|
||||
@@ -463,7 +463,7 @@ export class EBMLWriter {
|
||||
|
||||
export const MAX_VAR_INT_SIZE = 8;
|
||||
export const MIN_HEADER_SIZE = 2; // 1-byte ID and 1-byte size
|
||||
export const MAX_HEADER_SIZE = 2 * MAX_VAR_INT_SIZE; // 8-byte ID and 8-byte size
|
||||
export const MAX_HEADER_SIZE = /* #__PURE__ */ 2 * MAX_VAR_INT_SIZE; // 8-byte ID and 8-byte size
|
||||
|
||||
export const readVarIntSize = (slice: FileSlice) => {
|
||||
const firstByte = readU8(slice);
|
||||
|
||||
@@ -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;
|
||||
@@ -230,7 +228,7 @@ const METADATA_ELEMENTS = [
|
||||
{ id: EBMLId.Tracks, flag: 'tracksSeen' },
|
||||
{ id: EBMLId.Cues, flag: 'cuesSeen' },
|
||||
] as const;
|
||||
const MAX_RESYNC_LENGTH = 10 * 2 ** 20; // 10 MiB
|
||||
const MAX_RESYNC_LENGTH = /* #__PURE__ */ 10 * 2 ** 20; // 10 MiB
|
||||
|
||||
export class MatroskaDemuxer extends Demuxer {
|
||||
reader: Reader;
|
||||
@@ -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);
|
||||
@@ -844,13 +836,13 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
|
||||
blocks.splice(blockIndex, 1); // Remove the original block
|
||||
|
||||
const blockDuration = originalBlock.duration || frameCount * (track.defaultDuration ?? 0);
|
||||
|
||||
// Now, let's insert each frame as its own block
|
||||
for (let i = 0; i < frameCount; i++) {
|
||||
const frameSize = frameSizes[i]!;
|
||||
const frameData = readBytes(slice, frameSize);
|
||||
|
||||
const blockDuration = originalBlock.duration || (frameCount * (track.defaultDuration ?? 0));
|
||||
|
||||
// Distribute timestamps evenly across the block duration
|
||||
const frameTimestamp = originalBlock.timestamp + (blockDuration * i / frameCount);
|
||||
const frameDuration = blockDuration / frameCount;
|
||||
@@ -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,
|
||||
@@ -1383,9 +1374,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 +1392,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 +1404,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 +1428,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 +1478,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: {
|
||||
@@ -1886,7 +1874,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) {
|
||||
@@ -2319,6 +2307,7 @@ class MatroskaVideoTrackBacking extends MatroskaTrackBacking implements InputVid
|
||||
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,
|
||||
@@ -2379,43 +2368,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;
|
||||
};
|
||||
|
||||
@@ -65,8 +65,8 @@ import { EncodedPacket } from '../packet';
|
||||
import { parseOpusIdentificationHeader } from '../codec-data';
|
||||
import { AttachedFile } from '../tags';
|
||||
|
||||
const MIN_CLUSTER_TIMESTAMP_MS = -(2 ** 15);
|
||||
const MAX_CLUSTER_TIMESTAMP_MS = 2 ** 15 - 1;
|
||||
const MIN_CLUSTER_TIMESTAMP_MS = /* #__PURE__ */ -(2 ** 15);
|
||||
const MAX_CLUSTER_TIMESTAMP_MS = /* #__PURE__ */ 2 ** 15 - 1;
|
||||
const APP_NAME = 'Mediabunny';
|
||||
const SEGMENT_SIZE_BYTES = 6;
|
||||
const CLUSTER_SIZE_BYTES = 5;
|
||||
|
||||
+25
-5
@@ -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,15 +26,17 @@ import {
|
||||
getInt24,
|
||||
getUint24,
|
||||
insertSorted,
|
||||
isChromium,
|
||||
isFirefox,
|
||||
isNumber,
|
||||
isSafari,
|
||||
isWebKit,
|
||||
last,
|
||||
mapAsyncGenerator,
|
||||
promiseWithResolvers,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -918,7 +938,7 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
|
||||
} else {
|
||||
assert(this.decoder);
|
||||
|
||||
if (!isSafari()) {
|
||||
if (!isWebKit()) {
|
||||
insertSorted(this.inputTimestamps, packet.timestamp, x => x);
|
||||
}
|
||||
|
||||
@@ -1049,7 +1069,7 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
|
||||
|
||||
/** Handler for the WebCodecs VideoDecoder for ironing out browser differences. */
|
||||
sampleHandler(sample: VideoSample) {
|
||||
if (isSafari()) {
|
||||
if (isWebKit()) {
|
||||
// For correct B-frame handling, we don't just hand over the frames directly but instead add them to
|
||||
// a queue, because we want to ensure frames are emitted in presentation order. We flush the queue
|
||||
// each time we receive a frame with a timestamp larger than the highest we've seen so far, as we
|
||||
@@ -1137,7 +1157,7 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
|
||||
this.alphaRaslSkipped = false;
|
||||
}
|
||||
|
||||
if (isSafari()) {
|
||||
if (isWebKit()) {
|
||||
for (const sample of this.sampleQueue) {
|
||||
this.finalizeAndEmitSample(sample);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import {
|
||||
AUDIO_CODECS,
|
||||
AudioCodec,
|
||||
buildAacAudioSpecificConfig,
|
||||
parseAacAudioSpecificConfig,
|
||||
parsePcmCodec,
|
||||
PCM_AUDIO_CODECS,
|
||||
PcmAudioCodec,
|
||||
@@ -24,9 +26,11 @@ import {
|
||||
CallSerializer,
|
||||
clamp,
|
||||
isFirefox,
|
||||
last,
|
||||
promiseWithResolvers,
|
||||
setInt24,
|
||||
setUint24,
|
||||
toUint8Array,
|
||||
} from './misc';
|
||||
import { Muxer } from './muxer';
|
||||
import { SubtitleParser } from './subtitles';
|
||||
@@ -1290,6 +1294,8 @@ class AudioEncoderWrapper {
|
||||
private customEncoderCallSerializer = new CallSerializer();
|
||||
private customEncoderQueueSize = 0;
|
||||
|
||||
private lastEndSampleIndex: number | null = null;
|
||||
|
||||
/**
|
||||
* Encoders typically throw their errors "out of band", meaning asynchronously in some other execution context.
|
||||
* However, we want to surface these errors to the user within the normal control flow, so they don't go uncaught.
|
||||
@@ -1338,6 +1344,35 @@ class AudioEncoderWrapper {
|
||||
}
|
||||
assert(this.encoderInitialized);
|
||||
|
||||
// Handle padding of gaps with silence to avoid audio drift over time, like in
|
||||
// https://github.com/Vanilagy/mediabunny/issues/176
|
||||
// TODO An open question is how encoders deal with the first AudioData having a non-zero timestamp, and with
|
||||
// AudioDatas that have an overlapping timestamp range.
|
||||
{
|
||||
const startSampleIndex = Math.round(
|
||||
audioSample.timestamp * audioSample.sampleRate,
|
||||
);
|
||||
const endSampleIndex = Math.round(
|
||||
(audioSample.timestamp + audioSample.duration) * audioSample.sampleRate,
|
||||
);
|
||||
|
||||
if (this.lastEndSampleIndex !== null && startSampleIndex > this.lastEndSampleIndex) {
|
||||
const sampleCount = startSampleIndex - this.lastEndSampleIndex;
|
||||
const fillSample = new AudioSample({
|
||||
data: new Float32Array(sampleCount * audioSample.numberOfChannels),
|
||||
format: 'f32-planar',
|
||||
sampleRate: audioSample.sampleRate,
|
||||
numberOfChannels: audioSample.numberOfChannels,
|
||||
numberOfFrames: sampleCount,
|
||||
timestamp: this.lastEndSampleIndex / audioSample.sampleRate,
|
||||
});
|
||||
|
||||
await this.add(fillSample, true); // Recursive call
|
||||
}
|
||||
|
||||
this.lastEndSampleIndex = endSampleIndex;
|
||||
}
|
||||
|
||||
if (this.customEncoder) {
|
||||
this.customEncoderQueueSize++;
|
||||
|
||||
@@ -1515,6 +1550,32 @@ class AudioEncoderWrapper {
|
||||
|
||||
this.encoder = new AudioEncoder({
|
||||
output: (chunk, meta) => {
|
||||
// WebKit emits an invalid description for AAC (https://bugs.webkit.org/show_bug.cgi?id=302253),
|
||||
// which we try to detect here. If detected, we'll provide our own description instead, derived
|
||||
// from the codec string and audio parameters.
|
||||
if (this.encodingConfig.codec === 'aac' && meta?.decoderConfig) {
|
||||
let needsDescriptionOverwrite = false;
|
||||
if (!meta.decoderConfig.description || meta.decoderConfig.description.byteLength < 2) {
|
||||
needsDescriptionOverwrite = true;
|
||||
} else {
|
||||
const audioSpecificConfig = parseAacAudioSpecificConfig(
|
||||
toUint8Array(meta.decoderConfig.description),
|
||||
);
|
||||
|
||||
needsDescriptionOverwrite = audioSpecificConfig.objectType === 0;
|
||||
}
|
||||
|
||||
if (needsDescriptionOverwrite) {
|
||||
const objectType = Number(last(encoderConfig.codec.split('.')));
|
||||
|
||||
meta.decoderConfig.description = buildAacAudioSpecificConfig({
|
||||
objectType,
|
||||
numberOfChannels: meta.decoderConfig.numberOfChannels,
|
||||
sampleRate: meta.decoderConfig.sampleRate,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const packet = EncodedPacket.fromEncodedChunk(chunk);
|
||||
|
||||
this.encodingConfig.onEncodedPacket?.(packet, meta);
|
||||
|
||||
+29
-23
@@ -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) => {
|
||||
@@ -510,7 +515,7 @@ export const isIso639Dash2LanguageCode = (x: string) => {
|
||||
};
|
||||
|
||||
// Since the result will be truncated, add a bit of eps to compensate for floating point errors
|
||||
export const SECOND_TO_MICROSECOND_FACTOR = 1e6 * (1 + Number.EPSILON);
|
||||
export const SECOND_TO_MICROSECOND_FACTOR = /* #__PURE__ */ 1e6 * (1 + Number.EPSILON);
|
||||
|
||||
/**
|
||||
* Sets all keys K of T to be required.
|
||||
@@ -658,22 +663,14 @@ export class CallSerializer {
|
||||
}
|
||||
}
|
||||
|
||||
let isSafariCache: boolean | null = null;
|
||||
export const isSafari = () => {
|
||||
if (isSafariCache !== null) {
|
||||
return isSafariCache;
|
||||
let isWebKitCache: boolean | null = null;
|
||||
export const isWebKit = () => {
|
||||
if (isWebKitCache !== null) {
|
||||
return isWebKitCache;
|
||||
}
|
||||
|
||||
const result = !!(
|
||||
typeof navigator !== 'undefined'
|
||||
&& navigator.vendor?.match(/apple/i)
|
||||
&& !navigator.userAgent?.match(/crios/i)
|
||||
&& !navigator.userAgent?.match(/fxios/i)
|
||||
&& !navigator.userAgent?.match(/Opera|OPT\//)
|
||||
);
|
||||
|
||||
isSafariCache = result;
|
||||
return result;
|
||||
// This even returns true for WebKit-wrapping browsers such as Chrome on iOS
|
||||
return isWebKitCache = !!(typeof navigator !== 'undefined' && navigator.vendor?.match(/apple/i));
|
||||
};
|
||||
|
||||
let isFirefoxCache: boolean | null = null;
|
||||
@@ -685,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
|
||||
|
||||
+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.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
binarySearchLessOrEqual,
|
||||
findLast,
|
||||
last,
|
||||
roundToPrecision,
|
||||
roundIfAlmostInteger,
|
||||
toDataView,
|
||||
UNDETERMINED_LANGUAGE,
|
||||
} from '../misc';
|
||||
@@ -577,7 +577,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 +910,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(
|
||||
|
||||
@@ -11,7 +11,7 @@ import { OGGS } from './ogg-misc';
|
||||
|
||||
export const MIN_PAGE_HEADER_SIZE = 27;
|
||||
export const MAX_PAGE_HEADER_SIZE = 27 + 255;
|
||||
export const MAX_PAGE_SIZE = MAX_PAGE_HEADER_SIZE + 255 * 255;
|
||||
export const MAX_PAGE_SIZE = /* #__PURE__ */ MAX_PAGE_HEADER_SIZE + 255 * 255;
|
||||
|
||||
export type Page = {
|
||||
headerStartPos: number;
|
||||
|
||||
+5
-2
@@ -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
|
||||
@@ -126,7 +126,10 @@ export class EncodedPacket {
|
||||
}
|
||||
}
|
||||
|
||||
/** If this packet is a metadata-only packet. Metadata-only packets don't contain their packet data. */
|
||||
/**
|
||||
* If this packet is a metadata-only packet. Metadata-only packets don't contain their packet data. They are the
|
||||
* result of retrieving packets with {@link PacketRetrievalOptions.metadataOnly} set to `true`.
|
||||
*/
|
||||
get isMetadataOnly() {
|
||||
return this.data === PLACEHOLDER_DATA;
|
||||
}
|
||||
|
||||
+4
-1
@@ -552,7 +552,6 @@ export class VideoSample implements Disposable {
|
||||
dHeight,
|
||||
);
|
||||
|
||||
// Restore the previous transformation state
|
||||
context.restore();
|
||||
}
|
||||
|
||||
@@ -644,6 +643,8 @@ export class VideoSample implements Disposable {
|
||||
dy = (canvasHeight - newHeight) / 2;
|
||||
}
|
||||
|
||||
context.save();
|
||||
|
||||
const aspectRatioChange = rotation % 180 === 0 ? 1 : newWidth / newHeight;
|
||||
context.translate(canvasWidth / 2, canvasHeight / 2);
|
||||
context.rotate(rotation * Math.PI / 180);
|
||||
@@ -655,6 +656,8 @@ export class VideoSample implements Disposable {
|
||||
// Important that we don't use .draw() here since that would take rotation into account, but we wanna handle it
|
||||
// ourselves here
|
||||
context.drawImage(this.toCanvasImageSource(), sx, sy, sWidth, sHeight, dx, dy, newWidth, newHeight);
|
||||
|
||||
context.restore();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
||||
+10
-2
@@ -12,6 +12,7 @@ import {
|
||||
binarySearchLessOrEqual,
|
||||
closedIntervalsOverlap,
|
||||
isNumber,
|
||||
isWebKit,
|
||||
MaybePromise,
|
||||
mergeRequestInit,
|
||||
promiseWithResolvers,
|
||||
@@ -209,7 +210,14 @@ export class BlobSource extends Source {
|
||||
private async _runWorker(worker: ReadWorker) {
|
||||
let reader = this._readers.get(worker);
|
||||
if (reader === undefined) {
|
||||
if ('stream' in this._blob) {
|
||||
// https://github.com/Vanilagy/mediabunny/issues/184
|
||||
// WebKit has critical bugs with blob.stream():
|
||||
// - WebKitBlobResource error 1 when streaming large files
|
||||
// - Memory buildup and reload loops on iOS (network process crashes)
|
||||
// - ReadableStream stalls under backpressure (especially video)
|
||||
// Affects Safari and all iOS browsers (Chrome, Firefox, etc.).
|
||||
// Use arrayBuffer() fallback for WebKit browsers.
|
||||
if ('stream' in this._blob && !isWebKit()) {
|
||||
// Get a reader of the blob starting at the required offset, and then keep it around
|
||||
const slice = this._blob.slice(worker.currentPos);
|
||||
reader = slice.stream().getReader();
|
||||
@@ -261,7 +269,7 @@ export class BlobSource extends Source {
|
||||
}
|
||||
}
|
||||
|
||||
const URL_SOURCE_MIN_LOAD_AMOUNT = 0.5 * 2 ** 20; // 0.5 MiB
|
||||
const URL_SOURCE_MIN_LOAD_AMOUNT = /* #__PURE__ */ 0.5 * 2 ** 20; // 0.5 MiB
|
||||
const DEFAULT_RETRY_DELAY
|
||||
= ((previousAttempts, error, src) => {
|
||||
// Check if this could be a CORS error. If so, we cannot recover from it and
|
||||
|
||||
+3
-3
@@ -89,8 +89,8 @@ export abstract class Writer {
|
||||
}
|
||||
}
|
||||
|
||||
const ARRAY_BUFFER_INITIAL_SIZE = 2 ** 16;
|
||||
const ARRAY_BUFFER_MAX_SIZE = 2 ** 32;
|
||||
const ARRAY_BUFFER_INITIAL_SIZE = /* #__PURE__ */ 2 ** 16;
|
||||
const ARRAY_BUFFER_MAX_SIZE = /* #__PURE__ */ 2 ** 32;
|
||||
|
||||
export class BufferTargetWriter extends Writer {
|
||||
private pos = 0;
|
||||
@@ -184,7 +184,7 @@ export class BufferTargetWriter extends Writer {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_CHUNK_SIZE = 2 ** 24;
|
||||
const DEFAULT_CHUNK_SIZE = /* #__PURE__ */ 2 ** 24;
|
||||
const MAX_CHUNKS_AT_ONCE = 2;
|
||||
|
||||
interface Chunk {
|
||||
|
||||
Reference in New Issue
Block a user