Allow fractional frame rates in track metadata, document frameRate better, other small doc adjustments

This commit is contained in:
Vanilagy
2025-04-28 15:48:51 +02:00
parent cf19e0dcf1
commit e527fe1484
10 changed files with 113 additions and 41 deletions
+1 -1
View File
@@ -70,7 +70,7 @@ export abstract class InputTrack {
return this._backing.getId();
}
/** The ISO 639-2 language code for this track. If the language is unknown, this field is 'und' (undetermined). */
/** 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();
}
+7 -2
View File
@@ -2,7 +2,7 @@ import { Box, ftyp, IsobmffBoxWriter, mdat, mfra, moof, moov, vtta, vttc, vtte }
import { Muxer } from '../muxer';
import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from '../output';
import { BufferTargetWriter, Writer } from '../writer';
import { assert, last } from '../misc';
import { assert, computeRationalApproximation, last } from '../misc';
import { IsobmffOutputFormatOptions, IsobmffOutputFormat, MovOutputFormat } from '../output-format';
import { inlineTimestampRegex, SubtitleConfig, SubtitleCue, SubtitleMetadata } from '../subtitles';
import {
@@ -196,6 +196,11 @@ export class IsobmffMuxer extends Muxer {
assert(meta.decoderConfig.codedWidth !== undefined);
assert(meta.decoderConfig.codedHeight !== undefined);
// The frame rate set by the user may not be an integer. Since timescale is an integer, we'll approximate the
// frame time (inverse of frame rate) with a rational number, then use that approximation's denominator
// as the timescale.
const timescale = computeRationalApproximation(1 / (track.metadata.frameRate ?? 57600), 1e6).denominator;
const newTrackData: IsobmffVideoTrackData = {
track,
type: 'video',
@@ -204,7 +209,7 @@ export class IsobmffMuxer extends Muxer {
height: meta.decoderConfig.codedHeight,
decoderConfig: meta.decoderConfig,
},
timescale: track.metadata.frameRate ?? 57600,
timescale,
samples: [],
sampleQueue: [],
timestampProcessingQueue: [],
+11 -2
View File
@@ -7,6 +7,7 @@ import {
colorSpaceIsComplete,
normalizeRotation,
readBits,
roundToMultiple,
textEncoder,
toUint8Array,
writeBits,
@@ -510,8 +511,16 @@ export class MatroskaMuxer extends Muxer {
const trackData = this.getVideoTrackData(track, meta);
const isKeyFrame = packet.type === 'key';
const timestamp = this.validateAndNormalizeTimestamp(trackData.track, packet.timestamp, isKeyFrame);
const videoChunk = this.createInternalChunk(packet.data, timestamp, packet.duration, packet.type);
let timestamp = this.validateAndNormalizeTimestamp(trackData.track, packet.timestamp, isKeyFrame);
let duration = packet.duration;
if (track.metadata.frameRate !== undefined) {
// Constrain the time values to the frame rate
timestamp = roundToMultiple(timestamp, 1 / track.metadata.frameRate);
duration = roundToMultiple(duration, 1 / track.metadata.frameRate);
}
const videoChunk = this.createInternalChunk(packet.data, timestamp, duration, packet.type);
if (track.source._codec === 'vp9') this.fixVP9ColorSpace(trackData, videoChunk);
trackData.chunkQueue.push(videoChunk);
+44
View File
@@ -450,3 +450,47 @@ export const retriedFetch = async (
}
}
};
export const computeRationalApproximation = (x: number, maxDenominator: number) => {
// Handle negative numbers
const sign = x < 0 ? -1 : 1;
x = Math.abs(x);
let prevNumerator = 0, prevDenominator = 1;
let currNumerator = 1, currDenominator = 0;
// Continued fraction algorithm
let remainder = x;
while (true) {
const integer = Math.floor(remainder);
// Calculate next convergent
const nextNumerator = integer * currNumerator + prevNumerator;
const nextDenominator = integer * currDenominator + prevDenominator;
if (nextDenominator > maxDenominator) {
return {
numerator: sign * currNumerator,
denominator: currDenominator,
};
}
prevNumerator = currNumerator;
prevDenominator = currDenominator;
currNumerator = nextNumerator;
currDenominator = nextDenominator;
remainder = 1 / (remainder - integer);
// Guard against precision issues
if (!isFinite(remainder)) {
break;
}
}
return {
numerator: sign * currNumerator,
denominator: currDenominator,
};
};
+9 -5
View File
@@ -57,7 +57,7 @@ export type OutputSubtitleTrack = OutputTrack & { type: 'subtitle' };
* @public
*/
export type BaseTrackMetadata = {
/** The three-letter, ISO 639-2 language code specifying the language of this track. */
/** The three-letter, ISO 639-2/T language code specifying the language of this track. */
languageCode?: string;
};
@@ -68,7 +68,11 @@ export type BaseTrackMetadata = {
export type VideoTrackMetadata = BaseTrackMetadata & {
/** The angle in degrees by which the track's frames should be rotated (clockwise). */
rotation?: Rotation;
/** The expected video frame rate. You should not exceed the value you set here. */
/**
* The expected video frame rate in hertz. If set, all timestamps and durations of this track will be snapped to
* this frame rate. You should avoid adding more frames than the rate allows, as this will lead to multiple frames
* with the same timestamp.
*/
frameRate?: number;
};
/**
@@ -87,7 +91,7 @@ const validateBaseTrackMetadata = (metadata: BaseTrackMetadata) => {
throw new TypeError('metadata must be an object.');
}
if (metadata.languageCode !== undefined && !isIso639Dash2LanguageCode(metadata.languageCode)) {
throw new TypeError('metadata.languageCode must be a three-letter, ISO 639-2 language code.');
throw new TypeError('metadata.languageCode must be a three-letter, ISO 639-2/T language code.');
}
};
@@ -158,10 +162,10 @@ export class Output<
}
if (
metadata.frameRate !== undefined
&& (!Number.isInteger(metadata.frameRate) || metadata.frameRate <= 0)
&& (!Number.isFinite(metadata.frameRate) || metadata.frameRate <= 0)
) {
throw new TypeError(
`Invalid video frame rate: ${metadata.frameRate}. Must be a positive integer.`,
`Invalid video frame rate: ${metadata.frameRate}. Must be a positive number.`,
);
}