From 2f0c040fcda848486ac4dbd0f07d4a061e01d59c Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:53:27 +0200 Subject: [PATCH] Properly handle subrequests for redirected URLs --- src/hls/hls-demuxer.ts | 5 ++++- src/hls/hls-segmented-input.ts | 17 ++++++++++++----- src/source.ts | 19 ++++++++++++++++--- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/hls/hls-demuxer.ts b/src/hls/hls-demuxer.ts index 057c7da..7e9f588 100644 --- a/src/hls/hls-demuxer.ts +++ b/src/hls/hls-demuxer.ts @@ -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; diff --git a/src/hls/hls-segmented-input.ts b/src/hls/hls-segmented-input.ts index 4815652..537fcf4 100644 --- a/src/hls/hls-segmented-input.ts +++ b/src/hls/hls-segmented-input.ts @@ -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,12 +128,17 @@ 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; + } } let headerRead = false; @@ -219,7 +226,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, @@ -299,7 +306,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, @@ -375,7 +382,7 @@ export class HlsSegmentedInput extends SegmentedInput { currentKey = { method: 'AES-128', - keyUri: joinPaths(this.path, uri), + keyUri: joinPaths(this.rootPath, uri), iv, keyFormat, }; diff --git a/src/source.ts b/src/source.ts index a8975dd..7e9acec 100644 --- a/src/source.ts +++ b/src/source.ts @@ -289,10 +289,15 @@ export class SourceRef 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, + public readonly requestHandler: (request: SourceRequest) => MaybePromise, ) { 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