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.
This commit is contained in:
AudricV 2022-06-16 11:15:05 +02:00
parent 21c9530e8b
commit e3c2aea3cc
No known key found for this signature in database
GPG Key ID: DA92EC7905614198
4 changed files with 153 additions and 76 deletions

View File

@ -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.
*
* <p>
* 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.
* </p>
*
* <p>
* See {@link #createDataSource(int)} for changes and implementation details.
* </p>
*/
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.
*
* <p>
* 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}.
* </p>
*
* <p>
* 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).
* </p>
*
* @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();
}
}

View File

@ -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<HlsPlaylist> {
@Override
public HlsPlaylist parse(final Uri uri,
final InputStream inputStream) throws IOException {
return hlsPlaylist;
}
}
@NonNull
@Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() {
return new NonUriHlsPlayListParser();
}
@NonNull
@Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
@NonNull final HlsMultivariantPlaylist multivariantPlaylist,
@Nullable final HlsMediaPlaylist previousMediaPlaylist) {
return new NonUriHlsPlayListParser();
}
}

View File

@ -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() {

View File

@ -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<StreamInfo, MediaSource> {
.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());
}