mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 19:03:46 +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) {
|
if (true) {
|
||||||
input = new Mediabunny.Input({
|
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),
|
source: ({ path }) => new Mediabunny.UrlSource(path),
|
||||||
formats: Mediabunny.ALL_FORMATS,
|
formats: Mediabunny.ALL_FORMATS,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
const videoTrack = await input.getPrimaryVideoTrack();
|
const videoTrack = await input.getPrimaryVideoTrack();
|
||||||
const audioTrack = await input.getPrimaryAudioTrack();
|
const audioTrack = await input.getPrimaryAudioTrack();
|
||||||
|
|
||||||
@@ -75,6 +76,7 @@
|
|||||||
videoTrack.computeDuration({ skipLiveWait: true }),
|
videoTrack.computeDuration({ skipLiveWait: true }),
|
||||||
audioTrack.computeDuration({ skipLiveWait: true }),
|
audioTrack.computeDuration({ skipLiveWait: true }),
|
||||||
]));
|
]));
|
||||||
|
*/
|
||||||
} else {
|
} else {
|
||||||
const file = fileInput.files[0];
|
const file = fileInput.files[0];
|
||||||
const source = new Mediabunny.BlobSource(file);
|
const source = new Mediabunny.BlobSource(file);
|
||||||
@@ -90,7 +92,8 @@
|
|||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
audio: (track, n) => ({
|
audio: (track, n) => ({
|
||||||
discard: !tracks.includes(track),
|
//discard: true,
|
||||||
|
//discard: !tracks.includes(track),
|
||||||
//codec: 'eac3',
|
//codec: 'eac3',
|
||||||
//discard: true,
|
//discard: true,
|
||||||
//discard: n > 1,
|
//discard: n > 1,
|
||||||
@@ -137,7 +140,8 @@
|
|||||||
},
|
},
|
||||||
*/
|
*/
|
||||||
video: (track) => ({
|
video: (track) => ({
|
||||||
discard: !tracks.includes(track),
|
//discard: true,
|
||||||
|
//discard: !tracks.includes(track),
|
||||||
//forceTranscode: true,
|
//forceTranscode: true,
|
||||||
//forceTranscode: true,
|
//forceTranscode: true,
|
||||||
//width: 1280,
|
//width: 1280,
|
||||||
@@ -222,6 +226,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
trim: {
|
trim: {
|
||||||
|
end: 10,
|
||||||
//start,
|
//start,
|
||||||
//end: start + 5,
|
//end: start + 5,
|
||||||
////start: 0,
|
////start: 0,
|
||||||
|
|||||||
+11
-2
@@ -11,6 +11,7 @@
|
|||||||
document.body.append(fileInput);
|
document.body.append(fileInput);
|
||||||
|
|
||||||
fileInput.addEventListener('change', async () => {
|
fileInput.addEventListener('change', async () => {
|
||||||
|
/*
|
||||||
const file = fileInput.files[0];
|
const file = fileInput.files[0];
|
||||||
const input = new Mediabunny.Input({
|
const input = new Mediabunny.Input({
|
||||||
formats: Mediabunny.ALL_FORMATS,
|
formats: Mediabunny.ALL_FORMATS,
|
||||||
@@ -25,14 +26,22 @@
|
|||||||
|
|
||||||
//console.log(await input.getDurationFromMetadata(), await input.computeDuration());
|
//console.log(await input.getDurationFromMetadata(), await input.computeDuration());
|
||||||
return;
|
return;
|
||||||
|
*/
|
||||||
|
|
||||||
const manifest = new Mediabunny.Input({
|
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),
|
source: ({ path }) => new Mediabunny.UrlSource(path),
|
||||||
formats: Mediabunny.ALL_FORMATS,
|
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;
|
return;
|
||||||
|
|
||||||
const videoTrack = await manifest.getPrimaryVideoTrack();
|
const videoTrack = await manifest.getPrimaryVideoTrack();
|
||||||
|
|||||||
+107
-25
@@ -38,6 +38,7 @@ import {
|
|||||||
} from './media-source';
|
} from './media-source';
|
||||||
import {
|
import {
|
||||||
assert,
|
assert,
|
||||||
|
assertNever,
|
||||||
ceilToMultipleOfTwo,
|
ceilToMultipleOfTwo,
|
||||||
clamp,
|
clamp,
|
||||||
floorToDivisor,
|
floorToDivisor,
|
||||||
@@ -65,6 +66,17 @@ export type ConversionOptions = {
|
|||||||
/** The output file. */
|
/** The output file. */
|
||||||
output: Output;
|
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
|
* 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
|
* 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)) {
|
if (!(options.output instanceof Output)) {
|
||||||
throw new TypeError('options.output must be an 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 (
|
if (
|
||||||
options.output._tracks.length > 0
|
options.output._tracks.length > 0
|
||||||
|| Object.keys(options.output._metadataTags).length > 0
|
|| Object.keys(options.output._metadataTags).length > 0
|
||||||
@@ -607,21 +634,45 @@ export class Conversion {
|
|||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
async _init() {
|
async _init() {
|
||||||
this._startTimestamp = this._options.trim?.start ?? Math.max(
|
const inputFormat = await this.input.getFormat();
|
||||||
await this.input.getFirstTimestamp(),
|
|
||||||
// Samples can also have negative timestamps, but the meaning typically is "don't present me", so let's cut
|
let tracks: InputTrack[];
|
||||||
// those out by default.
|
|
||||||
0,
|
if (Array.isArray(this._options.tracks)) {
|
||||||
);
|
tracks = this._options.tracks;
|
||||||
this._endTimestamp = Math.max(this._options.trim?.end ?? Infinity, this._startTimestamp);
|
} 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();
|
const outputTrackCounts = this.output.format.getSupportedTrackCounts();
|
||||||
|
|
||||||
let nVideo = 1;
|
let nVideo = 1;
|
||||||
let nAudio = 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;
|
let trackOptions: ConversionVideoOptions | ConversionAudioOptions | undefined = undefined;
|
||||||
if (track.isVideoTrack()) {
|
if (track.isVideoTrack()) {
|
||||||
if (this._options.video) {
|
if (this._options.video) {
|
||||||
@@ -671,10 +722,39 @@ export class Conversion {
|
|||||||
continue;
|
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()) {
|
if (track.isVideoTrack()) {
|
||||||
await this._processVideoTrack(track, (trackOptions ?? {}) as ConversionVideoOptions);
|
await this._processVideoTrack(track, options as ConversionVideoOptions);
|
||||||
} else if (track.isAudioTrack()) {
|
} 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
|
// 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;
|
const rawTagsAreUnchanged = inputTags.raw === outputTags.raw;
|
||||||
|
|
||||||
if (inputTags.raw && rawTagsAreUnchanged && !inputAndOutputFormatMatch) {
|
if (inputTags.raw && rawTagsAreUnchanged && !inputAndOutputFormatMatch) {
|
||||||
@@ -832,7 +912,7 @@ export class Conversion {
|
|||||||
return Infinity; // Upper bound (assuming no universe heat death)
|
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));
|
const duration = Math.max(0, ...await Promise.all(durationPromises));
|
||||||
|
|
||||||
@@ -972,15 +1052,16 @@ export class Conversion {
|
|||||||
const sink = new EncodedPacketSink(track);
|
const sink = new EncodedPacketSink(track);
|
||||||
const decoderConfig = await track.getDecoderConfig();
|
const decoderConfig = await track.getDecoderConfig();
|
||||||
const meta: EncodedVideoChunkMetadata = { decoderConfig: decoderConfig ?? undefined };
|
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) {
|
if (this._canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (packet.timestamp >= this._endTimestamp) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const modifiedPacket = packet.clone({
|
const modifiedPacket = packet.clone({
|
||||||
timestamp: packet.timestamp - this._startTimestamp,
|
timestamp: packet.timestamp - this._startTimestamp,
|
||||||
sideData: alpha === 'discard'
|
sideData: alpha === 'discard'
|
||||||
@@ -989,7 +1070,7 @@ export class Conversion {
|
|||||||
});
|
});
|
||||||
assert(modifiedPacket.timestamp >= 0);
|
assert(modifiedPacket.timestamp >= 0);
|
||||||
|
|
||||||
this._reportProgress(track.id, modifiedPacket.timestamp);
|
this._reportProgress(track.id, modifiedPacket.timestamp + modifiedPacket.duration);
|
||||||
await source.add(modifiedPacket, meta);
|
await source.add(modifiedPacket, meta);
|
||||||
|
|
||||||
if (this._synchronizer.shouldWait(track.id, modifiedPacket.timestamp)) {
|
if (this._synchronizer.shouldWait(track.id, modifiedPacket.timestamp)) {
|
||||||
@@ -1292,7 +1373,7 @@ export class Conversion {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._reportProgress(track.id, sample.timestamp);
|
this._reportProgress(track.id, sample.timestamp + sample.duration);
|
||||||
|
|
||||||
let finalSamples: VideoSample[];
|
let finalSamples: VideoSample[];
|
||||||
if (!trackOptions.process) {
|
if (!trackOptions.process) {
|
||||||
@@ -1387,21 +1468,22 @@ export class Conversion {
|
|||||||
const sink = new EncodedPacketSink(track);
|
const sink = new EncodedPacketSink(track);
|
||||||
const decoderConfig = await track.getDecoderConfig();
|
const decoderConfig = await track.getDecoderConfig();
|
||||||
const meta: EncodedAudioChunkMetadata = { decoderConfig: decoderConfig ?? undefined };
|
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) {
|
if (this._canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (packet.timestamp >= this._endTimestamp) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const modifiedPacket = packet.clone({
|
const modifiedPacket = packet.clone({
|
||||||
timestamp: packet.timestamp - this._startTimestamp,
|
timestamp: packet.timestamp - this._startTimestamp,
|
||||||
});
|
});
|
||||||
assert(modifiedPacket.timestamp >= 0);
|
assert(modifiedPacket.timestamp >= 0);
|
||||||
|
|
||||||
this._reportProgress(track.id, modifiedPacket.timestamp);
|
this._reportProgress(track.id, modifiedPacket.timestamp + modifiedPacket.duration);
|
||||||
await source.add(modifiedPacket, meta);
|
await source.add(modifiedPacket, meta);
|
||||||
|
|
||||||
if (this._synchronizer.shouldWait(track.id, modifiedPacket.timestamp)) {
|
if (this._synchronizer.shouldWait(track.id, modifiedPacket.timestamp)) {
|
||||||
@@ -1540,7 +1622,7 @@ export class Conversion {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._reportProgress(track.id, sample.timestamp);
|
this._reportProgress(track.id, sample.timestamp + sample.duration);
|
||||||
|
|
||||||
let finalSamples: AudioSample[];
|
let finalSamples: AudioSample[];
|
||||||
if (!trackOptions.process) {
|
if (!trackOptions.process) {
|
||||||
|
|||||||
+4
-1
@@ -342,7 +342,7 @@ export class Input<S extends Source = Source> extends EventEmitter<InputEvents>
|
|||||||
return this._format;
|
return this._format;
|
||||||
}
|
}
|
||||||
|
|
||||||
async isSupported(): Promise<boolean> {
|
async canRead(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await this._getDemuxer();
|
await this._getDemuxer();
|
||||||
return true;
|
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
|
* Returns the timestamp at which the input file starts. More precisely, returns the smallest starting timestamp
|
||||||
* among all tracks.
|
* 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() {
|
async getFirstTimestamp() {
|
||||||
const tracks = await this.getTracks();
|
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.
|
* 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 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(
|
packets(
|
||||||
startPacket?: EncodedPacket,
|
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