From 77ed3004a84ad8fe2af4d70d4dec733e995751d6 Mon Sep 17 00:00:00 2001 From: karyogamy Date: Thu, 17 Mar 2022 22:56:22 -0400 Subject: [PATCH] added: documentations on lifecycles for FailedMediaSource and LoadedMediaSource. fixed: onPlaybackSynchronize to rewind when not playing, which was incorrectly removed in previous commit. fixed: sonar and checkstyle issues. --- .../org/schabi/newpipe/player/Player.java | 2 +- .../player/mediaitem/ExceptionTag.java | 10 -- .../player/mediaitem/MediaItemTag.java | 12 ++- .../player/mediaitem/PlaceholderTag.java | 11 -- .../player/mediaitem/StreamInfoTag.java | 2 + .../player/mediasource/FailedMediaSource.java | 102 ++++++++++++------ .../player/mediasource/LoadedMediaSource.java | 37 ++++++- .../mediasource/PlaceholderMediaSource.java | 6 +- 8 files changed, 120 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 355cd407969..ea0cab5e652 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -2791,7 +2791,7 @@ public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, final boole + "index=[" + currentPlayQueueIndex + "] with " + "playlist length=[" + currentPlaylistSize + "]"); - } else if (wasBlocked || currentPlaylistIndex != currentPlayQueueIndex) { + } else if (wasBlocked || currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) { if (DEBUG) { Log.d(TAG, "Playback - Rewinding to correct " + "index=[" + currentPlayQueueIndex + "], " diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java index 3e41262d660..7e28eda207e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java @@ -87,16 +87,6 @@ public StreamType getStreamType() { return item.getStreamType(); } - @Override - public Optional getMaybeStreamInfo() { - return Optional.empty(); - } - - @Override - public Optional getMaybeQuality() { - return Optional.empty(); - } - @Override public Optional getMaybeExtras(@NonNull final Class type) { return Optional.ofNullable(extras).map(type::cast); diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java index 6f9b9e9f245..8dbd552809f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java @@ -44,9 +44,15 @@ public interface MediaItemTag { StreamType getStreamType(); - Optional getMaybeStreamInfo(); + @NonNull + default Optional getMaybeStreamInfo() { + return Optional.empty(); + } - Optional getMaybeQuality(); + @NonNull + default Optional getMaybeQuality() { + return Optional.empty(); + } Optional getMaybeExtras(@NonNull Class type); @@ -86,7 +92,7 @@ default MediaItem asMediaItem() { .build(); } - class Quality { + final class Quality { @NonNull private final List sortedVideoStreams; private final int selectedVideoStreamIndex; diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java index 5b53e5660d2..fe7aa9d9241 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.player.mediaitem; -import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.util.Constants; @@ -74,16 +73,6 @@ public StreamType getStreamType() { return StreamType.NONE; } - @Override - public Optional getMaybeStreamInfo() { - return Optional.empty(); - } - - @Override - public Optional getMaybeQuality() { - return Optional.empty(); - } - @Override public Optional getMaybeExtras(@NonNull final Class type) { return Optional.ofNullable(extras).map(type::cast); diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java index 9ae25e07b35..6a942840d50 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java @@ -91,11 +91,13 @@ public StreamType getStreamType() { return streamInfo.getStreamType(); } + @NonNull @Override public Optional getMaybeStreamInfo() { return Optional.of(streamInfo); } + @NonNull @Override public Optional getMaybeQuality() { return Optional.ofNullable(quality); diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index d68eb1a1a60..6f7aa352f7e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -3,16 +3,16 @@ import android.util.Log; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.CompositeMediaSource; +import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.SilenceMediaSource; +import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.TransferListener; import org.schabi.newpipe.player.mediaitem.ExceptionTag; -import org.schabi.newpipe.player.mediaitem.MediaItemTag; import org.schabi.newpipe.player.playqueue.PlayQueueItem; import java.io.IOException; @@ -22,7 +22,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -public class FailedMediaSource extends CompositeMediaSource implements ManagedMediaSource { +public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSource { /** * Play 2 seconds of silenced audio when a stream fails to resolve due to a known issue, * such as {@link org.schabi.newpipe.extractor.exceptions.ExtractionException}. @@ -32,12 +32,12 @@ public class FailedMediaSource extends CompositeMediaSource implements Man * not recommended, it may cause ExoPlayer to buffer for a while. * */ public static final long SILENCE_DURATION_US = TimeUnit.SECONDS.toMicros(2); + public static final MediaPeriod SILENT_MEDIA = makeSilentMediaPeriod(SILENCE_DURATION_US); private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); private final PlayQueueItem playQueueItem; private final Throwable error; private final long retryTimestamp; - private final MediaSource source; private final MediaItem mediaItem; /** * Fail the play queue item associated with this source, with potential future retries. @@ -56,15 +56,10 @@ public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem, this.playQueueItem = playQueueItem; this.error = error; this.retryTimestamp = retryTimestamp; - - final MediaItemTag tag = ExceptionTag + this.mediaItem = ExceptionTag .of(playQueueItem, Collections.singletonList(error)) - .withExtras(this); - this.mediaItem = tag.asMediaItem(); - this.source = new SilenceMediaSource.Factory() - .setDurationUs(SILENCE_DURATION_US) - .setTag(tag) - .createMediaSource(); + .withExtras(this) + .asMediaItem(); } public static FailedMediaSource of(@NonNull final PlayQueueItem playQueueItem, @@ -91,49 +86,78 @@ private boolean canRetry() { return System.currentTimeMillis() >= retryTimestamp; } - /** - * Returns the {@link MediaItem} whose media is provided by the source. - */ @Override public MediaItem getMediaItem() { return mediaItem; } + /** + * Prepare the source with info on the silence playback {@link Timeline} when the error + * is known as {@link FailedMediaSourceException}. For exemple, the error is Extractor-related, + * e.g. {@link org.schabi.newpipe.extractor.exceptions.ExtractionException ExtractionException}. + * These types of error is swallowed by this + * {@link com.google.android.exoplayer2.source.MediaSource MediaSource}, + * and the exceptions are carried to the {@link MediaItem} metadata. + *

+ * If the exception is not known, e.g. {@link java.net.UnknownHostException} or some + * other network issue, then no source info is refreshed and + * {@link #maybeThrowSourceInfoRefreshError()} be will triggered. + *

+ * Note that the method is called only once until {@link #releaseSourceInternal()} is called, + * so if no action is done in this method, playback will stall unless + * {@link #maybeThrowSourceInfoRefreshError()} is called. + * + * @param mediaTransferListener No data transfer listener needed, ignored here. + */ @Override protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) { - super.prepareSourceInternal(mediaTransferListener); Log.e(TAG, "Loading failed source: ", error); if (error instanceof FailedMediaSourceException) { - prepareChildSource(null, source); + refreshSourceInfo(makeSilentMediaTimeline(SILENCE_DURATION_US, mediaItem)); } } - + /** + * If the error is not known, e.g. network issue, then the exception is not swallowed in this + * {@link com.google.android.exoplayer2.source.MediaSource MediaSource}. This error is then + * propagated to the player, which {@link org.schabi.newpipe.player.Player Player} can react to + * it in {@link com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException)}. + * + * @throws IOException An error which will always result in + * {@link com.google.android.exoplayer2.PlaybackException#ERROR_CODE_IO_UNSPECIFIED}. + */ @Override public void maybeThrowSourceInfoRefreshError() throws IOException { if (!(error instanceof FailedMediaSourceException)) { throw new IOException(error); } - super.maybeThrowSourceInfoRefreshError(); } + /** + * This method is only called if {@link #prepareSourceInternal(TransferListener)} + * refreshes the source info with no exception. All parameters are ignored as this + * returns a static piece of silence media. + * + * @param id The identifier of the period. + * @param allocator An {@link Allocator} from which to obtain media buffer allocations. + * @param startPositionUs The expected start position, in microseconds. + * @return The common {@link MediaPeriod} holding the silence. + */ @Override - protected void onChildSourceInfoRefreshed(final Void id, - final MediaSource mediaSource, - final Timeline timeline) { - refreshSourceInfo(timeline); + public MediaPeriod createPeriod(final MediaPeriodId id, + final Allocator allocator, + final long startPositionUs) { + return SILENT_MEDIA; } - @Override - public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator, - final long startPositionUs) { - return source.createPeriod(id, allocator, startPositionUs); + public void releasePeriod(final MediaPeriod mediaPeriod) { + /* Do Nothing (we want to keep re-using the Silent MediaPeriod) */ } @Override - public void releasePeriod(final MediaPeriod mediaPeriod) { - source.releasePeriod(mediaPeriod); + protected void releaseSourceInternal() { + /* Do Nothing, no clean-up for processing/extra thread is needed by this MediaSource */ } @Override @@ -168,4 +192,22 @@ public StreamInfoLoadException(final Throwable cause) { super(cause); } } + + private static Timeline makeSilentMediaTimeline(final long durationUs, + @NonNull final MediaItem mediaItem) { + return new SinglePeriodTimeline( + durationUs, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* useLiveConfiguration= */ false, + /* manifest= */ null, + mediaItem); + } + + private static MediaPeriod makeSilentMediaPeriod(final long durationUs) { + return new SilenceMediaSource.Factory() + .setDurationUs(durationUs) + .createMediaSource() + .createPeriod(null, null, 0); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java index 193b90271ac..7c7cee184a7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java @@ -14,12 +14,24 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -public class LoadedMediaSource extends CompositeMediaSource implements ManagedMediaSource { +public class LoadedMediaSource extends CompositeMediaSource implements ManagedMediaSource { private final MediaSource source; private final PlayQueueItem stream; private final MediaItem mediaItem; private final long expireTimestamp; + /** + * Uses a {@link CompositeMediaSource} to wrap one or more child {@link MediaSource}s with + * actual media. This wrapper {@link LoadedMediaSource} holds the expiration timestamp as + * a {@link ManagedMediaSource} to allow explicit playlist management under + * {@link ManagedMediaSourcePlaylist}. + * + * @param source The child media source with actual media. + * @param tag Metadata for the child media source. + * @param stream The queue item associated with the media source. + * @param expireTimestamp The timestamp when the media source expires and might not be + * available for playback. + */ public LoadedMediaSource(@NonNull final MediaSource source, @NonNull final MediaItemTag tag, @NonNull final PlayQueueItem stream, @@ -39,14 +51,33 @@ private boolean isExpired() { return System.currentTimeMillis() >= expireTimestamp; } + /** + * Delegates the preparation of child {@link MediaSource} to the + * {@link CompositeMediaSource} wrapper. Since all {@link LoadedMediaSource} has only + * a single child media, child id of 0 is always used (sonar doesn't like null as id here). + * + * @param mediaTransferListener A data transfer listener that will be registered by the + * {@link CompositeMediaSource} for child source preparation. + */ @Override protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); - prepareChildSource(null, source); + prepareChildSource(0, source); } + /** + * When any child {@link MediaSource} is prepared, the refreshed {@link Timeline} can + * be listened here. But since {@link LoadedMediaSource} has only a single child source, + * this method is called only once until {@link #releaseSourceInternal()} is called. + * On refresh, the {@link CompositeMediaSource} delegate will be notified with the + * new {@link Timeline}. + * + * @param id The unique id used to prepare the child source. + * @param mediaSource The child source whose source info has been refreshed. + * @param timeline The new timeline of the child source. + */ @Override - protected void onChildSourceInfoRefreshed(final Void id, + protected void onChildSourceInfoRefreshed(final Integer id, final MediaSource mediaSource, final Timeline timeline) { refreshSourceInfo(timeline); diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java index 6b3f91eb32c..92d4403c8b1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java @@ -18,9 +18,7 @@ final class PlaceholderMediaSource private static final MediaItem MEDIA_ITEM = PlaceholderTag.EMPTY.withExtras(COPY).asMediaItem(); private PlaceholderMediaSource() { } - /** - * Returns the {@link MediaItem} whose media is provided by the source. - */ + @Override public MediaItem getMediaItem() { return MEDIA_ITEM; @@ -30,7 +28,7 @@ public MediaItem getMediaItem() { protected void onChildSourceInfoRefreshed(final Void id, final MediaSource mediaSource, final Timeline timeline) { - /* Do nothing, no timeline updates will stall playback */ + /* Do nothing, no timeline updates or error will stall playback */ } @Override