mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 19:03:46 +02:00
Add new createInputFrom helper function
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
WrappedCanvas,
|
||||
asc,
|
||||
canDecodeAudio,
|
||||
createInputFrom,
|
||||
desc,
|
||||
prefer,
|
||||
} from 'mediabunny';
|
||||
@@ -102,6 +103,9 @@ const initMediaPlayer = async (resource: File | string) => {
|
||||
let videoTrack: InputVideoTrack | null = null;
|
||||
let audioTrack: InputAudioTrack | null = null;
|
||||
if (true || typeof resource === 'string' && resource.includes('.m3u8')) {
|
||||
const input = createInputFrom(resource, ALL_FORMATS);
|
||||
|
||||
/*
|
||||
const input = new Input({
|
||||
entryPath: resource, // 'master.m3u8',
|
||||
source: async ({ path }) => {
|
||||
@@ -112,15 +116,18 @@ const initMediaPlayer = async (resource: File | string) => {
|
||||
},
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
*/
|
||||
// const variant = (await manifestInput.getVariants())[0]!;
|
||||
|
||||
// console.log(await input.getFormat(), await input.getTracks());
|
||||
// return;
|
||||
|
||||
/*
|
||||
await input.getVideoTracks({
|
||||
filter: desc => desc.hasPairableAudioTrack(),
|
||||
sortBy: desc => asc(desc.bitrate),
|
||||
});
|
||||
*/
|
||||
|
||||
videoTrack = await input.getPrimaryVideoTrack({
|
||||
filter: async desc => (desc.displayHeight ?? (await desc.getTrack()).displayHeight) < 720,
|
||||
|
||||
@@ -192,6 +192,7 @@ export {
|
||||
WEBM,
|
||||
} from './input-format';
|
||||
export {
|
||||
createInputFrom,
|
||||
Input,
|
||||
InputOptions,
|
||||
InputEvents,
|
||||
|
||||
+134
-1
@@ -38,7 +38,19 @@ import {
|
||||
removeItem,
|
||||
} from './misc';
|
||||
import { Reader } from './reader';
|
||||
import { Source, SourceRef } from './source';
|
||||
import {
|
||||
BlobSource,
|
||||
BlobSourceOptions,
|
||||
BufferSource,
|
||||
FilePathSource,
|
||||
FilePathSourceOptions,
|
||||
ReadableStreamSource,
|
||||
ReadableStreamSourceOptions,
|
||||
Source,
|
||||
SourceRef,
|
||||
UrlSource,
|
||||
UrlSourceOptions,
|
||||
} from './source';
|
||||
|
||||
polyfillSymbolDispose();
|
||||
|
||||
@@ -654,3 +666,124 @@ export class InputDisposedError extends Error {
|
||||
this.name = 'InputDisposedError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for {@link Input.from}. Combines the options of all source types, plus `initInput`.
|
||||
* @group Input files & tracks
|
||||
* @public
|
||||
*/
|
||||
export type InputFromOptions =
|
||||
& Partial<UrlSourceOptions>
|
||||
& Partial<BlobSourceOptions>
|
||||
& Partial<FilePathSourceOptions>
|
||||
& Partial<ReadableStreamSourceOptions>
|
||||
& {
|
||||
initInput?: Input;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an {@link Input} backed by the passed-in data. An alternative to {@link Input}'s constructor, this helper
|
||||
* function automatically chooses the correct underlying {@link Source} based on the type of the data passed in.
|
||||
*
|
||||
* Legal data types are `ArrayBuffer`, `SharedArrayBuffer`, `ArrayBufferView`, `Blob` (and, by extension, `File`),
|
||||
* `ReadableStream<Uint8Array`, `string` (representing either a URL or a local file path), `URL`, and `Request`.
|
||||
*
|
||||
* The available options are the union of the options for each {@link Source}. Check the sources to see which field
|
||||
* applies to which source.
|
||||
*/
|
||||
export const createInputFrom = (
|
||||
data: AllowSharedBufferSource | Blob | ReadableStream<Uint8Array> | string | URL | Request,
|
||||
formats: InputFormat[],
|
||||
options: InputFromOptions = {},
|
||||
): Input => {
|
||||
if (!Array.isArray(formats) || !formats.every(x => x instanceof InputFormat)) {
|
||||
throw new TypeError('formats must be an array of InputFormat.');
|
||||
}
|
||||
if (typeof options !== 'object' || !options) {
|
||||
throw new TypeError('options must be an object.');
|
||||
}
|
||||
|
||||
const { initInput, ...sourceOptions } = options;
|
||||
|
||||
if (
|
||||
data instanceof ArrayBuffer
|
||||
|| (typeof SharedArrayBuffer !== 'undefined' && data instanceof SharedArrayBuffer)
|
||||
|| ArrayBuffer.isView(data)
|
||||
) {
|
||||
return new Input({
|
||||
formats,
|
||||
source: new BufferSource(data),
|
||||
initInput,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof Blob !== 'undefined' && data instanceof Blob) {
|
||||
return new Input({
|
||||
formats,
|
||||
source: new BlobSource(data, sourceOptions),
|
||||
initInput,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof ReadableStream !== 'undefined' && data instanceof ReadableStream) {
|
||||
return new Input({
|
||||
formats,
|
||||
source: new ReadableStreamSource(data, sourceOptions),
|
||||
initInput,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof URL !== 'undefined' && data instanceof URL) {
|
||||
const url = data.href;
|
||||
|
||||
return new Input({
|
||||
formats,
|
||||
source: (request: SourceRequest) => new UrlSource(request.path, sourceOptions),
|
||||
entryPath: url,
|
||||
initInput,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof Request !== 'undefined' && data instanceof Request) {
|
||||
const url = data.url;
|
||||
|
||||
return new Input({
|
||||
formats,
|
||||
source: (request: SourceRequest) => {
|
||||
const reqInit = (sourceOptions as UrlSourceOptions).requestInit;
|
||||
return new UrlSource(
|
||||
new Request(request.path, { ...reqInit, method: data.method, headers: data.headers }),
|
||||
sourceOptions,
|
||||
);
|
||||
},
|
||||
entryPath: url,
|
||||
initInput,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
const isUrl = data.includes('://');
|
||||
|
||||
if (isUrl) {
|
||||
return new Input({
|
||||
formats,
|
||||
source: (request: SourceRequest) => new UrlSource(request.path, sourceOptions),
|
||||
entryPath: data,
|
||||
initInput,
|
||||
});
|
||||
}
|
||||
|
||||
// File path, throws automatically if this isn't server-side
|
||||
return new Input({
|
||||
formats,
|
||||
source: (request: SourceRequest) => new FilePathSource(request.path, sourceOptions),
|
||||
entryPath: data,
|
||||
initInput,
|
||||
});
|
||||
}
|
||||
|
||||
throw new TypeError(
|
||||
'Input.from: first argument must be an ArrayBuffer, SharedArrayBuffer, ArrayBufferView, Blob,'
|
||||
+ ' ReadableStream, string, URL, or Request.',
|
||||
);
|
||||
};
|
||||
|
||||
@@ -745,6 +745,12 @@ export class FilePathSource extends Source {
|
||||
throw new TypeError('options.maxCacheSize, when provided, must be a non-negative number.');
|
||||
}
|
||||
|
||||
if (!node.fs) {
|
||||
throw new Error(
|
||||
'FilePathSource is only available in server-side environments (Node.js, Bun, Deno).',
|
||||
);
|
||||
}
|
||||
|
||||
super();
|
||||
|
||||
// Let's back this source with a StreamSource, makes the implementation very simple
|
||||
|
||||
+27
-99
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @stylistic/max-len */
|
||||
import { ALL_FORMATS, EncodedPacketSink, Input, InputAudioTrack, InputVideoTrack, UrlSource } from 'mediabunny';
|
||||
import { ALL_FORMATS, createInputFrom, EncodedPacketSink, InputAudioTrack, InputVideoTrack } from 'mediabunny';
|
||||
import { expect, test } from 'vitest';
|
||||
import { HLS, HlsInputFormat } from '../../src/input-format.js';
|
||||
import { assert, rejectAfter } from '../../src/misc.js';
|
||||
@@ -8,11 +8,7 @@ import { assert, rejectAfter } from '../../src/misc.js';
|
||||
// https://github.com/video-dev/hls.js/blob/master/tests/test-streams.js
|
||||
|
||||
test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8',
|
||||
source: ({ path }) => new UrlSource(path),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = createInputFrom('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', ALL_FORMATS);
|
||||
|
||||
let sourceCount = 0;
|
||||
input.on('source', () => sourceCount++);
|
||||
@@ -218,11 +214,7 @@ test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => {
|
||||
});
|
||||
|
||||
test.concurrent('Single-variant Big Buck Bunny', { timeout: 15_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://test-streams.mux.dev/x36xhzz/url_6/193039199_mp4_h264_aac_hq_7.m3u8',
|
||||
source: ({ path }) => new UrlSource(path),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = createInputFrom('https://test-streams.mux.dev/x36xhzz/url_6/193039199_mp4_h264_aac_hq_7.m3u8', ALL_FORMATS);
|
||||
|
||||
const tracks = await input.getTracks();
|
||||
expect(tracks).toHaveLength(2);
|
||||
@@ -237,11 +229,7 @@ test.concurrent('Single-variant Big Buck Bunny', { timeout: 15_000 }, async () =
|
||||
});
|
||||
|
||||
test.concurrent('Codec-less (underspecified) master playlist', { timeout: 15_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://test-streams.mux.dev/test_001/stream.m3u8',
|
||||
source: ({ path }) => new UrlSource(path),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = createInputFrom('https://test-streams.mux.dev/test_001/stream.m3u8', ALL_FORMATS);
|
||||
|
||||
const descriptors = await input.getTrackDescriptors();
|
||||
expect(descriptors).toHaveLength(12);
|
||||
@@ -253,11 +241,7 @@ test.concurrent('Codec-less (underspecified) master playlist', { timeout: 15_000
|
||||
});
|
||||
|
||||
test.concurrent('AES and discontinuities', { timeout: 15_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://test-streams.mux.dev/dai-discontinuity-deltatre/manifest.m3u8',
|
||||
source: ({ path }) => new UrlSource(path),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = createInputFrom('https://test-streams.mux.dev/dai-discontinuity-deltatre/manifest.m3u8', ALL_FORMATS);
|
||||
|
||||
let sourceCount = 0;
|
||||
input.on('source', () => sourceCount++);
|
||||
@@ -286,11 +270,7 @@ test.concurrent('AES and discontinuities', { timeout: 15_000 }, async () => {
|
||||
});
|
||||
|
||||
test.concurrent('Range requests', { timeout: 15_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://playertest.longtailvideo.com/adaptive/issue666/playlists/cisq0gim60007xzvi505emlxx.m3u8',
|
||||
source: ({ path }) => new UrlSource(path),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = createInputFrom('https://playertest.longtailvideo.com/adaptive/issue666/playlists/cisq0gim60007xzvi505emlxx.m3u8', ALL_FORMATS);
|
||||
|
||||
let sourceCount = 0;
|
||||
input.on('source', () => sourceCount++);
|
||||
@@ -314,11 +294,7 @@ test.concurrent('Range requests', { timeout: 15_000 }, async () => {
|
||||
});
|
||||
|
||||
test.concurrent('Custom IV', { timeout: 15_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://playertest.longtailvideo.com/adaptive/customIV/prog_index.m3u8',
|
||||
source: ({ path }) => new UrlSource(path),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = createInputFrom('https://playertest.longtailvideo.com/adaptive/customIV/prog_index.m3u8', ALL_FORMATS);
|
||||
|
||||
let sourceCount = 0;
|
||||
input.on('source', () => sourceCount++);
|
||||
@@ -330,11 +306,7 @@ test.concurrent('Custom IV', { timeout: 15_000 }, async () => {
|
||||
});
|
||||
|
||||
test.concurrent('Out-of-band audio track via ADTS', { timeout: 15_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://playertest.longtailvideo.com/adaptive/aes-with-tracks/master.m3u8',
|
||||
source: ({ path }) => new UrlSource(path),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = createInputFrom('https://playertest.longtailvideo.com/adaptive/aes-with-tracks/master.m3u8', ALL_FORMATS);
|
||||
|
||||
const descriptors = await input.getTrackDescriptors();
|
||||
expect(descriptors.some(x => x.isVideoTrackDescriptor() && x.hasOnlyKeyPackets)).toBe(true);
|
||||
@@ -362,11 +334,7 @@ test.concurrent('Out-of-band audio track via ADTS', { timeout: 15_000 }, async (
|
||||
});
|
||||
|
||||
test.concurrent('MP3 audio only', { timeout: 15_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://pl.streamingvideoprovider.com/mp3-playlist/playlist.m3u8',
|
||||
source: ({ path }) => new UrlSource(path),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = createInputFrom('https://pl.streamingvideoprovider.com/mp3-playlist/playlist.m3u8', ALL_FORMATS);
|
||||
|
||||
const audioDescriptor = (await input.getAudioTrackDescriptors())[0];
|
||||
assert(audioDescriptor);
|
||||
@@ -374,11 +342,7 @@ test.concurrent('MP3 audio only', { timeout: 15_000 }, async () => {
|
||||
});
|
||||
|
||||
test.concurrent('fMP4', { timeout: 15_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8',
|
||||
source: ({ path }) => new UrlSource(path),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = createInputFrom('https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8', ALL_FORMATS);
|
||||
|
||||
let sourceCount = 0;
|
||||
input.on('source', () => sourceCount++);
|
||||
@@ -404,11 +368,7 @@ test.concurrent('fMP4', { timeout: 15_000 }, async () => {
|
||||
});
|
||||
|
||||
test.concurrent('Track disposition & metadata', { timeout: 15_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8',
|
||||
source: ({ path }) => new UrlSource(path),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = createInputFrom('https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8', ALL_FORMATS);
|
||||
|
||||
const audioDescriptors = await input.getAudioTrackDescriptors();
|
||||
|
||||
@@ -444,20 +404,16 @@ test.concurrent('Track disposition & metadata', { timeout: 15_000 }, async () =>
|
||||
});
|
||||
|
||||
test.concurrent('fMP4 Bitmovin', { timeout: 15_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s-fmp4/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8',
|
||||
source: ({ path }) => new UrlSource(path, {
|
||||
requestInit: {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Origin': 'https://bitmovin.com',
|
||||
'Referer': 'https://bitmovin.com/',
|
||||
},
|
||||
using input = createInputFrom('https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s-fmp4/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8', ALL_FORMATS, {
|
||||
requestInit: {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Origin': 'https://bitmovin.com',
|
||||
'Referer': 'https://bitmovin.com/',
|
||||
},
|
||||
}),
|
||||
formats: ALL_FORMATS,
|
||||
},
|
||||
});
|
||||
|
||||
let sourceCount = 0;
|
||||
@@ -484,11 +440,7 @@ test.concurrent('fMP4 Bitmovin', { timeout: 15_000 }, async () => {
|
||||
});
|
||||
|
||||
test.concurrent('Single-value PDT', { timeout: 15_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://playertest.longtailvideo.com/adaptive/aviion/manifest.m3u8',
|
||||
source: ({ path }) => new UrlSource(path),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = createInputFrom('https://playertest.longtailvideo.com/adaptive/aviion/manifest.m3u8', ALL_FORMATS);
|
||||
|
||||
const tracks = await input.getTracks();
|
||||
expect(tracks.every(x => x.isRelativeToUnixEpoch)).toBe(true);
|
||||
@@ -516,11 +468,7 @@ test.concurrent('Single-value PDT', { timeout: 15_000 }, async () => {
|
||||
});
|
||||
|
||||
test.concurrent('Duplicate PDT', { timeout: 30_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://playertest.longtailvideo.com/adaptive/artbeats/manifest.m3u8',
|
||||
source: ({ path }) => new UrlSource(path),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = createInputFrom('https://playertest.longtailvideo.com/adaptive/artbeats/manifest.m3u8', ALL_FORMATS);
|
||||
|
||||
const audioTrack = await input.getPrimaryAudioTrack();
|
||||
assert(audioTrack);
|
||||
@@ -537,11 +485,7 @@ test.concurrent('Duplicate PDT', { timeout: 30_000 }, async () => {
|
||||
});
|
||||
|
||||
test.concurrent('PDT with large gaps', { timeout: 15_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://playertest.longtailvideo.com/adaptive/boxee/playlist.m3u8',
|
||||
source: ({ path }) => new UrlSource(path),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = createInputFrom('https://playertest.longtailvideo.com/adaptive/boxee/playlist.m3u8', ALL_FORMATS);
|
||||
|
||||
const audioTrack = await input.getPrimaryAudioTrack();
|
||||
assert(audioTrack);
|
||||
@@ -557,11 +501,7 @@ test.concurrent('PDT with large gaps', { timeout: 15_000 }, async () => {
|
||||
});
|
||||
|
||||
test.concurrent('PDT with bad values', { timeout: 15_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://playertest.longtailvideo.com/adaptive/progdatime/playlist2.m3u8',
|
||||
source: ({ path }) => new UrlSource(path),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = createInputFrom('https://playertest.longtailvideo.com/adaptive/progdatime/playlist2.m3u8', ALL_FORMATS);
|
||||
|
||||
const audioTrack = await input.getPrimaryAudioTrack();
|
||||
assert(audioTrack);
|
||||
@@ -570,11 +510,7 @@ test.concurrent('PDT with bad values', { timeout: 15_000 }, async () => {
|
||||
});
|
||||
|
||||
test.concurrent('Alternative audio only', { timeout: 15_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://playertest.longtailvideo.com/adaptive/alt-audio-no-video/sintel/playlist.m3u8',
|
||||
source: ({ path }) => new UrlSource(path),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = createInputFrom('https://playertest.longtailvideo.com/adaptive/alt-audio-no-video/sintel/playlist.m3u8', ALL_FORMATS);
|
||||
|
||||
const descriptors = await input.getTrackDescriptors();
|
||||
expect(descriptors).toHaveLength(2);
|
||||
@@ -584,11 +520,7 @@ test.concurrent('Alternative audio only', { timeout: 15_000 }, async () => {
|
||||
});
|
||||
|
||||
test.concurrent('Advanced Apple HLS', { timeout: 30_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_adv_example_hevc/master.m3u8',
|
||||
source: ({ path }) => new UrlSource(path),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = createInputFrom('https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_adv_example_hevc/master.m3u8', ALL_FORMATS);
|
||||
|
||||
const descriptors = await input.getTrackDescriptors();
|
||||
const videoDescriptors = descriptors.filter(x => x.isVideoTrackDescriptor());
|
||||
@@ -703,11 +635,7 @@ test.concurrent('Advanced Apple HLS', { timeout: 30_000 }, async () => {
|
||||
});
|
||||
|
||||
test.concurrent('Live HLS', { timeout: 30_000 }, async () => {
|
||||
using input = new Input({
|
||||
entryPath: 'https://stream.mux.com/v69RSHhFelSm4701snP22dYz2jICy4E4FUyk02rW4gxRM.m3u8',
|
||||
source: ({ path }) => new UrlSource(path),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = createInputFrom('https://stream.mux.com/v69RSHhFelSm4701snP22dYz2jICy4E4FUyk02rW4gxRM.m3u8', ALL_FORMATS);
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
assert(videoTrack);
|
||||
|
||||
Reference in New Issue
Block a user