Merge main into release for tag v1.48.0

This commit is contained in:
github-actions[bot]
2026-06-16 18:41:51 +00:00
26 changed files with 332 additions and 36 deletions
+11
View File
@@ -225,6 +225,11 @@ Create the sink like so:
import { VideoSampleSink } from 'mediabunny';
const sink = new VideoSampleSink(videoTrack);
// Optionally, configure the decoder:
const sink = new VideoSampleSink(videoTrack, {
hardwareAcceleration: 'prefer-software',
});
```
#### Single retrieval
@@ -363,6 +368,8 @@ type CanvasSinkOptions = {
rotation?: 0 | 90 | 180 | 270;
crop?: { left: number; top: number; width: number; height: number };
poolSize?: number;
alpha?: boolean;
decoderOptions?: VideoSinkDecoderOptions;
};
```
- `width`\
@@ -380,6 +387,10 @@ type CanvasSinkOptions = {
Specifies the rectangular region of the input video to crop to. The crop region will automatically be clamped to the dimensions of the input video track. Cropping is performed after rotation but before resizing. The crop region is in the _display pixel space_ of the underlying video data.
- `poolSize`\
See [Canvas pool](#canvas-pool).
- `alpha`\
Whether the output canvases should have transparency instead of a black background. Defaults to `false`. Set this to `true` when using this sink to read transparent videos.
- `decoderOptions`\
Additional preferences for the underlying video decoder.
Some examples:
```ts
+29
View File
@@ -233,6 +233,35 @@ This makes cross-track synchronization trivial. To know if a track's timestamps
await track.isRelativeToUnixEpoch(); // => boolean
```
### Disabling Unix offsets
If you don't want Mediabunny to offset packet timestamps to be in Unix time, you can set `offsetTimestampsByDateTime` to `false` in the input format options:
```ts
const input = new Input({
// ...
formatOptions: {
hls: {
offsetTimestampsByDateTime: false,
},
},
});
```
This way, track and packet timestamps behave as if no `#EXT-X-PROGRAM-DATE-TIME` tags existed. This also means that any date time gaps are completely collapsed.
You will still be able to query the Unix time metadata via a mapping function on the `InputTrack`:
```ts
const firstTimestamp = await inputTrack.getFirstTimestamp(); // => 0
await inputTrack.getUnixTimeForTimestamp(firstTimestamp); // => 1704067200 (Unix timestamp for 2024-01-01T00:00:00Z)
```
This function performs a piecewise-continuous mapping of timestamp space into Unix time space.
If no wall-clock time information is available, `getUnixTimeForTimestamp()` will return `null`. You can check the presence of Unix time metadata via:
```ts
await inputTrack.hasUnixTimeMapping(); // boolean
```
## Live HLS
HLS playlists may be live. You can check that a track is live via:
+7 -7
View File
@@ -1,12 +1,12 @@
{
"name": "mediabunny",
"version": "1.47.0",
"version": "1.48.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mediabunny",
"version": "1.47.0",
"version": "1.48.0",
"license": "MPL-2.0",
"workspaces": [
".",
@@ -12864,7 +12864,7 @@
},
"packages/aac-encoder": {
"name": "@mediabunny/aac-encoder",
"version": "1.47.0",
"version": "1.48.0",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
@@ -12879,7 +12879,7 @@
},
"packages/ac3": {
"name": "@mediabunny/ac3",
"version": "1.47.0",
"version": "1.48.0",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
@@ -12894,7 +12894,7 @@
},
"packages/flac-encoder": {
"name": "@mediabunny/flac-encoder",
"version": "1.47.0",
"version": "1.48.0",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
@@ -12909,7 +12909,7 @@
},
"packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder",
"version": "1.47.0",
"version": "1.48.0",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
@@ -12924,7 +12924,7 @@
},
"packages/server": {
"name": "@mediabunny/server",
"version": "1.47.0",
"version": "1.48.0",
"license": "MPL-2.0",
"dependencies": {
"node-av": "^6.0.0"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "mediabunny",
"author": "Vanilagy",
"version": "1.47.0",
"version": "1.48.0",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module",
"workspaces": [
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/aac-encoder",
"author": "Vanilagy",
"version": "1.47.0",
"version": "1.48.0",
"description": "AAC encoder extension for Mediabunny, based on FFmpeg.",
"main": "./dist/bundles/mediabunny-aac-encoder.mjs",
"module": "./dist/bundles/mediabunny-aac-encoder.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/ac3",
"author": "Vanilagy",
"version": "1.47.0",
"version": "1.48.0",
"description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on FFmpeg.",
"main": "./dist/bundles/mediabunny-ac3.mjs",
"module": "./dist/bundles/mediabunny-ac3.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/flac-encoder",
"author": "Vanilagy",
"version": "1.47.0",
"version": "1.48.0",
"description": "FLAC encoder extension for Mediabunny, based on libFLAC.",
"main": "./dist/bundles/mediabunny-flac-encoder.mjs",
"module": "./dist/bundles/mediabunny-flac-encoder.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/mp3-encoder",
"author": "Vanilagy",
"version": "1.47.0",
"version": "1.48.0",
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/server",
"author": "Vanilagy",
"version": "1.47.0",
"version": "1.48.0",
"description": "Adds full video and audio decoder and encoder support to Mediabunny for use in server-side environments (Node, Bun, Deno). Based on NodeAV.",
"main": "./dist/bundles/mediabunny-server.cjs",
"module": "./dist/bundles/mediabunny-server.mjs",
+4
View File
@@ -215,6 +215,10 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking {
return false;
}
getUnixTimeForTimestamp() {
return null;
}
getPairingMask() {
return 1n;
}
+4
View File
@@ -595,6 +595,10 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
return false;
}
getUnixTimeForTimestamp() {
return null;
}
getPairingMask() {
return 1n;
}
+8 -1
View File
@@ -77,12 +77,15 @@ export class HlsDemuxer extends Demuxer {
readMetadata() {
return this.metadataPromise ??= (async () => {
assert(this.input._rootSource instanceof PathedSource);
const { rootPath } = this.input._rootSource;
const slice = await this.input._reader.requestEntireFile();
assert(slice);
const lines = readAllLines(slice, slice.length, { ignore: canIgnoreLine });
// Important: get the root path AFTER reading data to get the final root path, possibly affected by
// redirects. Any follow requests should be related to the redirected path, not the original one.
const { rootPath } = this.input._rootSource;
const variantStreams: {
fullPath: string;
attributes: AttributeList;
@@ -756,6 +759,10 @@ abstract class HlsInputTrackBacking implements InputTrackBacking {
return this.delegate(() => this.internalTrack.backingTrack!.isRelativeToUnixEpoch());
}
getUnixTimeForTimestamp(timestamp: number): MaybePromise<number | null> {
return this.delegate(() => this.internalTrack.backingTrack!.getUnixTimeForTimestamp(timestamp));
}
getBitrate(): number | null {
return this.internalTrack.peakBitrate;
}
+32 -12
View File
@@ -20,7 +20,7 @@ import {
base64ToBytes,
} from '../misc';
import { readAllLines, readBytes, Reader } from '../reader';
import { CustomPathedSource, ReadableStreamSource, SourceRef, SourceRequest } from '../source';
import { CustomPathedSource, PathedSource, ReadableStreamSource, SourceRef, SourceRequest } from '../source';
import { HlsDemuxer } from './hls-demuxer';
import {
AttributeList,
@@ -68,6 +68,7 @@ export type HlsSegmentLocation = {
};
export class HlsSegmentedInput extends SegmentedInput {
rootPath: string;
demuxer: HlsDemuxer;
segments: HlsSegment[] = [];
nextLines: string[] | null = null;
@@ -84,6 +85,7 @@ export class HlsSegmentedInput extends SegmentedInput {
) {
super(demuxer.input, path, trackDeclarations);
this.rootPath = path;
this.demuxer = demuxer;
this.nextLines = lines;
}
@@ -126,16 +128,24 @@ export class HlsSegmentedInput extends SegmentedInput {
this.nextLines = null;
if (!lines) {
using ref = await this.demuxer.input._getSourceUncached({ path: this.path, isRoot: false });
using ref = await this.demuxer.input._getSourceUncached({ path: this.rootPath, isRoot: false });
const reader = new Reader(ref.source);
const slice = await reader.requestEntireFile();
assert(slice);
lines = readAllLines(slice, slice.length, { ignore: canIgnoreLine });
if (ref.source instanceof PathedSource) {
// Copy back the source's path to become aware of potential redirects
this.rootPath = ref.source.rootPath;
}
}
const offsetTimestampsByDateTime = this.input._formatOptions.hls?.offsetTimestampsByDateTime !== false;
let headerRead = false;
let accumulatedTime = 0;
let accumulatedUnixTime: number | null = null;
let nextSegmentDuration: number | null = null;
let currentKey: HlsEncryptionInfo | null = null;
let nextSequenceNumber = 0;
@@ -182,6 +192,9 @@ export class HlsSegmentedInput extends SegmentedInput {
currentFirstSegment = prevLastSegment.firstSegment;
currentInitSegment = prevLastSegment.initSegment;
lastProgramDateTimeSeconds = prevLastSegment.lastProgramDateTimeSeconds;
accumulatedUnixTime = prevLastSegment.unixEpochTimestamp !== null
? prevLastSegment.unixEpochTimestamp + prevLastSegment.duration
: null;
prevLastSegment = null;
}
}
@@ -219,7 +232,7 @@ export class HlsSegmentedInput extends SegmentedInput {
key = { ...key, iv };
}
const fullPath = joinPaths(this.path, line);
const fullPath = joinPaths(this.rootPath, line);
const location: HlsSegmentLocation = {
path: fullPath,
offset: nextByteRange?.offset ?? 0,
@@ -228,7 +241,7 @@ export class HlsSegmentedInput extends SegmentedInput {
const segment: HlsSegment = {
timestamp: accumulatedTime,
relativeToUnixEpoch: lastProgramDateTimeSeconds !== null,
unixEpochTimestamp: accumulatedUnixTime,
firstSegment: currentFirstSegment,
sequenceNumber: nextSequenceNumber,
location,
@@ -240,6 +253,9 @@ export class HlsSegmentedInput extends SegmentedInput {
currentFirstSegment ??= segment;
accumulatedTime += nextSegmentDuration;
if (accumulatedUnixTime !== null) {
accumulatedUnixTime += nextSegmentDuration;
}
this.segments.push(segment);
} else {
@@ -299,7 +315,7 @@ export class HlsSegmentedInput extends SegmentedInput {
}
if (!prevLastSegment) {
const fullPath = joinPaths(this.path, uri);
const fullPath = joinPaths(this.rootPath, uri);
const location: HlsSegmentLocation = {
path: fullPath,
offset: parsedByteRange?.offset ?? 0,
@@ -313,7 +329,7 @@ export class HlsSegmentedInput extends SegmentedInput {
const segment: HlsSegment = {
timestamp: accumulatedTime,
relativeToUnixEpoch: lastProgramDateTimeSeconds !== null,
unixEpochTimestamp: accumulatedUnixTime,
firstSegment: null,
sequenceNumber: null,
location,
@@ -375,7 +391,7 @@ export class HlsSegmentedInput extends SegmentedInput {
currentKey = {
method: 'AES-128',
keyUri: joinPaths(this.path, uri),
keyUri: joinPaths(this.rootPath, uri),
iv,
keyFormat,
};
@@ -471,15 +487,19 @@ export class HlsSegmentedInput extends SegmentedInput {
const offset = dateTimeSeconds - lastSegmentEnd;
for (const segment of this.segments) {
segment.timestamp += offset;
segment.relativeToUnixEpoch = true;
segment.unixEpochTimestamp = segment.timestamp + offset;
if (offsetTimestampsByDateTime) {
segment.timestamp = segment.unixEpochTimestamp;
}
}
accumulatedTime += offset;
}
lastProgramDateTimeSeconds = dateTimeSeconds;
accumulatedTime = dateTimeSeconds; // Snap the accumulated time to the datetime
accumulatedUnixTime = dateTimeSeconds;
if (offsetTimestampsByDateTime) {
accumulatedTime = dateTimeSeconds; // Snap the accumulated time into Unix space
}
} else if (line === TAG_DISCONTINUITY) {
currentFirstSegment = null;
// Note: the init segment is not reset; the #EXT-X-MAP statement simply lasts until the next
+2
View File
@@ -197,6 +197,7 @@ export {
IsobmffInputFormat,
type IsobmffInputFormatOptions,
HlsInputFormat,
type HlsInputFormatOptions,
MatroskaInputFormat,
Mp3InputFormat,
Mp4InputFormat,
@@ -270,6 +271,7 @@ export {
EncodedPacketSink,
type PacketRetrievalOptions,
VideoSampleSink,
type VideoSinkDecoderOptions,
type WrappedAudioBuffer,
type WrappedCanvas,
} from './media-sink';
+33
View File
@@ -727,6 +727,8 @@ export const HLS_FORMATS: InputFormat[] = [HLS, MP4, QTFF, MP3, ADTS, MPEG_TS];
export type InputFormatOptions = {
/** ISOBMFF-specific configuration. */
isobmff?: IsobmffInputFormatOptions;
/** HLS-specific configuration. */
hls?: HlsInputFormatOptions;
};
/**
@@ -755,6 +757,26 @@ export type IsobmffInputFormatOptions = {
_suppressPsshParsing?: boolean;
};
/**
* Additional HLS input configuration.
* @group Input formats
* @public
*/
export type HlsInputFormatOptions = {
/**
* Whether, in the presence of `#EXT-X-PROGRAM-DATE-TIME` tags, to offset track and packet timestamps to be relative
* to the Unix epoch.
*
* Defaults to `true`, meaning packet timestamps map directly to wall-clock time. This guarantees AV sync across
* multiple tracks, even with gaps present.
*
* When you don't want this mapping, you can set this value to `false`. In addition to timestamps not being Unix
* timestamps anymore, any gaps in the playlist are also naturally removed. When `false`, you can still access the
* wall-clock Unix timestamps via {@link InputTrack.getUnixTimeForTimestamp}.
*/
offsetTimestampsByDateTime?: boolean;
};
export const validateInputFormatOptions = (options: InputFormatOptions, prefix: string) => {
if (!options || typeof options !== 'object') {
throw new TypeError(`${prefix}, when provided, must be an object.`);
@@ -767,4 +789,15 @@ export const validateInputFormatOptions = (options: InputFormatOptions, prefix:
throw new TypeError(`${prefix}.isobmff.resolveKeyId, when provided, must be a function.`);
}
}
if (options.hls !== undefined) {
if (!options.hls || typeof options.hls !== 'object') {
throw new TypeError(`${prefix}.hls, when provided, must be an object.`);
}
if (
options.hls.offsetTimestampsByDateTime !== undefined
&& typeof options.hls.offsetTimestampsByDateTime !== 'boolean'
) {
throw new TypeError(`${prefix}.hls.offsetTimestampsByDateTime, when provided, must be a boolean.`);
}
}
};
+23
View File
@@ -42,6 +42,7 @@ export interface InputTrackBacking {
getLanguageCode(): MaybePromise<string>;
getTimeResolution(): MaybePromise<number>;
isRelativeToUnixEpoch(): MaybePromise<boolean>;
getUnixTimeForTimestamp(timestamp: number): MaybePromise<number | null>;
getDisposition(): MaybePromise<TrackDisposition>;
getPairingMask(): bigint;
getBitrate(): MaybePromise<number | null>;
@@ -203,6 +204,28 @@ export abstract class InputTrack {
return this._backing.isRelativeToUnixEpoch();
}
/**
* Returns the Unix time (in seconds since January 1, 1970 00:00:00 UTC) that the given track timestamp (in seconds)
* maps to, or `null` if there is no such mapping. This provides a piecewise-continuous mapping from this track's
* timestamp space into wall-clock time. Such mapping exists, for example, for HLS playlists with
* `#EXT-X-PROGRAM-DATE-TIME` tags present.
*
* This mapping can be available even when {@link InputTrack.isRelativeToUnixEpoch} is `false`, for example for HLS
* streams with program date time information but with {@link HlsInputFormatOptions.offsetTimestampsByDateTime}
* set to `false`.
*/
async getUnixTimeForTimestamp(timestamp: number): Promise<number | null> {
return this._backing.getUnixTimeForTimestamp(timestamp);
}
/**
* Whether the track's timestamps can be mapped to Unix wall clock time via
* {@link InputTrack.getUnixTimeForTimestamp}.
*/
async hasUnixTimeMapping(): Promise<boolean> {
return (await this._backing.getUnixTimeForTimestamp(await this.getFirstTimestamp())) !== null;
}
/** Returns the track's disposition, i.e. information about its intended usage. */
async getDisposition() {
return this._backing.getDisposition();
+4
View File
@@ -2811,6 +2811,10 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
return false;
}
getUnixTimeForTimestamp() {
return null;
}
getDisposition() {
return this.internalTrack.disposition;
}
+4
View File
@@ -1953,6 +1953,10 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
return false;
}
getUnixTimeForTimestamp() {
return null;
}
getDisposition() {
return this.internalTrack.disposition;
}
+54 -3
View File
@@ -1749,6 +1749,42 @@ const colorAlphaMergerWorkerCode = () => {
};
};
/**
* Describes additional decoder preferences for video sinks.
* @group Media sinks
* @public
*/
export type VideoSinkDecoderOptions = {
/**
* A hint that configures the hardware acceleration method of the decoder. This is best left on `'no-preference'`,
* the default.
*/
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
/**
* Hint that the selected decoder should be configured to minimize the number of packets that have to be decoded
* before video frames are output.
*/
optimizeForLatency?: boolean;
};
const validateVideoSinkDecoderOptions = (decoderOptions: VideoSinkDecoderOptions) => {
if (!decoderOptions || typeof decoderOptions !== 'object') {
throw new TypeError('decoderOptions must be an object.');
}
if (
decoderOptions.hardwareAcceleration !== undefined
&& !['no-preference', 'prefer-hardware', 'prefer-software'].includes(decoderOptions.hardwareAcceleration)
) {
throw new TypeError(
'decoderOptions.hardwareAcceleration, when provided, must be \'no-preference\', \'prefer-hardware\' or'
+ ' \'prefer-software\'.',
);
}
if (decoderOptions.optimizeForLatency !== undefined && typeof decoderOptions.optimizeForLatency !== 'boolean') {
throw new TypeError('decoderOptions.optimizeForLatency, when provided, must be a boolean.');
}
};
/**
* A sink that retrieves decoded video samples (video frames) from a video track.
* @group Media sinks
@@ -1757,16 +1793,20 @@ const colorAlphaMergerWorkerCode = () => {
export class VideoSampleSink extends BaseMediaSampleSink<VideoSample> {
/** @internal */
_track: InputVideoTrack;
/** @internal */
_decoderOptions: VideoSinkDecoderOptions;
/** Creates a new {@link VideoSampleSink} for the given {@link InputVideoTrack}. */
constructor(videoTrack: InputVideoTrack) {
constructor(videoTrack: InputVideoTrack, decoderOptions: VideoSinkDecoderOptions = {}) {
if (!(videoTrack instanceof InputVideoTrack)) {
throw new TypeError('videoTrack must be an InputVideoTrack.');
}
validateVideoSinkDecoderOptions(decoderOptions);
super();
this._track = videoTrack;
this._decoderOptions = decoderOptions;
}
/** @internal */
@@ -1783,10 +1823,16 @@ export class VideoSampleSink extends BaseMediaSampleSink<VideoSample> {
const codec = await this._track.getCodec();
const rotation = await this._track.getRotation();
const decoderConfig = await this._track.getDecoderConfig();
let decoderConfig = await this._track.getDecoderConfig();
const timeResolution = await this._track.getTimeResolution();
assert(codec && decoderConfig);
decoderConfig = {
...decoderConfig,
hardwareAcceleration: this._decoderOptions.hardwareAcceleration,
optimizeForLatency: this._decoderOptions.optimizeForLatency,
};
return new VideoDecoderWrapper(onSample, onError, codec, decoderConfig, rotation, timeResolution);
}
@@ -1903,6 +1949,8 @@ export type CanvasSinkOptions = {
* canvas is created each time.
*/
poolSize?: number;
/** Additional preferences for the underlying video decoder. */
decoderOptions?: VideoSinkDecoderOptions;
};
/**
@@ -1982,12 +2030,15 @@ export class CanvasSink {
) {
throw new TypeError('poolSize must be a non-negative integer.');
}
if (options.decoderOptions !== undefined) {
validateVideoSinkDecoderOptions(options.decoderOptions);
}
this._videoTrack = videoTrack;
this._alpha = options.alpha ?? false;
this._options = options;
this._fit = options.fit ?? 'fill';
this._videoSampleSink = new VideoSampleSink(videoTrack);
this._videoSampleSink = new VideoSampleSink(videoTrack, options.decoderOptions);
this._canvasPool = Array.from({ length: options.poolSize ?? 0 }, () => null);
}
+4
View File
@@ -264,6 +264,10 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
return false;
}
getUnixTimeForTimestamp() {
return null;
}
getPairingMask() {
return 1n;
}
+4
View File
@@ -1103,6 +1103,10 @@ abstract class MpegTsTrackBacking implements InputTrackBacking {
return false;
}
getUnixTimeForTimestamp() {
return null;
}
getPairingMask() {
return 1n;
}
+4
View File
@@ -454,6 +454,10 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
return false;
}
getUnixTimeForTimestamp() {
return null;
}
getPairingMask() {
return 1n;
}
+23 -2
View File
@@ -41,7 +41,12 @@ export type AssociatedGroup = {
export type Segment = {
timestamp: number;
duration: number;
relativeToUnixEpoch: boolean;
/**
* The Unix time (in seconds) corresponding to this segment's start timestamp, or null if unknown. This is computed
* whenever the source provides wall-clock information (e.g. HLS program date time), even if the segment timestamps
* themselves are not shifted into Unix time space.
*/
unixEpochTimestamp: number | null;
firstSegment: Segment | null;
};
@@ -97,6 +102,18 @@ export abstract class SegmentedInput {
return lastSegment.timestamp + lastSegment.duration;
}
async getUnixTimeForTimestamp(timestamp: number): Promise<number | null> {
let segment = await this.getSegmentAt(timestamp, {});
segment ??= await this.getFirstSegment({});
if (!segment || segment.unixEpochTimestamp === null) {
return null;
}
const elapsed = timestamp - segment.timestamp;
return segment.unixEpochTimestamp + elapsed;
}
async getTrackBackings(): Promise<InputTrackBacking[]> {
return this.trackBackingsPromise ??= (async () => {
const backings: InputTrackBacking[] = [];
@@ -310,7 +327,11 @@ class SegmentedInputInputTrackBacking implements InputTrackBacking {
await this.hydrate();
assert(this.segmentedInput.firstSegment);
return this.segmentedInput.firstSegment.relativeToUnixEpoch;
return this.segmentedInput.firstSegment.unixEpochTimestamp === this.segmentedInput.firstSegment.timestamp;
}
getUnixTimeForTimestamp(timestamp: number) {
return this.segmentedInput.getUnixTimeForTimestamp(timestamp);
}
getBitrate() {
+26 -5
View File
@@ -289,10 +289,15 @@ export class SourceRef<S extends Source = Source> implements Disposable {
*/
export abstract class PathedSource extends Source {
constructor(
/** The path that points to the root file; the entry file of the media. */
/**
* The path that points to the root file; the entry file of the media.
*
* This path may be modified by the source to indicate a redirect: an updated path to perform new requests
* relative to.
*/
public rootPath: FilePath,
/** The callback that is called for each requested file; must return a {@link Source} or {@link SourceRef}. */
public requestHandler: (request: SourceRequest) => MaybePromise<Source | SourceRef>,
public readonly requestHandler: (request: SourceRequest) => MaybePromise<Source | SourceRef>,
) {
if (typeof rootPath !== 'string') {
throw new TypeError('rootPath must be a string.');
@@ -778,7 +783,10 @@ export class UrlSource extends PathedSource {
? url.href
: url;
super(urlString, request => new UrlSource(request.path, this._options));
super(
urlString,
request => new UrlSource(request.path, this._options),
);
this._url = url;
this._options = options;
@@ -905,6 +913,11 @@ export class UrlSource extends PathedSource {
throw new Error(`Error fetching ${String(this._url)}: ${response.status} ${response.statusText}`);
}
if (response.redirected) {
// Modify our own root path so that future subrequests get made relative to the redirected URL
this.rootPath = response.url;
}
outer:
if (this._orchestrator.fileSize === null) {
// See if we can deduce the file size from the response
@@ -1989,6 +2002,14 @@ class ReadOrchestrator {
} satisfies ReadResult));
} else {
// The requested region was satisfied by the cache, but the entire prefetch region was not
promise.catch((error) => {
if (this.disposed) {
return; // Swallow the error
}
// Nobody's awaiting this result but an errored read is still notable
throw error;
});
}
return result;
@@ -2103,12 +2124,12 @@ class ReadOrchestrator {
if (worker.pendingSlices.length > 0) {
worker.pendingSlices.forEach(x => x.reject(error)); // Make sure to propagate any errors
worker.pendingSlices.length = 0;
} else {
} else if (!worker.aborted && !this.disposed) {
throw error; // So it doesn't get swallowed
}
})
.finally(() => {
if (worker.running) {
if (worker.running || this.workers.length >= this.options.maxWorkerCount) {
// Rare, but can happen with multiple concurrent reads. In this case, don't do anything.
return;
}
+4
View File
@@ -402,6 +402,10 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking {
return false;
}
getUnixTimeForTimestamp() {
return null;
}
getPairingMask() {
return 1n;
}
+46
View File
@@ -526,11 +526,14 @@ test.concurrent('Single-value PDT', { timeout: 15_000 }, async () => {
const tracks = await input.getTracks();
expect((await Promise.all(tracks.map(x => x.isRelativeToUnixEpoch()))).every(x => x)).toBe(true);
expect((await Promise.all(tracks.map(x => x.hasUnixTimeMapping()))).every(x => x)).toBe(true);
const track = tracks[0]!;
const firstTimestamp = await track.getFirstTimestamp();
expect(firstTimestamp).toBe(Date.parse('2013-05-08T17:40:50Z') / 1000);
expect(await track.getUnixTimeForTimestamp(firstTimestamp)).toBe(firstTimestamp);
const endTimestamp = await track.computeDuration();
expect(endTimestamp).toBe(firstTimestamp + 50);
@@ -600,6 +603,49 @@ test.concurrent('PDT with bad values', { timeout: 15_000 }, async () => {
expect(await audioTrack.isRelativeToUnixEpoch()).toBe(false);
});
test.concurrent('Single-value PDT with unix offsets disabled', { timeout: 15_000 }, async () => {
using input = new Input({
source: new UrlSource('https://playertest.longtailvideo.com/adaptive/aviion/manifest.m3u8'),
formats: ALL_FORMATS,
formatOptions: {
hls: {
offsetTimestampsByDateTime: false,
},
},
});
const tracks = await input.getTracks();
expect((await Promise.all(tracks.map(x => x.isRelativeToUnixEpoch()))).every(x => x)).toBe(false);
expect((await Promise.all(tracks.map(x => x.hasUnixTimeMapping()))).every(x => x)).toBe(true);
const track = tracks[0]!;
const firstTimestamp = await track.getFirstTimestamp();
expect(firstTimestamp).toBe(0);
const endTimestamp = await track.computeDuration();
expect(endTimestamp).toBe(firstTimestamp + 50);
const firstPacket = await new EncodedPacketSink(track).getFirstPacket();
assert(firstPacket);
expect(firstPacket.timestamp).toBe(firstTimestamp); // Kinda obvious check tbh
const unixStartTime = await track.getUnixTimeForTimestamp(firstTimestamp);
expect(unixStartTime).toBe(Date.parse('2013-05-08T17:40:50Z') / 1000);
const unixEndTime = await track.getUnixTimeForTimestamp(endTimestamp);
expect(unixEndTime).toBe(Date.parse('2013-05-08T17:41:40Z') / 1000);
const unixTimeBeforeStart = await track.getUnixTimeForTimestamp(firstTimestamp - 10);
expect(unixTimeBeforeStart).toBe(Date.parse('2013-05-08T17:40:40Z') / 1000);
const unixTimeAfterEnd = await track.getUnixTimeForTimestamp(endTimestamp + 10);
expect(unixTimeAfterEnd).toBe(Date.parse('2013-05-08T17:41:50Z') / 1000);
const timestampDt = 0.001;
const unixTimeDt = (await track.getUnixTimeForTimestamp(firstTimestamp + 0.001))! - unixStartTime!;
expect(unixTimeDt).toBeCloseTo(timestampDt);
});
test.concurrent('Alternative audio only', { timeout: 15_000 }, async () => {
using input = new Input({
source: new UrlSource('https://playertest.longtailvideo.com/adaptive/alt-audio-no-video/sintel/playlist.m3u8'),