mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 10:53:50 +02:00
Merge branch 'main' into stop-retrying-on-cors-error
This commit is contained in:
@@ -2,8 +2,9 @@ name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
||||
@@ -2,8 +2,9 @@ name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
@@ -124,6 +124,7 @@ type ConversionVideoOptions = {
|
||||
codec?: VideoCodec;
|
||||
bitrate?: number | Quality;
|
||||
alpha?: 'discard' | 'keep'; // Defaults to 'discard'
|
||||
keyFrameInterval?: number;
|
||||
forceTranscode?: boolean;
|
||||
};
|
||||
```
|
||||
@@ -180,6 +181,8 @@ Use the `codec` property to control the codec of the output track. This should b
|
||||
|
||||
Use the `bitrate` property to control the bitrate of the output video. For example, you can use this field to compress the video track. Accepted values are the number of bits per second or a [subjective quality](./media-sources#subjective-qualities). If this property is set, transcoding will always happen. If this property is not set but transcoding is still required, `QUALITY_HIGH` will be used as the value.
|
||||
|
||||
Use the `keyFrameInterval` property to control the maximum interval in seconds between key frames in the output video. Setting this fields forces a transcode.
|
||||
|
||||
If you want to prevent direct copying of media data and force a transcoding step, use `forceTranscode: true`.
|
||||
|
||||
## Audio options
|
||||
|
||||
@@ -109,6 +109,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/2698271', name: 'Matthew Gardner', url: 'https://github.com/spheric' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/5475819', name: 'AJ Funk', url: 'https://github.com/AJFunk' },
|
||||
{ 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/1836701', name: 'Bean Deng', url: 'https://github.com/HADB' },
|
||||
|
||||
+17
-1
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user