Skip to content

Resolve HLS segment and key urls against the playlist url - #3174

Merged
fire-light42 merged 1 commit into
recloudstream:masterfrom
Dev-next-gen:fix-hls-relative-segment-urls
Sep 21, 2026
Merged

fire-light42 merged 1 commit into
recloudstream:masterfrom
Dev-next-gen:fix-hls-relative-segment-urls

Conversation

@Dev-next-gen

Copy link
Copy Markdown
Contributor

While reading M3u8Helper2.hslLazy I noticed that segment and key URIs from a media playlist are joined to the playlist's parent directory as plain strings, while the variant URLs in the same flow are resolved by HlsPlaylistParser.UrlUtil.resolveToUrl (RFC 3986). Only plain relative entries survive that join.

With a playlist at https://host/a/b/index.m3u8:

  • a root-relative entry /seg/1.ts became https://host/a/b//seg/1.ts
  • a scheme-relative entry //cdn/seg.ts became https://host/a/b///cdn/seg.ts
  • #EXT-X-KEY:METHOD=AES-128,URI="/keys/k.bin" was fetched from https://host/a/b//keys/k.bin

Those URLs don't exist on the server, so downloads and preview generation of such streams fail. This change resolves non-absolute entries with the same UrlUtil.resolveToUrl the parser already uses. Plain relative entries like seg0.ts resolve to the same URL as before, and absolute http(s):// entries are left as they are. The segment value also carried the trailing newline captured by TS_EXTENSION_REGEX, which I trim before resolving. LazyHlsDownloadData.relativeUrl is unchanged.

To test it I added library/src/jvmTest/.../M3u8HelperTest.kt. It serves playlists from a local com.sun.net.httpserver, checks the resolved segment URLs, downloads every segment through resolveLink, and checks that the key is requested at /keys/k.bin. On master both tests fail:

expected:<[http://127.0.0.1:41035/a/b/seg0.ts, http://127.0.0.1:41035/root/seg1.ts, http://127.0.0.1:41035/proto/seg2.ts]>
but was:<[http://127.0.0.1:41035/a/b/seg0.ts\n, http://127.0.0.1:41035/a/b//root/seg1.ts\n, http://127.0.0.1:41035/a/b///127.0.0.1:41035/proto/seg2.ts\n]>

requested: [/a/b/encrypted.m3u8, /a/b//keys/k.bin]

With the change, ./gradlew library:jvmTest passes (543 tests) on JDK 17 and 21, and ./gradlew library:checkKotlinAbi passes on JDK 17 like in CI. I haven't tried it in the app against a real stream, only through these tests.

AI tools used

hslLazy built segment and key urls by appending the entry to the parent directory of the playlist, so root-relative entries (/seg.ts) and scheme-relative entries (//host/seg.ts) produced urls such as /a/b//seg.ts that do not exist. Use the RFC 3986 resolution that HlsPlaylistParser already applies to variant urls, and trim the newline captured by TS_EXTENSION_REGEX.

@fire-light42 fire-light42 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, great fix and test!

You are running a very cool project 😄

@fire-light42
fire-light42 merged commit 2c64d28 into recloudstream:master Sep 21, 2026
2 checks passed
@Dev-next-gen

Copy link
Copy Markdown
Contributor Author

@fire-light42 Thanks a lot!! I’m putting a huge amount of effort into ensuring the entire process is extremely rigorous and genuinely useful, rather than just a massive spam of garbage code. See you at the next PR 😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants