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.
This commit is contained in:
parent
69646e5b5d
commit
b81eb35f3d
8 changed files with 122 additions and 63 deletions
|
@ -2791,7 +2791,7 @@ public final class Player implements
|
||||||
+ "index=[" + currentPlayQueueIndex + "] with "
|
+ "index=[" + currentPlayQueueIndex + "] with "
|
||||||
+ "playlist length=[" + currentPlaylistSize + "]");
|
+ "playlist length=[" + currentPlaylistSize + "]");
|
||||||
|
|
||||||
} else if (wasBlocked || currentPlaylistIndex != currentPlayQueueIndex) {
|
} else if (wasBlocked || currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Playback - Rewinding to correct "
|
Log.d(TAG, "Playback - Rewinding to correct "
|
||||||
+ "index=[" + currentPlayQueueIndex + "], "
|
+ "index=[" + currentPlayQueueIndex + "], "
|
||||||
|
|
|
@ -87,16 +87,6 @@ public final class ExceptionTag implements MediaItemTag {
|
||||||
return item.getStreamType();
|
return item.getStreamType();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<StreamInfo> getMaybeStreamInfo() {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Quality> getMaybeQuality() {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> Optional<T> getMaybeExtras(@NonNull final Class<T> type) {
|
public <T> Optional<T> getMaybeExtras(@NonNull final Class<T> type) {
|
||||||
return Optional.ofNullable(extras).map(type::cast);
|
return Optional.ofNullable(extras).map(type::cast);
|
||||||
|
|
|
@ -44,9 +44,15 @@ public interface MediaItemTag {
|
||||||
|
|
||||||
StreamType getStreamType();
|
StreamType getStreamType();
|
||||||
|
|
||||||
Optional<StreamInfo> getMaybeStreamInfo();
|
@NonNull
|
||||||
|
default Optional<StreamInfo> getMaybeStreamInfo() {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
Optional<Quality> getMaybeQuality();
|
@NonNull
|
||||||
|
default Optional<Quality> getMaybeQuality() {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
<T> Optional<T> getMaybeExtras(@NonNull Class<T> type);
|
<T> Optional<T> getMaybeExtras(@NonNull Class<T> type);
|
||||||
|
|
||||||
|
@ -86,7 +92,7 @@ public interface MediaItemTag {
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
class Quality {
|
final class Quality {
|
||||||
@NonNull
|
@NonNull
|
||||||
private final List<VideoStream> sortedVideoStreams;
|
private final List<VideoStream> sortedVideoStreams;
|
||||||
private final int selectedVideoStreamIndex;
|
private final int selectedVideoStreamIndex;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package org.schabi.newpipe.player.mediaitem;
|
package org.schabi.newpipe.player.mediaitem;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
|
|
||||||
|
@ -74,16 +73,6 @@ public final class PlaceholderTag implements MediaItemTag {
|
||||||
return StreamType.NONE;
|
return StreamType.NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<StreamInfo> getMaybeStreamInfo() {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Quality> getMaybeQuality() {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> Optional<T> getMaybeExtras(@NonNull final Class<T> type) {
|
public <T> Optional<T> getMaybeExtras(@NonNull final Class<T> type) {
|
||||||
return Optional.ofNullable(extras).map(type::cast);
|
return Optional.ofNullable(extras).map(type::cast);
|
||||||
|
|
|
@ -91,11 +91,13 @@ public final class StreamInfoTag implements MediaItemTag {
|
||||||
return streamInfo.getStreamType();
|
return streamInfo.getStreamType();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Optional<StreamInfo> getMaybeStreamInfo() {
|
public Optional<StreamInfo> getMaybeStreamInfo() {
|
||||||
return Optional.of(streamInfo);
|
return Optional.of(streamInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Optional<Quality> getMaybeQuality() {
|
public Optional<Quality> getMaybeQuality() {
|
||||||
return Optional.ofNullable(quality);
|
return Optional.ofNullable(quality);
|
||||||
|
|
|
@ -3,16 +3,16 @@ package org.schabi.newpipe.player.mediasource;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
|
import com.google.android.exoplayer2.PlaybackException;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
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.MediaPeriod;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.SilenceMediaSource;
|
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.Allocator;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
|
|
||||||
import org.schabi.newpipe.player.mediaitem.ExceptionTag;
|
import org.schabi.newpipe.player.mediaitem.ExceptionTag;
|
||||||
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -22,7 +22,7 @@ import java.util.concurrent.TimeUnit;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
public class FailedMediaSource extends CompositeMediaSource<Void> 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,
|
* 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}.
|
* such as {@link org.schabi.newpipe.extractor.exceptions.ExtractionException}.
|
||||||
|
@ -32,12 +32,12 @@ public class FailedMediaSource extends CompositeMediaSource<Void> implements Man
|
||||||
* not recommended, it may cause ExoPlayer to buffer for a while.
|
* 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 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 String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode());
|
||||||
private final PlayQueueItem playQueueItem;
|
private final PlayQueueItem playQueueItem;
|
||||||
private final Throwable error;
|
private final Throwable error;
|
||||||
private final long retryTimestamp;
|
private final long retryTimestamp;
|
||||||
private final MediaSource source;
|
|
||||||
private final MediaItem mediaItem;
|
private final MediaItem mediaItem;
|
||||||
/**
|
/**
|
||||||
* Fail the play queue item associated with this source, with potential future retries.
|
* Fail the play queue item associated with this source, with potential future retries.
|
||||||
|
@ -56,15 +56,10 @@ public class FailedMediaSource extends CompositeMediaSource<Void> implements Man
|
||||||
this.playQueueItem = playQueueItem;
|
this.playQueueItem = playQueueItem;
|
||||||
this.error = error;
|
this.error = error;
|
||||||
this.retryTimestamp = retryTimestamp;
|
this.retryTimestamp = retryTimestamp;
|
||||||
|
this.mediaItem = ExceptionTag
|
||||||
final MediaItemTag tag = ExceptionTag
|
|
||||||
.of(playQueueItem, Collections.singletonList(error))
|
.of(playQueueItem, Collections.singletonList(error))
|
||||||
.withExtras(this);
|
.withExtras(this)
|
||||||
this.mediaItem = tag.asMediaItem();
|
.asMediaItem();
|
||||||
this.source = new SilenceMediaSource.Factory()
|
|
||||||
.setDurationUs(SILENCE_DURATION_US)
|
|
||||||
.setTag(tag)
|
|
||||||
.createMediaSource();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FailedMediaSource of(@NonNull final PlayQueueItem playQueueItem,
|
public static FailedMediaSource of(@NonNull final PlayQueueItem playQueueItem,
|
||||||
|
@ -91,49 +86,77 @@ public class FailedMediaSource extends CompositeMediaSource<Void> implements Man
|
||||||
return System.currentTimeMillis() >= retryTimestamp;
|
return System.currentTimeMillis() >= retryTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the {@link MediaItem} whose media is provided by the source.
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public MediaItem getMediaItem() {
|
public MediaItem getMediaItem() {
|
||||||
return mediaItem;
|
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.
|
||||||
|
* <br><br>
|
||||||
|
* 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.
|
||||||
|
* <br><br>
|
||||||
|
* 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
|
@Override
|
||||||
protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) {
|
protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) {
|
||||||
super.prepareSourceInternal(mediaTransferListener);
|
|
||||||
Log.e(TAG, "Loading failed source: ", error);
|
Log.e(TAG, "Loading failed source: ", error);
|
||||||
if (error instanceof FailedMediaSourceException) {
|
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
|
@Override
|
||||||
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
||||||
if (!(error instanceof FailedMediaSourceException)) {
|
if (!(error instanceof FailedMediaSourceException)) {
|
||||||
throw new IOException(error);
|
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
|
@Override
|
||||||
protected void onChildSourceInfoRefreshed(final Void id,
|
public MediaPeriod createPeriod(final MediaPeriodId id,
|
||||||
final MediaSource mediaSource,
|
final Allocator allocator,
|
||||||
final Timeline timeline) {
|
|
||||||
refreshSourceInfo(timeline);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator,
|
|
||||||
final long startPositionUs) {
|
final long startPositionUs) {
|
||||||
return source.createPeriod(id, allocator, startPositionUs);
|
return SILENT_MEDIA;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void releasePeriod(final MediaPeriod mediaPeriod) {
|
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
|
@Override
|
||||||
|
@ -168,4 +191,22 @@ public class FailedMediaSource extends CompositeMediaSource<Void> implements Man
|
||||||
super(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,12 +14,24 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
public class LoadedMediaSource extends CompositeMediaSource<Void> implements ManagedMediaSource {
|
public class LoadedMediaSource extends CompositeMediaSource<Integer> implements ManagedMediaSource {
|
||||||
private final MediaSource source;
|
private final MediaSource source;
|
||||||
private final PlayQueueItem stream;
|
private final PlayQueueItem stream;
|
||||||
private final MediaItem mediaItem;
|
private final MediaItem mediaItem;
|
||||||
private final long expireTimestamp;
|
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,
|
public LoadedMediaSource(@NonNull final MediaSource source,
|
||||||
@NonNull final MediaItemTag tag,
|
@NonNull final MediaItemTag tag,
|
||||||
@NonNull final PlayQueueItem stream,
|
@NonNull final PlayQueueItem stream,
|
||||||
|
@ -39,14 +51,35 @@ public class LoadedMediaSource extends CompositeMediaSource<Void> implements Man
|
||||||
return System.currentTimeMillis() >= expireTimestamp;
|
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
|
@Override
|
||||||
protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) {
|
protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) {
|
||||||
super.prepareSourceInternal(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.
|
||||||
|
* <br><br>
|
||||||
|
* 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
|
@Override
|
||||||
protected void onChildSourceInfoRefreshed(final Void id,
|
protected void onChildSourceInfoRefreshed(final Integer id,
|
||||||
final MediaSource mediaSource,
|
final MediaSource mediaSource,
|
||||||
final Timeline timeline) {
|
final Timeline timeline) {
|
||||||
refreshSourceInfo(timeline);
|
refreshSourceInfo(timeline);
|
||||||
|
|
|
@ -18,9 +18,7 @@ final class PlaceholderMediaSource
|
||||||
private static final MediaItem MEDIA_ITEM = PlaceholderTag.EMPTY.withExtras(COPY).asMediaItem();
|
private static final MediaItem MEDIA_ITEM = PlaceholderTag.EMPTY.withExtras(COPY).asMediaItem();
|
||||||
|
|
||||||
private PlaceholderMediaSource() { }
|
private PlaceholderMediaSource() { }
|
||||||
/**
|
|
||||||
* Returns the {@link MediaItem} whose media is provided by the source.
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public MediaItem getMediaItem() {
|
public MediaItem getMediaItem() {
|
||||||
return MEDIA_ITEM;
|
return MEDIA_ITEM;
|
||||||
|
@ -30,7 +28,7 @@ final class PlaceholderMediaSource
|
||||||
protected void onChildSourceInfoRefreshed(final Void id,
|
protected void onChildSourceInfoRefreshed(final Void id,
|
||||||
final MediaSource mediaSource,
|
final MediaSource mediaSource,
|
||||||
final Timeline timeline) {
|
final Timeline timeline) {
|
||||||
/* Do nothing, no timeline updates will stall playback */
|
/* Do nothing, no timeline updates or error will stall playback */
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
Loading…
Reference in a new issue