mediabunny
Version:
Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.
260 lines (259 loc) • 9.63 kB
JavaScript
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { determineVideoPacketType } from './codec-data.js';
import { customAudioDecoders, customVideoDecoders } from './custom-coder.js';
import { EncodedPacketSink } from './media-sink.js';
import { assert } from './misc.js';
import { EncodedPacket } from './packet.js';
/**
* Represents a media track in an input file.
* @public
*/
export class InputTrack {
/** @internal */
constructor(backing) {
this._backing = backing;
}
/** Returns true iff this track is a video track. */
isVideoTrack() {
return this instanceof InputVideoTrack;
}
/** Returns true iff this track is an audio track. */
isAudioTrack() {
return this instanceof InputAudioTrack;
}
/** The unique ID of this track in the input file. */
get id() {
return this._backing.getId();
}
/** The ISO 639-2/T language code for this track. If the language is unknown, this field is 'und' (undetermined). */
get languageCode() {
return this._backing.getLanguageCode();
}
/**
* A positive number x such that all timestamps and durations of all packets of this track are
* integer multiples of 1/x.
*/
get timeResolution() {
return this._backing.getTimeResolution();
}
/**
* Returns the start timestamp of the first packet of this track, in seconds. While often near zero, this value
* may be positive or even negative. A negative starting timestamp means the track's timing has been offset. Samples
* with a negative timestamp should not be presented.
*/
getFirstTimestamp() {
return this._backing.getFirstTimestamp();
}
/** Returns the end timestamp of the last packet of this track, in seconds. */
computeDuration() {
return this._backing.computeDuration();
}
/**
* Computes aggregate packet statistics for this track, such as average packet rate or bitrate.
*
* @param targetPacketCount - This optional parameter sets a target for how many packets this method must have
* looked at before it can return early; this means, you can use it to aggregate only a subset (prefix) of all
* packets. This is very useful for getting a great estimate of video frame rate without having to scan through the
* entire file.
*/
async computePacketStats(targetPacketCount = Infinity) {
const sink = new EncodedPacketSink(this);
let startTimestamp = Infinity;
let endTimestamp = -Infinity;
let packetCount = 0;
let totalPacketBytes = 0;
for await (const packet of sink.packets(undefined, undefined, { metadataOnly: true })) {
if (packetCount >= targetPacketCount
// This additional condition is needed to produce correct results with out-of-presentation-order packets
&& packet.timestamp >= endTimestamp) {
break;
}
startTimestamp = Math.min(startTimestamp, packet.timestamp);
endTimestamp = Math.max(endTimestamp, packet.timestamp + packet.duration);
packetCount++;
totalPacketBytes += packet.byteLength;
}
return {
packetCount,
averagePacketRate: packetCount
? Number((packetCount / (endTimestamp - startTimestamp)).toPrecision(16))
: 0,
averageBitrate: packetCount
? Number((8 * totalPacketBytes / (endTimestamp - startTimestamp)).toPrecision(16))
: 0,
};
}
}
/**
* Represents a video track in an input file.
* @public
*/
export class InputVideoTrack extends InputTrack {
/** @internal */
constructor(backing) {
super(backing);
this._backing = backing;
}
get type() {
return 'video';
}
get codec() {
return this._backing.getCodec();
}
/** The width in pixels of the track's coded samples, before any transformations or rotations. */
get codedWidth() {
return this._backing.getCodedWidth();
}
/** The height in pixels of the track's coded samples, before any transformations or rotations. */
get codedHeight() {
return this._backing.getCodedHeight();
}
/** The angle in degrees by which the track's frames should be rotated (clockwise). */
get rotation() {
return this._backing.getRotation();
}
/** The width in pixels of the track's frames after rotation. */
get displayWidth() {
const rotation = this._backing.getRotation();
return rotation % 180 === 0 ? this._backing.getCodedWidth() : this._backing.getCodedHeight();
}
/** The height in pixels of the track's frames after rotation. */
get displayHeight() {
const rotation = this._backing.getRotation();
return rotation % 180 === 0 ? this._backing.getCodedHeight() : this._backing.getCodedWidth();
}
/** Returns the color space of the track's samples. */
getColorSpace() {
return this._backing.getColorSpace();
}
/** If this method returns true, the track's samples use a high dynamic range (HDR). */
async hasHighDynamicRange() {
const colorSpace = await this._backing.getColorSpace();
return colorSpace.primaries === 'bt2020' || colorSpace.primaries === 'smpte432'
|| colorSpace.transfer === 'pg' || colorSpace.transfer === 'hlg'
|| colorSpace.matrix === 'bt2020-ncl';
}
/**
* Returns the decoder configuration for decoding the track's packets using a VideoDecoder. Returns null if the
* track's codec is unknown.
*/
getDecoderConfig() {
return this._backing.getDecoderConfig();
}
async getCodecParameterString() {
const decoderConfig = await this._backing.getDecoderConfig();
return decoderConfig?.codec ?? null;
}
async canDecode() {
try {
const decoderConfig = await this._backing.getDecoderConfig();
if (!decoderConfig) {
return false;
}
const codec = this._backing.getCodec();
assert(codec !== null);
if (customVideoDecoders.some(x => x.supports(codec, decoderConfig))) {
return true;
}
if (typeof VideoDecoder === 'undefined') {
return false;
}
const support = await VideoDecoder.isConfigSupported(decoderConfig);
return support.supported === true;
}
catch (error) {
console.error('Error during decodability check:', error);
return false;
}
}
async determinePacketType(packet) {
if (!(packet instanceof EncodedPacket)) {
throw new TypeError('packet must be an EncodedPacket.');
}
if (packet.isMetadataOnly) {
throw new TypeError('packet must not be metadata-only to determine its type.');
}
if (this.codec === null) {
return null;
}
return determineVideoPacketType(this, packet);
}
}
/**
* Represents an audio track in an input file.
* @public
*/
export class InputAudioTrack extends InputTrack {
/** @internal */
constructor(backing) {
super(backing);
this._backing = backing;
}
get type() {
return 'audio';
}
get codec() {
return this._backing.getCodec();
}
/** The number of audio channels in the track. */
get numberOfChannels() {
return this._backing.getNumberOfChannels();
}
/** The track's audio sample rate in hertz. */
get sampleRate() {
return this._backing.getSampleRate();
}
/**
* Returns the decoder configuration for decoding the track's packets using an AudioDecoder. Returns null if the
* track's codec is unknown.
*/
getDecoderConfig() {
return this._backing.getDecoderConfig();
}
async getCodecParameterString() {
const decoderConfig = await this._backing.getDecoderConfig();
return decoderConfig?.codec ?? null;
}
async canDecode() {
try {
const decoderConfig = await this._backing.getDecoderConfig();
if (!decoderConfig) {
return false;
}
const codec = this._backing.getCodec();
assert(codec !== null);
if (customAudioDecoders.some(x => x.supports(codec, decoderConfig))) {
return true;
}
if (decoderConfig.codec.startsWith('pcm-')) {
return true; // Since we decode it ourselves
}
else {
if (typeof AudioDecoder === 'undefined') {
return false;
}
const support = await AudioDecoder.isConfigSupported(decoderConfig);
return support.supported === true;
}
}
catch (error) {
console.error('Error during decodability check:', error);
return false;
}
}
async determinePacketType(packet) {
if (!(packet instanceof EncodedPacket)) {
throw new TypeError('packet must be an EncodedPacket.');
}
if (this.codec === null) {
return null;
}
return 'key'; // No audio codec with delta packets
}
}