mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 10:53:50 +02:00
Add ConversionOptions.tracks, add better converstion start timestamp computation, optimize endPacket logic, utilize metadata duration for progress callback for better performance
This commit is contained in:
+8
-3
@@ -60,11 +60,12 @@
|
||||
|
||||
if (true) {
|
||||
input = new Mediabunny.Input({
|
||||
entryPath: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8',
|
||||
entryPath: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8',
|
||||
source: ({ path }) => new Mediabunny.UrlSource(path),
|
||||
formats: Mediabunny.ALL_FORMATS,
|
||||
});
|
||||
|
||||
/*
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
const audioTrack = await input.getPrimaryAudioTrack();
|
||||
|
||||
@@ -75,6 +76,7 @@
|
||||
videoTrack.computeDuration({ skipLiveWait: true }),
|
||||
audioTrack.computeDuration({ skipLiveWait: true }),
|
||||
]));
|
||||
*/
|
||||
} else {
|
||||
const file = fileInput.files[0];
|
||||
const source = new Mediabunny.BlobSource(file);
|
||||
@@ -90,7 +92,8 @@
|
||||
input,
|
||||
output,
|
||||
audio: (track, n) => ({
|
||||
discard: !tracks.includes(track),
|
||||
//discard: true,
|
||||
//discard: !tracks.includes(track),
|
||||
//codec: 'eac3',
|
||||
//discard: true,
|
||||
//discard: n > 1,
|
||||
@@ -137,7 +140,8 @@
|
||||
},
|
||||
*/
|
||||
video: (track) => ({
|
||||
discard: !tracks.includes(track),
|
||||
//discard: true,
|
||||
//discard: !tracks.includes(track),
|
||||
//forceTranscode: true,
|
||||
//forceTranscode: true,
|
||||
//width: 1280,
|
||||
@@ -222,6 +226,7 @@
|
||||
}
|
||||
},
|
||||
trim: {
|
||||
end: 10,
|
||||
//start,
|
||||
//end: start + 5,
|
||||
////start: 0,
|
||||
|
||||
+11
-2
@@ -11,6 +11,7 @@
|
||||
document.body.append(fileInput);
|
||||
|
||||
fileInput.addEventListener('change', async () => {
|
||||
/*
|
||||
const file = fileInput.files[0];
|
||||
const input = new Mediabunny.Input({
|
||||
formats: Mediabunny.ALL_FORMATS,
|
||||
@@ -25,14 +26,22 @@
|
||||
|
||||
//console.log(await input.getDurationFromMetadata(), await input.computeDuration());
|
||||
return;
|
||||
*/
|
||||
|
||||
const manifest = new Mediabunny.Input({
|
||||
entryPath: 'https://playertest.longtailvideo.com/adaptive/alt-audio-no-video/sintel/playlist.m3u8',
|
||||
entryPath: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8',
|
||||
source: ({ path }) => new Mediabunny.UrlSource(path),
|
||||
formats: Mediabunny.ALL_FORMATS,
|
||||
});
|
||||
|
||||
const audioTrack = await manifest.getPrimaryAudioTrack();
|
||||
const ugh = new Mediabunny.EncodedPacketSink(audioTrack);
|
||||
|
||||
console.log(await manifest.getTracks());
|
||||
for await (const packet of ugh.packets()) {
|
||||
console.log(packet);
|
||||
}
|
||||
|
||||
//console.log(await manifest.getTracks());
|
||||
return;
|
||||
|
||||
const videoTrack = await manifest.getPrimaryVideoTrack();
|
||||
|
||||
+107
-25
@@ -38,6 +38,7 @@ import {
|
||||
} from './media-source';
|
||||
import {
|
||||
assert,
|
||||
assertNever,
|
||||
ceilToMultipleOfTwo,
|
||||
clamp,
|
||||
floorToDivisor,
|
||||
@@ -65,6 +66,17 @@ export type ConversionOptions = {
|
||||
/** The output file. */
|
||||
output: Output;
|
||||
|
||||
/**
|
||||
* Defines which input tracks are used for conversion. Defaults to `'all'` unless the input is an HLS input, in
|
||||
* which case it defaults to `'primary'`.
|
||||
*
|
||||
* - `'all'`: All input tracks are eligible for conversion.
|
||||
* - `'primary'`: Only the primary video and audio track from the input are eligible for conversion.
|
||||
* - Array: A user-specified list of tracks that are eligible for conversion. These must all belong to the
|
||||
* {@link Input} specified in {@link ConversionOptions.input}.
|
||||
*/
|
||||
tracks?: 'all' | 'primary' | InputTrack[];
|
||||
|
||||
/**
|
||||
* Video-specific options. When passing an object, the same options are applied to all video tracks. When passing a
|
||||
* function, it will be invoked for each video track and is expected to return or resolve to the options
|
||||
@@ -547,6 +559,21 @@ export class Conversion {
|
||||
if (!(options.output instanceof Output)) {
|
||||
throw new TypeError('options.output must be an Output.');
|
||||
}
|
||||
if (
|
||||
options.tracks !== undefined
|
||||
&& !(
|
||||
options.tracks === 'all'
|
||||
|| options.tracks === 'primary'
|
||||
|| (Array.isArray(options.tracks) && options.tracks.every(x => x instanceof InputTrack))
|
||||
)
|
||||
) {
|
||||
throw new TypeError(
|
||||
'options.tracks, when provded, must be either \'all\', \'primary\', or an array of InputTrack.',
|
||||
);
|
||||
}
|
||||
if (Array.isArray(options.tracks) && !options.tracks.every(x => x.input === options.input)) {
|
||||
throw new TypeError('Currently, all tracks in options.tracks must belong to options.input.');
|
||||
}
|
||||
if (
|
||||
options.output._tracks.length > 0
|
||||
|| Object.keys(options.output._metadataTags).length > 0
|
||||
@@ -607,21 +634,45 @@ export class Conversion {
|
||||
|
||||
/** @internal */
|
||||
async _init() {
|
||||
this._startTimestamp = this._options.trim?.start ?? Math.max(
|
||||
await this.input.getFirstTimestamp(),
|
||||
// Samples can also have negative timestamps, but the meaning typically is "don't present me", so let's cut
|
||||
// those out by default.
|
||||
0,
|
||||
);
|
||||
this._endTimestamp = Math.max(this._options.trim?.end ?? Infinity, this._startTimestamp);
|
||||
const inputFormat = await this.input.getFormat();
|
||||
|
||||
let tracks: InputTrack[];
|
||||
|
||||
if (Array.isArray(this._options.tracks)) {
|
||||
tracks = this._options.tracks;
|
||||
} else {
|
||||
let trackMode = this._options.tracks;
|
||||
if (trackMode === undefined) {
|
||||
// HACK to keep bundle size low, temp for now
|
||||
const defaultTrackMode = inputFormat.name.includes('(HLS)')
|
||||
? 'primary'
|
||||
: 'all';
|
||||
|
||||
trackMode = defaultTrackMode;
|
||||
}
|
||||
|
||||
if (trackMode === 'all') {
|
||||
tracks = await this.input.getTracks();
|
||||
} else if (trackMode === 'primary') {
|
||||
const primaryVideoTrack = await this.input.getPrimaryVideoTrack();
|
||||
const primaryAudioTrack = await this.input.getPrimaryAudioTrack();
|
||||
|
||||
tracks = [primaryVideoTrack, primaryAudioTrack].filter(x => x !== null);
|
||||
} else {
|
||||
assertNever(trackMode);
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
const inputTracks = await this.input.getTracks();
|
||||
const outputTrackCounts = this.output.format.getSupportedTrackCounts();
|
||||
|
||||
let nVideo = 1;
|
||||
let nAudio = 1;
|
||||
|
||||
for (const track of inputTracks) {
|
||||
const filteredTracks: InputTrack[] = [];
|
||||
const filteredTrackOptions: (ConversionVideoOptions | ConversionAudioOptions)[] = [];
|
||||
|
||||
for (const track of tracks) {
|
||||
let trackOptions: ConversionVideoOptions | ConversionAudioOptions | undefined = undefined;
|
||||
if (track.isVideoTrack()) {
|
||||
if (this._options.video) {
|
||||
@@ -671,10 +722,39 @@ export class Conversion {
|
||||
continue;
|
||||
}
|
||||
|
||||
filteredTracks.push(track);
|
||||
filteredTrackOptions.push(trackOptions ?? {});
|
||||
}
|
||||
|
||||
if (this._options.trim?.start !== undefined) {
|
||||
this._startTimestamp = this._options.trim.start;
|
||||
} else {
|
||||
// Compute the start timestamp from the set of filtered tracks. Techncially these can still be narrowed
|
||||
// down later due to discarded tracks, but we need to fix the start timestamp now due to track processing
|
||||
// depending on it.
|
||||
this._startTimestamp = Math.max(
|
||||
Math.min(
|
||||
...await Promise.all(filteredTracks.map(x => x.getFirstTimestamp())),
|
||||
),
|
||||
// Samples can also have negative timestamps, but the meaning typically is "don't present me", so let's
|
||||
// cut those out by default.
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
this._endTimestamp = Math.max(this._options.trim?.end ?? Infinity, this._startTimestamp);
|
||||
|
||||
// Run these sequentially so that output tracks have a deterministic order
|
||||
for (let i = 0; i < filteredTracks.length; i++) {
|
||||
const track = filteredTracks[i]!;
|
||||
const options = filteredTrackOptions[i]!;
|
||||
|
||||
if (track.isVideoTrack()) {
|
||||
await this._processVideoTrack(track, (trackOptions ?? {}) as ConversionVideoOptions);
|
||||
await this._processVideoTrack(track, options as ConversionVideoOptions);
|
||||
} else if (track.isAudioTrack()) {
|
||||
await this._processAudioTrack(track, (trackOptions ?? {}) as ConversionAudioOptions);
|
||||
await this._processAudioTrack(track, options as ConversionAudioOptions);
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -695,7 +775,7 @@ export class Conversion {
|
||||
}
|
||||
|
||||
// Somewhat dirty but pragmatic
|
||||
const inputAndOutputFormatMatch = (await this.input.getFormat()).mimeType === this.output.format.mimeType;
|
||||
const inputAndOutputFormatMatch = inputFormat.mimeType === this.output.format.mimeType;
|
||||
const rawTagsAreUnchanged = inputTags.raw === outputTags.raw;
|
||||
|
||||
if (inputTags.raw && rawTagsAreUnchanged && !inputAndOutputFormatMatch) {
|
||||
@@ -832,7 +912,7 @@ export class Conversion {
|
||||
return Infinity; // Upper bound (assuming no universe heat death)
|
||||
}
|
||||
|
||||
return track.computeDuration();
|
||||
return (await track.getDurationFromMetadata()) ?? (await track.computeDuration());
|
||||
});
|
||||
const duration = Math.max(0, ...await Promise.all(durationPromises));
|
||||
|
||||
@@ -972,15 +1052,16 @@ export class Conversion {
|
||||
const sink = new EncodedPacketSink(track);
|
||||
const decoderConfig = await track.getDecoderConfig();
|
||||
const meta: EncodedVideoChunkMetadata = { decoderConfig: decoderConfig ?? undefined };
|
||||
const endPacket = Number.isFinite(this._endTimestamp)
|
||||
? await sink.getPacket(this._endTimestamp, { metadataOnly: true }) ?? undefined
|
||||
: undefined;
|
||||
|
||||
for await (const packet of sink.packets(undefined, endPacket, { verifyKeyPackets: true })) {
|
||||
for await (const packet of sink.packets(undefined, undefined, { verifyKeyPackets: true })) {
|
||||
if (this._canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet.timestamp >= this._endTimestamp) {
|
||||
break;
|
||||
}
|
||||
|
||||
const modifiedPacket = packet.clone({
|
||||
timestamp: packet.timestamp - this._startTimestamp,
|
||||
sideData: alpha === 'discard'
|
||||
@@ -989,7 +1070,7 @@ export class Conversion {
|
||||
});
|
||||
assert(modifiedPacket.timestamp >= 0);
|
||||
|
||||
this._reportProgress(track.id, modifiedPacket.timestamp);
|
||||
this._reportProgress(track.id, modifiedPacket.timestamp + modifiedPacket.duration);
|
||||
await source.add(modifiedPacket, meta);
|
||||
|
||||
if (this._synchronizer.shouldWait(track.id, modifiedPacket.timestamp)) {
|
||||
@@ -1292,7 +1373,7 @@ export class Conversion {
|
||||
return;
|
||||
}
|
||||
|
||||
this._reportProgress(track.id, sample.timestamp);
|
||||
this._reportProgress(track.id, sample.timestamp + sample.duration);
|
||||
|
||||
let finalSamples: VideoSample[];
|
||||
if (!trackOptions.process) {
|
||||
@@ -1387,21 +1468,22 @@ export class Conversion {
|
||||
const sink = new EncodedPacketSink(track);
|
||||
const decoderConfig = await track.getDecoderConfig();
|
||||
const meta: EncodedAudioChunkMetadata = { decoderConfig: decoderConfig ?? undefined };
|
||||
const endPacket = Number.isFinite(this._endTimestamp)
|
||||
? await sink.getPacket(this._endTimestamp, { metadataOnly: true }) ?? undefined
|
||||
: undefined;
|
||||
|
||||
for await (const packet of sink.packets(undefined, endPacket)) {
|
||||
for await (const packet of sink.packets()) {
|
||||
if (this._canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet.timestamp >= this._endTimestamp) {
|
||||
break;
|
||||
}
|
||||
|
||||
const modifiedPacket = packet.clone({
|
||||
timestamp: packet.timestamp - this._startTimestamp,
|
||||
});
|
||||
assert(modifiedPacket.timestamp >= 0);
|
||||
|
||||
this._reportProgress(track.id, modifiedPacket.timestamp);
|
||||
this._reportProgress(track.id, modifiedPacket.timestamp + modifiedPacket.duration);
|
||||
await source.add(modifiedPacket, meta);
|
||||
|
||||
if (this._synchronizer.shouldWait(track.id, modifiedPacket.timestamp)) {
|
||||
@@ -1540,7 +1622,7 @@ export class Conversion {
|
||||
return;
|
||||
}
|
||||
|
||||
this._reportProgress(track.id, sample.timestamp);
|
||||
this._reportProgress(track.id, sample.timestamp + sample.duration);
|
||||
|
||||
let finalSamples: AudioSample[];
|
||||
if (!trackOptions.process) {
|
||||
|
||||
+4
-1
@@ -342,7 +342,7 @@ export class Input<S extends Source = Source> extends EventEmitter<InputEvents>
|
||||
return this._format;
|
||||
}
|
||||
|
||||
async isSupported(): Promise<boolean> {
|
||||
async canRead(): Promise<boolean> {
|
||||
try {
|
||||
await this._getDemuxer();
|
||||
return true;
|
||||
@@ -376,6 +376,9 @@ export class Input<S extends Source = Source> extends EventEmitter<InputEvents>
|
||||
/**
|
||||
* Returns the timestamp at which the input file starts. More precisely, returns the smallest starting timestamp
|
||||
* among all tracks.
|
||||
*
|
||||
* Note that this method is potentially expensive for inputs with many tracks (such as HLS manifests), since it
|
||||
* probes every track.
|
||||
*/
|
||||
async getFirstTimestamp() {
|
||||
const tracks = await this.getTracks();
|
||||
|
||||
+1
-1
@@ -291,7 +291,7 @@ export class EncodedPacketSink {
|
||||
* method will intelligently preload packets based on the speed of the consumer.
|
||||
*
|
||||
* @param startPacket - (optional) The packet from which iteration should begin. This packet will also be yielded.
|
||||
* @param endTimestamp - (optional) The timestamp at which iteration should end. This packet will _not_ be yielded.
|
||||
* @param endPacket - (optional) The packet at which iteration should end. This packet will _not_ be yielded.
|
||||
*/
|
||||
packets(
|
||||
startPacket?: EncodedPacket,
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
idea: discontinuities with extra decoder config on the packet.
|
||||
Also, why not just have the packet metadata on the packet? I think that would make things easier overall.
|
||||
|
||||
- keep input/demuxer.isSupported()?
|
||||
|
||||
- conversion behavior for HLS; only take primary tracks by default?
|
||||
Reference in New Issue
Block a user