Merge branch 'main' into stop-retrying-on-cors-error

This commit is contained in:
David P.
2025-10-02 10:59:54 +02:00
committed by GitHub
7 changed files with 80 additions and 49 deletions
+17 -1
View File
@@ -166,6 +166,14 @@ export type ConversionVideoOptions = {
* VP9.
*/
alpha?: 'discard' | 'keep';
/**
* The interval, in seconds, of how often frames are encoded as a key frame. The default is 5 seconds. Frequent key
* frames improve seeking behavior but increase file size. When using multiple video tracks, you should give them
* all the same key frame interval.
*
* Setting this fields forces a transcode.
*/
keyFrameInterval?: number;
/** When `true`, video will always be re-encoded instead of directly copying over the encoded samples. */
forceTranscode?: boolean;
};
@@ -252,6 +260,12 @@ const validateVideoOptions = (videoOptions: ConversionVideoOptions | undefined)
if (videoOptions?.alpha !== undefined && !['discard', 'keep'].includes(videoOptions.alpha)) {
throw new TypeError('options.video.alpha, when provided, must be either \'discard\' or \'keep\'.');
}
if (
videoOptions?.keyFrameInterval !== undefined
&& (!Number.isFinite(videoOptions.keyFrameInterval) || videoOptions.keyFrameInterval < 0)
) {
throw new TypeError('config.keyFrameInterval, when provided, must be a non-negative number.');
}
};
const validateAudioOptions = (audioOptions: ConversionAudioOptions | undefined) => {
@@ -775,7 +789,8 @@ export class Conversion {
const needsTranscode = !!trackOptions.forceTranscode
|| this._startTimestamp > 0
|| firstTimestamp < 0
|| !!trackOptions.frameRate;
|| !!trackOptions.frameRate
|| trackOptions.keyFrameInterval !== undefined;
let needsRerender = width !== originalWidth
|| height !== originalHeight
|| (totalRotation !== 0 && !outputSupportsRotation)
@@ -858,6 +873,7 @@ export class Conversion {
const encodingConfig: VideoEncodingConfig = {
codec: encodableCodec,
bitrate,
keyFrameInterval: trackOptions.keyFrameInterval,
sizeChangeBehavior: trackOptions.fit ?? 'passThrough',
alpha,
onEncodedPacket: sample => this._reportProgress(track.id, sample.timestamp + sample.duration),
+18 -7
View File
@@ -539,14 +539,9 @@ export class IsobmffDemuxer extends Demuxer {
// lookup starts sequentially from the start, incrementally summing up all fragment durations. It's sort
// of implicit, but it ends up working nicely.
const lookupEntryIndex = binarySearchExact(
track.fragmentLookupTable,
fragment.moofOffset,
x => x.moofOffset,
);
if (lookupEntryIndex !== -1) {
const lookupEntry = track.fragmentLookupTable.find(x => x.moofOffset === fragment.moofOffset);
if (lookupEntry) {
// There's a lookup entry, let's use its timestamp
const lookupEntry = track.fragmentLookupTable[lookupEntryIndex]!;
offsetFragmentTrackDataByTimestamp(trackData, lookupEntry.timestamp);
} else {
const lastCacheIndex = binarySearchLessOrEqual(
@@ -1742,6 +1737,20 @@ export class IsobmffDemuxer extends Demuxer {
moofOffset,
});
}
// Sort by timestamp in case it's not naturally sorted
track.fragmentLookupTable.sort((a, b) => a.timestamp - b.timestamp);
// Remove multiple entries for the same time
for (let i = 0; i < track.fragmentLookupTable.length - 1; i++) {
const entry1 = track.fragmentLookupTable[i]!;
const entry2 = track.fragmentLookupTable[i + 1]!;
if (entry1.timestamp === entry2.timestamp) {
track.fragmentLookupTable.splice(i + 1, 1);
i--;
}
}
}; break;
case 'moof': {
@@ -2747,6 +2756,8 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
// The lookup table entry lied to us! We found a lookup entry but no fragment there that satisfied
// the match. In this case, let's search again but using the lookup entry before that.
const previousLookupEntry = this.internalTrack.fragmentLookupTable[lookupEntryIndex - 1];
assert(!previousLookupEntry || previousLookupEntry.timestamp < lookupEntry.timestamp);
const newSearchTimestamp = previousLookupEntry?.timestamp ?? -Infinity;
return this.performFragmentedLookup(
null,
+37 -39
View File
@@ -541,53 +541,49 @@ export class MatroskaDemuxer extends Demuxer {
// Put default tracks first
this.currentSegment.tracks.sort((a, b) => Number(b.isDefault) - Number(a.isDefault));
// Sort cue points by cluster position (required for the next algorithm)
this.currentSegment.cuePoints.sort((a, b) => a.clusterPosition - b.clusterPosition);
// Now, let's distribute the cue points to each track. Ideally, each track has their own cue point, but some
// Matroska files may only specify cue points for a single track. In this case, we still wanna use those cue
// points for all tracks.
const allTrackIds = this.currentSegment.tracks.map(x => x.id);
const remainingTrackIds = new Set<number>();
let lastClusterPosition: number | null = null;
let lastCuePoint: CuePoint | null = null;
// Now, let's distribute the cue points to the tracks
const idToTrack = new Map(this.currentSegment.tracks.map(x => [x.id, x]));
// Assign cue points to their respective tracks
for (const cuePoint of this.currentSegment.cuePoints) {
if (cuePoint.clusterPosition !== lastClusterPosition) {
for (const id of remainingTrackIds) {
// These tracks didn't receive a cue point for the last cluster, so let's give them one
assert(lastCuePoint);
const track = this.currentSegment.tracks.find(x => x.id === id)!;
track.cuePoints.push(lastCuePoint);
}
for (const id of allTrackIds) {
remainingTrackIds.add(id);
}
const track = idToTrack.get(cuePoint.trackId);
if (track) {
track.cuePoints.push(cuePoint);
}
lastCuePoint = cuePoint;
if (!remainingTrackIds.has(cuePoint.trackId)) {
continue;
}
const track = this.currentSegment.tracks.find(x => x.id === cuePoint.trackId)!;
track.cuePoints.push(cuePoint);
remainingTrackIds.delete(cuePoint.trackId);
lastClusterPosition = cuePoint.clusterPosition;
}
for (const id of remainingTrackIds) {
assert(lastCuePoint);
const track = this.currentSegment.tracks.find(x => x.id === id)!;
track.cuePoints.push(lastCuePoint);
}
for (const track of this.currentSegment.tracks) {
// Sort cue points by time
track.cuePoints.sort((a, b) => a.time - b.time);
// Remove multiple cue points for the same time
for (let i = 0; i < track.cuePoints.length - 1; i++) {
const cuePoint1 = track.cuePoints[i]!;
const cuePoint2 = track.cuePoints[i + 1]!;
if (cuePoint1.time === cuePoint2.time) {
track.cuePoints.splice(i + 1, 1);
i--;
}
}
}
let trackWithMostCuePoints: InternalTrack | null = null;
let maxCuePointCount = -Infinity;
for (const track of this.currentSegment.tracks) {
if (track.cuePoints.length > maxCuePointCount) {
maxCuePointCount = track.cuePoints.length;
trackWithMostCuePoints = track;
}
}
// For every track that has received 0 cue points (can happen, often only the video track receives cue points),
// we still want to have better seeking. Therefore, let's give it the cue points of the track with the most cue
// points, which should provide us with the most fine-grained seeking.
for (const track of this.currentSegment.tracks) {
if (track.cuePoints.length === 0) {
track.cuePoints = trackWithMostCuePoints!.cuePoints;
}
}
this.currentSegment = null;
@@ -2239,6 +2235,8 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
// The cue point lied to us! We found a cue point but no cluster there that satisfied the match. In this
// case, let's search again but using the cue point before that.
const previousCuePoint = this.internalTrack.cuePoints[cuePointIndex - 1];
assert(!previousCuePoint || previousCuePoint.time < cuePoint.time);
const newSearchTimestamp = previousCuePoint?.time ?? -Infinity;
return this.performClusterLookup(null, getMatchInCluster, newSearchTimestamp, latestTimestamp, options);
}