diff --git a/dev/demux.html b/dev/demux.html
index 30cff7b..126237b 100644
--- a/dev/demux.html
+++ b/dev/demux.html
@@ -33,11 +33,12 @@
Mediabunny.ALL_FORMATS,
);
- const [track] = await manifest.getTracks();
- console.log(track);
+ const tracks = await manifest.getTracks();
+ console.log(tracks);
+ const [track] = tracks;
window.kekw = () => {
- console.log(track.getFirstTimestamp())
+ console.log(track.getDurationFromMetadata())
};
return;
diff --git a/scripts/generate-api-docs.ts b/scripts/generate-api-docs.ts
index 250df45..8876810 100644
--- a/scripts/generate-api-docs.ts
+++ b/scripts/generate-api-docs.ts
@@ -68,6 +68,10 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
const classHierarchy = new Map(); // Maps parent class to array of subclasses
const classInstances = new Map(); // Maps class name to array of instance variable names
+ const hasDeprecatedTag = (node: ts.Node): boolean => {
+ return ts.getJSDocTags(node).some(tag => tag.tagName.text === 'deprecated');
+ };
+
const collectExportedTypes = (module: ts.Symbol, visited = new Set()): void => {
if (visited.has(module)) return;
visited.add(module);
@@ -77,12 +81,12 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
const declaration = exportSymbol.valueDeclaration || exportSymbol.declarations?.[0];
if (!declaration) return;
- // Collect classes, interfaces, types, enums, variables (only if @public)
+ // Collect classes, interfaces, types, enums, variables (only if @public and not @deprecated)
if (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)
|| ts.isTypeAliasDeclaration(declaration) || ts.isEnumDeclaration(declaration)
|| ts.isVariableDeclaration(declaration)) {
const hasPublicTag = ts.getJSDocTags(declaration).some(tag => tag.tagName.text === 'public');
- if (hasPublicTag) {
+ if (hasPublicTag && !hasDeprecatedTag(declaration)) {
exportedTypes.add(exportSymbol.getName());
}
}
@@ -92,9 +96,9 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
const aliasedSymbol = typeChecker.getAliasedSymbol(exportSymbol);
const aliasedDeclaration = aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0];
if (aliasedDeclaration) {
- // Check if the aliased symbol has @public tag
+ // Check if the aliased symbol has @public tag and is not deprecated
const hasPublicTag = ts.getJSDocTags(aliasedDeclaration).some(tag => tag.tagName.text === 'public');
- if (hasPublicTag) {
+ if (hasPublicTag && !hasDeprecatedTag(aliasedDeclaration)) {
exportedTypes.add(exportSymbol.getName());
}
@@ -133,10 +137,10 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
}
}
}
- // Otherwise, add any symbol with @public tag (we'll filter by type later)
+ // Otherwise, add any symbol with @public tag and not @deprecated (we'll filter by type later)
else {
const hasPublicTag = ts.getJSDocTags(declaration).some(tag => tag.tagName.text === 'public');
- if (hasPublicTag) {
+ if (hasPublicTag && !hasDeprecatedTag(declaration)) {
symbols.push(exportSymbol);
}
}
@@ -621,9 +625,10 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
const nodeKind = ts.SyntaxKind[declaration.kind];
const symbolName = (declaration as any).name?.getText() || exportSymbol.getName();
- // Only process symbols with @public tag
+ // Only process symbols with @public tag, and skip deprecated ones entirely
const hasPublicTag = ts.getJSDocTags(declaration).some(tag => tag.tagName.text === 'public');
if (!hasPublicTag) return;
+ if (hasDeprecatedTag(declaration)) return;
// Check for @group tag (handle re-exports by looking at the original declaration)
let targetDeclaration = declaration;
@@ -766,6 +771,8 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
const events: string[] = [];
const methods: string[] = [];
const staticMethods: string[] = [];
+ const deprecatedProperties: string[] = [];
+ const deprecatedMethods: string[] = [];
let constructor: string | null = null;
let extendsClause = '';
let implementsClause = '';
@@ -1021,6 +1028,53 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
const hasInternalTag = ts.getJSDocTags(member).some(tag => tag.tagName.text === 'internal');
if (hasInternalTag) return;
+ const isDeprecatedMember = hasDeprecatedTag(member);
+
+ const getDeprecationNotice = () => {
+ const tag = ts.getJSDocTags(member).find(t => t.tagName.text === 'deprecated');
+ let text = '';
+ if (tag?.comment) {
+ if (typeof tag.comment === 'string') {
+ text = processLinkTags(tag.comment.trim(), className);
+ } else {
+ // comment is a NodeArray of JSDocComment elements (text + inline tags)
+ const raw = tag.comment.map((part) => {
+ if (ts.isJSDocLinkLike(part)) {
+ const linkName = part.name?.getText() ?? '';
+ const linkText = part.text?.trim() ?? '';
+ // Reconstruct as {@link Name text}
+ return `{@link ${linkName}${linkText ? ' ' + linkText : ''}}`;
+ }
+ return part.text ?? '';
+ }).join('');
+ text = processLinkTags(raw.trim(), className);
+ }
+ }
+ return text ? `> **Deprecated.** ${text}\n\n` : '> **Deprecated.**\n\n';
+ };
+
+ const addDeprecationNotice = (content: string) => {
+ // Insert the deprecation notice right after the heading line
+ const headingEnd = content.indexOf('\n');
+ return content.slice(0, headingEnd) + '\n\n' + getDeprecationNotice() + content.slice(headingEnd + 1);
+ };
+
+ const pushProperty = (content: string) => {
+ if (isDeprecatedMember) {
+ deprecatedProperties.push(addDeprecationNotice(content));
+ } else {
+ properties.push(content);
+ }
+ };
+
+ const pushMethod = (content: string) => {
+ if (isDeprecatedMember) {
+ deprecatedMethods.push(addDeprecationNotice(content));
+ } else {
+ methods.push(content);
+ }
+ };
+
if (ts.isConstructorDeclaration(member) && !isAbstract) {
// Skip private constructors
const isPrivate = member.modifiers?.some(mod => mod.kind === ts.SyntaxKind.PrivateKeyword);
@@ -1231,7 +1285,7 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
if (isEventHandler) {
events.push(propertyContent);
} else {
- properties.push(propertyContent);
+ pushProperty(propertyContent);
}
} else if (ts.isGetAccessorDeclaration(member) && member.name) {
const name = member.name.getText();
@@ -1264,7 +1318,7 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
references.forEach(ref => addUsage(ref, className, name, 'property'));
const inheritedBadge = '';
- properties.push(`### \`${name}\`${inheritedBadge}\n\n\`\`\`ts\n${accessorDef}\n\`\`\`${desc ? `\n\n${desc}` : ''}${referencesText}`);
+ pushProperty(`### \`${name}\`${inheritedBadge}\n\n\`\`\`ts\n${accessorDef}\n\`\`\`${desc ? `\n\n${desc}` : ''}${referencesText}`);
} else if (ts.isSetAccessorDeclaration(member) && member.name) {
const name = member.name.getText();
const param = member.parameters[0];
@@ -1287,7 +1341,7 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
references.forEach(ref => addUsage(ref, className, name, 'property'));
const inheritedBadge = '';
- properties.push(`### \`${name}\`${inheritedBadge}\n\n\`\`\`ts\n${accessorDef}\n\`\`\`${desc ? `\n\n${desc}` : ''}${referencesText}`);
+ pushProperty(`### \`${name}\`${inheritedBadge}\n\n\`\`\`ts\n${accessorDef}\n\`\`\`${desc ? `\n\n${desc}` : ''}${referencesText}`);
} else if ((ts.isMethodDeclaration(member) || ts.isMethodSignature(member)) && member.name) {
const name = member.name.getText();
const isStatic = member.modifiers?.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword);
@@ -1385,7 +1439,7 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
if (isStatic) {
staticMethods.push(methodContent);
} else {
- methods.push(methodContent);
+ pushMethod(methodContent);
}
}
};
@@ -1497,20 +1551,27 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
}
}
- // Sort properties and methods alphabetically (but not for type aliases - keep source order)
+ // Sort properties and methods alphabetically, but with bracketed names like
+ // `[Symbol.dispose]()` always at the bottom. Type aliases keep source order.
+ const compareMemberNames = (a: string, b: string) => {
+ const nameA = a.match(/### `([^`]+)`/)?.[1] ?? '';
+ const nameB = b.match(/### `([^`]+)`/)?.[1] ?? '';
+ const aIsBracket = nameA.startsWith('[');
+ const bIsBracket = nameB.startsWith('[');
+ if (aIsBracket !== bIsBracket) {
+ return aIsBracket ? 1 : -1;
+ }
+ return nameA.localeCompare(nameB);
+ };
+
if (!ts.isTypeAliasDeclaration(declaration)) {
- properties.sort((a, b) => {
- const nameA = a.match(/### (.+)/)?.[1] || '';
- const nameB = b.match(/### (.+)/)?.[1] || '';
- return nameA.localeCompare(nameB);
- });
+ properties.sort(compareMemberNames);
+ methods.sort(compareMemberNames);
}
- staticMethods.sort((a, b) => {
- const nameA = a.match(/### (.+)/)?.[1] || '';
- const nameB = b.match(/### (.+)/)?.[1] || '';
- return nameA.localeCompare(nameB);
- });
+ staticMethods.sort(compareMemberNames);
+ deprecatedProperties.sort(compareMemberNames);
+ deprecatedMethods.sort(compareMemberNames);
let markdown = '';
@@ -1685,6 +1746,11 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
markdown += `\n## Methods\n\n${methods.join('\n\n')}\n`;
}
+ const deprecated = [...deprecatedProperties, ...deprecatedMethods];
+ if (deprecated.length > 0) {
+ markdown += `\n## Deprecated\n\n${deprecated.join('\n\n')}\n`;
+ }
+
generatedDocs.set(className, markdown);
}
});
diff --git a/src/hls/hls-demuxer.ts b/src/hls/hls-demuxer.ts
index 7144980..240479e 100644
--- a/src/hls/hls-demuxer.ts
+++ b/src/hls/hls-demuxer.ts
@@ -11,7 +11,6 @@ import { Demuxer, DurationMetadataRequestOptions } from '../demuxer';
import { Input } from '../input';
import {
InputAudioTrackBacking,
- InputTrack,
InputTrackBacking,
InputVideoTrackBacking,
} from '../input-track';
@@ -24,11 +23,12 @@ import { readAllLines } from '../reader';
import { AttributeList, canIgnoreLine } from './hls-misc';
import { HlsSegmentedInput } from './hls-segmented-input';
import { PathedSource } from '../source';
+import { SegmentedInputTrackDeclaration } from '../segmented-input';
type InternalTrack = {
id: number;
demuxer: HlsDemuxer;
- backingTrack: InputTrack | null;
+ backingTrack: InputTrackBacking | null;
default: boolean;
autoselect: boolean;
languageCode: string;
@@ -56,8 +56,8 @@ type InternalAudioTrack = InternalTrack & { info: { type: 'audio' } };
export class HlsDemuxer extends Demuxer {
metadataPromise: Promise | null = null;
- trackBackings: InputTrackBacking[] = [];
- internalTracks: InternalTrack[] = [];
+ trackBackings: InputTrackBacking[] | null = null;
+ internalTracks: InternalTrack[] | null = null;
segmentedInputs: HlsSegmentedInput[] = [];
hasMasterPlaylist = true;
@@ -161,13 +161,11 @@ export class HlsDemuxer extends Demuxer {
// iFramesOnlyTagFound = true;
} else if (line.startsWith('#EXTINF:')) {
// This is a media playlist, not a master playlist
- const segmentedInput = new HlsSegmentedInput(this, source.rootPath, lines);
+ const segmentedInput = new HlsSegmentedInput(this, source.rootPath, null, lines);
+
this.segmentedInputs = [segmentedInput];
this.hasMasterPlaylist = false;
-
- const input = segmentedInput.toInput();
- const demuxer = await input._getDemuxer();
- this.trackBackings = await demuxer.getTrackBackings();
+ this.trackBackings = await segmentedInput.getTrackBackings();
return;
}
@@ -197,17 +195,16 @@ export class HlsDemuxer extends Demuxer {
} else {
// No codecs were specified, we need to read the underlying media data
const segmentedInput = this.getSegmentedInputForPath(variantStream.fullPath);
- const input = segmentedInput.toInput();
- const tracks = await input.getTracks();
+ const trackBackings = await segmentedInput.getTrackBackings();
const tracksWithCodec = await Promise.all(
- tracks.map(async t => ({ track: t, codec: await t.getCodec() })),
+ trackBackings.map(async t => ({ track: t, codec: await t.getCodec() })),
);
codecStrings = await Promise.all(
tracksWithCodec
.filter(x => x.codec !== null)
- .map(x => x.track.getCodecParameterString()),
- ) as string[];
+ .map(x => x.track.getDecoderConfig().then(x => x!.codec)),
+ );
}
const videoGroupId = variantStream.attributes.get('video');
@@ -243,14 +240,14 @@ export class HlsDemuxer extends Demuxer {
const fullPath = joinPaths(source.rootPath, uri);
const segmentedInput = this.getSegmentedInputForPath(fullPath);
- const input = segmentedInput.toInput();
- const videoTrack = await input.getPrimaryVideoTrack();
+ const trackBackings = await segmentedInput.getTrackBackings();
+ const videoTrack = trackBackings.find(x => x.getType() === 'video');
if (!videoTrack || (await videoTrack.getCodec()) === null) {
return null;
}
- const codecParameterString = await videoTrack.getCodecParameterString();
+ const codecParameterString = await videoTrack.getDecoderConfig().then(x => x?.codec ?? null);
assert(codecParameterString !== null);
return codecParameterString;
}));
@@ -284,14 +281,14 @@ export class HlsDemuxer extends Demuxer {
const fullPath = joinPaths(source.rootPath, uri);
const segmentedInput = this.getSegmentedInputForPath(fullPath);
- const input = segmentedInput.toInput();
- const audioTrack = await input.getPrimaryAudioTrack();
+ const trackBackings = await segmentedInput.getTrackBackings();
+ const audioTrack = trackBackings.find(x => x.getType() === 'audio');
if (!audioTrack || (await audioTrack.getCodec()) === null) {
return null;
}
- const codecParameterString = await audioTrack.getCodecParameterString();
+ const codecParameterString = await audioTrack.getDecoderConfig().then(x => x?.codec ?? null);
assert(codecParameterString !== null);
return codecParameterString;
}));
@@ -563,6 +560,7 @@ export class HlsDemuxer extends Demuxer {
// Order tracks by how they appear in the file
internalTracks.sort((a, b) => a.lineNumber - b.lineNumber);
+ this.trackBackings = [];
for (const internalTrack of internalTracks) {
if (internalTrack.info.type === 'video') {
this.trackBackings.push(
@@ -581,6 +579,8 @@ export class HlsDemuxer extends Demuxer {
async getTrackBackings() {
await this.readMetadata();
+ assert(this.trackBackings);
+
return this.trackBackings;
}
@@ -590,7 +590,16 @@ export class HlsDemuxer extends Demuxer {
return segmentedInput;
}
- segmentedInput = new HlsSegmentedInput(this, path, null);
+ let decls: SegmentedInputTrackDeclaration[] | null = null;
+ if (this.internalTracks) {
+ const tracks = this.internalTracks.filter(x => x.fullPath === path);
+ decls = tracks.map(x => ({
+ id: x.id,
+ type: x.info.type,
+ }));
+ }
+
+ segmentedInput = new HlsSegmentedInput(this, path, decls, null);
this.segmentedInputs.push(segmentedInput);
return segmentedInput;
@@ -605,10 +614,12 @@ export class HlsDemuxer extends Demuxer {
}
override dispose(): void {
- for (const segInput of this.segmentedInputs) {
- segInput.dispose();
+ if (this.segmentedInputs) {
+ for (const segInput of this.segmentedInputs) {
+ segInput.dispose();
+ }
+ this.segmentedInputs.length = 0;
}
- this.segmentedInputs.length = 0;
}
}
@@ -623,25 +634,40 @@ abstract class HlsInputTrackBacking implements InputTrackBacking {
hydrate() {
return this.hydrationPromise ??= (async () => {
const segmentedInput = this.internalTrack.demuxer.getSegmentedInputForPath(this.internalTrack.fullPath);
- const input = segmentedInput.toInput();
- let track: InputTrack | null;
- if (this instanceof HlsInputVideoTrackBacking) {
- track = await input.getPrimaryVideoTrack({
- filter: async t => (await t.getCodec()) === this.getCodec(),
- });
+ let trackBacking: InputTrackBacking | null = null;
+
+ const trackBackings = await segmentedInput.getTrackBackings();
+ const matchingType = trackBackings.filter(x => x.getType() === this.getType());
+
+ if (matchingType.length === 1) {
+ // Avoids reading fields on the track
+ trackBacking = matchingType[0]!;
} else {
- assert(this instanceof HlsInputAudioTrackBacking);
- track = await input.getPrimaryAudioTrack({
- filter: async t => (await t.getCodec()) === this.getCodec(),
- });
+ if (this instanceof HlsInputVideoTrackBacking) {
+ for (const backing of matchingType) {
+ if ((await backing.getCodec()) === this.getCodec()) {
+ trackBacking = backing;
+ break;
+ }
+ }
+ } else {
+ assert(this instanceof HlsInputAudioTrackBacking);
+
+ for (const backing of matchingType) {
+ if ((await backing.getCodec()) === this.getCodec()) {
+ trackBacking = backing;
+ break;
+ }
+ }
+ }
}
- if (!track) {
+ if (!trackBacking) {
throw new Error('Could not find matching track in underlying media data.');
}
- this.internalTrack.backingTrack = track;
+ this.internalTrack.backingTrack = trackBacking;
})();
}
@@ -688,6 +714,8 @@ abstract class HlsInputTrackBacking implements InputTrackBacking {
}
getNumber(): number {
+ assert(this.internalTrack.demuxer.internalTracks);
+
const trackType = this.internalTrack.info.type;
let number = 0;
for (const track of this.internalTrack.demuxer.internalTracks) {
@@ -704,11 +732,11 @@ abstract class HlsInputTrackBacking implements InputTrackBacking {
}
getTimeResolution(): MaybePromise {
- return this.delegate(() => this.internalTrack.backingTrack!._backing.getTimeResolution());
+ return this.delegate(() => this.internalTrack.backingTrack!.getTimeResolution());
}
isRelativeToUnixEpoch(): MaybePromise {
- return this.delegate(() => this.internalTrack.backingTrack!._backing.isRelativeToUnixEpoch());
+ return this.delegate(() => this.internalTrack.backingTrack!.isRelativeToUnixEpoch());
}
getBitrate(): number | null {
@@ -721,12 +749,12 @@ abstract class HlsInputTrackBacking implements InputTrackBacking {
async getDurationFromMetadata(options: DurationMetadataRequestOptions): Promise {
await this.hydrate();
- return this.internalTrack.backingTrack!._backing.getDurationFromMetadata(options);
+ return this.internalTrack.backingTrack!.getDurationFromMetadata(options);
}
async getLiveRefreshInterval(): Promise {
await this.hydrate();
- return this.internalTrack.backingTrack!._backing.getLiveRefreshInterval();
+ return this.internalTrack.backingTrack!.getLiveRefreshInterval();
}
getHasOnlyKeyPackets() {
@@ -735,27 +763,27 @@ abstract class HlsInputTrackBacking implements InputTrackBacking {
async getFirstPacket(options: PacketRetrievalOptions): Promise {
await this.hydrate();
- return this.internalTrack.backingTrack!._backing.getFirstPacket(options);
+ return this.internalTrack.backingTrack!.getFirstPacket(options);
}
async getPacket(timestamp: number, options: PacketRetrievalOptions): Promise {
await this.hydrate();
- return this.internalTrack.backingTrack!._backing.getPacket(timestamp, options);
+ return this.internalTrack.backingTrack!.getPacket(timestamp, options);
}
async getKeyPacket(timestamp: number, options: PacketRetrievalOptions): Promise {
await this.hydrate();
- return this.internalTrack.backingTrack!._backing.getKeyPacket(timestamp, options);
+ return this.internalTrack.backingTrack!.getKeyPacket(timestamp, options);
}
async getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise {
await this.hydrate();
- return this.internalTrack.backingTrack!._backing.getNextPacket(packet, options);
+ return this.internalTrack.backingTrack!.getNextPacket(packet, options);
}
async getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise {
await this.hydrate();
- return this.internalTrack.backingTrack!._backing.getNextKeyPacket(packet, options);
+ return this.internalTrack.backingTrack!.getNextKeyPacket(packet, options);
}
}
@@ -768,13 +796,12 @@ class HlsInputVideoTrackBacking
super(internalTrack);
}
- getType() {
- return 'video' as const;
+ get backingVideoTrack() {
+ return this.internalTrack.backingTrack as InputVideoTrackBacking | null;
}
- get backingVideoTrack() {
- const bt = this.internalTrack.backingTrack;
- return bt?.isVideoTrack() ? bt : null;
+ getType() {
+ return 'video' as const;
}
override getCodec(): VideoCodec | null {
@@ -783,19 +810,19 @@ class HlsInputVideoTrackBacking
}
getCodedWidth(): MaybePromise {
- return this.delegate(() => this.backingVideoTrack!._backing.getCodedWidth());
+ return this.delegate(() => this.backingVideoTrack!.getCodedWidth());
}
getCodedHeight(): MaybePromise {
- return this.delegate(() => this.backingVideoTrack!._backing.getCodedHeight());
+ return this.delegate(() => this.backingVideoTrack!.getCodedHeight());
}
getSquarePixelWidth(): MaybePromise {
- return this.delegate(() => this.backingVideoTrack!._backing.getSquarePixelWidth());
+ return this.delegate(() => this.backingVideoTrack!.getSquarePixelWidth());
}
getSquarePixelHeight(): MaybePromise {
- return this.delegate(() => this.backingVideoTrack!._backing.getSquarePixelHeight());
+ return this.delegate(() => this.backingVideoTrack!.getSquarePixelHeight());
}
getMetadataDisplayWidth(): number | null {
@@ -815,17 +842,17 @@ class HlsInputVideoTrackBacking
}
getRotation(): MaybePromise {
- return this.delegate(() => this.backingVideoTrack!._backing.getRotation());
+ return this.delegate(() => this.backingVideoTrack!.getRotation());
}
async getColorSpace(): Promise {
await this.hydrate();
- return this.backingVideoTrack!._backing.getColorSpace();
+ return this.backingVideoTrack!.getColorSpace();
}
async canBeTransparent(): Promise {
await this.hydrate();
- return this.backingVideoTrack!._backing.canBeTransparent();
+ return this.backingVideoTrack!.canBeTransparent();
}
getMetadataCodecParameterString(): string | null {
@@ -837,7 +864,7 @@ class HlsInputVideoTrackBacking
async getDecoderConfig(): Promise {
await this.hydrate();
- return this.backingVideoTrack!._backing.getDecoderConfig();
+ return this.backingVideoTrack!.getDecoderConfig();
}
}
@@ -850,13 +877,12 @@ class HlsInputAudioTrackBacking
super(internalTrack);
}
- getType() {
- return 'audio' as const;
+ get backingAudioTrack() {
+ return this.internalTrack.backingTrack as InputAudioTrackBacking | null;
}
- get backingAudioTrack() {
- const bt = this.internalTrack.backingTrack;
- return bt?.isAudioTrack() ? bt : null;
+ getType() {
+ return 'audio' as const;
}
override getCodec(): AudioCodec | null {
@@ -869,11 +895,11 @@ class HlsInputAudioTrackBacking
return this.internalTrack.info.numberOfChannels;
}
- return this.delegate(() => this.backingAudioTrack!._backing.getNumberOfChannels());
+ return this.delegate(() => this.backingAudioTrack!.getNumberOfChannels());
}
getSampleRate(): MaybePromise {
- return this.delegate(() => this.backingAudioTrack!._backing.getSampleRate());
+ return this.delegate(() => this.backingAudioTrack!.getSampleRate());
}
getMetadataCodecParameterString(): string | null {
@@ -885,7 +911,7 @@ class HlsInputAudioTrackBacking
async getDecoderConfig(): Promise {
await this.hydrate();
- return this.backingAudioTrack!._backing.getDecoderConfig();
+ return this.backingAudioTrack!.getDecoderConfig();
}
}
diff --git a/src/hls/hls-segmented-input.ts b/src/hls/hls-segmented-input.ts
index 579cd34..17ae125 100644
--- a/src/hls/hls-segmented-input.ts
+++ b/src/hls/hls-segmented-input.ts
@@ -8,7 +8,7 @@
import { AES_128_BLOCK_SIZE, createAes128CbcDecryptStream } from '../aes';
import { ENCRYPTION_KEY_CACHE_GROUP, Input } from '../input';
-import { Segment, SegmentedInput, SegmentRetrievalOptions } from '../segmented-input';
+import { Segment, SegmentedInput, SegmentedInputTrackDeclaration, SegmentRetrievalOptions } from '../segmented-input';
import { toDataView, joinPaths, last, assert, binarySearchLessOrEqual, arrayArgmin, wait } from '../misc';
import { readAllLines, readBytes, Reader } from '../reader';
import { PathedSource, ReadableStreamSource, SourceRef } from '../source';
@@ -51,9 +51,10 @@ export class HlsSegmentedInput extends SegmentedInput {
constructor(
demuxer: HlsDemuxer,
path: string,
+ trackDeclarations: SegmentedInputTrackDeclaration[] | null,
lines: string[] | null,
) {
- super(demuxer.input, path);
+ super(demuxer.input, path, trackDeclarations);
this.demuxer = demuxer;
this.nextLines = lines;
diff --git a/src/input-format.ts b/src/input-format.ts
index a563009..de09cc6 100644
--- a/src/input-format.ts
+++ b/src/input-format.ts
@@ -614,37 +614,6 @@ export class HlsInputFormat extends InputFormat {
}
}
-export class VirtualInputFormat extends InputFormat {
- /** @internal */
- _createDemuxerFn: (input: Input) => Demuxer;
-
- /** @internal */
- constructor(createDemuxer: (input: Input) => Demuxer) {
- super();
- this._createDemuxerFn = createDemuxer;
- }
-
- /** @internal */
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- async _canReadInput(input: Input) {
- return true;
- }
-
- /** @internal */
-
- _createDemuxer(input: Input): Demuxer {
- return this._createDemuxerFn(input);
- }
-
- get name() {
- return 'Virtual input format';
- }
-
- get mimeType() {
- return 'application/magic';
- }
-}
-
/**
* MP4 input format singleton.
* @group Input formats
diff --git a/src/input-track.ts b/src/input-track.ts
index c0347c5..a735425 100644
--- a/src/input-track.ts
+++ b/src/input-track.ts
@@ -77,7 +77,7 @@ export abstract class InputTrack {
/** The type of the track. */
abstract get type(): TrackType;
- /** The codec of the track's packets. */
+ /** Returns the codec of the track's packets. */
abstract getCodec(): Promise;
/**
* The codec of the track's packets.
@@ -94,7 +94,10 @@ export abstract class InputTrack {
* into its bitstream. Returns null if the type couldn't be determined.
*/
abstract determinePacketType(packet: EncodedPacket): Promise;
- /** Whether the track metadata says that this track only contains key packets. The actual packets may differ. */
+ /**
+ * Returns whether the track metadata says that this track only contains key packets. The actual packets may
+ * differ.
+ */
abstract getHasOnlyKeyPackets(): Promise;
/**
* Whether the track metadata says that this track only contains key packets. The actual packets may differ.
@@ -128,7 +131,7 @@ export abstract class InputTrack {
}
/**
- * The identifier of the codec used internally by the container. It is not homogenized by Mediabunny
+ * Returns the identifier of the codec used internally by the container. It is not homogenized by Mediabunny
* and depends entirely on the container format.
*
* This method can be used to determine the codec of a track in case Mediabunny doesn't know that codec.
@@ -153,7 +156,7 @@ export abstract class InputTrack {
}
/**
- * The ISO 639-2/T language code for this track. If the language is unknown, this resolves to `'und'`
+ * Returns the ISO 639-2/T language code for this track. If the language is unknown, this resolves to `'und'`
* (undetermined).
*/
async getLanguageCode() {
@@ -168,7 +171,7 @@ export abstract class InputTrack {
return requireSync(this._backing.getLanguageCode(), 'languageCode', 'getLanguageCode');
}
- /** A user-defined name for this track. */
+ /** Returns the user-defined name for this track. */
async getName() {
return this._backing.getName();
}
@@ -182,7 +185,7 @@ export abstract class InputTrack {
}
/**
- * A positive number x such that all timestamps and durations of all packets of this track are
+ * Returns a positive number x such that all timestamps and durations of all packets of this track are
* integer multiples of 1/x.
*/
async getTimeResolution() {
@@ -199,8 +202,8 @@ export abstract class InputTrack {
}
/**
- * Whether the timestamps of this track are relative to the Unix epoch (January 1, 1970 00:00:00 UTC). When `true`,
- * each timestamp maps to a definitive point in time.
+ * Returns whether the timestamps of this track are relative to the Unix epoch (January 1, 1970 00:00:00 UTC).
+ * When `true`, each timestamp maps to a definitive point in time.
*/
async getIsRelativeToUnixEpoch() {
return this._backing.isRelativeToUnixEpoch();
@@ -219,7 +222,7 @@ export abstract class InputTrack {
);
}
- /** The track's disposition, i.e. information about its intended usage. */
+ /** Returns the track's disposition, i.e. information about its intended usage. */
async getDisposition() {
return this._backing.getDisposition();
}
@@ -233,8 +236,8 @@ export abstract class InputTrack {
}
/**
- * The peak bitrate of the track, in bits per second, as specified in the track's metadata. This might not match the
- * actual media data's bitrate.
+ * Returns the peak bitrate of the track in bits per second, as specified in the track's metadata. This might not
+ * match the actual media data's bitrate.
*/
async getBitrate() {
return this._backing.getBitrate();
@@ -250,8 +253,8 @@ export abstract class InputTrack {
}
/**
- * The average bitrate of the track, in bits per second, as specified in the track's metadata. This might not match
- * the actual media data's bitrate.
+ * Returns the average bitrate of the track in bits per second, as specified in the track's metadata. This might
+ * not match the actual media data's bitrate.
*/
async getAverageBitrate() {
return this._backing.getAverageBitrate();
@@ -552,7 +555,10 @@ export class InputVideoTrack extends InputTrack {
return requireSync(this._backing.getCodec(), 'codec', 'getCodec');
}
- /** Whether the track metadata says that this track only contains key packets. The actual packets may differ. */
+ /**
+ * Returns whether the track metadata says that this track only contains key packets. The actual packets may
+ * differ.
+ */
async getHasOnlyKeyPackets() {
return (await this._backing.getHasOnlyKeyPackets?.()) ?? false;
}
@@ -569,7 +575,7 @@ export class InputVideoTrack extends InputTrack {
return requireSync(raw, 'hasOnlyKeyPackets', 'getHasOnlyKeyPackets') ?? false;
}
- /** The width in pixels of the track's coded samples, before any transformations or rotations. */
+ /** Returns the width in pixels of the track's coded samples, before any transformations or rotations. */
async getCodedWidth() {
return this._backing.getCodedWidth();
}
@@ -582,7 +588,7 @@ export class InputVideoTrack extends InputTrack {
return requireSync(this._backing.getCodedWidth(), 'codedWidth', 'getCodedWidth');
}
- /** The height in pixels of the track's coded samples, before any transformations or rotations. */
+ /** Returns the height in pixels of the track's coded samples, before any transformations or rotations. */
async getCodedHeight() {
return this._backing.getCodedHeight();
}
@@ -595,7 +601,7 @@ export class InputVideoTrack extends InputTrack {
return requireSync(this._backing.getCodedHeight(), 'codedHeight', 'getCodedHeight');
}
- /** The angle in degrees by which the track's frames should be rotated (clockwise). */
+ /** Returns the angle in degrees by which the track's frames should be rotated (clockwise). */
async getRotation() {
return this._backing.getRotation();
}
@@ -608,7 +614,9 @@ export class InputVideoTrack extends InputTrack {
return requireSync(this._backing.getRotation(), 'rotation', 'getRotation');
}
- /** The width of the track's frames in square pixels, adjusted for pixel aspect ratio but before rotation. */
+ /**
+ * Returns the width of the track's frames in square pixels, adjusted for pixel aspect ratio but before rotation.
+ */
async getSquarePixelWidth() {
return this._backing.getSquarePixelWidth();
}
@@ -621,7 +629,9 @@ export class InputVideoTrack extends InputTrack {
return requireSync(this._backing.getSquarePixelWidth(), 'squarePixelWidth', 'getSquarePixelWidth');
}
- /** The height of the track's frames in square pixels, adjusted for pixel aspect ratio but before rotation. */
+ /**
+ * Returns the height of the track's frames in square pixels, adjusted for pixel aspect ratio but before rotation.
+ */
async getSquarePixelHeight() {
return this._backing.getSquarePixelHeight();
}
@@ -635,7 +645,7 @@ export class InputVideoTrack extends InputTrack {
}
/**
- * The pixel aspect ratio of the track's frames, as a rational number in its reduced form. Most videos use
+ * Returns the pixel aspect ratio of the track's frames as a rational number in its reduced form. Most videos use
* square pixels (1:1).
*/
async getPixelAspectRatio() {
@@ -661,7 +671,7 @@ export class InputVideoTrack extends InputTrack {
});
}
- /** The display width of the track's frames in pixels, after aspect ratio adjustment and rotation. */
+ /** Returns the display width of the track's frames in pixels, after aspect ratio adjustment and rotation. */
async getDisplayWidth() {
const metadata = await this._backing.getMetadataDisplayWidth?.();
if (metadata != null) {
@@ -692,7 +702,7 @@ export class InputVideoTrack extends InputTrack {
return requireSync(value, 'displayWidth', 'getDisplayWidth');
}
- /** The display height of the track's frames in pixels, after aspect ratio adjustment and rotation. */
+ /** Returns the display height of the track's frames in pixels, after aspect ratio adjustment and rotation. */
async getDisplayHeight() {
const metadata = await this._backing.getMetadataDisplayHeight?.();
if (metadata != null) {
@@ -848,7 +858,10 @@ export class InputAudioTrack extends InputTrack {
return requireSync(this._backing.getCodec(), 'codec', 'getCodec');
}
- /** Whether the track metadata says that this track only contains key packets. The actual packets may differ. */
+ /**
+ * Returns whether the track metadata says that this track only contains key packets. The actual packets may
+ * differ.
+ */
async getHasOnlyKeyPackets() {
return (await this._backing.getHasOnlyKeyPackets?.()) ?? true;
}
@@ -865,7 +878,7 @@ export class InputAudioTrack extends InputTrack {
return requireSync(raw, 'hasOnlyKeyPackets', 'getHasOnlyKeyPackets') ?? true;
}
- /** The number of audio channels in the track. */
+ /** Returns the number of audio channels in the track. */
async getNumberOfChannels() {
return this._backing.getNumberOfChannels();
}
@@ -878,7 +891,7 @@ export class InputAudioTrack extends InputTrack {
return requireSync(this._backing.getNumberOfChannels(), 'numberOfChannels', 'getNumberOfChannels');
}
- /** The track's audio sample rate in hertz. */
+ /** Returns the track's audio sample rate in hertz. */
async getSampleRate() {
return this._backing.getSampleRate();
}
diff --git a/src/segmented-input.ts b/src/segmented-input.ts
index 58b3781..dc6ce0b 100644
--- a/src/segmented-input.ts
+++ b/src/segmented-input.ts
@@ -8,9 +8,8 @@
import { TrackType } from './output';
import { MediaCodec } from './codec';
-import { Demuxer, DurationMetadataRequestOptions } from './demuxer';
+import { DurationMetadataRequestOptions } from './demuxer';
import { Input } from './input';
-import { VirtualInputFormat } from './input-format';
import {
InputAudioTrack,
InputAudioTrackBacking,
@@ -20,10 +19,8 @@ import {
InputVideoTrackBacking,
} from './input-track';
import { PacketRetrievalOptions } from './media-sink';
-import { MetadataTags } from './metadata';
-import { arrayCount, assert, roundToDivisor } from './misc';
+import { arrayCount, assert, MaybePromise, roundToDivisor } from './misc';
import { EncodedPacket } from './packet';
-import { NullSource } from './source';
export type SegmentedInputMetadata = {
name: string | null;
@@ -52,11 +49,16 @@ export type SegmentRetrievalOptions = {
skipLiveWait?: boolean;
};
+export type SegmentedInputTrackDeclaration = {
+ id: number;
+ type: TrackType;
+};
+
export abstract class SegmentedInput {
input: Input;
path: string;
+ trackDeclarations: SegmentedInputTrackDeclaration[] | null;
- virtualInput: Input | null = null;
nextInputCacheAge = 0;
inputCache: {
segment: Segment;
@@ -64,9 +66,14 @@ export abstract class SegmentedInput {
age: number;
}[] = [];
- constructor(input: Input, path: string) {
+ trackBackingsPromise: Promise | null = null;
+ firstSegment: Segment | null = null;
+ firstSegmentFirstTimestamps = new WeakMap();
+
+ constructor(input: Input, path: string, trackDeclarations: SegmentedInputTrackDeclaration[] | null) {
this.input = input;
this.path = path;
+ this.trackDeclarations = trackDeclarations;
}
abstract getFirstSegment(options: SegmentRetrievalOptions): Promise;
@@ -88,67 +95,56 @@ export abstract class SegmentedInput {
return lastSegment.timestamp + lastSegment.duration;
}
- toInput() {
- return this.virtualInput ??= new Input({
- source: new NullSource(),
- formats: [new VirtualInputFormat(() => new SegmentedInputDemuxer(this.input, this))],
- });
- }
-
- dispose() {
- for (const entry of this.inputCache) {
- entry.input.dispose();
- }
- this.inputCache.length = 0;
-
- this.virtualInput?.dispose();
- }
-}
-
-class SegmentedInputDemuxer extends Demuxer {
- segmentedInput: SegmentedInput;
- trackBackingsPromise: Promise | null = null;
- firstSegment: Segment | null = null;
- firstSegmentFirstTimestamps = new WeakMap();
-
- constructor(input: Input, segmentedInput: SegmentedInput) {
- super(input);
-
- this.segmentedInput = segmentedInput;
- }
-
- async getMetadataTags(): Promise {
- throw new Error('Unreachable');
- }
-
- async getMimeType(): Promise {
- throw new Error('Unreachable');
- }
-
async getTrackBackings(): Promise {
return this.trackBackingsPromise ??= (async () => {
- this.firstSegment = await this.segmentedInput.getFirstSegment({});
- if (!this.firstSegment) {
- return [];
- }
-
- const input = this.segmentedInput.getInputForSegment(this.firstSegment);
- const inputTracks = await input.getTracks();
-
const backings: InputTrackBacking[] = [];
- for (const track of inputTracks) {
- if (track.type === 'video') {
- const number = arrayCount(backings, x => x.getType() === 'video') + 1;
- backings.push(
- new SegmentedInputInputVideoTrackBacking(track, this, number),
- );
- } else if (track.type === 'audio') {
- const number = arrayCount(backings, x => x.getType() === 'audio') + 1;
+ if (this.trackDeclarations) {
+ for (const decl of this.trackDeclarations) {
+ if (decl.type === 'video') {
+ const number = arrayCount(backings, x => x.getType() === 'video') + 1;
- backings.push(
- new SegmentedInputInputAudioTrackBacking(track, this, number),
- );
+ backings.push(
+ new SegmentedInputInputVideoTrackBacking(this, decl, number),
+ );
+ } else if (decl.type === 'audio') {
+ const number = arrayCount(backings, x => x.getType() === 'audio') + 1;
+
+ backings.push(
+ new SegmentedInputInputAudioTrackBacking(this, decl, number),
+ );
+ }
+ }
+ } else {
+ // There are no declarations, we must determine the tracks from the first segment
+ this.firstSegment = await this.getFirstSegment({});
+ if (!this.firstSegment) {
+ return [];
+ }
+
+ const input = this.getInputForSegment(this.firstSegment);
+ const inputTracks = await input.getTracks();
+
+ for (const track of inputTracks) {
+ if (track.type === 'video') {
+ const number = arrayCount(backings, x => x.getType() === 'video') + 1;
+
+ backings.push(
+ new SegmentedInputInputVideoTrackBacking(this, {
+ id: backings.length + 1,
+ type: 'video',
+ }, number),
+ );
+ } else if (track.type === 'audio') {
+ const number = arrayCount(backings, x => x.getType() === 'audio') + 1;
+
+ backings.push(
+ new SegmentedInputInputAudioTrackBacking(this, {
+ id: backings.length + 1,
+ type: 'audio',
+ }, number),
+ );
+ }
}
}
@@ -163,7 +159,7 @@ class SegmentedInputDemuxer extends Demuxer {
if (this.firstSegmentFirstTimestamps.has(firstSegment)) {
firstSegmentFirstTimestamp = this.firstSegmentFirstTimestamps.get(firstSegment)!;
} else {
- const firstInput = this.segmentedInput.getInputForSegment(firstSegment);
+ const firstInput = this.getInputForSegment(firstSegment);
firstSegmentFirstTimestamp = await firstInput.getFirstTimestamp();
this.firstSegmentFirstTimestamps.set(firstSegment, firstSegmentFirstTimestamp);
}
@@ -189,6 +185,13 @@ class SegmentedInputDemuxer extends Demuxer {
return segment.timestamp - segmentFirstTimestamp;
}
}
+
+ dispose() {
+ for (const entry of this.inputCache) {
+ entry.input.dispose();
+ }
+ this.inputCache.length = 0;
+ }
}
type PacketInfo = {
@@ -198,93 +201,126 @@ type PacketInfo = {
};
class SegmentedInputInputTrackBacking implements InputTrackBacking {
- firstInputTrack: InputTrack;
- demuxer: SegmentedInputDemuxer;
- packetInfos = new WeakMap();
+ segmentedInput: SegmentedInput;
+ decl: SegmentedInputTrackDeclaration;
number: number;
+ packetInfos = new WeakMap();
- constructor(firstInputTrack: InputTrack, demuxer: SegmentedInputDemuxer, number: number) {
- this.firstInputTrack = firstInputTrack;
- this.demuxer = demuxer;
+ hydrationPromise: Promise | null = null;
+ firstInputTrack: InputTrack | null = null;
+
+ constructor(segmentedInput: SegmentedInput, decl: SegmentedInputTrackDeclaration, number: number) {
+ this.segmentedInput = segmentedInput;
+ this.decl = decl;
this.number = number;
}
- getType(): TrackType {
- return this.firstInputTrack._backing.getType();
- }
+ hydrate() {
+ return this.hydrationPromise ??= (async () => {
+ this.segmentedInput.firstSegment ??= await this.segmentedInput.getFirstSegment({});
+ if (!this.segmentedInput.firstSegment) {
+ throw new Error('Missing first segment, can\'t retrieve track.');
+ }
- getDecoderConfig() {
- return this.firstInputTrack._backing.getDecoderConfig();
- }
+ const input = this.segmentedInput.getInputForSegment(this.segmentedInput.firstSegment);
+ const inputTracks = await input.getTracks();
- getHasOnlyKeyPackets() {
- return this.firstInputTrack.getHasOnlyKeyPackets();
+ const track = inputTracks.find(x => x.type === this.decl.type && x.number === this.number);
+ if (!track) {
+ throw new Error('No matching track found in underlying media data.');
+ }
+
+ this.firstInputTrack = track;
+ })();
}
getId(): number {
- return this.firstInputTrack._backing.getId();
+ return this.decl.id;
}
- getPairingMask() {
- return this.firstInputTrack._backing.getPairingMask();
+ getType(): TrackType {
+ return this.decl.type;
}
getNumber(): number {
return this.number;
}
+ /** If the backing track is already present, delegate synchronously; otherwise, hydrate first. */
+ delegate(fn: () => MaybePromise): MaybePromise {
+ if (this.firstInputTrack) {
+ return fn();
+ }
+
+ return this.hydrate().then(fn);
+ }
+
+ async getDecoderConfig() {
+ return this.delegate(() => this.firstInputTrack!._backing.getDecoderConfig());
+ }
+
+ getHasOnlyKeyPackets() {
+ return this.delegate(() => this.firstInputTrack!._backing.getHasOnlyKeyPackets?.() ?? null);
+ }
+
+ getPairingMask() {
+ return 1n;
+ }
+
getCodec() {
- return this.firstInputTrack._backing.getCodec();
+ return this.delegate(() => this.firstInputTrack!._backing.getCodec());
}
getInternalCodecId() {
- return this.firstInputTrack._backing.getInternalCodecId();
+ return this.delegate(() => this.firstInputTrack!._backing.getInternalCodecId());
}
getDisposition() {
- return this.firstInputTrack._backing.getDisposition();
+ return this.delegate(() => this.firstInputTrack!._backing.getDisposition());
}
getLanguageCode() {
- return this.firstInputTrack._backing.getLanguageCode();
+ return this.delegate(() => this.firstInputTrack!._backing.getLanguageCode());
}
getName() {
- return this.firstInputTrack._backing.getName();
+ return this.delegate(() => this.firstInputTrack!._backing.getName());
}
getTimeResolution() {
- return this.firstInputTrack._backing.getTimeResolution();
+ return this.delegate(() => this.firstInputTrack!._backing.getTimeResolution());
}
- isRelativeToUnixEpoch() {
- assert(this.demuxer.firstSegment);
- return this.demuxer.firstSegment.relativeToUnixEpoch;
+ async isRelativeToUnixEpoch() {
+ await this.hydrate();
+
+ assert(this.segmentedInput.firstSegment);
+ return this.segmentedInput.firstSegment.relativeToUnixEpoch;
}
getBitrate() {
- return this.firstInputTrack._backing.getBitrate();
+ return this.delegate(() => this.firstInputTrack!._backing.getBitrate());
}
getAverageBitrate() {
- return this.firstInputTrack._backing.getAverageBitrate();
+ return this.delegate(() => this.firstInputTrack!._backing.getAverageBitrate());
}
getDurationFromMetadata(options: DurationMetadataRequestOptions): Promise {
- return this.demuxer.segmentedInput.getDurationFromMetadata(options);
+ return this.segmentedInput.getDurationFromMetadata(options);
}
getLiveRefreshInterval(): Promise {
- return this.demuxer.segmentedInput.getLiveRefreshInterval();
+ return this.segmentedInput.getLiveRefreshInterval();
}
async createAdjustedPacket(packet: EncodedPacket, segment: Segment, track: InputTrack) {
assert(packet.sequenceNumber >= 0);
- assert(this.demuxer.firstSegment);
+ assert(this.segmentedInput.firstSegment);
- const mediaOffset = await this.demuxer.getMediaOffset(segment, track.input);
+ const mediaOffset = await this.segmentedInput.getMediaOffset(segment, track.input);
// If we didn't do this then sequence numbers would exceed Number.MAX_SAFE_INTEGER for Unix-timestamped segments
- const segmentTimestampRelativeToFirst = segment.timestamp - this.demuxer.firstSegment.timestamp;
+ const segmentTimestampRelativeToFirst = segment.timestamp - this.segmentedInput.firstSegment.timestamp;
const modified = packet.clone({
timestamp: roundToDivisor(
@@ -306,14 +342,17 @@ class SegmentedInputInputTrackBacking implements InputTrackBacking {
}
async getFirstPacket(options: PacketRetrievalOptions): Promise {
- assert(this.demuxer.firstSegment);
+ await this.hydrate();
+
+ assert(this.segmentedInput.firstSegment);
+ assert(this.firstInputTrack);
const packet = await this.firstInputTrack._backing.getFirstPacket(options);
if (!packet) {
return null;
}
- return this.createAdjustedPacket(packet, this.demuxer.firstSegment, this.firstInputTrack);
+ return this.createAdjustedPacket(packet, this.segmentedInput.firstSegment, this.firstInputTrack);
}
getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise {
@@ -343,14 +382,14 @@ class SegmentedInputInputTrackBacking implements InputTrackBacking {
let currentSegment: Segment | null = info.segment;
while (true) {
- const nextSegment = await this.demuxer.segmentedInput.getNextSegment(currentSegment, {
+ const nextSegment = await this.segmentedInput.getNextSegment(currentSegment, {
skipLiveWait: options.skipLiveWait,
});
if (!nextSegment) {
return null;
}
- const nextInput = this.demuxer.segmentedInput.getInputForSegment(nextSegment);
+ const nextInput = this.segmentedInput.getInputForSegment(nextSegment);
const nextTracks = await nextInput.getTracks();
const nextTrack = nextTracks.find(t => t.type === info.track.type && t.number === info.track.number);
@@ -381,29 +420,31 @@ class SegmentedInputInputTrackBacking implements InputTrackBacking {
options: PacketRetrievalOptions,
keyframesOnly: boolean,
): Promise {
- let currentSegment = await this.demuxer.segmentedInput.getSegmentAt(timestamp, {
+ let currentSegment = await this.segmentedInput.getSegmentAt(timestamp, {
skipLiveWait: options.skipLiveWait,
});
if (!currentSegment) {
return null;
}
+ await this.hydrate();
+
while (currentSegment) {
- const input = this.demuxer.segmentedInput.getInputForSegment(currentSegment);
+ const input = this.segmentedInput.getInputForSegment(currentSegment);
const tracks = await input.getTracks();
const track = tracks.find(t => (
- t.type === this.firstInputTrack.type && t.number === this.firstInputTrack.number
+ t.type === this.firstInputTrack!.type && t.number === this.firstInputTrack!.number
));
if (!track) {
// Search the previous segment
- currentSegment = await this.demuxer.segmentedInput.getPreviousSegment(currentSegment, {
+ currentSegment = await this.segmentedInput.getPreviousSegment(currentSegment, {
skipLiveWait: options.skipLiveWait,
});
continue;
}
- const mediaOffset = await this.demuxer.getMediaOffset(currentSegment, input);
+ const mediaOffset = await this.segmentedInput.getMediaOffset(currentSegment, input);
const offsetTimestamp = timestamp - mediaOffset;
const packet = keyframesOnly
@@ -412,7 +453,7 @@ class SegmentedInputInputTrackBacking implements InputTrackBacking {
if (!packet) {
// Search the previous segment
- currentSegment = await this.demuxer.segmentedInput.getPreviousSegment(currentSegment, {
+ currentSegment = await this.segmentedInput.getPreviousSegment(currentSegment, {
skipLiveWait: options.skipLiveWait,
});
continue;
@@ -428,46 +469,46 @@ class SegmentedInputInputTrackBacking implements InputTrackBacking {
class SegmentedInputInputVideoTrackBacking
extends SegmentedInputInputTrackBacking
implements InputVideoTrackBacking {
- override firstInputTrack!: InputVideoTrack;
+ override firstInputTrack!: InputVideoTrack | null;
override getType() {
return 'video' as const;
}
override getCodec() {
- return this.firstInputTrack._backing.getCodec();
+ return this.delegate(() => this.firstInputTrack!._backing.getCodec());
}
getCodedWidth() {
- return this.firstInputTrack._backing.getCodedWidth();
+ return this.delegate(() => this.firstInputTrack!._backing.getCodedWidth());
}
getCodedHeight() {
- return this.firstInputTrack._backing.getCodedHeight();
+ return this.delegate(() => this.firstInputTrack!._backing.getCodedHeight());
}
getSquarePixelWidth() {
- return this.firstInputTrack._backing.getSquarePixelWidth();
+ return this.delegate(() => this.firstInputTrack!._backing.getSquarePixelWidth());
}
getSquarePixelHeight() {
- return this.firstInputTrack._backing.getSquarePixelHeight();
+ return this.delegate(() => this.firstInputTrack!._backing.getSquarePixelHeight());
}
getRotation() {
- return this.firstInputTrack._backing.getRotation();
+ return this.delegate(() => this.firstInputTrack!._backing.getRotation());
}
- getColorSpace(): Promise {
- return this.firstInputTrack._backing.getColorSpace();
+ async getColorSpace(): Promise {
+ return this.delegate(() => this.firstInputTrack!._backing.getColorSpace());
}
- canBeTransparent(): Promise {
- return this.firstInputTrack._backing.canBeTransparent();
+ async canBeTransparent(): Promise {
+ return this.delegate(() => this.firstInputTrack!._backing.canBeTransparent());
}
- override getDecoderConfig(): Promise {
- return this.firstInputTrack._backing.getDecoderConfig();
+ override async getDecoderConfig(): Promise {
+ return this.delegate(() => this.firstInputTrack!._backing.getDecoderConfig());
}
}
@@ -481,18 +522,18 @@ class SegmentedInputInputAudioTrackBacking
}
override getCodec() {
- return this.firstInputTrack._backing.getCodec();
+ return this.delegate(() => this.firstInputTrack._backing.getCodec());
}
getNumberOfChannels() {
- return this.firstInputTrack._backing.getNumberOfChannels();
+ return this.delegate(() => this.firstInputTrack._backing.getNumberOfChannels());
}
getSampleRate() {
- return this.firstInputTrack._backing.getSampleRate();
+ return this.delegate(() => this.firstInputTrack._backing.getSampleRate());
}
- override getDecoderConfig(): Promise {
- return this.firstInputTrack._backing.getDecoderConfig();
+ override async getDecoderConfig(): Promise {
+ return this.delegate(() => this.firstInputTrack._backing.getDecoderConfig());
}
}
diff --git a/test/node/hls-input.test.ts b/test/node/hls-input.test.ts
index ae31bd1..d4a2631 100644
--- a/test/node/hls-input.test.ts
+++ b/test/node/hls-input.test.ts
@@ -204,7 +204,7 @@ test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => {
expect(await input.getDurationFromMetadata()).not.toBe(null);
});
-test.concurrent('Big Buck Bunny codec parameter strings from master playlist', { timeout: 15_000 }, async () => {
+test.concurrent('Big Buck Bunny, codec parameter strings from master playlist', { timeout: 15_000 }, async () => {
using input = createInputFrom('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', ALL_FORMATS);
let sourceCount = 0;
@@ -228,6 +228,23 @@ test.concurrent('Big Buck Bunny codec parameter strings from master playlist', {
expect(sourceCount).toBe(1);
});
+test.concurrent('Big Buck Bunny, determining duration from metadata', { timeout: 15_000 }, async () => {
+ using input = createInputFrom('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', ALL_FORMATS);
+
+ let sourceCount = 0;
+ input.on('source', () => sourceCount++);
+
+ const track = await input.getPrimaryVideoTrack();
+ assert(track);
+
+ expect(sourceCount).toBe(1);
+
+ const approxDuration = await track.getDurationFromMetadata();
+ expect(approxDuration).toBe(634.567);
+
+ expect(sourceCount).toBe(2); // We needed to read the playlist, but not any segment
+});
+
test.concurrent('Single-variant Big Buck Bunny', { timeout: 15_000 }, async () => {
using input = createInputFrom('https://test-streams.mux.dev/x36xhzz/url_6/193039199_mp4_h264_aac_hq_7.m3u8', ALL_FORMATS);