Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import androidx.media3.exoplayer.hls.playlist.HlsPlaylist;
import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParser;
import androidx.media3.exoplayer.offline.SegmentDownloader;
import androidx.media3.exoplayer.upstream.ParsingLoadable;
import androidx.media3.exoplayer.upstream.ParsingLoadable.Parser;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
Expand Down Expand Up @@ -220,21 +221,37 @@ private HlsDownloader(
protected List<Segment> getSegments(DataSource dataSource, HlsPlaylist manifest, boolean removing)
throws IOException, InterruptedException {
ArrayList<DataSpec> mediaPlaylistDataSpecs = new ArrayList<>();
HlsMultivariantPlaylist multivariantPlaylist = HlsMultivariantPlaylist.EMPTY;
if (manifest instanceof HlsMultivariantPlaylist) {
HlsMultivariantPlaylist multivariantPlaylist = (HlsMultivariantPlaylist) manifest;
multivariantPlaylist = (HlsMultivariantPlaylist) manifest;
addMediaPlaylistDataSpecs(multivariantPlaylist.mediaPlaylistUrls, mediaPlaylistDataSpecs);
} else {
mediaPlaylistDataSpecs.add(
SegmentDownloader.getCompressibleDataSpec(Uri.parse(manifest.baseUri)));
}

// When the multivariant playlist defines variables, create a parser that propagates
// them to child manifests for IMPORT resolution. Otherwise use the default path.
boolean hasVariables = !multivariantPlaylist.variableDefinitions.isEmpty();
@Nullable HlsPlaylistParser childManifestParser = hasVariables

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

rename from childManifestParser to mediaPlaylistParser ?

? new HlsPlaylistParser(multivariantPlaylist, /* previousMediaPlaylist= */ null)
: null;

ArrayList<Segment> segments = new ArrayList<>();
HashSet<Uri> seenEncryptionKeyUris = new HashSet<>();
for (DataSpec mediaPlaylistDataSpec : mediaPlaylistDataSpecs) {
segments.add(new Segment(/* startTimeUs= */ 0, mediaPlaylistDataSpec));
HlsMediaPlaylist mediaPlaylist;
try {
mediaPlaylist = (HlsMediaPlaylist) getManifest(dataSource, mediaPlaylistDataSpec, removing);
if (childManifestParser != null) {
mediaPlaylist =

@marcbaechinger marcbaechinger Jun 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we should not load here. When looking at SegmentDownloader.getManifest (from which HlsDownaloder) extends, we see that internally this is processed through execute. This method handles quite some cases that need to be handled properly and also integrates with the PriorityTaskManager to do some priority management.

I think instead we should overload SegmentDownloader.getManifest and add an additional parameter that takes a Parser<M>. We can then in HlsSegmentParser pass down either the childManfestParser or the original parser that we pass down to super in the constructor. After calling super we cab assign it to a new field HlsPlaylistParser hlsPlaylistParser of HlsDownloader and we can then use it like:

Parser<HlsPlaylist> childManifestParser =
        hasVariables
            ? new HlsPlaylistParser(multivariantPlaylist, /* previousMediaPlaylist= */ null)
            : hlsPlaylistParser;

Instead of the if (childManfestParser != null) { we can then instead just do:

// calling the new overload method in the super class `SegementDownloader`
mediaPlaylist =
            (HlsMediaPlaylist)
                getManifest(dataSource, childManifestParser, mediaPlaylistDataSpec, removing);

This way we take advantage of the harness in execute in the same way as before.

(HlsMediaPlaylist)
ParsingLoadable.load(
dataSource, childManifestParser, mediaPlaylistDataSpec, C.DATA_TYPE_MANIFEST);
} else {
mediaPlaylist = (HlsMediaPlaylist) getManifest(dataSource, mediaPlaylistDataSpec,
removing);
}
} catch (IOException e) {
if (!removing) {
throw e;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,128 @@ public void downloadEncMediaPlaylist() throws Exception {
assertCachedData(cache, fakeDataSet);
}

@Test
public void download_withVariableSubstitutionInSegmentUrls_resolvesVariables() throws Exception {
// Multivariant playlist defines a variable via EXT-X-DEFINE NAME/VALUE
byte[] multivariantPlaylistData =
("#EXTM3U\n"
+ "#EXT-X-DEFINE:NAME=\"cdnPrefix\",VALUE=\"https://cdn.example.com/\"\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=232370,CODECS=\"mp4a.40.2, avc1.4d4015\"\n"
+ "media_with_vars.m3u8\n")
.getBytes(java.nio.charset.StandardCharsets.UTF_8);

// Media playlist uses the variable in segment URLs with IMPORT
byte[] mediaPlaylistData =
("#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:8\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-DEFINE:IMPORT=\"cdnPrefix\"\n"
+ "#EXTINF:9.97667,\n"
+ "{$cdnPrefix}segment0.ts\n"
+ "#EXTINF:9.97667,\n"
+ "{$cdnPrefix}segment1.ts\n"
+ "#EXT-X-ENDLIST")
.getBytes(java.nio.charset.StandardCharsets.UTF_8);

fakeDataSet =
new FakeDataSet()
.setData("master_vars.m3u8", multivariantPlaylistData)
.setData("media_with_vars.m3u8", mediaPlaylistData)
.setRandomData("https://cdn.example.com/segment0.ts", 10)
.setRandomData("https://cdn.example.com/segment1.ts", 11);

HlsDownloader downloader = getHlsDownloader("master_vars.m3u8", getKeys(0));
downloader.download(progressListener);

// Verify segments were downloaded with resolved URLs
assertCachedData(cache, fakeDataSet);
}

@Test
public void download_withMultipleVariablesInSegmentUrls_resolvesAllVariables() throws Exception {
// Multivariant playlist defines multiple variables
byte[] multivariantPlaylistData =
("#EXTM3U\n"
+ "#EXT-X-DEFINE:NAME=\"contentPrefix\",VALUE=\"https://media.example.com/\"\n"
+ "#EXT-X-DEFINE:NAME=\"sessionId\",VALUE=\"abc123\"\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=500000,CODECS=\"avc1.4d401f,mp4a.40.2\"\n"
+ "{$contentPrefix}video/720p.m3u8?sid={$sessionId}\n")
.getBytes(java.nio.charset.StandardCharsets.UTF_8);

// Media playlist imports and uses both variables
byte[] mediaPlaylistData =
("#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:6\n"
+ "#EXT-X-VERSION:8\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-DEFINE:IMPORT=\"contentPrefix\"\n"
+ "#EXT-X-DEFINE:IMPORT=\"sessionId\"\n"
+ "#EXTINF:6.0,\n"
+ "{$contentPrefix}seg/s0.ts?sid={$sessionId}\n"
+ "#EXTINF:6.0,\n"
+ "{$contentPrefix}seg/s1.ts?sid={$sessionId}\n"
+ "#EXT-X-ENDLIST")
.getBytes(java.nio.charset.StandardCharsets.UTF_8);

fakeDataSet =
new FakeDataSet()
.setData("master_multi_vars.m3u8", multivariantPlaylistData)
.setData(
"https://media.example.com/video/720p.m3u8?sid=abc123", mediaPlaylistData)
.setRandomData(
"https://media.example.com/seg/s0.ts?sid=abc123", 10)
.setRandomData(
"https://media.example.com/seg/s1.ts?sid=abc123", 11);

HlsDownloader downloader = getHlsDownloader("master_multi_vars.m3u8", getKeys(0));
downloader.download(progressListener);

assertCachedData(cache, fakeDataSet);
}

@Test
public void download_withVariableInChildManifestUri_resolvesVariables() throws Exception {
// Multivariant playlist uses variable in the child manifest URI itself
byte[] multivariantPlaylistData =
("#EXTM3U\n"
+ "#EXT-X-DEFINE:NAME=\"manifestBase\",VALUE=\"https://manifest.example.com/\"\n"
+ "#EXT-X-DEFINE:NAME=\"segmentBase\",VALUE=\"https://segments.example.com/\"\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=128000,CODECS=\"mp4a.40.2\"\n"
+ "{$manifestBase}audio/playlist.m3u8\n")
.getBytes(java.nio.charset.StandardCharsets.UTF_8);

// Media playlist uses a different variable for segment URLs
byte[] mediaPlaylistData =
("#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:8\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-DEFINE:IMPORT=\"segmentBase\"\n"
+ "#EXTINF:10.0,\n"
+ "{$segmentBase}audio/chunk_0.ts\n"
+ "#EXTINF:10.0,\n"
+ "{$segmentBase}audio/chunk_1.ts\n"
+ "#EXT-X-ENDLIST")
.getBytes(java.nio.charset.StandardCharsets.UTF_8);

fakeDataSet =
new FakeDataSet()
.setData("master_child_var.m3u8", multivariantPlaylistData)
.setData(
"https://manifest.example.com/audio/playlist.m3u8", mediaPlaylistData)
.setRandomData("https://segments.example.com/audio/chunk_0.ts", 10)
.setRandomData("https://segments.example.com/audio/chunk_1.ts", 11);

HlsDownloader downloader = getHlsDownloader("master_child_var.m3u8", getKeys(0));
downloader.download(progressListener);

assertCachedData(cache, fakeDataSet);
}

private HlsDownloader getHlsDownloader(String mediaPlaylistUri, List<StreamKey> keys) {
CacheDataSource.Factory cacheDataSourceFactory =
new CacheDataSource.Factory()
Expand Down