Throw a dedicated exception when errors occur in PlaybackResolver

A new exception, ResolverException, a subclass of PlaybackResolver, is now thrown when errors occur in PlaybackResolver, instead of an IOException
This commit is contained in:
AudricV 2022-06-16 11:14:08 +02:00
parent 036196a487
commit 21c9530e8b
No known key found for this signature in database
GPG Key ID: DA92EC7905614198
3 changed files with 112 additions and 86 deletions

View File

@ -17,7 +17,6 @@ import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.mediaitem.StreamInfoTag; import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import java.io.IOException;
import java.util.List; import java.util.List;
public class AudioPlaybackResolver implements PlaybackResolver { public class AudioPlaybackResolver implements PlaybackResolver {
@ -55,8 +54,8 @@ public class AudioPlaybackResolver implements PlaybackResolver {
try { try {
return PlaybackResolver.buildMediaSource( return PlaybackResolver.buildMediaSource(
dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag); dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag);
} catch (final IOException e) { } catch (final ResolverException e) {
Log.e(TAG, "Unable to create audio source:", e); Log.e(TAG, "Unable to create audio source", e);
return null; return null;
} }
} }

View File

@ -97,17 +97,22 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
} }
/** /**
* Builds the cache key of a video stream. A cache key is unique to the features of the * Builds the cache key of a {@link VideoStream video stream}.
* provided video stream, and when possible independent of <i>transient</i> parameters (such as
* the url of the stream). This ensures that there are no conflicts, but also that the cache is
* used as much as possible: the same cache should be used for two streams which have the same
* features but e.g. a different url, since the url might have been reloaded in the meantime,
* but the stream actually referenced by the url is still the same.
* *
* @param info the stream info, to distinguish between streams with the same features but coming * <p>
* from different stream infos * A cache key is unique to the features of the provided video stream, and when possible
* @param videoStream the video stream for which the cache key should be created * independent of <i>transient</i> parameters (such as the URL of the stream).
* @return a key to be used to store the cache of the provided video stream * This ensures that there are no conflicts, but also that the cache is used as much as
* possible: the same cache should be used for two streams which have the same features but
* e.g. a different URL, since the URL might have been reloaded in the meantime, but the stream
* actually referenced by the URL is still the same.
* </p>
*
* @param info the {@link StreamInfo stream info}, to distinguish between streams with
* the same features but coming from different stream infos
* @param videoStream the {@link VideoStream video stream} for which the cache key should be
* created
* @return a key to be used to store the cache of the provided {@link VideoStream video stream}
*/ */
static String cacheKeyOf(final StreamInfo info, final VideoStream videoStream) { static String cacheKeyOf(final StreamInfo info, final VideoStream videoStream) {
final boolean resolutionUnknown = videoStream.getResolution().equals(RESOLUTION_UNKNOWN); final boolean resolutionUnknown = videoStream.getResolution().equals(RESOLUTION_UNKNOWN);
@ -127,17 +132,22 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
} }
/** /**
* Builds the cache key of an audio stream. A cache key is unique to the features of the * Builds the cache key of an audio stream.
* provided audio stream, and when possible independent of <i>transient</i> parameters (such as
* the url of the stream). This ensures that there are no conflicts, but also that the cache is
* used as much as possible: the same cache should be used for two streams which have the same
* features but e.g. a different url, since the url might have been reloaded in the meantime,
* but the stream actually referenced by the url is still the same.
* *
* @param info the stream info, to distinguish between streams with the same features but coming * <p>
* from different stream infos * A cache key is unique to the features of the provided {@link AudioStream audio stream}, and
* @param audioStream the audio stream for which the cache key should be created * when possible independent of <i>transient</i> parameters (such as the URL of the stream).
* @return a key to be used to store the cache of the provided audio stream * This ensures that there are no conflicts, but also that the cache is used as much as
* possible: the same cache should be used for two streams which have the same features but
* e.g. a different URL, since the URL might have been reloaded in the meantime, but the stream
* actually referenced by the URL is still the same.
* </p>
*
* @param info the {@link StreamInfo stream info}, to distinguish between streams with
* the same features but coming from different stream infos
* @param audioStream the {@link AudioStream audio stream} for which the cache key should be
* created
* @return a key to be used to store the cache of the provided {@link AudioStream audio stream}
*/ */
static String cacheKeyOf(final StreamInfo info, final AudioStream audioStream) { static String cacheKeyOf(final StreamInfo info, final AudioStream audioStream) {
final boolean averageBitrateUnknown = audioStream.getAverageBitrate() == UNKNOWN_BITRATE; final boolean averageBitrateUnknown = audioStream.getAverageBitrate() == UNKNOWN_BITRATE;
@ -158,16 +168,20 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
@Nullable @Nullable
static MediaSource maybeBuildLiveMediaSource(final PlayerDataSource dataSource, static MediaSource maybeBuildLiveMediaSource(final PlayerDataSource dataSource,
final StreamInfo info) { final StreamInfo info) {
final StreamType streamType = info.getStreamType(); if (!StreamTypeUtil.isLiveStream(info.getStreamType())) {
if (!StreamTypeUtil.isLiveStream(streamType)) {
return null; return null;
} }
final StreamInfoTag tag = StreamInfoTag.of(info); try {
if (!info.getHlsUrl().isEmpty()) { final StreamInfoTag tag = StreamInfoTag.of(info);
return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag); if (!info.getHlsUrl().isEmpty()) {
} else if (!info.getDashMpdUrl().isEmpty()) { return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag);
return buildLiveMediaSource(dataSource, info.getDashMpdUrl(), C.TYPE_DASH, tag); } else if (!info.getDashMpdUrl().isEmpty()) {
return buildLiveMediaSource(dataSource, info.getDashMpdUrl(), C.TYPE_DASH, tag);
}
} catch (final Exception e) {
Log.w(TAG, "Error when generating live media source, falling back to standard sources",
e);
} }
return null; return null;
@ -176,7 +190,7 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
static MediaSource buildLiveMediaSource(final PlayerDataSource dataSource, static MediaSource buildLiveMediaSource(final PlayerDataSource dataSource,
final String sourceUrl, final String sourceUrl,
@C.ContentType final int type, @C.ContentType final int type,
final MediaItemTag metadata) { final MediaItemTag metadata) throws ResolverException {
final MediaSource.Factory factory; final MediaSource.Factory factory;
switch (type) { switch (type) {
case C.TYPE_SS: case C.TYPE_SS:
@ -188,8 +202,10 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
case C.TYPE_HLS: case C.TYPE_HLS:
factory = dataSource.getLiveHlsMediaSourceFactory(); factory = dataSource.getLiveHlsMediaSourceFactory();
break; break;
case C.TYPE_OTHER: case C.TYPE_RTSP: default: case C.TYPE_OTHER:
throw new IllegalStateException("Unsupported type: " + type); case C.TYPE_RTSP:
default:
throw new ResolverException("Unsupported type: " + type);
} }
return factory.createMediaSource( return factory.createMediaSource(
@ -210,8 +226,7 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
final Stream stream, final Stream stream,
final StreamInfo streamInfo, final StreamInfo streamInfo,
final String cacheKey, final String cacheKey,
final MediaItemTag metadata) final MediaItemTag metadata) throws ResolverException {
throws IOException {
if (streamInfo.getService() == ServiceList.YouTube) { if (streamInfo.getService() == ServiceList.YouTube) {
return createYoutubeMediaSource(stream, streamInfo, dataSource, cacheKey, metadata); return createYoutubeMediaSource(stream, streamInfo, dataSource, cacheKey, metadata);
} }
@ -228,7 +243,7 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
return buildSSMediaSource(dataSource, stream, cacheKey, metadata); return buildSSMediaSource(dataSource, stream, cacheKey, metadata);
// Torrent streams are not supported by ExoPlayer // Torrent streams are not supported by ExoPlayer
default: default:
throw new IllegalArgumentException("Unsupported delivery type: " + deliveryMethod); throw new ResolverException("Unsupported delivery type: " + deliveryMethod);
} }
} }
@ -236,11 +251,11 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
final PlayerDataSource dataSource, final PlayerDataSource dataSource,
final Stream stream, final Stream stream,
final String cacheKey, final String cacheKey,
final MediaItemTag metadata) throws IOException { final MediaItemTag metadata) throws ResolverException {
final String url = stream.getContent(); final String url = stream.getContent();
if (isNullOrEmpty(url)) { if (isNullOrEmpty(url)) {
throw new IOException( throw new ResolverException(
"Try to generate a progressive media source from an empty string or from a " "Try to generate a progressive media source from an empty string or from a "
+ "null object"); + "null object");
} else { } else {
@ -257,11 +272,11 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
final Stream stream, final Stream stream,
final String cacheKey, final String cacheKey,
final MediaItemTag metadata) final MediaItemTag metadata)
throws IOException { throws ResolverException {
final boolean isUrlStream = stream.isUrl(); final boolean isUrlStream = stream.isUrl();
if (isUrlStream && isNullOrEmpty(stream.getContent())) { if (isUrlStream && isNullOrEmpty(stream.getContent())) {
throw new IOException("Try to generate a DASH media source from an empty string or " throw new ResolverException(
+ "from a null object"); "Could not build a DASH media source from an empty or a null URL content");
} }
if (isUrlStream) { if (isUrlStream) {
@ -279,41 +294,42 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
final Uri uri = Uri.parse(baseUrl); final Uri uri = Uri.parse(baseUrl);
return dataSource.getDashMediaSourceFactory().createMediaSource( try {
createDashManifest(stream.getContent(), stream), return dataSource.getDashMediaSourceFactory().createMediaSource(
new MediaItem.Builder() createDashManifest(stream.getContent(), stream),
.setTag(metadata) new MediaItem.Builder()
.setUri(uri) .setTag(metadata)
.setCustomCacheKey(cacheKey) .setUri(uri)
.build()); .setCustomCacheKey(cacheKey)
.build());
} catch (final IOException e) {
throw new ResolverException(
"Could not create a DASH media source/manifest from the manifest text");
}
} }
} }
private static DashManifest createDashManifest(final String manifestContent, private static DashManifest createDashManifest(final String manifestContent,
final Stream stream) throws IOException { final Stream stream) throws IOException {
try { final ByteArrayInputStream dashManifestInput = new ByteArrayInputStream(
final ByteArrayInputStream dashManifestInput = new ByteArrayInputStream( manifestContent.getBytes(StandardCharsets.UTF_8));
manifestContent.getBytes(StandardCharsets.UTF_8)); String baseUrl = stream.getManifestUrl();
String baseUrl = stream.getManifestUrl(); if (baseUrl == null) {
if (baseUrl == null) { baseUrl = "";
baseUrl = "";
}
return new DashManifestParser().parse(Uri.parse(baseUrl), dashManifestInput);
} catch (final IOException e) {
throw new IOException("Error when parsing manual DASH manifest", e);
} }
return new DashManifestParser().parse(Uri.parse(baseUrl), dashManifestInput);
} }
private static HlsMediaSource buildHlsMediaSource(final PlayerDataSource dataSource, private static HlsMediaSource buildHlsMediaSource(final PlayerDataSource dataSource,
final Stream stream, final Stream stream,
final String cacheKey, final String cacheKey,
final MediaItemTag metadata) final MediaItemTag metadata)
throws IOException { throws ResolverException {
final boolean isUrlStream = stream.isUrl(); final boolean isUrlStream = stream.isUrl();
if (isUrlStream && isNullOrEmpty(stream.getContent())) { if (isUrlStream && isNullOrEmpty(stream.getContent())) {
throw new IOException("Try to generate an HLS media source from an empty string or " throw new ResolverException(
+ "from a null object"); "Could not build a HLS media source from an empty or a null URL content");
} }
if (isUrlStream) { if (isUrlStream) {
@ -337,7 +353,7 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
stream.getContent().getBytes(StandardCharsets.UTF_8)); stream.getContent().getBytes(StandardCharsets.UTF_8));
hlsPlaylist = new HlsPlaylistParser().parse(uri, hlsManifestInput); hlsPlaylist = new HlsPlaylistParser().parse(uri, hlsManifestInput);
} catch (final IOException e) { } catch (final IOException e) {
throw new IOException("Error when parsing manual HLS manifest", e); throw new ResolverException("Error when parsing manual HLS manifest", e);
} }
return dataSource.getHlsMediaSourceFactory( return dataSource.getHlsMediaSourceFactory(
@ -354,11 +370,11 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
final Stream stream, final Stream stream,
final String cacheKey, final String cacheKey,
final MediaItemTag metadata) final MediaItemTag metadata)
throws IOException { throws ResolverException {
final boolean isUrlStream = stream.isUrl(); final boolean isUrlStream = stream.isUrl();
if (isUrlStream && isNullOrEmpty(stream.getContent())) { if (isUrlStream && isNullOrEmpty(stream.getContent())) {
throw new IOException("Try to generate an SmoothStreaming media source from an empty " throw new ResolverException(
+ "string or from a null object"); "Could not build a SS media source from an empty or a null URL content");
} }
if (isUrlStream) { if (isUrlStream) {
@ -383,7 +399,7 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
smoothStreamingManifest = new SsManifestParser().parse(uri, smoothStreamingManifest = new SsManifestParser().parse(uri,
smoothStreamingManifestInput); smoothStreamingManifestInput);
} catch (final IOException e) { } catch (final IOException e) {
throw new IOException("Error when parsing manual SmoothStreaming manifest", e); throw new ResolverException("Error when parsing manual SS manifest", e);
} }
return dataSource.getSSMediaSourceFactory().createMediaSource( return dataSource.getSSMediaSourceFactory().createMediaSource(
@ -404,10 +420,10 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
final PlayerDataSource dataSource, final PlayerDataSource dataSource,
final String cacheKey, final String cacheKey,
final MediaItemTag metadata) final MediaItemTag metadata)
throws IOException { throws ResolverException {
if (!(stream instanceof AudioStream || stream instanceof VideoStream)) { if (!(stream instanceof AudioStream || stream instanceof VideoStream)) {
throw new IOException("Try to generate a DASH manifest of a YouTube " throw new ResolverException("Generation of YouTube DASH manifest for "
+ stream.getClass() + " " + stream.getContent()); + stream.getClass().getSimpleName() + " is not supported");
} }
final StreamType streamType = streamInfo.getStreamType(); final StreamType streamType = streamInfo.getStreamType();
@ -430,15 +446,15 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
return buildYoutubeManualDashMediaSource(dataSource, return buildYoutubeManualDashMediaSource(dataSource,
createDashManifest(manifestString, stream), stream, cacheKey, createDashManifest(manifestString, stream), stream, cacheKey,
metadata); metadata);
} catch (final CreationException | NullPointerException e) { } catch (final CreationException | IOException | NullPointerException e) {
Log.e(TAG, "Error when generating the DASH manifest of YouTube ended live stream", Log.e(TAG, "Error when generating the DASH manifest of YouTube ended live stream",
e); e);
throw new IOException("Error when generating the DASH manifest of YouTube ended " throw new ResolverException(
+ "live stream " + stream.getContent(), e); "Error when generating the DASH manifest of YouTube ended live stream", e);
} }
} else { } else {
throw new IllegalArgumentException("DASH manifest generation of YouTube livestreams is " throw new ResolverException(
+ "not supported"); "DASH manifest generation of YouTube livestreams is not supported");
} }
} }
@ -447,7 +463,7 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
final Stream stream, final Stream stream,
final StreamInfo streamInfo, final StreamInfo streamInfo,
final String cacheKey, final String cacheKey,
final MediaItemTag metadata) throws IOException { final MediaItemTag metadata) throws ResolverException {
final DeliveryMethod deliveryMethod = stream.getDeliveryMethod(); final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
switch (deliveryMethod) { switch (deliveryMethod) {
case PROGRESSIVE_HTTP: case PROGRESSIVE_HTTP:
@ -488,12 +504,11 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
return buildYoutubeManualDashMediaSource(dataSource, return buildYoutubeManualDashMediaSource(dataSource,
createDashManifest(manifestString, stream), stream, cacheKey, createDashManifest(manifestString, stream), stream, cacheKey,
metadata); metadata);
} catch (final CreationException | NullPointerException e) { } catch (final CreationException | IOException | NullPointerException e) {
Log.e(TAG, Log.e(TAG,
"Error when generating the DASH manifest of YouTube OTF stream", e); "Error when generating the DASH manifest of YouTube OTF stream", e);
throw new IOException( throw new ResolverException(
"Error when generating the DASH manifest of YouTube OTF stream " "Error when generating the DASH manifest of YouTube OTF stream", e);
+ stream.getContent(), e);
} }
case HLS: case HLS:
return dataSource.getYoutubeHlsMediaSourceFactory().createMediaSource( return dataSource.getYoutubeHlsMediaSourceFactory().createMediaSource(
@ -503,7 +518,7 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
.setCustomCacheKey(cacheKey) .setCustomCacheKey(cacheKey)
.build()); .build());
default: default:
throw new IOException("Unsupported delivery method for YouTube contents: " throw new ResolverException("Unsupported delivery method for YouTube contents: "
+ deliveryMethod); + deliveryMethod);
} }
} }
@ -535,4 +550,17 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
.build()); .build());
} }
//endregion //endregion
//region resolver exception
final class ResolverException extends Exception {
public ResolverException(final String message) {
super(message);
}
public ResolverException(final String message, final Throwable cause) {
super(message, cause);
}
}
//endregion
} }

View File

@ -23,7 +23,6 @@ import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.mediaitem.StreamInfoTag; import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -94,8 +93,8 @@ public class VideoPlaybackResolver implements PlaybackResolver {
final MediaSource streamSource = PlaybackResolver.buildMediaSource( final MediaSource streamSource = PlaybackResolver.buildMediaSource(
dataSource, video, info, PlaybackResolver.cacheKeyOf(info, video), tag); dataSource, video, info, PlaybackResolver.cacheKeyOf(info, video), tag);
mediaSources.add(streamSource); mediaSources.add(streamSource);
} catch (final IOException e) { } catch (final ResolverException e) {
Log.e(TAG, "Unable to create video source:", e); Log.e(TAG, "Unable to create video source", e);
return null; return null;
} }
} }
@ -113,8 +112,8 @@ public class VideoPlaybackResolver implements PlaybackResolver {
dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag); dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag);
mediaSources.add(audioSource); mediaSources.add(audioSource);
streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO; streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
} catch (final IOException e) { } catch (final ResolverException e) {
Log.e(TAG, "Unable to create audio source:", e); Log.e(TAG, "Unable to create audio source", e);
return null; return null;
} }
} else { } else {