From b81eb35f3d045e9bbaa77e8fcfc13ab1fb43cd09 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 | 103 ++++++++++++------ .../player/mediasource/LoadedMediaSource.java | 39 ++++++- .../mediasource/PlaceholderMediaSource.java | 6 +- 8 files changed, 122 insertions(+), 63 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 355cd4079..ea0cab5e6 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 final class Player implements + "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 3e41262d6..7e28eda20 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 final class ExceptionTag implements MediaItemTag { 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 6f9b9e9f2..8dbd55280 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 @@ public interface MediaItemTag { .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 5b53e5660..fe7aa9d92 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 final class PlaceholderTag implements MediaItemTag { 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 9ae25e07b..6a942840d 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 final class StreamInfoTag implements MediaItemTag { 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 d68eb1a1a..299ae845d 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 @@ package org.schabi.newpipe.player.mediasource; 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 java.util.concurrent.TimeUnit; 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 class FailedMediaSource extends CompositeMediaSource implements Man 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,77 @@ public class FailedMediaSource extends CompositeMediaSource implements Man return System.currentTimeMillis() >= retryTimestamp; } - /** - * Returns the {@link MediaItem} whose media is provided by the source. - */ @Override public MediaItem getMediaItem() { return mediaItem; } + /** + * Prepares the source with {@link Timeline} info on the silence playback when the error + * is classed as {@link FailedMediaSourceException}, for example, when the error is + * {@link org.schabi.newpipe.extractor.exceptions.ExtractionException ExtractionException}. + * These types of error are swallowed by {@link FailedMediaSource}, and the underlying + * exception is carried to the {@link MediaItem} metadata during playback. + *

+ * 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 this method is called only once until {@link #releaseSourceInternal()} is called, + * so if no action is done in here, 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 here in + * {@link FailedMediaSource}. The exception is then propagated to the player, which + * {@link org.schabi.newpipe.player.Player Player} can react to inside + * {@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 and reused piece of silent audio. + * + * @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); - } - - - @Override - public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator, + public MediaPeriod createPeriod(final MediaPeriodId id, + final Allocator allocator, final long startPositionUs) { - return source.createPeriod(id, allocator, startPositionUs); + return SILENT_MEDIA; } @Override public void releasePeriod(final MediaPeriod mediaPeriod) { - source.releasePeriod(mediaPeriod); + /* Do Nothing (we want to keep re-using the Silent MediaPeriod) */ + } + + @Override + protected void releaseSourceInternal() { + /* Do Nothing, no clean-up for processing/extra thread is needed by this MediaSource */ } @Override @@ -168,4 +191,22 @@ public class FailedMediaSource extends CompositeMediaSource implements Man 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 193b90271..95524cf69 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 org.schabi.newpipe.player.playqueue.PlayQueueItem; 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 + * containing 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,35 @@ public class LoadedMediaSource extends CompositeMediaSource implements Man return System.currentTimeMillis() >= expireTimestamp; } + /** + * Delegates the preparation of child {@link MediaSource}s to the + * {@link CompositeMediaSource} wrapper. Since all {@link LoadedMediaSource}s use only + * a single child media, the 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 to 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}, otherwise {@link #createPeriod(MediaPeriodId, Allocator, long)} + * will not be called and playback may be stalled. + * + * @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 6b3f91eb3..92d4403c8 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 @@ final class PlaceholderMediaSource 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