Various fixes

This commit is contained in:
Vanilagy
2026-03-06 15:49:24 +01:00
parent 22716b1f83
commit c088a31384
14 changed files with 506 additions and 142 deletions
+21 -2
View File
@@ -17,14 +17,33 @@
source: new Mediabunny.BlobSource(file),
});
const audTrack = await input.getPrimaryAudioTrack();
console.log((await audTrack.getDecoderConfig()).description.join(','));
return;
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),
formats: Mediabunny.ALL_FORMATS,
});
/*
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({
+54 -1
View File
@@ -196,7 +196,60 @@ Codec encodability checks take [custom encoders](#custom-encoders) into account.
## 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
+5 -2
View File
@@ -10,6 +10,7 @@ import {
WrappedAudioBuffer,
WrappedCanvas,
asc,
canDecodeAudio,
desc,
prefer,
} from 'mediabunny';
@@ -113,10 +114,10 @@ const initMediaPlayer = async (resource: File | string) => {
});
videoTrack = await input.getPrimaryVideoTrack({
filter: async track => (await track.resolve('displayHeight')) <= 720,
filter: async track => (await track.resolve('displayHeight')) < 1080,
});
audioTrack = await input.getPrimaryAudioTrack({
filter: track => !videoTrack || videoTrack.canBePairedWith(track),
sortBy: track => prefer(track.canBePairedWith(videoTrack)),
});
await videoTrack?.hydrate();
@@ -127,6 +128,8 @@ const initMediaPlayer = async (resource: File | string) => {
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_1000k_48k_640x360_050.ts
} else {
+41 -3
View File
File diff suppressed because one or more lines are too long
+263
View File
@@ -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]);
};
+89 -92
View File
@@ -25,6 +25,9 @@ import { customAudioEncoders, customVideoEncoders } from './custom-coder';
import { isFirefox } from './misc';
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.
* @group Encoding
@@ -494,38 +497,7 @@ export const canEncodeVideo = async (
}
validateVideoEncodingAdditionalOptions(codec, restOptions);
let encoderConfig: VideoEncoderConfig | null = null;
if (customVideoEncoders.length > 0) {
encoderConfig ??= buildVideoEncoderConfig({
codec,
width,
height,
bitrate,
framerate: undefined,
...restOptions,
});
if (customVideoEncoders.some(x => x.supports(codec, encoderConfig!))) {
// There's a custom encoder
return true;
}
}
if (typeof VideoEncoder === 'undefined') {
return false;
}
const hasOddDimension = width % 2 === 1 || height % 2 === 1;
if (
hasOddDimension
&& (codec === 'avc' || codec === 'hevc')
) {
// Disallow odd dimensions for certain codecs
return false;
}
encoderConfig ??= buildVideoEncoderConfig({
const encoderConfig = buildVideoEncoderConfig({
codec,
width,
height,
@@ -535,46 +507,74 @@ export const canEncodeVideo = async (
alpha: 'discard', // Since we handle alpha ourselves
});
const support = await VideoEncoder.isConfigSupported(encoderConfig);
if (!support.supported) {
return false;
const key = JSON.stringify(encoderConfig);
const memoized = canEncodeVideoMemo.get(key);
if (memoized) {
return memoized;
}
if (isFirefox()) {
// isConfigSupported on Firefox appears to unreliably indicate if encoding will actually succeed. Therefore, we
// just try encoding a frame to see if it actually works.
// https://github.com/Vanilagy/mediabunny/issues/222
const promise = (async () => {
if (customVideoEncoders.some(x => x.supports(codec, encoderConfig))) {
// There's a custom encoder
return true;
}
if (typeof VideoEncoder === 'undefined') {
return false;
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises, no-async-promise-executor
return new Promise<boolean>(async (resolve) => {
try {
const encoder = new VideoEncoder({
output: () => {},
error: () => resolve(false),
});
encoder.configure(encoderConfig);
const hasOddDimension = width % 2 === 1 || height % 2 === 1;
if (
hasOddDimension
&& (codec === 'avc' || codec === 'hevc')
) {
// Disallow odd dimensions for certain codecs
return false;
}
const frameData = new Uint8Array(width * height * 4);
const frame = new VideoFrame(frameData, {
format: 'RGBA',
codedWidth: width,
codedHeight: height,
timestamp: 0,
});
const support = await VideoEncoder.isConfigSupported(encoderConfig);
if (!support.supported) {
return false;
}
encoder.encode(frame);
frame.close();
if (isFirefox()) {
// isConfigSupported on Firefox appears to unreliably indicate if encoding will actually succeed. Therefore,
// we just try encoding a frame to see if it actually works.
// https://github.com/Vanilagy/mediabunny/issues/222
await encoder.flush();
// eslint-disable-next-line @typescript-eslint/no-misused-promises, no-async-promise-executor
return new Promise<boolean>(async (resolve) => {
try {
const encoder = new VideoEncoder({
output: () => {},
error: () => resolve(false),
});
encoder.configure(encoderConfig);
const frameData = new Uint8Array(width * height * 4);
const frame = new VideoFrame(frameData, {
format: 'RGBA',
codedWidth: width,
codedHeight: height,
timestamp: 0,
});
encoder.encode(frame);
frame.close();
await encoder.flush();
resolve(true);
} catch {
resolve(false);
}
});
}
resolve(true);
} catch {
resolve(false);
}
});
} else {
return true;
}
})();
canEncodeVideoMemo.set(key, promise);
return promise;
};
/**
@@ -611,32 +611,7 @@ export const canEncodeAudio = async (
}
validateAudioEncodingAdditionalOptions(codec, restOptions);
let encoderConfig: AudioEncoderConfig | null = null;
if (customAudioEncoders.length > 0) {
encoderConfig ??= buildAudioEncoderConfig({
codec,
numberOfChannels,
sampleRate,
bitrate,
...restOptions,
});
if (customAudioEncoders.some(x => x.supports(codec, encoderConfig!))) {
// There's a custom encoder
return true;
}
}
if ((PCM_AUDIO_CODECS as readonly string[]).includes(codec)) {
return true; // Because we encode these ourselves
}
if (typeof AudioEncoder === 'undefined') {
return false;
}
encoderConfig ??= buildAudioEncoderConfig({
const encoderConfig = buildAudioEncoderConfig({
codec,
numberOfChannels,
sampleRate,
@@ -644,8 +619,30 @@ export const canEncodeAudio = async (
...restOptions,
});
const support = await AudioEncoder.isConfigSupported(encoderConfig);
return support.supported === true;
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
return true;
}
if ((PCM_AUDIO_CODECS as readonly string[]).includes(codec)) {
return true; // Because we encode these ourselves
}
if (typeof AudioEncoder === 'undefined') {
return false;
}
const support = await AudioEncoder.isConfigSupported(encoderConfig);
return support.supported === true;
})();
canEncodeAudioMemo.set(key, promise);
return promise;
};
/**
+1 -1
View File
@@ -18,7 +18,7 @@ export class AttributeList {
inValue = true;
} else if (char === ',' && !inQuotes) {
if (key) {
this._attributes[key.toLowerCase()] = value;
this._attributes[key.trim().toLowerCase()] = value;
}
key = '';
+6 -3
View File
@@ -209,7 +209,10 @@ export class HlsSegmentedInput extends SegmentedInput {
keyFormat: attributes.get('keyformat') ?? 'identity',
};
} 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:')) {
const value = line.slice(22);
@@ -241,11 +244,11 @@ export class HlsSegmentedInput extends SegmentedInput {
// backward from that tag (using EXTINF durations and/or media
// timestamps) to associate dates with those segments."
const lastSegment = last(segments)!;
const lastSegmentEnd = lastSegment.relativeTimestamp + lastSegment.duration;
const lastSegmentEnd = lastSegment.timestamp + lastSegment.duration;
const offset = dateTimeSeconds - lastSegmentEnd;
for (const segment of segments) {
segment.relativeTimestamp += offset;
segment.timestamp += offset;
segment.relativeToUnixEpoch = true;
}
+9
View File
@@ -80,6 +80,14 @@ export {
NON_PCM_AUDIO_CODECS,
SUBTITLE_CODECS,
} from './codec';
export {
canDecode,
canDecodeVideo,
canDecodeAudio,
getDecodableCodecs,
getDecodableVideoCodecs,
getDecodableAudioCodecs,
} from './decode';
export {
VideoEncodingConfig,
VideoEncodingAdditionalOptions,
@@ -119,6 +127,7 @@ export {
Rational,
Rectangle,
Rotation,
SetOptional,
SetRequired,
asc,
desc,
+7
View File
@@ -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>>;
/**
* 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.
* Headers are merged case-insensitively, but original casing is preserved.
+3 -3
View File
@@ -21,7 +21,7 @@ export type SegmentLocation = {
export class Segment {
input: SegmentedInput;
location: SegmentLocation;
relativeTimestamp: number;
timestamp: number;
relativeToUnixEpoch: boolean;
duration: number;
title: string | null;
@@ -32,7 +32,7 @@ export class Segment {
constructor(
input: SegmentedInput,
location: SegmentLocation,
relativeTimestamp: number,
timestamp: number,
relativeToUnixEpoch: boolean,
duration: number,
title: string | null,
@@ -42,7 +42,7 @@ export class Segment {
) {
this.input = input;
this.location = location;
this.relativeTimestamp = relativeTimestamp;
this.timestamp = timestamp;
this.relativeToUnixEpoch = relativeToUnixEpoch;
this.duration = duration;
this.title = title;
+6 -6
View File
@@ -67,7 +67,7 @@ export abstract class SegmentedInput {
async getSegmentAt(timestamp: number) {
const segments = await this.getSegments();
const index = binarySearchLessOrEqual(segments, timestamp, x => x.relativeTimestamp);
const index = binarySearchLessOrEqual(segments, timestamp, x => x.timestamp);
if (index === -1) {
return null;
}
@@ -175,24 +175,24 @@ class SegmentedInputDemuxer extends Demuxer {
}
if (firstSegment === segment) {
return firstSegment.relativeTimestamp - firstSegmentFirstTimestamp;
return firstSegment.timestamp - firstSegmentFirstTimestamp;
}
const segmentFirstTimestamp = await input.getFirstTimestamp();
const segmentElapsed = segment.relativeTimestamp - firstSegment.relativeTimestamp;
const segmentElapsed = segment.timestamp - firstSegment.timestamp;
const inputElapsed = segmentFirstTimestamp - firstSegmentFirstTimestamp;
const difference = inputElapsed - segmentElapsed;
if (Math.abs(difference) <= Math.min(0.25, segmentElapsed)) { // Heuristic
// We're close enough
return firstSegment.relativeTimestamp - firstSegmentFirstTimestamp;
return firstSegment.timestamp - firstSegmentFirstTimestamp;
} else {
// 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
// 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
// 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
// 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, {
+1 -21
View File
@@ -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.
}
/** @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 */
_dispose() {
this._orchestrator.dispose();
@@ -1847,6 +1826,7 @@ class ReadOrchestrator {
const index = this.workers.indexOf(worker);
assert(index !== -1);
worker.running = false;
this.workers.splice(index, 1);
if (this.fileSize === null) {
-8
View File
@@ -1,15 +1,7 @@
- "manifest" -> "playlist"?
- Retaining Input instances for segments without clogging up memory indefinitely
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: 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