Compare commits

...
11 Commits
17 changed files with 170 additions and 99 deletions
+6 -3
View File
@@ -24,7 +24,7 @@
chunked: true, chunked: true,
chunkSize: 2**20 chunkSize: 2**20
}); });
const outputFormat = new Mediabunny.Mp4OutputFormat({}); const outputFormat = new Mediabunny.WavOutputFormat({});
const button = document.createElement('button'); const button = document.createElement('button');
button.textContent = 'Cancel'; button.textContent = 'Cancel';
@@ -72,6 +72,9 @@
}), }),
output, output,
audio: { audio: {
codec: 'pcm-s16',
//sampleRate: 16000,
//numberOfChannels: 1,
//discard: true, //discard: true,
//codec: 'opus', //codec: 'opus',
//bitrate: 128000, //bitrate: 128000,
@@ -106,7 +109,7 @@
*/ */
video: () => ({ video: () => ({
//discard: true, //discard: true,
forceTranscode: true, //forceTranscode: true,
//codec: 'avc', //codec: 'avc',
//fit: 'contain', //fit: 'contain',
//frameRate: 27.123, //frameRate: 27.123,
@@ -129,7 +132,7 @@
//height: 100, //height: 100,
}), }),
trim: { trim: {
start: 1, start: 0,
end: 10 end: 10
}, },
}); });
+13 -3
View File
@@ -14,10 +14,20 @@
source: new Mediabunny.BlobSource(file), source: new Mediabunny.BlobSource(file),
}); });
const videoTrack = await input.getPrimaryVideoTrack(); const audioTrack = await input.getPrimaryAudioTrack();
const sink = new Mediabunny.VideoSampleSink(videoTrack); const sink = new Mediabunny.EncodedPacketSink(audioTrack);
console.log(await sink.getSample(await videoTrack.getFirstTimestamp())) for await (const packet of sink.packets()) {
console.log(packet);
}
/*
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
for await (const packet of sink.packets()) {
console.log(packet)
}
*/
/* /*
for await (const sample of sink.samples(0.99)) { for await (const sample of sink.samples(0.99)) {
+6 -6
View File
@@ -60,6 +60,12 @@ await input.computeDuration(); // => 1905.4615
``` ```
More specifically, the duration is defined as the maximum end timestamp across all tracks. More specifically, the duration is defined as the maximum end timestamp across all tracks.
Mediabunny also lets you read descriptive metadata tags from media files, such as title, artist, or cover art:
```ts
await input.getMetadataTags(); // => MetadataTags
```
For more info, see [`MetadataTags`](../api/MetadataTags).
## Reading track metadata ## Reading track metadata
You can extract the list of all media tracks in the file like so: You can extract the list of all media tracks in the file like so:
@@ -76,12 +82,6 @@ await input.getPrimaryVideoTrack(); // => InputVideoTrack | null
await input.getPrimaryAudioTrack(); // => InputAudioTrack | null await input.getPrimaryAudioTrack(); // => InputAudioTrack | null
``` ```
Mediabunny also lets you read descriptive metadata tags from media files, such as title, artist, or cover art:
```ts
await input.getMetadataTags(); // => MetadataTags
```
For more info, see [`MetadataTags`](../api/MetadataTags).
::: info ::: info
Subtitle tracks are currently not supported for reading. Subtitle tracks are currently not supported for reading.
::: :::
+1
View File
@@ -105,6 +105,7 @@ const sponsors = {
{ image: 'https://avatars.githubusercontent.com/u/9549394', name: 'studnitz', url: 'https://github.com/studnitz' }, { image: 'https://avatars.githubusercontent.com/u/9549394', name: 'studnitz', url: 'https://github.com/studnitz' },
{ image: 'https://avatars.githubusercontent.com/u/504909', name: 'Hirbod', url: 'https://github.com/hirbod' }, { image: 'https://avatars.githubusercontent.com/u/504909', name: 'Hirbod', url: 'https://github.com/hirbod' },
{ image: 'https://avatars.githubusercontent.com/u/30229596', name: 'Pablo Bonilla', url: 'https://github.com/devPablo' }, { image: 'https://avatars.githubusercontent.com/u/30229596', name: 'Pablo Bonilla', url: 'https://github.com/devPablo' },
{ image: 'https://avatars.githubusercontent.com/u/38181164', name: 'wcw', url: 'https://github.com/asd55667' },
{ image: 'https://avatars.githubusercontent.com/u/63088713', name: 'taf2000', url: 'https://github.com/taf2000' }, { image: 'https://avatars.githubusercontent.com/u/63088713', name: 'taf2000', url: 'https://github.com/taf2000' },
{ image: 'https://avatars.githubusercontent.com/u/58149663', name: 'H7GhosT', url: 'https://github.com/H7GhosT' }, { image: 'https://avatars.githubusercontent.com/u/58149663', name: 'H7GhosT', url: 'https://github.com/H7GhosT' },
{ image: 'https://avatars.githubusercontent.com/u/91711202', name: 'ihasq', url: 'https://github.com/ihasq' }, { image: 'https://avatars.githubusercontent.com/u/91711202', name: 'ihasq', url: 'https://github.com/ihasq' },
@@ -96,6 +96,13 @@ const generateThumbnails = async (resource: File | string) => {
timestampElement.className timestampElement.className
= 'absolute bottom-0 right-0 bg-black/30 text-white px-1 py-0.5 text-[11px] rounded-tl-lg'; = 'absolute bottom-0 right-0 bg-black/30 text-white px-1 py-0.5 text-[11px] rounded-tl-lg';
container.append(timestampElement); container.append(timestampElement);
} else {
// Add something to indicate that the thumbnail is missing
const p = document.createElement('p');
p.textContent = '?';
p.className = 'absolute inset-0 flex items-center justify-center text-3xl opacity-50';
container.append(p);
} }
i++; i++;
+6 -6
View File
@@ -1,12 +1,12 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"version": "1.14.2", "version": "1.14.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mediabunny", "name": "mediabunny",
"version": "1.14.2", "version": "1.14.4",
"license": "MPL-2.0", "license": "MPL-2.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@@ -7749,9 +7749,9 @@
} }
}, },
"node_modules/mediabunny": { "node_modules/mediabunny": {
"version": "1.14.1", "version": "1.14.3",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.14.1.tgz", "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.14.3.tgz",
"integrity": "sha512-TjLg8GQGGsnGePcA6i0NItGe9a5G8WaleCGaXpb5V5okwuK5KpNKfcmTZXItnjfPLo7FvfEZI0NFp1lIR8Os7Q==", "integrity": "sha512-kCvieRo6X1QDcdWLjn7o2BY/VCDeyU9nNGBVjOIOiWPoTtIekHR+viKAYaZafLEm0poBv+O2PwttL37PaSo/kA==",
"license": "MPL-2.0", "license": "MPL-2.0",
"peer": true, "peer": true,
"workspaces": [ "workspaces": [
@@ -12242,7 +12242,7 @@
}, },
"packages/mp3-encoder": { "packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"version": "1.14.2", "version": "1.14.4",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.14.2", "version": "1.14.4",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.14.2", "version": "1.14.4",
"description": "MP3 encoder extension for Mediabunny, based on LAME.", "description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
+67 -50
View File
@@ -26,8 +26,27 @@ import { EncodedPacket, PacketType } from './packet';
// Rec. ITU-T H.265 // Rec. ITU-T H.265
// https://stackoverflow.com/questions/24884827 // https://stackoverflow.com/questions/24884827
export enum AvcNalUnitType {
IDR = 5,
SPS = 7,
PPS = 8,
SPS_EXT = 13,
}
export enum HevcNalUnitType {
RASL_N = 8,
RASL_R = 9,
BLA_W_LP = 16,
RSV_IRAP_VCL23 = 23,
VPS_NUT = 32,
SPS_NUT = 33,
PPS_NUT = 34,
PREFIX_SEI_NUT = 39,
SUFFIX_SEI_NUT = 40,
}
/** Finds all NAL units in an AVC packet in Annex B format. */ /** Finds all NAL units in an AVC packet in Annex B format. */
const findNalUnitsInAnnexB = (packetData: Uint8Array) => { export const findNalUnitsInAnnexB = (packetData: Uint8Array) => {
const nalUnits: Uint8Array[] = []; const nalUnits: Uint8Array[] = [];
let i = 0; let i = 0;
@@ -184,6 +203,21 @@ export type AvcDecoderConfigurationRecord = {
sequenceParameterSetExt: Uint8Array[] | null; sequenceParameterSetExt: Uint8Array[] | null;
}; };
export const extractAvcNalUnits = (packetData: Uint8Array, decoderConfig: VideoDecoderConfig) => {
if (decoderConfig.description) {
// Stream is length-prefixed. Let's extract the size of the length prefix from the decoder config
const bytes = toUint8Array(decoderConfig.description);
const lengthSizeMinusOne = bytes[4]! & 0b11;
const lengthSize = (lengthSizeMinusOne + 1) as 1 | 2 | 3 | 4;
return findNalUnitsInLengthPrefixed(packetData, lengthSize);
} else {
// Stream is in Annex B format
return findNalUnitsInAnnexB(packetData);
}
};
const extractNalUnitTypeForAvc = (data: Uint8Array) => { const extractNalUnitTypeForAvc = (data: Uint8Array) => {
return data[0]! & 0x1F; return data[0]! & 0x1F;
}; };
@@ -193,9 +227,9 @@ export const extractAvcDecoderConfigurationRecord = (packetData: Uint8Array) =>
try { try {
const nalUnits = findNalUnitsInAnnexB(packetData); const nalUnits = findNalUnitsInAnnexB(packetData);
const spsUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === 7); const spsUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === AvcNalUnitType.SPS);
const ppsUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === 8); const ppsUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === AvcNalUnitType.PPS);
const spsExtUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === 13); const spsExtUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === AvcNalUnitType.SPS_EXT);
if (spsUnits.length === 0) { if (spsUnits.length === 0) {
return null; return null;
@@ -337,12 +371,6 @@ export const serializeAvcDecoderConfigurationRecord = (record: AvcDecoderConfigu
return new Uint8Array(bytes); return new Uint8Array(bytes);
}; };
const NALU_TYPE_VPS = 32;
const NALU_TYPE_SPS = 33;
const NALU_TYPE_PPS = 34;
const NALU_TYPE_SEI_PREFIX = 39;
const NALU_TYPE_SEI_SUFFIX = 40;
// Data specified in ISO 14496-15 // Data specified in ISO 14496-15
export type HevcDecoderConfigurationRecord = { export type HevcDecoderConfigurationRecord = {
configurationVersion: number; configurationVersion: number;
@@ -369,7 +397,22 @@ export type HevcDecoderConfigurationRecord = {
}[]; }[];
}; };
const extractNalUnitTypeForHevc = (data: Uint8Array) => { export const extractHevcNalUnits = (packetData: Uint8Array, decoderConfig: VideoDecoderConfig) => {
if (decoderConfig.description) {
// Stream is length-prefixed. Let's extract the size of the length prefix from the decoder config
const bytes = toUint8Array(decoderConfig.description);
const lengthSizeMinusOne = bytes[21]! & 0b11;
const lengthSize = (lengthSizeMinusOne + 1) as 1 | 2 | 3 | 4;
return findNalUnitsInLengthPrefixed(packetData, lengthSize);
} else {
// Stream is in Annex B format
return findNalUnitsInAnnexB(packetData);
}
};
export const extractNalUnitTypeForHevc = (data: Uint8Array) => {
return (data[0]! >> 1) & 0x3F; return (data[0]! >> 1) & 0x3F;
}; };
@@ -380,12 +423,12 @@ export const extractHevcDecoderConfigurationRecord = (
try { try {
const nalUnits = findNalUnitsInAnnexB(packetData); const nalUnits = findNalUnitsInAnnexB(packetData);
const vpsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === NALU_TYPE_VPS); const vpsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === HevcNalUnitType.VPS_NUT);
const spsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === NALU_TYPE_SPS); const spsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === HevcNalUnitType.SPS_NUT);
const ppsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === NALU_TYPE_PPS); const ppsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === HevcNalUnitType.PPS_NUT);
const seiUnits = nalUnits.filter( const seiUnits = nalUnits.filter(
unit => extractNalUnitTypeForHevc(unit) === NALU_TYPE_SEI_PREFIX unit => extractNalUnitTypeForHevc(unit) === HevcNalUnitType.PREFIX_SEI_NUT
|| extractNalUnitTypeForHevc(unit) === NALU_TYPE_SEI_SUFFIX, || extractNalUnitTypeForHevc(unit) === HevcNalUnitType.SUFFIX_SEI_NUT,
); );
if (spsUnits.length === 0 || ppsUnits.length === 0) return null; if (spsUnits.length === 0 || ppsUnits.length === 0) return null;
@@ -521,7 +564,7 @@ export const extractHevcDecoderConfigurationRecord = (
? [ ? [
{ {
arrayCompleteness: 1, arrayCompleteness: 1,
nalUnitType: NALU_TYPE_VPS, nalUnitType: HevcNalUnitType.VPS_NUT,
nalUnits: vpsUnits, nalUnits: vpsUnits,
}, },
] ]
@@ -530,7 +573,7 @@ export const extractHevcDecoderConfigurationRecord = (
? [ ? [
{ {
arrayCompleteness: 1, arrayCompleteness: 1,
nalUnitType: NALU_TYPE_SPS, nalUnitType: HevcNalUnitType.SPS_NUT,
nalUnits: spsUnits, nalUnits: spsUnits,
}, },
] ]
@@ -539,7 +582,7 @@ export const extractHevcDecoderConfigurationRecord = (
? [ ? [
{ {
arrayCompleteness: 1, arrayCompleteness: 1,
nalUnitType: NALU_TYPE_PPS, nalUnitType: HevcNalUnitType.PPS_NUT,
nalUnits: ppsUnits, nalUnits: ppsUnits,
}, },
] ]
@@ -1440,22 +1483,9 @@ export const determineVideoPacketType = async (
const decoderConfig = await videoTrack.getDecoderConfig(); const decoderConfig = await videoTrack.getDecoderConfig();
assert(decoderConfig); assert(decoderConfig);
let nalUnits: Uint8Array[]; const nalUnits = extractAvcNalUnits(packet.data, decoderConfig);
const isKeyframe = nalUnits.some(x => extractNalUnitTypeForAvc(x) === AvcNalUnitType.IDR);
if (decoderConfig.description) {
// Stream is length-prefixed. Let's extract the size of the length prefix from the decoder config
const bytes = toUint8Array(decoderConfig.description);
const lengthSizeMinusOne = bytes[4]! & 0b11;
const lengthSize = (lengthSizeMinusOne + 1) as 1 | 2 | 3 | 4;
nalUnits = findNalUnitsInLengthPrefixed(packet.data, lengthSize);
} else {
// Stream is in Annex B format
nalUnits = findNalUnitsInAnnexB(packet.data);
}
const isKeyframe = nalUnits.some(x => extractNalUnitTypeForAvc(x) === 5);
return isKeyframe ? 'key' : 'delta'; return isKeyframe ? 'key' : 'delta';
}; };
@@ -1463,25 +1493,12 @@ export const determineVideoPacketType = async (
const decoderConfig = await videoTrack.getDecoderConfig(); const decoderConfig = await videoTrack.getDecoderConfig();
assert(decoderConfig); assert(decoderConfig);
let nalUnits: Uint8Array[]; const nalUnits = extractHevcNalUnits(packet.data, decoderConfig);
if (decoderConfig.description) {
// Stream is length-prefixed. Let's extract the size of the length prefix from the decoder config
const bytes = toUint8Array(decoderConfig.description);
const lengthSizeMinusOne = bytes[21]! & 0b11;
const lengthSize = (lengthSizeMinusOne + 1) as 1 | 2 | 3 | 4;
nalUnits = findNalUnitsInLengthPrefixed(packet.data, lengthSize);
} else {
// Stream is in Annex B format
nalUnits = findNalUnitsInAnnexB(packet.data);
}
const isKeyframe = nalUnits.some((x) => { const isKeyframe = nalUnits.some((x) => {
const type = extractNalUnitTypeForHevc(x); const type = extractNalUnitTypeForHevc(x);
return 16 <= type && type <= 23; return HevcNalUnitType.BLA_W_LP <= type && type <= HevcNalUnitType.RSV_IRAP_VCL23;
}); });
return isKeyframe ? 'key' : 'delta'; return isKeyframe ? 'key' : 'delta';
}; };
+1 -1
View File
@@ -621,7 +621,7 @@ export const parseAacAudioSpecificConfig = (bytes: Uint8Array | null): AacAudioS
}; };
}; };
export const OPUS_INTERNAL_SAMPLE_RATE = 48000; export const OPUS_SAMPLE_RATE = 48_000;
const PCM_CODEC_REGEX = /^pcm-([usf])(\d+)+(be)?$/; const PCM_CODEC_REGEX = /^pcm-([usf])(\d+)+(be)?$/;
+17 -17
View File
@@ -1125,8 +1125,6 @@ export class Conversion {
await this._started; await this._started;
const resampler = new AudioResampler({ const resampler = new AudioResampler({
sourceNumberOfChannels: track.numberOfChannels,
sourceSampleRate: track.sampleRate,
targetNumberOfChannels, targetNumberOfChannels,
targetSampleRate, targetSampleRate,
startTime: this._startTimestamp, startTime: this._startTimestamp,
@@ -1246,9 +1244,9 @@ class TrackSynchronizer {
* OfflineAudioContext. * OfflineAudioContext.
*/ */
export class AudioResampler { export class AudioResampler {
sourceSampleRate: number; sourceSampleRate: number | null = null;
targetSampleRate: number; targetSampleRate: number;
sourceNumberOfChannels: number; sourceNumberOfChannels: number | null = null;
targetNumberOfChannels: number; targetNumberOfChannels: number;
startTime: number; startTime: number;
endTime: number; endTime: number;
@@ -1262,20 +1260,16 @@ export class AudioResampler {
/** The highest index written to in the current buffer */ /** The highest index written to in the current buffer */
maxWrittenFrame: number; maxWrittenFrame: number;
channelMixer!: (sourceData: Float32Array, sourceFrameIndex: number, targetChannelIndex: number) => number; channelMixer!: (sourceData: Float32Array, sourceFrameIndex: number, targetChannelIndex: number) => number;
tempSourceBuffer: Float32Array; tempSourceBuffer!: Float32Array;
constructor(options: { constructor(options: {
sourceSampleRate: number;
targetSampleRate: number; targetSampleRate: number;
sourceNumberOfChannels: number;
targetNumberOfChannels: number; targetNumberOfChannels: number;
startTime: number; startTime: number;
endTime: number; endTime: number;
onSample: (sample: AudioSample) => Promise<void>; onSample: (sample: AudioSample) => Promise<void>;
}) { }) {
this.sourceSampleRate = options.sourceSampleRate;
this.targetSampleRate = options.targetSampleRate; this.targetSampleRate = options.targetSampleRate;
this.sourceNumberOfChannels = options.sourceNumberOfChannels;
this.targetNumberOfChannels = options.targetNumberOfChannels; this.targetNumberOfChannels = options.targetNumberOfChannels;
this.startTime = options.startTime; this.startTime = options.startTime;
this.endTime = options.endTime; this.endTime = options.endTime;
@@ -1287,17 +1281,14 @@ export class AudioResampler {
this.outputBuffer = new Float32Array(this.bufferSizeInSamples); this.outputBuffer = new Float32Array(this.bufferSizeInSamples);
this.bufferStartFrame = 0; this.bufferStartFrame = 0;
this.maxWrittenFrame = -1; this.maxWrittenFrame = -1;
this.setupChannelMixer();
// Pre-allocate temporary buffer for source data
this.tempSourceBuffer = new Float32Array(this.sourceSampleRate * this.sourceNumberOfChannels);
} }
/** /**
* Sets up the channel mixer to handle up/downmixing in the case where input and output channel counts don't match. * Sets up the channel mixer to handle up/downmixing in the case where input and output channel counts don't match.
*/ */
setupChannelMixer(): void { doChannelMixerSetup(): void {
assert(this.sourceNumberOfChannels !== null);
const sourceNum = this.sourceNumberOfChannels; const sourceNum = this.sourceNumberOfChannels;
const targetNum = this.targetNumberOfChannels; const targetNum = this.targetNumberOfChannels;
@@ -1415,8 +1406,17 @@ export class AudioResampler {
} }
async add(audioSample: AudioSample) { async add(audioSample: AudioSample) {
if (!audioSample || audioSample._closed) { if (this.sourceSampleRate === null) {
return; // This is the first sample, so let's init the missing data. Initting the sample rate from the decoded
// sample is more reliable than using the file's metadata, because decoders are free to emit any sample rate
// they see fit.
this.sourceSampleRate = audioSample.sampleRate;
this.sourceNumberOfChannels = audioSample.numberOfChannels;
// Pre-allocate temporary buffer for source data
this.tempSourceBuffer = new Float32Array(this.sourceSampleRate * this.sourceNumberOfChannels);
this.doChannelMixerSetup();
} }
const requiredSamples = audioSample.numberOfFrames * audioSample.numberOfChannels; const requiredSamples = audioSample.numberOfFrames * audioSample.numberOfChannels;
+6 -1
View File
@@ -12,6 +12,7 @@ import {
extractAudioCodecString, extractAudioCodecString,
extractVideoCodecString, extractVideoCodecString,
MediaCodec, MediaCodec,
OPUS_SAMPLE_RATE,
parseAacAudioSpecificConfig, parseAacAudioSpecificConfig,
parsePcmCodec, parsePcmCodec,
PCM_AUDIO_CODECS, PCM_AUDIO_CODECS,
@@ -1049,6 +1050,10 @@ export class IsobmffDemuxer extends Demuxer {
} }
} }
if (track.info.codec === 'opus') {
sampleRate = OPUS_SAMPLE_RATE; // Always the same
}
track.info.numberOfChannels = channelCount; track.info.numberOfChannels = channelCount;
track.info.sampleRate = sampleRate; track.info.sampleRate = sampleRate;
@@ -1421,7 +1426,7 @@ export class IsobmffDemuxer extends Demuxer {
track.info.codecDescription = description; track.info.codecDescription = description;
track.info.numberOfChannels = outputChannelCount; track.info.numberOfChannels = outputChannelCount;
track.info.sampleRate = inputSampleRate; // Don't copy the input sample rate, irrelevant, and output sample rate is fixed
}; break; }; break;
case 'dfLa': { // Used for FLAC audio case 'dfLa': { // Used for FLAC audio
+2
View File
@@ -18,6 +18,7 @@ import {
extractAudioCodecString, extractAudioCodecString,
extractVideoCodecString, extractVideoCodecString,
MediaCodec, MediaCodec,
OPUS_SAMPLE_RATE,
VideoCodec, VideoCodec,
} from '../codec'; } from '../codec';
import { Demuxer } from '../demuxer'; import { Demuxer } from '../demuxer';
@@ -988,6 +989,7 @@ export class MatroskaDemuxer extends Demuxer {
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.opus) { } else if (codecIdWithoutSuffix === CODEC_STRING_MAP.opus) {
this.currentTrack.info.codec = 'opus'; this.currentTrack.info.codec = 'opus';
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate; this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
this.currentTrack.info.sampleRate = OPUS_SAMPLE_RATE; // Always the same
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.vorbis) { } else if (codecIdWithoutSuffix === CODEC_STRING_MAP.vorbis) {
this.currentTrack.info.codec = 'vorbis'; this.currentTrack.info.codec = 'vorbis';
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate; this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
+2 -2
View File
@@ -47,7 +47,7 @@ import {
parseSubtitleTimestamp, parseSubtitleTimestamp,
} from '../subtitles'; } from '../subtitles';
import { import {
OPUS_INTERNAL_SAMPLE_RATE, OPUS_SAMPLE_RATE,
PCM_AUDIO_CODECS, PCM_AUDIO_CODECS,
PcmAudioCodec, PcmAudioCodec,
SubtitleCodec, SubtitleCodec,
@@ -293,7 +293,7 @@ export class MatroskaMuxer extends Muxer {
const header = parseOpusIdentificationHeader(bytes); const header = parseOpusIdentificationHeader(bytes);
// Use the preSkip value from the header // Use the preSkip value from the header
seekPreRollNs = Math.round(1e9 * (header.preSkip / OPUS_INTERNAL_SAMPLE_RATE)); seekPreRollNs = Math.round(1e9 * (header.preSkip / OPUS_SAMPLE_RATE));
} }
} }
+29 -3
View File
@@ -7,6 +7,7 @@
*/ */
import { parsePcmCodec, PCM_AUDIO_CODECS, PcmAudioCodec, VideoCodec, AudioCodec } from './codec'; import { parsePcmCodec, PCM_AUDIO_CODECS, PcmAudioCodec, VideoCodec, AudioCodec } from './codec';
import { extractHevcNalUnits, extractNalUnitTypeForHevc, HevcNalUnitType } from './codec-data';
import { CustomVideoDecoder, customVideoDecoders, CustomAudioDecoder, customAudioDecoders } from './custom-coder'; import { CustomVideoDecoder, customVideoDecoders, CustomAudioDecoder, customAudioDecoders } from './custom-coder';
import { InputAudioTrack, InputTrack, InputVideoTrack } from './input-track'; import { InputAudioTrack, InputTrack, InputVideoTrack } from './input-track';
import { import {
@@ -624,8 +625,8 @@ export abstract class BaseMediaSampleSink<
const nextPacket = await packetSink.getNextPacket(currentPacket); const nextPacket = await packetSink.getNextPacket(currentPacket);
assert(nextPacket); assert(nextPacket);
currentPacket = nextPacket;
decoder.decode(nextPacket); decoder.decode(nextPacket);
currentPacket = nextPacket;
} }
maxSequenceNumber = -1; maxSequenceNumber = -1;
@@ -757,12 +758,14 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
inputTimestamps: number[] = []; // Timestamps input into the decoder, sorted. inputTimestamps: number[] = []; // Timestamps input into the decoder, sorted.
sampleQueue: VideoSample[] = []; // Safari-specific thing, check usage. sampleQueue: VideoSample[] = []; // Safari-specific thing, check usage.
currentPacketIndex = 0;
raslSkipped = false; // For HEVC stuff
constructor( constructor(
onSample: (sample: VideoSample) => unknown, onSample: (sample: VideoSample) => unknown,
onError: (error: DOMException) => unknown, onError: (error: DOMException) => unknown,
codec: VideoCodec, public codec: VideoCodec,
decoderConfig: VideoDecoderConfig, public decoderConfig: VideoDecoderConfig,
public rotation: Rotation, public rotation: Rotation,
public timeResolution: number, public timeResolution: number,
) { ) {
@@ -848,6 +851,26 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
} }
decode(packet: EncodedPacket) { decode(packet: EncodedPacket) {
if (this.codec === 'hevc' && this.currentPacketIndex > 0 && !this.raslSkipped) {
// If we're using HEVC, we need to make sure to skip any RASL slices that follow a non-IDR key frame such as
// CRA_NUT. This is because RASL slices cannot be decoded without data before the CRA_NUT. Browsers behave
// differently here: Chromium drops the packets, Safari throws a decoder error. Either way, it's not good
// and causes bugs upstream. So, let's take the dropping into our own hands.
const nalUnits = extractHevcNalUnits(packet.data, this.decoderConfig);
const hasRaslPicture = nalUnits.some((x) => {
const type = extractNalUnitTypeForHevc(x);
return type === HevcNalUnitType.RASL_N || type === HevcNalUnitType.RASL_R;
});
if (hasRaslPicture) {
return; // Drop
}
this.raslSkipped = true;
}
this.currentPacketIndex++;
if (this.customDecoder) { if (this.customDecoder) {
this.customDecoderQueueSize++; this.customDecoderQueueSize++;
void this.customDecoderCallSerializer void this.customDecoderCallSerializer
@@ -879,6 +902,9 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
this.sampleQueue.length = 0; this.sampleQueue.length = 0;
} }
this.currentPacketIndex = 0;
this.raslSkipped = false;
} }
close() { close() {
+3 -3
View File
@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { OPUS_INTERNAL_SAMPLE_RATE } from '../codec'; import { OPUS_SAMPLE_RATE } from '../codec';
import { parseModesFromVorbisSetupPacket, parseOpusIdentificationHeader } from '../codec-data'; import { parseModesFromVorbisSetupPacket, parseOpusIdentificationHeader } from '../codec-data';
import { Demuxer } from '../demuxer'; import { Demuxer } from '../demuxer';
import { Input } from '../input'; import { Input } from '../input';
@@ -249,7 +249,7 @@ export class OggDemuxer extends Demuxer {
const header = parseOpusIdentificationHeader(firstPacket.data); const header = parseOpusIdentificationHeader(firstPacket.data);
bitstream.numberOfChannels = header.outputChannelCount; bitstream.numberOfChannels = header.outputChannelCount;
bitstream.sampleRate = header.inputSampleRate; bitstream.sampleRate = OPUS_SAMPLE_RATE; // Always the same
bitstream.codecInfo.opusInfo = { bitstream.codecInfo.opusInfo = {
preSkip: header.preSkip, preSkip: header.preSkip,
@@ -574,7 +574,7 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
constructor(public bitstream: LogicalBitstream, public demuxer: OggDemuxer) { constructor(public bitstream: LogicalBitstream, public demuxer: OggDemuxer) {
// Opus always uses a fixed sample rate for its internal calculations, even if the actual rate is different // Opus always uses a fixed sample rate for its internal calculations, even if the actual rate is different
this.internalSampleRate = bitstream.codecInfo.codec === 'opus' this.internalSampleRate = bitstream.codecInfo.codec === 'opus'
? OPUS_INTERNAL_SAMPLE_RATE ? OPUS_SAMPLE_RATE
: bitstream.sampleRate; : bitstream.sampleRate;
} }
+2 -2
View File
@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { OPUS_INTERNAL_SAMPLE_RATE, validateAudioChunkMetadata } from '../codec'; import { OPUS_SAMPLE_RATE, validateAudioChunkMetadata } from '../codec';
import { parseModesFromVorbisSetupPacket, parseOpusIdentificationHeader } from '../codec-data'; import { parseModesFromVorbisSetupPacket, parseOpusIdentificationHeader } from '../codec-data';
import { import {
assert, assert,
@@ -119,7 +119,7 @@ export class OggMuxer extends Muxer {
track, track,
serialNumber, serialNumber,
internalSampleRate: track.source._codec === 'opus' internalSampleRate: track.source._codec === 'opus'
? OPUS_INTERNAL_SAMPLE_RATE ? OPUS_SAMPLE_RATE
: meta.decoderConfig.sampleRate, : meta.decoderConfig.sampleRate,
codecInfo: { codecInfo: {
codec: track.source._codec, codec: track.source._codec,