diff --git a/app/src/main/java/org/schabi/newpipe/player/datasource/NonUriHlsDataSourceFactory.java b/app/src/main/java/org/schabi/newpipe/player/datasource/NonUriHlsDataSourceFactory.java new file mode 100644 index 000000000..676443a9c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/datasource/NonUriHlsDataSourceFactory.java @@ -0,0 +1,136 @@ +package org.schabi.newpipe.player.datasource; + +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + +import androidx.annotation.NonNull; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; +import com.google.android.exoplayer2.upstream.ByteArrayDataSource; +import com.google.android.exoplayer2.upstream.DataSource; + +import java.nio.charset.StandardCharsets; + +/** + * A {@link HlsDataSourceFactory} which allows playback of non-URI media HLS playlists for + * {@link com.google.android.exoplayer2.source.hls.HlsMediaSource HlsMediaSource}s. + * + *

+ * 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 + * {@link com.google.android.exoplayer2.upstream.DefaultDataSource DefaultDataSource}s. + *

+ * + *

+ * See {@link #createDataSource(int)} for changes and implementation details. + *

+ */ +public final class NonUriHlsDataSourceFactory implements HlsDataSourceFactory { + + /** + * Builder class of {@link NonUriHlsDataSourceFactory} instances. + */ + public static final class Builder { + private DataSource.Factory dataSourceFactory; + private String playlistString; + + /** + * Set the {@link DataSource.Factory} which will be used to create non manifest contents + * {@link DataSource}s. + * + * @param dataSourceFactoryForNonManifestContents the {@link DataSource.Factory} which will + * be used to create non manifest contents + * {@link DataSource}s, which cannot be null + */ + public void setDataSourceFactory( + @NonNull final DataSource.Factory dataSourceFactoryForNonManifestContents) { + this.dataSourceFactory = dataSourceFactoryForNonManifestContents; + } + + /** + * Set the HLS playlist which will be used for manifests requests. + * + * @param hlsPlaylistString the string which correspond to the response of the HLS + * manifest, which cannot be null or empty + */ + public void setPlaylistString(@NonNull final String hlsPlaylistString) { + this.playlistString = hlsPlaylistString; + } + + /** + * Create a new {@link NonUriHlsDataSourceFactory} with the given data source factory and + * the given HLS playlist. + * + * @return a {@link NonUriHlsDataSourceFactory} + * @throws IllegalArgumentException if the data source factory is null or if the HLS + * playlist string set is null or empty + */ + @NonNull + public NonUriHlsDataSourceFactory build() { + if (dataSourceFactory == null) { + throw new IllegalArgumentException( + "No DataSource.Factory valid instance has been specified."); + } + + if (isNullOrEmpty(playlistString)) { + throw new IllegalArgumentException("No HLS valid playlist has been specified."); + } + + return new NonUriHlsDataSourceFactory(dataSourceFactory, + playlistString.getBytes(StandardCharsets.UTF_8)); + } + } + + private final DataSource.Factory dataSourceFactory; + private final byte[] playlistStringByteArray; + + /** + * Create a {@link NonUriHlsDataSourceFactory} instance. + * + * @param dataSourceFactory the {@link DataSource.Factory} which will be used to build + * non manifests {@link DataSource}s, which must not be null + * @param playlistStringByteArray a byte array of the HLS playlist, which must not be null + */ + private NonUriHlsDataSourceFactory(@NonNull final DataSource.Factory dataSourceFactory, + @NonNull final byte[] playlistStringByteArray) { + this.dataSourceFactory = dataSourceFactory; + this.playlistStringByteArray = playlistStringByteArray; + } + + /** + * Create a {@link DataSource} for the given data type. + * + *

+ * Contrary to {@link com.google.android.exoplayer2.source.hls.DefaultHlsDataSourceFactory + * ExoPlayer's default implementation}, this implementation is not always using the + * {@link DataSource.Factory} passed to the + * {@link com.google.android.exoplayer2.source.hls.HlsMediaSource.Factory + * HlsMediaSource.Factory} constructor, only when it's not + * {@link C#DATA_TYPE_MANIFEST the manifest type}. + *

+ * + *

+ * This change allow playback of non-URI HLS contents, when the manifest is not a master + * manifest/playlist (otherwise, endless loops should be encountered because the + * {@link DataSource}s created for media playlists should use the master playlist response + * instead). + *

+ * + * @param dataType the data type for which the {@link DataSource} will be used, which is one of + * {@link C} {@code .DATA_TYPE_*} constants + * @return a {@link DataSource} for the given data type + */ + @NonNull + @Override + public DataSource createDataSource(final int dataType) { + // The manifest is already downloaded and provided with playlistStringByteArray, so we + // don't need to download it again and we can use a ByteArrayDataSource instead + if (dataType == C.DATA_TYPE_MANIFEST) { + return new ByteArrayDataSource(playlistStringByteArray); + } + + return dataSourceFactory.createDataSource(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/NonUriHlsPlaylistParserFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/NonUriHlsPlaylistParserFactory.java deleted file mode 100644 index a3a25fd1d..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/helper/NonUriHlsPlaylistParserFactory.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.schabi.newpipe.player.helper; - -import android.net.Uri; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParserFactory; -import com.google.android.exoplayer2.upstream.ParsingLoadable; - -import java.io.IOException; -import java.io.InputStream; - -/** - * A {@link HlsPlaylistParserFactory} for non-URI HLS sources. - */ -public final class NonUriHlsPlaylistParserFactory implements HlsPlaylistParserFactory { - - private final HlsPlaylist hlsPlaylist; - - public NonUriHlsPlaylistParserFactory(final HlsPlaylist hlsPlaylist) { - this.hlsPlaylist = hlsPlaylist; - } - - private final class NonUriHlsPlayListParser implements ParsingLoadable.Parser { - - @Override - public HlsPlaylist parse(final Uri uri, - final InputStream inputStream) throws IOException { - return hlsPlaylist; - } - } - - @NonNull - @Override - public ParsingLoadable.Parser createPlaylistParser() { - return new NonUriHlsPlayListParser(); - } - - @NonNull - @Override - public ParsingLoadable.Parser createPlaylistParser( - @NonNull final HlsMultivariantPlaylist multivariantPlaylist, - @Nullable final HlsMediaPlaylist previousMediaPlaylist) { - return new NonUriHlsPlayListParser(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java index 8cb423b51..82fb3a0e7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java @@ -12,7 +12,6 @@ import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParserFactory; import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.upstream.DataSource; @@ -26,6 +25,7 @@ import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator; import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubePostLiveStreamDvrDashManifestCreator; import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator; +import org.schabi.newpipe.player.datasource.NonUriHlsDataSourceFactory; import org.schabi.newpipe.player.datasource.YoutubeHttpDataSource; import java.io.File; @@ -132,10 +132,13 @@ public class PlayerDataSource { //region Generic media source factories public HlsMediaSource.Factory getHlsMediaSourceFactory( - @Nullable final HlsPlaylistParserFactory hlsPlaylistParserFactory) { - final HlsMediaSource.Factory factory = new HlsMediaSource.Factory(cacheDataSourceFactory); - factory.setPlaylistParserFactory(hlsPlaylistParserFactory); - return factory; + @Nullable final NonUriHlsDataSourceFactory.Builder hlsDataSourceFactoryBuilder) { + if (hlsDataSourceFactoryBuilder != null) { + hlsDataSourceFactoryBuilder.setDataSourceFactory(cacheDataSourceFactory); + return new HlsMediaSource.Factory(hlsDataSourceFactoryBuilder.build()); + } + + return new HlsMediaSource.Factory(cacheDataSourceFactory); } public DashMediaSource.Factory getDashMediaSourceFactory() { diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java index d7f04774c..763a67623 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java @@ -18,8 +18,6 @@ import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; @@ -37,7 +35,7 @@ import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; -import org.schabi.newpipe.player.helper.NonUriHlsPlaylistParserFactory; +import org.schabi.newpipe.player.datasource.NonUriHlsDataSourceFactory; import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.mediaitem.MediaItemTag; import org.schabi.newpipe.player.mediaitem.StreamInfoTag; @@ -340,27 +338,17 @@ public interface PlaybackResolver extends Resolver { .setCustomCacheKey(cacheKey) .build()); } else { - String baseUrl = stream.getManifestUrl(); - if (baseUrl == null) { - baseUrl = ""; + final NonUriHlsDataSourceFactory.Builder hlsDataSourceFactoryBuilder = + new NonUriHlsDataSourceFactory.Builder(); + hlsDataSourceFactoryBuilder.setPlaylistString(stream.getContent()); + String manifestUrl = stream.getManifestUrl(); + if (manifestUrl == null) { + manifestUrl = ""; } - - final Uri uri = Uri.parse(baseUrl); - - final HlsPlaylist hlsPlaylist; - try { - final ByteArrayInputStream hlsManifestInput = new ByteArrayInputStream( - stream.getContent().getBytes(StandardCharsets.UTF_8)); - hlsPlaylist = new HlsPlaylistParser().parse(uri, hlsManifestInput); - } catch (final IOException e) { - throw new ResolverException("Error when parsing manual HLS manifest", e); - } - - return dataSource.getHlsMediaSourceFactory( - new NonUriHlsPlaylistParserFactory(hlsPlaylist)) + return dataSource.getHlsMediaSourceFactory(hlsDataSourceFactoryBuilder) .createMediaSource(new MediaItem.Builder() .setTag(metadata) - .setUri(Uri.parse(stream.getContent())) + .setUri(Uri.parse(manifestUrl)) .setCustomCacheKey(cacheKey) .build()); }