Skip to content

SideloadedSubtitle: Keep the query string when resolving relative playlist URIs - #132

Open
olejnjak wants to merge 3 commits into
THEOplayer:mainfrom
olejnjak:fix/sideloaded-subtitles-relative-url-query
Open

olejnjak wants to merge 3 commits into
THEOplayer:mainfrom
olejnjak:fix/sideloaded-subtitles-relative-url-query

Conversation

@olejnjak

@olejnjak olejnjak commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Problem

When a source has sideloaded subtitles, the connector rewrites the HLS playlists. If the master
playlist lists variant playlists as relative URIs with a query string, playback fails.

Example of a variant URI in the master playlist:

video=391132.m3u8?hdntl=<token>

The connector requests it as:

https://example.com/path/video=391132.m3u8%3Fhdntl=<token>

The ? is percent-encoded, so the query becomes part of the path. The CDN returns HTTP 400 for
each variant playlist, and THEOplayer reports a network error (6400). The same stream plays
correctly without sideloaded subtitles.

Cause

PlaylistParser.getFullURL(from:) builds the absolute URL of a relative URI with
manifestURL.deletingLastPathComponent().appendingPathComponent(path).
appendingPathComponent treats its whole argument as a path component, so it percent-encodes ?
to %3F.

Fix

Resolve a relative URI against the manifest URL with URL(string:relativeTo:). This keeps the
query string as a query and also handles ../ and subdirectory paths. If that initializer
returns nil, the code falls back to the previous appendingPathComponent behavior. Absolute
URIs are not affected.

A relative URI without its own query no longer gets the playlist's query string. The previous
code added it as a side effect of appendingPathComponent. The new result follows RFC 3986
section 5.2.2, which RFC 8216bis section 4.1 refers to for resolving relative URIs, and matches
AVPlayer's own resolution. A stream that needs the playlist's query on other URIs can use
EXT-X-DEFINE with QUERYPARAM.

Tests

Added PlaylistParserTests with these cases, resolved against
https://cdn.example.com/vod/movie/index.m3u8?token=abc:

Relative URI Result
video=391132.m3u8?hdntl=… …/movie/video=391132.m3u8?hdntl=…
keyframes/video.m3u8?accountId=1 …/movie/keyframes/video.m3u8?accountId=1
../other/video.m3u8?accountId=1 …/vod/other/video.m3u8?accountId=1
video.m3u8 …/movie/video.m3u8
https://other.example.com/video.m3u8?token=xyz (absolute) unchanged

All five pass with the fix. With the previous getFullURL, the four relative cases fail.

Also checked on an iOS device with an HLS VOD stream whose master playlist has relative variant
URIs with a signed hdntl query, plus sideloaded WebVTT subtitles. Before the fix every variant
playlist request returned HTTP 400 and playback failed. After the fix the variant playlists load
with their query string, and the stream plays.

Changelog

Added an entry under Unreleased → Fixed → SideloadedSubtitle.

getFullURL built absolute URLs for relative URIs with appendingPathComponent. That method percent-encodes '?' into '%3F', so a variant URI such as 'video.m3u8?hdntl=...' became part of the path and the server returned HTTP 400. Resolve the URI against the manifest URL with URL(string:relativeTo:) instead. Fall back to the previous behavior when that fails.
Cover relative URIs with a query string in the same directory, in a subdirectory and in a parent directory, a relative URI without a query, and an absolute URI. A relative URI without its own query does not get the playlist's query, as RFC 3986 section 5.2.2 specifies.
@ceyhun-o
ceyhun-o requested review from ceyhun-o and therama November 5, 2026 12:16
@ceyhun-o ceyhun-o added the bug Something isn't working label Nov 5, 2026
@ceyhun-o

ceyhun-o commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Hi @olejnjak,

Thank you very much for your PR!

This is indeed fixing a real problem with signed playlists URLs being broken, but it seems to introduce an edge case where ? in file names are escaped.

While reviewing this PR, we also found 3 other edge cases where some encoding/decoding issues happen. Instead of merging this PR, we will work on a more robust solution that fixes all the cases we have found so that we fix the root cause and not a specific case.

Would you please create a ticket on our Service Desk in the meantime with a source description that reproduced an issue on your end so that we can also use it to test the fix?

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants