mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-03 22:03:49 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f06631355 | ||
|
|
f314f04037 | ||
|
|
b9f7ab2fa2 | ||
|
|
a295cd76c6 | ||
|
|
b9efcc867d | ||
|
|
d63d7f6ff9 | ||
|
|
b3396727f0 | ||
|
|
e672a51dc6 | ||
|
|
9f9809fbd1 | ||
|
|
dcda90fddb | ||
|
|
d7e8273185 |
+6
-3
@@ -24,7 +24,7 @@
|
||||
chunked: true,
|
||||
chunkSize: 2**20
|
||||
});
|
||||
const outputFormat = new Mediabunny.Mp4OutputFormat({});
|
||||
const outputFormat = new Mediabunny.WavOutputFormat({});
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.textContent = 'Cancel';
|
||||
@@ -72,6 +72,9 @@
|
||||
}),
|
||||
output,
|
||||
audio: {
|
||||
codec: 'pcm-s16',
|
||||
//sampleRate: 16000,
|
||||
//numberOfChannels: 1,
|
||||
//discard: true,
|
||||
//codec: 'opus',
|
||||
//bitrate: 128000,
|
||||
@@ -106,7 +109,7 @@
|
||||
*/
|
||||
video: () => ({
|
||||
//discard: true,
|
||||
forceTranscode: true,
|
||||
//forceTranscode: true,
|
||||
//codec: 'avc',
|
||||
//fit: 'contain',
|
||||
//frameRate: 27.123,
|
||||
@@ -129,7 +132,7 @@
|
||||
//height: 100,
|
||||
}),
|
||||
trim: {
|
||||
start: 1,
|
||||
start: 0,
|
||||
end: 10
|
||||
},
|
||||
});
|
||||
|
||||
+13
-3
@@ -14,10 +14,20 @@
|
||||
source: new Mediabunny.BlobSource(file),
|
||||
});
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
const sink = new Mediabunny.VideoSampleSink(videoTrack);
|
||||
const audioTrack = await input.getPrimaryAudioTrack();
|
||||
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)) {
|
||||
|
||||
@@ -60,6 +60,12 @@ await input.computeDuration(); // => 1905.4615
|
||||
```
|
||||
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
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
Subtitle tracks are currently not supported for reading.
|
||||
:::
|
||||
|
||||
@@ -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/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/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/58149663', name: 'H7GhosT', url: 'https://github.com/H7GhosT' },
|
||||
{ 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
|
||||
= 'absolute bottom-0 right-0 bg-black/30 text-white px-1 py-0.5 text-[11px] rounded-tl-lg';
|
||||
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++;
|
||||
|
||||
Generated
+6
-6
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"version": "1.14.2",
|
||||
"version": "1.14.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mediabunny",
|
||||
"version": "1.14.2",
|
||||
"version": "1.14.4",
|
||||
"license": "MPL-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
@@ -7749,9 +7749,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mediabunny": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.14.1.tgz",
|
||||
"integrity": "sha512-TjLg8GQGGsnGePcA6i0NItGe9a5G8WaleCGaXpb5V5okwuK5KpNKfcmTZXItnjfPLo7FvfEZI0NFp1lIR8Os7Q==",
|
||||
"version": "1.14.3",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.14.3.tgz",
|
||||
"integrity": "sha512-kCvieRo6X1QDcdWLjn7o2BY/VCDeyU9nNGBVjOIOiWPoTtIekHR+viKAYaZafLEm0poBv+O2PwttL37PaSo/kA==",
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"workspaces": [
|
||||
@@ -12242,7 +12242,7 @@
|
||||
},
|
||||
"packages/mp3-encoder": {
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"version": "1.14.2",
|
||||
"version": "1.14.4",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"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.",
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.14.2",
|
||||
"version": "1.14.4",
|
||||
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
|
||||
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
|
||||
+67
-50
@@ -26,8 +26,27 @@ import { EncodedPacket, PacketType } from './packet';
|
||||
// Rec. ITU-T H.265
|
||||
// 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. */
|
||||
const findNalUnitsInAnnexB = (packetData: Uint8Array) => {
|
||||
export const findNalUnitsInAnnexB = (packetData: Uint8Array) => {
|
||||
const nalUnits: Uint8Array[] = [];
|
||||
let i = 0;
|
||||
|
||||
@@ -184,6 +203,21 @@ export type AvcDecoderConfigurationRecord = {
|
||||
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) => {
|
||||
return data[0]! & 0x1F;
|
||||
};
|
||||
@@ -193,9 +227,9 @@ export const extractAvcDecoderConfigurationRecord = (packetData: Uint8Array) =>
|
||||
try {
|
||||
const nalUnits = findNalUnitsInAnnexB(packetData);
|
||||
|
||||
const spsUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === 7);
|
||||
const ppsUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === 8);
|
||||
const spsExtUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === 13);
|
||||
const spsUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === AvcNalUnitType.SPS);
|
||||
const ppsUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === AvcNalUnitType.PPS);
|
||||
const spsExtUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === AvcNalUnitType.SPS_EXT);
|
||||
|
||||
if (spsUnits.length === 0) {
|
||||
return null;
|
||||
@@ -337,12 +371,6 @@ export const serializeAvcDecoderConfigurationRecord = (record: AvcDecoderConfigu
|
||||
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
|
||||
export type HevcDecoderConfigurationRecord = {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -380,12 +423,12 @@ export const extractHevcDecoderConfigurationRecord = (
|
||||
try {
|
||||
const nalUnits = findNalUnitsInAnnexB(packetData);
|
||||
|
||||
const vpsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === NALU_TYPE_VPS);
|
||||
const spsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === NALU_TYPE_SPS);
|
||||
const ppsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === NALU_TYPE_PPS);
|
||||
const vpsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === HevcNalUnitType.VPS_NUT);
|
||||
const spsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === HevcNalUnitType.SPS_NUT);
|
||||
const ppsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === HevcNalUnitType.PPS_NUT);
|
||||
const seiUnits = nalUnits.filter(
|
||||
unit => extractNalUnitTypeForHevc(unit) === NALU_TYPE_SEI_PREFIX
|
||||
|| extractNalUnitTypeForHevc(unit) === NALU_TYPE_SEI_SUFFIX,
|
||||
unit => extractNalUnitTypeForHevc(unit) === HevcNalUnitType.PREFIX_SEI_NUT
|
||||
|| extractNalUnitTypeForHevc(unit) === HevcNalUnitType.SUFFIX_SEI_NUT,
|
||||
);
|
||||
|
||||
if (spsUnits.length === 0 || ppsUnits.length === 0) return null;
|
||||
@@ -521,7 +564,7 @@ export const extractHevcDecoderConfigurationRecord = (
|
||||
? [
|
||||
{
|
||||
arrayCompleteness: 1,
|
||||
nalUnitType: NALU_TYPE_VPS,
|
||||
nalUnitType: HevcNalUnitType.VPS_NUT,
|
||||
nalUnits: vpsUnits,
|
||||
},
|
||||
]
|
||||
@@ -530,7 +573,7 @@ export const extractHevcDecoderConfigurationRecord = (
|
||||
? [
|
||||
{
|
||||
arrayCompleteness: 1,
|
||||
nalUnitType: NALU_TYPE_SPS,
|
||||
nalUnitType: HevcNalUnitType.SPS_NUT,
|
||||
nalUnits: spsUnits,
|
||||
},
|
||||
]
|
||||
@@ -539,7 +582,7 @@ export const extractHevcDecoderConfigurationRecord = (
|
||||
? [
|
||||
{
|
||||
arrayCompleteness: 1,
|
||||
nalUnitType: NALU_TYPE_PPS,
|
||||
nalUnitType: HevcNalUnitType.PPS_NUT,
|
||||
nalUnits: ppsUnits,
|
||||
},
|
||||
]
|
||||
@@ -1440,22 +1483,9 @@ export const determineVideoPacketType = async (
|
||||
const decoderConfig = await videoTrack.getDecoderConfig();
|
||||
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';
|
||||
};
|
||||
|
||||
@@ -1463,25 +1493,12 @@ export const determineVideoPacketType = async (
|
||||
const decoderConfig = await videoTrack.getDecoderConfig();
|
||||
assert(decoderConfig);
|
||||
|
||||
let nalUnits: Uint8Array[];
|
||||
|
||||
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 nalUnits = extractHevcNalUnits(packet.data, decoderConfig);
|
||||
const isKeyframe = nalUnits.some((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';
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -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)?$/;
|
||||
|
||||
|
||||
+17
-17
@@ -1125,8 +1125,6 @@ export class Conversion {
|
||||
await this._started;
|
||||
|
||||
const resampler = new AudioResampler({
|
||||
sourceNumberOfChannels: track.numberOfChannels,
|
||||
sourceSampleRate: track.sampleRate,
|
||||
targetNumberOfChannels,
|
||||
targetSampleRate,
|
||||
startTime: this._startTimestamp,
|
||||
@@ -1246,9 +1244,9 @@ class TrackSynchronizer {
|
||||
* OfflineAudioContext.
|
||||
*/
|
||||
export class AudioResampler {
|
||||
sourceSampleRate: number;
|
||||
sourceSampleRate: number | null = null;
|
||||
targetSampleRate: number;
|
||||
sourceNumberOfChannels: number;
|
||||
sourceNumberOfChannels: number | null = null;
|
||||
targetNumberOfChannels: number;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
@@ -1262,20 +1260,16 @@ export class AudioResampler {
|
||||
/** The highest index written to in the current buffer */
|
||||
maxWrittenFrame: number;
|
||||
channelMixer!: (sourceData: Float32Array, sourceFrameIndex: number, targetChannelIndex: number) => number;
|
||||
tempSourceBuffer: Float32Array;
|
||||
tempSourceBuffer!: Float32Array;
|
||||
|
||||
constructor(options: {
|
||||
sourceSampleRate: number;
|
||||
targetSampleRate: number;
|
||||
sourceNumberOfChannels: number;
|
||||
targetNumberOfChannels: number;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
onSample: (sample: AudioSample) => Promise<void>;
|
||||
}) {
|
||||
this.sourceSampleRate = options.sourceSampleRate;
|
||||
this.targetSampleRate = options.targetSampleRate;
|
||||
this.sourceNumberOfChannels = options.sourceNumberOfChannels;
|
||||
this.targetNumberOfChannels = options.targetNumberOfChannels;
|
||||
this.startTime = options.startTime;
|
||||
this.endTime = options.endTime;
|
||||
@@ -1287,17 +1281,14 @@ export class AudioResampler {
|
||||
this.outputBuffer = new Float32Array(this.bufferSizeInSamples);
|
||||
this.bufferStartFrame = 0;
|
||||
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.
|
||||
*/
|
||||
setupChannelMixer(): void {
|
||||
doChannelMixerSetup(): void {
|
||||
assert(this.sourceNumberOfChannels !== null);
|
||||
|
||||
const sourceNum = this.sourceNumberOfChannels;
|
||||
const targetNum = this.targetNumberOfChannels;
|
||||
|
||||
@@ -1415,8 +1406,17 @@ export class AudioResampler {
|
||||
}
|
||||
|
||||
async add(audioSample: AudioSample) {
|
||||
if (!audioSample || audioSample._closed) {
|
||||
return;
|
||||
if (this.sourceSampleRate === null) {
|
||||
// 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;
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
extractAudioCodecString,
|
||||
extractVideoCodecString,
|
||||
MediaCodec,
|
||||
OPUS_SAMPLE_RATE,
|
||||
parseAacAudioSpecificConfig,
|
||||
parsePcmCodec,
|
||||
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.sampleRate = sampleRate;
|
||||
|
||||
@@ -1421,7 +1426,7 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
|
||||
track.info.codecDescription = description;
|
||||
track.info.numberOfChannels = outputChannelCount;
|
||||
track.info.sampleRate = inputSampleRate;
|
||||
// Don't copy the input sample rate, irrelevant, and output sample rate is fixed
|
||||
}; break;
|
||||
|
||||
case 'dfLa': { // Used for FLAC audio
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
extractAudioCodecString,
|
||||
extractVideoCodecString,
|
||||
MediaCodec,
|
||||
OPUS_SAMPLE_RATE,
|
||||
VideoCodec,
|
||||
} from '../codec';
|
||||
import { Demuxer } from '../demuxer';
|
||||
@@ -988,6 +989,7 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.opus) {
|
||||
this.currentTrack.info.codec = 'opus';
|
||||
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
|
||||
this.currentTrack.info.sampleRate = OPUS_SAMPLE_RATE; // Always the same
|
||||
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.vorbis) {
|
||||
this.currentTrack.info.codec = 'vorbis';
|
||||
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
|
||||
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
parseSubtitleTimestamp,
|
||||
} from '../subtitles';
|
||||
import {
|
||||
OPUS_INTERNAL_SAMPLE_RATE,
|
||||
OPUS_SAMPLE_RATE,
|
||||
PCM_AUDIO_CODECS,
|
||||
PcmAudioCodec,
|
||||
SubtitleCodec,
|
||||
@@ -293,7 +293,7 @@ export class MatroskaMuxer extends Muxer {
|
||||
const header = parseOpusIdentificationHeader(bytes);
|
||||
|
||||
// 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
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
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 { InputAudioTrack, InputTrack, InputVideoTrack } from './input-track';
|
||||
import {
|
||||
@@ -624,8 +625,8 @@ export abstract class BaseMediaSampleSink<
|
||||
const nextPacket = await packetSink.getNextPacket(currentPacket);
|
||||
assert(nextPacket);
|
||||
|
||||
currentPacket = nextPacket;
|
||||
decoder.decode(nextPacket);
|
||||
currentPacket = nextPacket;
|
||||
}
|
||||
|
||||
maxSequenceNumber = -1;
|
||||
@@ -757,12 +758,14 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
|
||||
|
||||
inputTimestamps: number[] = []; // Timestamps input into the decoder, sorted.
|
||||
sampleQueue: VideoSample[] = []; // Safari-specific thing, check usage.
|
||||
currentPacketIndex = 0;
|
||||
raslSkipped = false; // For HEVC stuff
|
||||
|
||||
constructor(
|
||||
onSample: (sample: VideoSample) => unknown,
|
||||
onError: (error: DOMException) => unknown,
|
||||
codec: VideoCodec,
|
||||
decoderConfig: VideoDecoderConfig,
|
||||
public codec: VideoCodec,
|
||||
public decoderConfig: VideoDecoderConfig,
|
||||
public rotation: Rotation,
|
||||
public timeResolution: number,
|
||||
) {
|
||||
@@ -848,6 +851,26 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
|
||||
}
|
||||
|
||||
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) {
|
||||
this.customDecoderQueueSize++;
|
||||
void this.customDecoderCallSerializer
|
||||
@@ -879,6 +902,9 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
|
||||
|
||||
this.sampleQueue.length = 0;
|
||||
}
|
||||
|
||||
this.currentPacketIndex = 0;
|
||||
this.raslSkipped = false;
|
||||
}
|
||||
|
||||
close() {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* 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 { Demuxer } from '../demuxer';
|
||||
import { Input } from '../input';
|
||||
@@ -249,7 +249,7 @@ export class OggDemuxer extends Demuxer {
|
||||
|
||||
const header = parseOpusIdentificationHeader(firstPacket.data);
|
||||
bitstream.numberOfChannels = header.outputChannelCount;
|
||||
bitstream.sampleRate = header.inputSampleRate;
|
||||
bitstream.sampleRate = OPUS_SAMPLE_RATE; // Always the same
|
||||
|
||||
bitstream.codecInfo.opusInfo = {
|
||||
preSkip: header.preSkip,
|
||||
@@ -574,7 +574,7 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
||||
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
|
||||
this.internalSampleRate = bitstream.codecInfo.codec === 'opus'
|
||||
? OPUS_INTERNAL_SAMPLE_RATE
|
||||
? OPUS_SAMPLE_RATE
|
||||
: bitstream.sampleRate;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* 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 {
|
||||
assert,
|
||||
@@ -119,7 +119,7 @@ export class OggMuxer extends Muxer {
|
||||
track,
|
||||
serialNumber,
|
||||
internalSampleRate: track.source._codec === 'opus'
|
||||
? OPUS_INTERNAL_SAMPLE_RATE
|
||||
? OPUS_SAMPLE_RATE
|
||||
: meta.decoderConfig.sampleRate,
|
||||
codecInfo: {
|
||||
codec: track.source._codec,
|
||||
|
||||
Reference in New Issue
Block a user