Skip to content

Commit e3c2aea

Browse files
committed
Fix playback of non-URI HLS streams
A custom HlsPlaylistParserFactory cannot be used anymore to play HLS streams. This needs to be replaced by a custom HlsDataSourceFactory, which returns a ByteArrayDataSource (where the bytes of this DataSource correspond to the bytes of the playlist string) and a specified DataSource for other request types. This model has two limitations: - if media requests are relative, the URI from which the manifest comes from (either the manifest URI (preferred) or the master URI (if applicable)) must be returned, otherwise the content will be not playable, as it will be an invalid URL, or it may be treat as something unexpected, for instance as a file for DefaultDataSources; - if the playlist is a master playlist, endless loops should be encountered because the DataSources created for media playlists will use the master playlist response instead of fetching the corresponding playlist. With the current model of HlsDataSourceFactory, there is no possibility to distinguish the playlist type or the URI that is requested. If ExoPlayer provides a way to create HlsMediaSources with an HlsPlaylist in the future, it should be used instead of this solution.
1 parent 21c9530 commit e3c2aea

4 files changed

Lines changed: 153 additions & 76 deletions

File tree

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package org.schabi.newpipe.player.datasource;
2+
3+
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
4+
5+
import androidx.annotation.NonNull;
6+
7+
import com.google.android.exoplayer2.C;
8+
import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;
9+
import com.google.android.exoplayer2.upstream.ByteArrayDataSource;
10+
import com.google.android.exoplayer2.upstream.DataSource;
11+
12+
import java.nio.charset.StandardCharsets;
13+
14+
/**
15+
* A {@link HlsDataSourceFactory} which allows playback of non-URI media HLS playlists for
16+
* {@link com.google.android.exoplayer2.source.hls.HlsMediaSource HlsMediaSource}s.
17+
*
18+
* <p>
19+
* If media requests are relative, the URI from which the manifest comes from (either the
20+
* manifest URI (preferred) or the master URI (if applicable)) must be returned, otherwise the
21+
* content will be not playable, as it will be an invalid URL, or it may be treat as something
22+
* unexpected, for instance as a file for
23+
* {@link com.google.android.exoplayer2.upstream.DefaultDataSource DefaultDataSource}s.
24+
* </p>
25+
*
26+
* <p>
27+
* See {@link #createDataSource(int)} for changes and implementation details.
28+
* </p>
29+
*/
30+
public final class NonUriHlsDataSourceFactory implements HlsDataSourceFactory {
31+
32+
/**
33+
* Builder class of {@link NonUriHlsDataSourceFactory} instances.
34+
*/
35+
public static final class Builder {
36+
private DataSource.Factory dataSourceFactory;
37+
private String playlistString;
38+
39+
/**
40+
* Set the {@link DataSource.Factory} which will be used to create non manifest contents
41+
* {@link DataSource}s.
42+
*
43+
* @param dataSourceFactoryForNonManifestContents the {@link DataSource.Factory} which will
44+
* be used to create non manifest contents
45+
* {@link DataSource}s, which cannot be null
46+
*/
47+
public void setDataSourceFactory(
48+
@NonNull final DataSource.Factory dataSourceFactoryForNonManifestContents) {
49+
this.dataSourceFactory = dataSourceFactoryForNonManifestContents;
50+
}
51+
52+
/**
53+
* Set the HLS playlist which will be used for manifests requests.
54+
*
55+
* @param hlsPlaylistString the string which correspond to the response of the HLS
56+
* manifest, which cannot be null or empty
57+
*/
58+
public void setPlaylistString(@NonNull final String hlsPlaylistString) {
59+
this.playlistString = hlsPlaylistString;
60+
}
61+
62+
/**
63+
* Create a new {@link NonUriHlsDataSourceFactory} with the given data source factory and
64+
* the given HLS playlist.
65+
*
66+
* @return a {@link NonUriHlsDataSourceFactory}
67+
* @throws IllegalArgumentException if the data source factory is null or if the HLS
68+
* playlist string set is null or empty
69+
*/
70+
@NonNull
71+
public NonUriHlsDataSourceFactory build() {
72+
if (dataSourceFactory == null) {
73+
throw new IllegalArgumentException(
74+
"No DataSource.Factory valid instance has been specified.");
75+
}
76+
77+
if (isNullOrEmpty(playlistString)) {
78+
throw new IllegalArgumentException("No HLS valid playlist has been specified.");
79+
}
80+
81+
return new NonUriHlsDataSourceFactory(dataSourceFactory,
82+
playlistString.getBytes(StandardCharsets.UTF_8));
83+
}
84+
}
85+
86+
private final DataSource.Factory dataSourceFactory;
87+
private final byte[] playlistStringByteArray;
88+
89+
/**
90+
* Create a {@link NonUriHlsDataSourceFactory} instance.
91+
*
92+
* @param dataSourceFactory the {@link DataSource.Factory} which will be used to build
93+
* non manifests {@link DataSource}s, which must not be null
94+
* @param playlistStringByteArray a byte array of the HLS playlist, which must not be null
95+
*/
96+
private NonUriHlsDataSourceFactory(@NonNull final DataSource.Factory dataSourceFactory,
97+
@NonNull final byte[] playlistStringByteArray) {
98+
this.dataSourceFactory = dataSourceFactory;
99+
this.playlistStringByteArray = playlistStringByteArray;
100+
}
101+
102+
/**
103+
* Create a {@link DataSource} for the given data type.
104+
*
105+
* <p>
106+
* Contrary to {@link com.google.android.exoplayer2.source.hls.DefaultHlsDataSourceFactory
107+
* ExoPlayer's default implementation}, this implementation is not always using the
108+
* {@link DataSource.Factory} passed to the
109+
* {@link com.google.android.exoplayer2.source.hls.HlsMediaSource.Factory
110+
* HlsMediaSource.Factory} constructor, only when it's not
111+
* {@link C#DATA_TYPE_MANIFEST the manifest type}.
112+
* </p>
113+
*
114+
* <p>
115+
* This change allow playback of non-URI HLS contents, when the manifest is not a master
116+
* manifest/playlist (otherwise, endless loops should be encountered because the
117+
* {@link DataSource}s created for media playlists should use the master playlist response
118+
* instead).
119+
* </p>
120+
*
121+
* @param dataType the data type for which the {@link DataSource} will be used, which is one of
122+
* {@link C} {@code .DATA_TYPE_*} constants
123+
* @return a {@link DataSource} for the given data type
124+
*/
125+
@NonNull
126+
@Override
127+
public DataSource createDataSource(final int dataType) {
128+
// The manifest is already downloaded and provided with playlistStringByteArray, so we
129+
// don't need to download it again and we can use a ByteArrayDataSource instead
130+
if (dataType == C.DATA_TYPE_MANIFEST) {
131+
return new ByteArrayDataSource(playlistStringByteArray);
132+
}
133+
134+
return dataSourceFactory.createDataSource();
135+
}
136+
}

