diff --git a/dev/convert.html b/dev/convert.html
index 6b88382..fc1d2cf 100644
--- a/dev/convert.html
+++ b/dev/convert.html
@@ -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,
diff --git a/dev/demux.html b/dev/demux.html
index d7a8a32..7aaf58a 100644
--- a/dev/demux.html
+++ b/dev/demux.html
@@ -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();
diff --git a/src/conversion.ts b/src/conversion.ts
index 2829490..e4430a7 100644
--- a/src/conversion.ts
+++ b/src/conversion.ts
@@ -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) {
diff --git a/src/input.ts b/src/input.ts
index 7c095ca..3fb0b24 100644
--- a/src/input.ts
+++ b/src/input.ts
@@ -342,7 +342,7 @@ export class Input extends EventEmitter
return this._format;
}
- async isSupported(): Promise {
+ async canRead(): Promise {
try {
await this._getDemuxer();
return true;
@@ -376,6 +376,9 @@ export class Input extends EventEmitter
/**
* 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();
diff --git a/src/media-sink.ts b/src/media-sink.ts
index 1078733..9814906 100644
--- a/src/media-sink.ts
+++ b/src/media-sink.ts
@@ -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,
diff --git a/todo.txt b/todo.txt
deleted file mode 100644
index 6538a2f..0000000
--- a/todo.txt
+++ /dev/null
@@ -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?