mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-10 01:03:45 +02:00
Various fixes
This commit is contained in:
+21
-2
@@ -17,14 +17,33 @@
|
|||||||
source: new Mediabunny.BlobSource(file),
|
source: new Mediabunny.BlobSource(file),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const audTrack = await input.getPrimaryAudioTrack();
|
||||||
|
console.log((await audTrack.getDecoderConfig()).description.join(','));
|
||||||
|
return;
|
||||||
|
|
||||||
const manifest = new Mediabunny.Input({
|
const manifest = new Mediabunny.Input({
|
||||||
entryPath: 'https://playertest.longtailvideo.com/adaptive/elephants_dream_v4/redundant.m3u8',
|
entryPath: 'https://s3.amazonaws.com/qa.jwplayer.com/hlsjs/muxed-fmp4/hls.m3u8',
|
||||||
source: ({ path }) => new Mediabunny.UrlSource(path),
|
source: ({ path }) => new Mediabunny.UrlSource(path),
|
||||||
formats: Mediabunny.ALL_FORMATS,
|
formats: Mediabunny.ALL_FORMATS,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
const tracks = await manifest.getTracks();
|
const tracks = await manifest.getTracks();
|
||||||
console.log(tracks)
|
//await input.hydrateAllTracks();
|
||||||
|
console.log(tracks, tracks.map(x => [x.getFirstTimestamp(), x.computeDuration()]));
|
||||||
|
|
||||||
|
return;
|
||||||
|
*/
|
||||||
|
|
||||||
|
const videoTrack = await manifest.getPrimaryVideoTrack();
|
||||||
|
const audioTrack = await manifest.getPrimaryAudioTrack();
|
||||||
|
|
||||||
|
console.log(await videoTrack.getFirstTimestamp(), await audioTrack.getFirstTimestamp());
|
||||||
|
/*
|
||||||
|
for await (using sample of sink.samples()) {
|
||||||
|
console.log(sample.timestamp);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
const manifest = new Mediabunny.ManifestInput({
|
const manifest = new Mediabunny.ManifestInput({
|
||||||
|
|||||||
@@ -196,7 +196,60 @@ Codec encodability checks take [custom encoders](#custom-encoders) into account.
|
|||||||
|
|
||||||
## Querying codec decodability
|
## Querying codec decodability
|
||||||
|
|
||||||
Whether a codec can be decoded depends on the specific codec configuration of an `InputTrack`; you can use its [`canDecode`](./reading-media-files#codec-information) method to check.
|
If you already have an `InputTrack`, you can check its decodability using its [`canDecode`](./reading-media-files#codec-information) method, which uses the track's actual codec configuration:
|
||||||
|
```ts
|
||||||
|
const canDecodeTrack = await inputTrack.canDecode(); // => boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
However, you can also gauge decodability even in the absence of any concrete track. `canDecode` tests whether a codec can be decoded using typical settings:
|
||||||
|
```ts
|
||||||
|
import { canDecode } from 'mediabunny';
|
||||||
|
|
||||||
|
canDecode('avc'); // => Promise<boolean>
|
||||||
|
canDecode('opus'); // => Promise<boolean>
|
||||||
|
```
|
||||||
|
Video codecs are checked using 1280x720, while audio codecs are checked using 2 channels, 48 kHz.
|
||||||
|
|
||||||
|
You can also check decodability using specific configurations:
|
||||||
|
```ts
|
||||||
|
import { canDecodeVideo, canDecodeAudio } from 'mediabunny';
|
||||||
|
|
||||||
|
canDecodeVideo('hevc', {
|
||||||
|
codedWidth: 1920, codedHeight: 1080
|
||||||
|
}); // => Promise<boolean>
|
||||||
|
|
||||||
|
canDecodeAudio('aac', {
|
||||||
|
numberOfChannels: 1, sampleRate: 44100
|
||||||
|
}); // => Promise<boolean>
|
||||||
|
```
|
||||||
|
|
||||||
|
All additional properties of [`VideoDecoderConfig`](https://developer.mozilla.org/en-US/docs/Web/API/VideoDecoder/configure#config) and [`AudioDecoderConfig`](https://developer.mozilla.org/en-US/docs/Web/API/AudioDecoder/configure#config) can be used here as well.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
In addition, you can use the following functions to check decodability for multiple codecs at once, getting back a list of supported codecs:
|
||||||
|
```ts
|
||||||
|
import {
|
||||||
|
getDecodableCodecs,
|
||||||
|
getDecodableVideoCodecs,
|
||||||
|
getDecodableAudioCodecs,
|
||||||
|
} from 'mediabunny';
|
||||||
|
|
||||||
|
getDecodableCodecs(); // => Promise<MediaCodec[]>
|
||||||
|
getDecodableVideoCodecs(); // => Promise<VideoCodec[]>
|
||||||
|
getDecodableAudioCodecs(); // => Promise<AudioCodec[]>
|
||||||
|
|
||||||
|
// These functions also accept optional configuration options.
|
||||||
|
// Here, we check which of AVC, HEVC and VP8 can be decoded at 1920x1080:
|
||||||
|
getDecodableVideoCodecs(
|
||||||
|
['avc', 'hevc', 'vp8'],
|
||||||
|
{ codedWidth: 1920, codedHeight: 1080 },
|
||||||
|
); // => Promise<VideoCodec[]>
|
||||||
|
```
|
||||||
|
|
||||||
|
::: info
|
||||||
|
Codec decodability checks take [custom decoders](#custom-decoders) into account.
|
||||||
|
:::
|
||||||
|
|
||||||
## Custom coders
|
## Custom coders
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
WrappedAudioBuffer,
|
WrappedAudioBuffer,
|
||||||
WrappedCanvas,
|
WrappedCanvas,
|
||||||
asc,
|
asc,
|
||||||
|
canDecodeAudio,
|
||||||
desc,
|
desc,
|
||||||
prefer,
|
prefer,
|
||||||
} from 'mediabunny';
|
} from 'mediabunny';
|
||||||
@@ -113,10 +114,10 @@ const initMediaPlayer = async (resource: File | string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
videoTrack = await input.getPrimaryVideoTrack({
|
videoTrack = await input.getPrimaryVideoTrack({
|
||||||
filter: async track => (await track.resolve('displayHeight')) <= 720,
|
filter: async track => (await track.resolve('displayHeight')) < 1080,
|
||||||
});
|
});
|
||||||
audioTrack = await input.getPrimaryAudioTrack({
|
audioTrack = await input.getPrimaryAudioTrack({
|
||||||
filter: track => !videoTrack || videoTrack.canBePairedWith(track),
|
sortBy: track => prefer(track.canBePairedWith(videoTrack)),
|
||||||
});
|
});
|
||||||
|
|
||||||
await videoTrack?.hydrate();
|
await videoTrack?.hydrate();
|
||||||
@@ -127,6 +128,8 @@ const initMediaPlayer = async (resource: File | string) => {
|
|||||||
await audioTrack?.computeDuration() ?? 0,
|
await audioTrack?.computeDuration() ?? 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log(videoTrack, audioTrack, totalDuration);
|
||||||
|
|
||||||
// https://test-streams.mux.dev/test_001/stream.m3u8
|
// https://test-streams.mux.dev/test_001/stream.m3u8
|
||||||
// https://test-streams.mux.dev/test_001/stream_1000k_48k_640x360_050.ts
|
// https://test-streams.mux.dev/test_001/stream_1000k_48k_640x360_050.ts
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+41
-3
File diff suppressed because one or more lines are too long
+263
@@ -0,0 +1,263 @@
|
|||||||
|
/*!
|
||||||
|
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
AUDIO_CODECS,
|
||||||
|
AudioCodec,
|
||||||
|
buildAudioCodecString,
|
||||||
|
buildVideoCodecString,
|
||||||
|
guessDescriptionForAudio,
|
||||||
|
guessDescriptionForVideo,
|
||||||
|
inferCodecFromCodecString,
|
||||||
|
MediaCodec,
|
||||||
|
PCM_AUDIO_CODECS,
|
||||||
|
VIDEO_CODECS,
|
||||||
|
VideoCodec,
|
||||||
|
} from './codec';
|
||||||
|
import { customAudioDecoders, customVideoDecoders } from './custom-coder';
|
||||||
|
import { isAllowSharedBufferSource, SetOptional } from './misc';
|
||||||
|
|
||||||
|
const canDecodeVideoMemo = new Map<string, Promise<boolean>>();
|
||||||
|
const canDecodeAudioMemo = new Map<string, Promise<boolean>>();
|
||||||
|
|
||||||
|
const validateVideoDecodingConfig = (codec: VideoCodec, options: SetOptional<VideoDecoderConfig, 'codec'>) => {
|
||||||
|
if (!options || typeof options !== 'object') {
|
||||||
|
throw new TypeError('options must be an object.');
|
||||||
|
}
|
||||||
|
if (options.codec !== undefined && typeof options.codec !== 'string') {
|
||||||
|
throw new TypeError('options.codec, when provided, must be a string.');
|
||||||
|
}
|
||||||
|
if (options.codec !== undefined && inferCodecFromCodecString(options.codec) !== codec) {
|
||||||
|
throw new TypeError(`options.codec, when provided, must match the specified codec (${codec}).`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
options.codedWidth !== undefined
|
||||||
|
&& (!Number.isInteger(options.codedWidth) || options.codedWidth <= 0)
|
||||||
|
) {
|
||||||
|
throw new TypeError('options.codedWidth, when provided, must be a positive integer.');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
options.codedHeight !== undefined
|
||||||
|
&& (!Number.isInteger(options.codedHeight) || options.codedHeight <= 0)
|
||||||
|
) {
|
||||||
|
throw new TypeError('options.codedHeight, when provided, must be a positive integer.');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
options.displayAspectWidth !== undefined
|
||||||
|
&& (!Number.isInteger(options.displayAspectWidth) || options.displayAspectWidth <= 0)
|
||||||
|
) {
|
||||||
|
throw new TypeError('options.displayAspectWidth, when provided, must be a positive integer.');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
options.displayAspectHeight !== undefined
|
||||||
|
&& (!Number.isInteger(options.displayAspectHeight) || options.displayAspectHeight <= 0)
|
||||||
|
) {
|
||||||
|
throw new TypeError('options.displayAspectHeight, when provided, must be a positive integer.');
|
||||||
|
}
|
||||||
|
if (options.description !== undefined && !isAllowSharedBufferSource(options.description)) {
|
||||||
|
throw new TypeError('options.description, when provided, must be a buffer source.');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
options.hardwareAcceleration !== undefined
|
||||||
|
&& !['no-preference', 'prefer-hardware', 'prefer-software'].includes(options.hardwareAcceleration)
|
||||||
|
) {
|
||||||
|
throw new TypeError(
|
||||||
|
'options.hardwareAcceleration, when provided, must be \'no-preference\', \'prefer-hardware\' or'
|
||||||
|
+ ' \'prefer-software\'.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (options.optimizeForLatency !== undefined && typeof options.optimizeForLatency !== 'boolean') {
|
||||||
|
throw new TypeError('options.optimizeForLatency, when provided, must be a boolean.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateAudioDecodingConfig = (
|
||||||
|
codec: AudioCodec,
|
||||||
|
options: SetOptional<AudioDecoderConfig, 'codec' | 'numberOfChannels' | 'sampleRate'>,
|
||||||
|
) => {
|
||||||
|
if (!options || typeof options !== 'object') {
|
||||||
|
throw new TypeError('options must be an object.');
|
||||||
|
}
|
||||||
|
if (options.codec !== undefined && typeof options.codec !== 'string') {
|
||||||
|
throw new TypeError('options.codec, when provided, must be a string.');
|
||||||
|
}
|
||||||
|
if (options.codec !== undefined && inferCodecFromCodecString(options.codec) !== codec) {
|
||||||
|
throw new TypeError(`options.codec, when provided, must match the specified codec (${codec}).`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
options.numberOfChannels !== undefined
|
||||||
|
&& (!Number.isInteger(options.numberOfChannels) || options.numberOfChannels <= 0)
|
||||||
|
) {
|
||||||
|
throw new TypeError('options.numberOfChannels, when provided, must be a positive integer.');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
options.sampleRate !== undefined
|
||||||
|
&& (!Number.isInteger(options.sampleRate) || options.sampleRate <= 0)
|
||||||
|
) {
|
||||||
|
throw new TypeError('options.sampleRate, when provided, must be a positive integer.');
|
||||||
|
}
|
||||||
|
if (options.description !== undefined && !isAllowSharedBufferSource(options.description)) {
|
||||||
|
throw new TypeError('options.description, when provided, must be a buffer source.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the browser is able to decode the given codec.
|
||||||
|
* @group Decoding
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const canDecode = (codec: MediaCodec) => {
|
||||||
|
if ((VIDEO_CODECS as readonly string[]).includes(codec)) {
|
||||||
|
return canDecodeVideo(codec as VideoCodec);
|
||||||
|
} else if ((AUDIO_CODECS as readonly string[]).includes(codec)) {
|
||||||
|
return canDecodeAudio(codec as AudioCodec);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the browser is able to decode the given video codec with the given parameters.
|
||||||
|
* @group Decoding
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const canDecodeVideo = async (
|
||||||
|
codec: VideoCodec,
|
||||||
|
options: SetOptional<VideoDecoderConfig, 'codec'> = {},
|
||||||
|
) => {
|
||||||
|
if (!VIDEO_CODECS.includes(codec)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateVideoDecodingConfig(codec, options);
|
||||||
|
|
||||||
|
const resolvedOptions: VideoDecoderConfig = {
|
||||||
|
...options,
|
||||||
|
codedWidth: options.codedWidth ?? 1280,
|
||||||
|
codedHeight: options.codedHeight ?? 720,
|
||||||
|
codec: options.codec ?? buildVideoCodecString(codec, 1280, 720, 1e6),
|
||||||
|
};
|
||||||
|
resolvedOptions.description ??= guessDescriptionForVideo(resolvedOptions);
|
||||||
|
|
||||||
|
const key = JSON.stringify(resolvedOptions);
|
||||||
|
const memoized = canDecodeVideoMemo.get(key);
|
||||||
|
if (memoized) {
|
||||||
|
return memoized;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
if (customVideoDecoders.some(x => x.supports(codec, resolvedOptions))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof VideoDecoder === 'undefined') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const support = await VideoDecoder.isConfigSupported(resolvedOptions);
|
||||||
|
return support.supported === true;
|
||||||
|
})();
|
||||||
|
canDecodeVideoMemo.set(key, promise);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the browser is able to decode the given audio codec with the given parameters.
|
||||||
|
* @group Decoding
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const canDecodeAudio = async (
|
||||||
|
codec: AudioCodec,
|
||||||
|
options: SetOptional<AudioDecoderConfig, 'codec' | 'numberOfChannels' | 'sampleRate'> = {},
|
||||||
|
) => {
|
||||||
|
if (!AUDIO_CODECS.includes(codec)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateAudioDecodingConfig(codec, options);
|
||||||
|
|
||||||
|
const resolvedOptions: AudioDecoderConfig = {
|
||||||
|
...options,
|
||||||
|
numberOfChannels: options.numberOfChannels ?? 2,
|
||||||
|
sampleRate: options.sampleRate ?? 48000,
|
||||||
|
codec: options.codec ?? buildAudioCodecString(codec, 2, 48000),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (resolvedOptions.description === undefined) {
|
||||||
|
const generatedDescription = guessDescriptionForAudio(resolvedOptions);
|
||||||
|
if (generatedDescription === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedOptions.description = generatedDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = JSON.stringify(resolvedOptions);
|
||||||
|
const memoized = canDecodeAudioMemo.get(key);
|
||||||
|
if (memoized) {
|
||||||
|
return memoized;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
if (customAudioDecoders.some(x => x.supports(codec, resolvedOptions))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ((PCM_AUDIO_CODECS as readonly string[]).includes(codec)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof AudioDecoder === 'undefined') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const support = await AudioDecoder.isConfigSupported(resolvedOptions);
|
||||||
|
return support.supported === true;
|
||||||
|
})();
|
||||||
|
canDecodeAudioMemo.set(key, promise);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of all media codecs that can be decoded by the browser.
|
||||||
|
* @group Decoding
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const getDecodableCodecs = async (): Promise<MediaCodec[]> => {
|
||||||
|
const [videoCodecs, audioCodecs] = await Promise.all([
|
||||||
|
getDecodableVideoCodecs(),
|
||||||
|
getDecodableAudioCodecs(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [...videoCodecs, ...audioCodecs];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of all video codecs that can be decoded by the browser.
|
||||||
|
* @group Decoding
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const getDecodableVideoCodecs = async (
|
||||||
|
checkedCodecs: VideoCodec[] = VIDEO_CODECS as unknown as VideoCodec[],
|
||||||
|
options?: SetOptional<VideoDecoderConfig, 'codec'>,
|
||||||
|
): Promise<VideoCodec[]> => {
|
||||||
|
const bools = await Promise.all(checkedCodecs.map(codec => canDecodeVideo(codec, options)));
|
||||||
|
return checkedCodecs.filter((_, i) => bools[i]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of all audio codecs that can be decoded by the browser.
|
||||||
|
* @group Decoding
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const getDecodableAudioCodecs = async (
|
||||||
|
checkedCodecs: AudioCodec[] = AUDIO_CODECS as unknown as AudioCodec[],
|
||||||
|
options?: SetOptional<AudioDecoderConfig, 'codec' | 'numberOfChannels' | 'sampleRate'>,
|
||||||
|
): Promise<AudioCodec[]> => {
|
||||||
|
const bools = await Promise.all(checkedCodecs.map(codec => canDecodeAudio(codec, options)));
|
||||||
|
return checkedCodecs.filter((_, i) => bools[i]);
|
||||||
|
};
|
||||||
+34
-37
@@ -25,6 +25,9 @@ import { customAudioEncoders, customVideoEncoders } from './custom-coder';
|
|||||||
import { isFirefox } from './misc';
|
import { isFirefox } from './misc';
|
||||||
import { EncodedPacket } from './packet';
|
import { EncodedPacket } from './packet';
|
||||||
|
|
||||||
|
const canEncodeVideoMemo = new Map<string, Promise<boolean>>();
|
||||||
|
const canEncodeAudioMemo = new Map<string, Promise<boolean>>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration object that controls video encoding. Can be used to set codec, quality, and more.
|
* Configuration object that controls video encoding. Can be used to set codec, quality, and more.
|
||||||
* @group Encoding
|
* @group Encoding
|
||||||
@@ -494,24 +497,27 @@ export const canEncodeVideo = async (
|
|||||||
}
|
}
|
||||||
validateVideoEncodingAdditionalOptions(codec, restOptions);
|
validateVideoEncodingAdditionalOptions(codec, restOptions);
|
||||||
|
|
||||||
let encoderConfig: VideoEncoderConfig | null = null;
|
const encoderConfig = buildVideoEncoderConfig({
|
||||||
|
|
||||||
if (customVideoEncoders.length > 0) {
|
|
||||||
encoderConfig ??= buildVideoEncoderConfig({
|
|
||||||
codec,
|
codec,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
bitrate,
|
bitrate,
|
||||||
framerate: undefined,
|
framerate: undefined,
|
||||||
...restOptions,
|
...restOptions,
|
||||||
|
alpha: 'discard', // Since we handle alpha ourselves
|
||||||
});
|
});
|
||||||
|
|
||||||
if (customVideoEncoders.some(x => x.supports(codec, encoderConfig!))) {
|
const key = JSON.stringify(encoderConfig);
|
||||||
|
const memoized = canEncodeVideoMemo.get(key);
|
||||||
|
if (memoized) {
|
||||||
|
return memoized;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
if (customVideoEncoders.some(x => x.supports(codec, encoderConfig))) {
|
||||||
// There's a custom encoder
|
// There's a custom encoder
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof VideoEncoder === 'undefined') {
|
if (typeof VideoEncoder === 'undefined') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -525,24 +531,14 @@ export const canEncodeVideo = async (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
encoderConfig ??= buildVideoEncoderConfig({
|
|
||||||
codec,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
bitrate,
|
|
||||||
framerate: undefined,
|
|
||||||
...restOptions,
|
|
||||||
alpha: 'discard', // Since we handle alpha ourselves
|
|
||||||
});
|
|
||||||
|
|
||||||
const support = await VideoEncoder.isConfigSupported(encoderConfig);
|
const support = await VideoEncoder.isConfigSupported(encoderConfig);
|
||||||
if (!support.supported) {
|
if (!support.supported) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFirefox()) {
|
if (isFirefox()) {
|
||||||
// isConfigSupported on Firefox appears to unreliably indicate if encoding will actually succeed. Therefore, we
|
// isConfigSupported on Firefox appears to unreliably indicate if encoding will actually succeed. Therefore,
|
||||||
// just try encoding a frame to see if it actually works.
|
// we just try encoding a frame to see if it actually works.
|
||||||
// https://github.com/Vanilagy/mediabunny/issues/222
|
// https://github.com/Vanilagy/mediabunny/issues/222
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises, no-async-promise-executor
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises, no-async-promise-executor
|
||||||
@@ -572,9 +568,13 @@ export const canEncodeVideo = async (
|
|||||||
resolve(false);
|
resolve(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})();
|
||||||
|
canEncodeVideoMemo.set(key, promise);
|
||||||
|
|
||||||
|
return promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -611,10 +611,7 @@ export const canEncodeAudio = async (
|
|||||||
}
|
}
|
||||||
validateAudioEncodingAdditionalOptions(codec, restOptions);
|
validateAudioEncodingAdditionalOptions(codec, restOptions);
|
||||||
|
|
||||||
let encoderConfig: AudioEncoderConfig | null = null;
|
const encoderConfig = buildAudioEncoderConfig({
|
||||||
|
|
||||||
if (customAudioEncoders.length > 0) {
|
|
||||||
encoderConfig ??= buildAudioEncoderConfig({
|
|
||||||
codec,
|
codec,
|
||||||
numberOfChannels,
|
numberOfChannels,
|
||||||
sampleRate,
|
sampleRate,
|
||||||
@@ -622,30 +619,30 @@ export const canEncodeAudio = async (
|
|||||||
...restOptions,
|
...restOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (customAudioEncoders.some(x => x.supports(codec, encoderConfig!))) {
|
const key = JSON.stringify(encoderConfig);
|
||||||
|
const memoized = canEncodeAudioMemo.get(key);
|
||||||
|
if (memoized) {
|
||||||
|
return memoized;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
if (customAudioEncoders.some(x => x.supports(codec, encoderConfig))) {
|
||||||
// There's a custom encoder
|
// There's a custom encoder
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ((PCM_AUDIO_CODECS as readonly string[]).includes(codec)) {
|
if ((PCM_AUDIO_CODECS as readonly string[]).includes(codec)) {
|
||||||
return true; // Because we encode these ourselves
|
return true; // Because we encode these ourselves
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof AudioEncoder === 'undefined') {
|
if (typeof AudioEncoder === 'undefined') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
encoderConfig ??= buildAudioEncoderConfig({
|
|
||||||
codec,
|
|
||||||
numberOfChannels,
|
|
||||||
sampleRate,
|
|
||||||
bitrate,
|
|
||||||
...restOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
const support = await AudioEncoder.isConfigSupported(encoderConfig);
|
const support = await AudioEncoder.isConfigSupported(encoderConfig);
|
||||||
return support.supported === true;
|
return support.supported === true;
|
||||||
|
})();
|
||||||
|
canEncodeAudioMemo.set(key, promise);
|
||||||
|
|
||||||
|
return promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ export class AttributeList {
|
|||||||
inValue = true;
|
inValue = true;
|
||||||
} else if (char === ',' && !inQuotes) {
|
} else if (char === ',' && !inQuotes) {
|
||||||
if (key) {
|
if (key) {
|
||||||
this._attributes[key.toLowerCase()] = value;
|
this._attributes[key.trim().toLowerCase()] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
key = '';
|
key = '';
|
||||||
|
|||||||
@@ -209,7 +209,10 @@ export class HlsSegmentedInput extends SegmentedInput {
|
|||||||
keyFormat: attributes.get('keyformat') ?? 'identity',
|
keyFormat: attributes.get('keyformat') ?? 'identity',
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported encryption method '${method}'.`);
|
throw new Error(
|
||||||
|
`Unsupported encryption method '${method}'. If you think this method should be supported,`
|
||||||
|
+ ` please raise an issue.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (line.startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
|
} else if (line.startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
|
||||||
const value = line.slice(22);
|
const value = line.slice(22);
|
||||||
@@ -241,11 +244,11 @@ export class HlsSegmentedInput extends SegmentedInput {
|
|||||||
// backward from that tag (using EXTINF durations and/or media
|
// backward from that tag (using EXTINF durations and/or media
|
||||||
// timestamps) to associate dates with those segments."
|
// timestamps) to associate dates with those segments."
|
||||||
const lastSegment = last(segments)!;
|
const lastSegment = last(segments)!;
|
||||||
const lastSegmentEnd = lastSegment.relativeTimestamp + lastSegment.duration;
|
const lastSegmentEnd = lastSegment.timestamp + lastSegment.duration;
|
||||||
const offset = dateTimeSeconds - lastSegmentEnd;
|
const offset = dateTimeSeconds - lastSegmentEnd;
|
||||||
|
|
||||||
for (const segment of segments) {
|
for (const segment of segments) {
|
||||||
segment.relativeTimestamp += offset;
|
segment.timestamp += offset;
|
||||||
segment.relativeToUnixEpoch = true;
|
segment.relativeToUnixEpoch = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,14 @@ export {
|
|||||||
NON_PCM_AUDIO_CODECS,
|
NON_PCM_AUDIO_CODECS,
|
||||||
SUBTITLE_CODECS,
|
SUBTITLE_CODECS,
|
||||||
} from './codec';
|
} from './codec';
|
||||||
|
export {
|
||||||
|
canDecode,
|
||||||
|
canDecodeVideo,
|
||||||
|
canDecodeAudio,
|
||||||
|
getDecodableCodecs,
|
||||||
|
getDecodableVideoCodecs,
|
||||||
|
getDecodableAudioCodecs,
|
||||||
|
} from './decode';
|
||||||
export {
|
export {
|
||||||
VideoEncodingConfig,
|
VideoEncodingConfig,
|
||||||
VideoEncodingAdditionalOptions,
|
VideoEncodingAdditionalOptions,
|
||||||
@@ -119,6 +127,7 @@ export {
|
|||||||
Rational,
|
Rational,
|
||||||
Rectangle,
|
Rectangle,
|
||||||
Rotation,
|
Rotation,
|
||||||
|
SetOptional,
|
||||||
SetRequired,
|
SetRequired,
|
||||||
asc,
|
asc,
|
||||||
desc,
|
desc,
|
||||||
|
|||||||
@@ -463,6 +463,13 @@ export const SECOND_TO_MICROSECOND_FACTOR = 1e6 * (1 + Number.EPSILON);
|
|||||||
*/
|
*/
|
||||||
export type SetRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
|
export type SetRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets all keys K of T to be optional.
|
||||||
|
* @group Miscellaneous
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export type SetOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merges two RequestInit objects with special handling for headers.
|
* Merges two RequestInit objects with special handling for headers.
|
||||||
* Headers are merged case-insensitively, but original casing is preserved.
|
* Headers are merged case-insensitively, but original casing is preserved.
|
||||||
|
|||||||
+3
-3
@@ -21,7 +21,7 @@ export type SegmentLocation = {
|
|||||||
export class Segment {
|
export class Segment {
|
||||||
input: SegmentedInput;
|
input: SegmentedInput;
|
||||||
location: SegmentLocation;
|
location: SegmentLocation;
|
||||||
relativeTimestamp: number;
|
timestamp: number;
|
||||||
relativeToUnixEpoch: boolean;
|
relativeToUnixEpoch: boolean;
|
||||||
duration: number;
|
duration: number;
|
||||||
title: string | null;
|
title: string | null;
|
||||||
@@ -32,7 +32,7 @@ export class Segment {
|
|||||||
constructor(
|
constructor(
|
||||||
input: SegmentedInput,
|
input: SegmentedInput,
|
||||||
location: SegmentLocation,
|
location: SegmentLocation,
|
||||||
relativeTimestamp: number,
|
timestamp: number,
|
||||||
relativeToUnixEpoch: boolean,
|
relativeToUnixEpoch: boolean,
|
||||||
duration: number,
|
duration: number,
|
||||||
title: string | null,
|
title: string | null,
|
||||||
@@ -42,7 +42,7 @@ export class Segment {
|
|||||||
) {
|
) {
|
||||||
this.input = input;
|
this.input = input;
|
||||||
this.location = location;
|
this.location = location;
|
||||||
this.relativeTimestamp = relativeTimestamp;
|
this.timestamp = timestamp;
|
||||||
this.relativeToUnixEpoch = relativeToUnixEpoch;
|
this.relativeToUnixEpoch = relativeToUnixEpoch;
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export abstract class SegmentedInput {
|
|||||||
|
|
||||||
async getSegmentAt(timestamp: number) {
|
async getSegmentAt(timestamp: number) {
|
||||||
const segments = await this.getSegments();
|
const segments = await this.getSegments();
|
||||||
const index = binarySearchLessOrEqual(segments, timestamp, x => x.relativeTimestamp);
|
const index = binarySearchLessOrEqual(segments, timestamp, x => x.timestamp);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -175,24 +175,24 @@ class SegmentedInputDemuxer extends Demuxer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (firstSegment === segment) {
|
if (firstSegment === segment) {
|
||||||
return firstSegment.relativeTimestamp - firstSegmentFirstTimestamp;
|
return firstSegment.timestamp - firstSegmentFirstTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
const segmentFirstTimestamp = await input.getFirstTimestamp();
|
const segmentFirstTimestamp = await input.getFirstTimestamp();
|
||||||
const segmentElapsed = segment.relativeTimestamp - firstSegment.relativeTimestamp;
|
const segmentElapsed = segment.timestamp - firstSegment.timestamp;
|
||||||
const inputElapsed = segmentFirstTimestamp - firstSegmentFirstTimestamp;
|
const inputElapsed = segmentFirstTimestamp - firstSegmentFirstTimestamp;
|
||||||
const difference = inputElapsed - segmentElapsed;
|
const difference = inputElapsed - segmentElapsed;
|
||||||
|
|
||||||
if (Math.abs(difference) <= Math.min(0.25, segmentElapsed)) { // Heuristic
|
if (Math.abs(difference) <= Math.min(0.25, segmentElapsed)) { // Heuristic
|
||||||
// We're close enough
|
// We're close enough
|
||||||
return firstSegment.relativeTimestamp - firstSegmentFirstTimestamp;
|
return firstSegment.timestamp - firstSegmentFirstTimestamp;
|
||||||
} else {
|
} else {
|
||||||
// Ideally, each segment has absolute timestamps that are relative to some outside clock which is
|
// Ideally, each segment has absolute timestamps that are relative to some outside clock which is
|
||||||
// consistent across segments. This is often the case, but not always. Either the container format used is
|
// consistent across segments. This is often the case, but not always. Either the container format used is
|
||||||
// not timestamped at all (like ADTS), or the segments are just fucky. In this case, use the segment's
|
// not timestamped at all (like ADTS), or the segments are just fucky. In this case, use the segment's
|
||||||
// relative timestamp to determine where we are, and completely offset out the segment's input start
|
// relative timestamp to determine where we are, and completely offset out the segment's input start
|
||||||
// timestamp.
|
// timestamp.
|
||||||
return segment.relativeTimestamp - segmentFirstTimestamp;
|
return segment.timestamp - segmentFirstTimestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,7 +278,7 @@ class SegmentedInputInputTrackBacking implements InputTrackBacking {
|
|||||||
),
|
),
|
||||||
// The 1e8 assumes a max of 100 MB per second, highly unlikely to be hit, so this should guarantee
|
// The 1e8 assumes a max of 100 MB per second, highly unlikely to be hit, so this should guarantee
|
||||||
// monotonically increasing sequence numbers across segments.
|
// monotonically increasing sequence numbers across segments.
|
||||||
sequenceNumber: Math.floor(1e8 * segment.relativeTimestamp) + packet.sequenceNumber,
|
sequenceNumber: Math.floor(1e8 * segment.timestamp) + packet.sequenceNumber,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.packetInfos.set(modified, {
|
this.packetInfos.set(modified, {
|
||||||
|
|||||||
+1
-21
@@ -641,27 +641,6 @@ export class UrlSource extends Source {
|
|||||||
// logic for that has vanished for now. Leaving a comment here if this becomes relevant again.
|
// logic for that has vanished for now. Leaving a comment here if this becomes relevant again.
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
private _getTotalLengthFromRangeResponse(response: Response) {
|
|
||||||
const contentRange = response.headers.get('Content-Range');
|
|
||||||
if (contentRange) {
|
|
||||||
const match = /\/(\d+)/.exec(contentRange);
|
|
||||||
if (match) {
|
|
||||||
return Number(match[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentLength = response.headers.get('Content-Length');
|
|
||||||
if (contentLength) {
|
|
||||||
return Number(contentLength);
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
'Partial HTTP response (status 206) must surface either Content-Range or'
|
|
||||||
+ ' Content-Length header.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_dispose() {
|
_dispose() {
|
||||||
this._orchestrator.dispose();
|
this._orchestrator.dispose();
|
||||||
@@ -1847,6 +1826,7 @@ class ReadOrchestrator {
|
|||||||
const index = this.workers.indexOf(worker);
|
const index = this.workers.indexOf(worker);
|
||||||
assert(index !== -1);
|
assert(index !== -1);
|
||||||
|
|
||||||
|
worker.running = false;
|
||||||
this.workers.splice(index, 1);
|
this.workers.splice(index, 1);
|
||||||
|
|
||||||
if (this.fileSize === null) {
|
if (this.fileSize === null) {
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
- "manifest" -> "playlist"?
|
|
||||||
- Retaining Input instances for segments without clogging up memory indefinitely
|
- Retaining Input instances for segments without clogging up memory indefinitely
|
||||||
|
|
||||||
idea: discontinuities with extra decoder config on the packet.
|
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.
|
Also, why not just have the packet metadata on the packet? I think that would make things easier overall.
|
||||||
Also: for robustness, do a different track matching algorithm for hls playback. so not use pid but make it simpler like
|
|
||||||
if theres only 1 video track then its obvious yknow. Need to see if this is actually needed in the case of ext-x-discontinuity
|
|
||||||
|
|
||||||
- EXT-X-MEDIA (DONE, but surface more metadata from it, like language and shit) - should it be passed down to the tracks??
|
|
||||||
- getSegments() API? Any point in partially reading the playlist file? Idk
|
|
||||||
|
|
||||||
- Timestamp across variants; i think the date should actually be used. Make the timestamp relative to the date? How does that play with timeResolution?
|
|
||||||
|
|
||||||
- ALL_FORMATS but for HLS only
|
- ALL_FORMATS but for HLS only
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user