app/src/main/java/org/schabi/newpipe/player/helper/NonUriHlsPlaylistParserFactory.java

Lines changed: 0 additions & 50 deletions
This file was deleted.

app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
1313
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
1414
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker;
15-
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParserFactory;
1615
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
1716
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
1817
import com.google.android.exoplayer2.upstream.DataSource;
@@ -26,6 +25,7 @@
2625
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator;
2726
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubePostLiveStreamDvrDashManifestCreator;
2827
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator;
28+
import org.schabi.newpipe.player.datasource.NonUriHlsDataSourceFactory;
2929
import org.schabi.newpipe.player.datasource.YoutubeHttpDataSource;
3030

3131
import java.io.File;
@@ -132,10 +132,13 @@ public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
132132

133133
//region Generic media source factories
134134
public HlsMediaSource.Factory getHlsMediaSourceFactory(
135-
@Nullable final HlsPlaylistParserFactory hlsPlaylistParserFactory) {
136-
final HlsMediaSource.Factory factory = new HlsMediaSource.Factory(cacheDataSourceFactory);
137-
factory.setPlaylistParserFactory(hlsPlaylistParserFactory);
138-
return factory;
135+
@Nullable final NonUriHlsDataSourceFactory.Builder hlsDataSourceFactoryBuilder) {
136+
if (hlsDataSourceFactoryBuilder != null) {
137+
hlsDataSourceFactoryBuilder.setDataSourceFactory(cacheDataSourceFactory);
138+
return new HlsMediaSource.Factory(hlsDataSourceFactoryBuilder.build());
139+
}
140+
141+
return new HlsMediaSource.Factory(cacheDataSourceFactory);
139142
}
140143

141144
public DashMediaSource.Factory getDashMediaSourceFactory() {

app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
1919
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
2020
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
21-
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
22-
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
2321
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
2422
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
2523
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
@@ -37,7 +35,7 @@
3735
import org.schabi.newpipe.extractor.stream.StreamInfo;
3836
import org.schabi.newpipe.extractor.stream.StreamType;
3937
import org.schabi.newpipe.extractor.stream.VideoStream;
40-
import org.schabi.newpipe.player.helper.NonUriHlsPlaylistParserFactory;
38+
import org.schabi.newpipe.player.datasource.NonUriHlsDataSourceFactory;
4139
import org.schabi.newpipe.player.helper.PlayerDataSource;
4240
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
4341
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
@@ -340,27 +338,17 @@ private static HlsMediaSource buildHlsMediaSource(final PlayerDataSource dataSou
340338
.setCustomCacheKey(cacheKey)
341339
.build());
342340
} else {
343-
String baseUrl = stream.getManifestUrl();
344-
if (baseUrl == null) {
345-
baseUrl = "";
346-
}
347-
348-
final Uri uri = Uri.parse(baseUrl);
349-
350-
final HlsPlaylist hlsPlaylist;
351-
try {
352-
final ByteArrayInputStream hlsManifestInput = new ByteArrayInputStream(
353-
stream.getContent().getBytes(StandardCharsets.UTF_8));
354-
hlsPlaylist = new HlsPlaylistParser().parse(uri, hlsManifestInput);
355-
} catch (final IOException e) {
356-
throw new ResolverException("Error when parsing manual HLS manifest", e);
341+
final NonUriHlsDataSourceFactory.Builder hlsDataSourceFactoryBuilder =
342+
new NonUriHlsDataSourceFactory.Builder();
343+
hlsDataSourceFactoryBuilder.setPlaylistString(stream.getContent());
344+
String manifestUrl = stream.getManifestUrl();
345+
if (manifestUrl == null) {
346+
manifestUrl = "";
357347
}
358-
359-
return dataSource.getHlsMediaSourceFactory(
360-
new NonUriHlsPlaylistParserFactory(hlsPlaylist))
348+
return dataSource.getHlsMediaSourceFactory(hlsDataSourceFactoryBuilder)
361349
.createMediaSource(new MediaItem.Builder()
362350
.setTag(metadata)
363-
.setUri(Uri.parse(stream.getContent()))
351+
.setUri(Uri.parse(manifestUrl))
364352
.setCustomCacheKey(cacheKey)
365353
.build());
366354
}

0 commit comments

Comments
 (0)