updated: ExoPlayer to 2.17.1.
added: MediaItemTag for ManagedMediaSources. added: silent track for FailedMediaSource. added: keyframe fast forward at initial playback buffer. added: error notification on silently skipped streams.
This commit is contained in:
parent
8c5e8bdf78
commit
4e459b3383
29 changed files with 892 additions and 613 deletions
|
@ -104,7 +104,7 @@ ext {
|
||||||
androidxRoomVersion = '2.4.2'
|
androidxRoomVersion = '2.4.2'
|
||||||
|
|
||||||
icepickVersion = '3.2.0'
|
icepickVersion = '3.2.0'
|
||||||
exoPlayerVersion = '2.14.2'
|
exoPlayerVersion = '2.17.1'
|
||||||
googleAutoServiceVersion = '1.0.1'
|
googleAutoServiceVersion = '1.0.1'
|
||||||
groupieVersion = '2.10.0'
|
groupieVersion = '2.10.0'
|
||||||
markwonVersion = '4.6.2'
|
markwonVersion = '4.6.2'
|
||||||
|
|
|
@ -43,7 +43,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.PlaybackException;
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.material.appbar.AppBarLayout;
|
import com.google.android.material.appbar.AppBarLayout;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
|
@ -1884,9 +1884,8 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerError(final ExoPlaybackException error) {
|
public void onPlayerError(final PlaybackException error, final boolean isCatchableException) {
|
||||||
if (error.type == ExoPlaybackException.TYPE_SOURCE
|
if (!isCatchableException) {
|
||||||
|| error.type == ExoPlaybackException.TYPE_UNEXPECTED) {
|
|
||||||
// Properly exit from fullscreen
|
// Properly exit from fullscreen
|
||||||
toggleFullscreenIfInFullscreenMode();
|
toggleFullscreenIfInFullscreenMode();
|
||||||
hideMainPlayerOnLoadingNewStream();
|
hideMainPlayerOnLoadingNewStream();
|
||||||
|
|
|
@ -15,6 +15,7 @@ import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
|
import com.google.android.exoplayer2.PlaybackException;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||||
|
@ -28,6 +29,10 @@ import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_DECODING_FAILED;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECIFIED;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Outsourced logic for crashing the player in the {@link VideoDetailFragment}.
|
* Outsourced logic for crashing the player in the {@link VideoDetailFragment}.
|
||||||
*/
|
*/
|
||||||
|
@ -51,7 +56,8 @@ public final class VideoDetailPlayerCrasher {
|
||||||
exceptionTypes.put(
|
exceptionTypes.put(
|
||||||
"Source",
|
"Source",
|
||||||
() -> ExoPlaybackException.createForSource(
|
() -> ExoPlaybackException.createForSource(
|
||||||
new IOException(defaultMsg)
|
new IOException(defaultMsg),
|
||||||
|
ERROR_CODE_BEHIND_LIVE_WINDOW
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
exceptionTypes.put(
|
exceptionTypes.put(
|
||||||
|
@ -61,13 +67,16 @@ public final class VideoDetailPlayerCrasher {
|
||||||
"Dummy renderer",
|
"Dummy renderer",
|
||||||
0,
|
0,
|
||||||
null,
|
null,
|
||||||
C.FORMAT_HANDLED
|
C.FORMAT_HANDLED,
|
||||||
|
/*isRecoverable=*/false,
|
||||||
|
ERROR_CODE_DECODING_FAILED
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
exceptionTypes.put(
|
exceptionTypes.put(
|
||||||
"Unexpected",
|
"Unexpected",
|
||||||
() -> ExoPlaybackException.createForUnexpected(
|
() -> ExoPlaybackException.createForUnexpected(
|
||||||
new RuntimeException(defaultMsg)
|
new RuntimeException(defaultMsg),
|
||||||
|
ERROR_CODE_UNSPECIFIED
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
exceptionTypes.put(
|
exceptionTypes.put(
|
||||||
|
@ -139,7 +148,7 @@ public final class VideoDetailPlayerCrasher {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note that this method does not crash the underlying exoplayer directly (it's not possible).
|
* Note that this method does not crash the underlying exoplayer directly (it's not possible).
|
||||||
* It simply supplies a Exception to {@link Player#onPlayerError(ExoPlaybackException)}.
|
* It simply supplies a Exception to {@link Player#onPlayerError(PlaybackException)}.
|
||||||
* @param player
|
* @param player
|
||||||
* @param exception
|
* @param exception
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,5 +1,21 @@
|
||||||
package org.schabi.newpipe.player;
|
package org.schabi.newpipe.player;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_NO_PERMISSION;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_UNSPECIFIED;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_TIMEOUT;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECIFIED;
|
||||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AUTO_TRANSITION;
|
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AUTO_TRANSITION;
|
||||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL;
|
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL;
|
||||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_REMOVE;
|
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_REMOVE;
|
||||||
|
@ -112,20 +128,20 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.PlaybackException;
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.exoplayer2.Player.PositionInfo;
|
import com.google.android.exoplayer2.Player.PositionInfo;
|
||||||
import com.google.android.exoplayer2.RenderersFactory;
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SeekParameters;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
import com.google.android.exoplayer2.TracksInfo;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.TrackGroup;
|
import com.google.android.exoplayer2.source.TrackGroup;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.text.Cue;
|
import com.google.android.exoplayer2.text.Cue;
|
||||||
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
|
||||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||||
import com.google.android.exoplayer2.ui.CaptionStyleCompat;
|
import com.google.android.exoplayer2.ui.CaptionStyleCompat;
|
||||||
import com.google.android.exoplayer2.ui.SubtitleView;
|
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||||
|
@ -145,6 +161,7 @@ import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
|
import org.schabi.newpipe.extractor.Info;
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamSegment;
|
import org.schabi.newpipe.extractor.stream.StreamSegment;
|
||||||
|
@ -168,7 +185,7 @@ import org.schabi.newpipe.player.helper.PlayerDataSource;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
import org.schabi.newpipe.player.listeners.view.PlaybackSpeedClickListener;
|
import org.schabi.newpipe.player.listeners.view.PlaybackSpeedClickListener;
|
||||||
import org.schabi.newpipe.player.listeners.view.QualityClickListener;
|
import org.schabi.newpipe.player.listeners.view.QualityClickListener;
|
||||||
import org.schabi.newpipe.player.playback.CustomTrackSelector;
|
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
||||||
import org.schabi.newpipe.player.playback.MediaSourceManager;
|
import org.schabi.newpipe.player.playback.MediaSourceManager;
|
||||||
import org.schabi.newpipe.player.playback.PlaybackListener;
|
import org.schabi.newpipe.player.playback.PlaybackListener;
|
||||||
import org.schabi.newpipe.player.playback.PlayerMediaSession;
|
import org.schabi.newpipe.player.playback.PlayerMediaSession;
|
||||||
|
@ -180,7 +197,6 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
|
||||||
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
|
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
|
||||||
import org.schabi.newpipe.player.resolver.MediaSourceTag;
|
|
||||||
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
||||||
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType;
|
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType;
|
||||||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
|
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
|
||||||
|
@ -196,8 +212,8 @@ import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.views.ExpandableSurfaceView;
|
import org.schabi.newpipe.views.ExpandableSurfaceView;
|
||||||
import org.schabi.newpipe.views.player.PlayerFastSeekOverlay;
|
import org.schabi.newpipe.views.player.PlayerFastSeekOverlay;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -278,19 +294,19 @@ public final class Player implements
|
||||||
@Nullable private MediaSourceManager playQueueManager;
|
@Nullable private MediaSourceManager playQueueManager;
|
||||||
|
|
||||||
@Nullable private PlayQueueItem currentItem;
|
@Nullable private PlayQueueItem currentItem;
|
||||||
@Nullable private MediaSourceTag currentMetadata;
|
@Nullable private MediaItemTag currentMetadata;
|
||||||
@Nullable private Bitmap currentThumbnail;
|
@Nullable private Bitmap currentThumbnail;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Player
|
// Player
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private SimpleExoPlayer simpleExoPlayer;
|
private ExoPlayer simpleExoPlayer;
|
||||||
private AudioReactor audioReactor;
|
private AudioReactor audioReactor;
|
||||||
private MediaSessionManager mediaSessionManager;
|
private MediaSessionManager mediaSessionManager;
|
||||||
@Nullable private SurfaceHolderCallback surfaceHolderCallback;
|
@Nullable private SurfaceHolderCallback surfaceHolderCallback;
|
||||||
|
|
||||||
@NonNull private final CustomTrackSelector trackSelector;
|
@NonNull private final DefaultTrackSelector trackSelector;
|
||||||
@NonNull private final LoadController loadController;
|
@NonNull private final LoadController loadController;
|
||||||
@NonNull private final RenderersFactory renderFactory;
|
@NonNull private final RenderersFactory renderFactory;
|
||||||
|
|
||||||
|
@ -415,7 +431,7 @@ public final class Player implements
|
||||||
|
|
||||||
setupBroadcastReceiver();
|
setupBroadcastReceiver();
|
||||||
|
|
||||||
trackSelector = new CustomTrackSelector(context, PlayerHelper.getQualitySelector());
|
trackSelector = new DefaultTrackSelector(context, PlayerHelper.getQualitySelector());
|
||||||
final PlayerDataSource dataSource = new PlayerDataSource(context, DownloaderImpl.USER_AGENT,
|
final PlayerDataSource dataSource = new PlayerDataSource(context, DownloaderImpl.USER_AGENT,
|
||||||
new DefaultBandwidthMeter.Builder(context).build());
|
new DefaultBandwidthMeter.Builder(context).build());
|
||||||
loadController = new LoadController();
|
loadController = new LoadController();
|
||||||
|
@ -498,7 +514,7 @@ public final class Player implements
|
||||||
Log.d(TAG, "initPlayer() called with: playOnReady = [" + playOnReady + "]");
|
Log.d(TAG, "initPlayer() called with: playOnReady = [" + playOnReady + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
simpleExoPlayer = new SimpleExoPlayer.Builder(context, renderFactory)
|
simpleExoPlayer = new ExoPlayer.Builder(context, renderFactory)
|
||||||
.setTrackSelector(trackSelector)
|
.setTrackSelector(trackSelector)
|
||||||
.setLoadControl(loadController)
|
.setLoadControl(loadController)
|
||||||
.build();
|
.build();
|
||||||
|
@ -1642,8 +1658,7 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean getPlaybackSkipSilence() {
|
public boolean getPlaybackSkipSilence() {
|
||||||
return !exoPlayerIsNull() && simpleExoPlayer.getAudioComponent() != null
|
return !exoPlayerIsNull() && simpleExoPlayer.getSkipSilenceEnabled();
|
||||||
&& simpleExoPlayer.getAudioComponent().getSkipSilenceEnabled();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlaybackParameters getPlaybackParameters() {
|
public PlaybackParameters getPlaybackParameters() {
|
||||||
|
@ -1669,9 +1684,7 @@ public final class Player implements
|
||||||
savePlaybackParametersToPrefs(this, roundedSpeed, roundedPitch, skipSilence);
|
savePlaybackParametersToPrefs(this, roundedSpeed, roundedPitch, skipSilence);
|
||||||
simpleExoPlayer.setPlaybackParameters(
|
simpleExoPlayer.setPlaybackParameters(
|
||||||
new PlaybackParameters(roundedSpeed, roundedPitch));
|
new PlaybackParameters(roundedSpeed, roundedPitch));
|
||||||
if (simpleExoPlayer.getAudioComponent() != null) {
|
simpleExoPlayer.setSkipSilenceEnabled(skipSilence);
|
||||||
simpleExoPlayer.getAudioComponent().setSkipSilenceEnabled(skipSilence);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
@ -1950,10 +1963,12 @@ public final class Player implements
|
||||||
final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size();
|
final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size();
|
||||||
final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected();
|
final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected();
|
||||||
boolean showSegment = false;
|
boolean showSegment = false;
|
||||||
if (currentMetadata != null) {
|
showSegment = /*only when stream has segment and playing in fullscreen player*/
|
||||||
showSegment = !currentMetadata.getMetadata().getStreamSegments().isEmpty()
|
!popupPlayerSelected()
|
||||||
&& !popupPlayerSelected();
|
&& !getCurrentStreamInfo()
|
||||||
}
|
.map(StreamInfo::getStreamSegments)
|
||||||
|
.map(List::isEmpty)
|
||||||
|
.orElse(/*no stream info=*/true);
|
||||||
|
|
||||||
binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE);
|
binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE);
|
||||||
binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f);
|
binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f);
|
||||||
|
@ -1993,9 +2008,30 @@ public final class Player implements
|
||||||
// Playback states
|
// Playback states
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region Playback states
|
//region Playback states
|
||||||
|
@Override
|
||||||
|
public void onPlayWhenReadyChanged(final boolean playWhenReady, final int reason) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "ExoPlayer - onPlayWhenReadyChanged() called with: "
|
||||||
|
+ "playWhenReady = [" + playWhenReady + "], "
|
||||||
|
+ "reason = [" + reason + "]");
|
||||||
|
}
|
||||||
|
final int playbackState = simpleExoPlayer == null
|
||||||
|
? com.google.android.exoplayer2.Player.STATE_IDLE
|
||||||
|
: simpleExoPlayer.getPlaybackState();
|
||||||
|
updatePlaybackState(playWhenReady, playbackState);
|
||||||
|
}
|
||||||
|
|
||||||
@Override // exoplayer listener
|
@Override
|
||||||
public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
|
public void onPlaybackStateChanged(final int playbackState) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "ExoPlayer - onPlaybackStateChanged() called with: "
|
||||||
|
+ "playbackState = [" + playbackState + "]");
|
||||||
|
}
|
||||||
|
final boolean playWhenReady = simpleExoPlayer != null && simpleExoPlayer.getPlayWhenReady();
|
||||||
|
updatePlaybackState(playWhenReady, playbackState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePlaybackState(final boolean playWhenReady, final int playbackState) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: "
|
Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: "
|
||||||
+ "playWhenReady = [" + playWhenReady + "], "
|
+ "playWhenReady = [" + playWhenReady + "], "
|
||||||
|
@ -2004,7 +2040,7 @@ public final class Player implements
|
||||||
|
|
||||||
if (currentState == STATE_PAUSED_SEEK) {
|
if (currentState == STATE_PAUSED_SEEK) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "ExoPlayer - onPlayerStateChanged() is currently blocked");
|
Log.d(TAG, "updatePlaybackState() is currently blocked");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -2019,8 +2055,6 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case com.google.android.exoplayer2.Player.STATE_READY: //3
|
case com.google.android.exoplayer2.Player.STATE_READY: //3
|
||||||
maybeUpdateCurrentMetadata();
|
|
||||||
maybeCorrectSeekPosition();
|
|
||||||
if (!isPrepared) {
|
if (!isPrepared) {
|
||||||
isPrepared = true;
|
isPrepared = true;
|
||||||
onPrepared(playWhenReady);
|
onPrepared(playWhenReady);
|
||||||
|
@ -2037,18 +2071,11 @@ public final class Player implements
|
||||||
|
|
||||||
@Override // exoplayer listener
|
@Override // exoplayer listener
|
||||||
public void onIsLoadingChanged(final boolean isLoading) {
|
public void onIsLoadingChanged(final boolean isLoading) {
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "ExoPlayer - onLoadingChanged() called with: "
|
|
||||||
+ "isLoading = [" + isLoading + "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLoading && currentState == STATE_PAUSED && isProgressLoopRunning()) {
|
if (!isLoading && currentState == STATE_PAUSED && isProgressLoopRunning()) {
|
||||||
stopProgressLoop();
|
stopProgressLoop();
|
||||||
} else if (isLoading && !isProgressLoopRunning()) {
|
} else if (isLoading && !isProgressLoopRunning()) {
|
||||||
startProgressLoop();
|
startProgressLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
maybeUpdateCurrentMetadata();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override // own playback listener
|
@Override // own playback listener
|
||||||
|
@ -2460,27 +2487,37 @@ public final class Player implements
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region ExoPlayer listeners (that didn't fit in other categories)
|
//region ExoPlayer listeners (that didn't fit in other categories)
|
||||||
|
|
||||||
@Override
|
public void onEvents(@NonNull final com.google.android.exoplayer2.Player player,
|
||||||
public void onTimelineChanged(@NonNull final Timeline timeline, final int reason) {
|
@NonNull final com.google.android.exoplayer2.Player.Events events) {
|
||||||
if (DEBUG) {
|
Listener.super.onEvents(player, events);
|
||||||
Log.d(TAG, "ExoPlayer - onTimelineChanged() called with "
|
MediaItemTag.from(player.getCurrentMediaItem()).ifPresent(tag -> {
|
||||||
+ "timeline size = [" + timeline.getWindowCount() + "], "
|
if (tag == currentMetadata) {
|
||||||
+ "reason = [" + reason + "]");
|
return;
|
||||||
}
|
}
|
||||||
|
currentMetadata = tag;
|
||||||
maybeUpdateCurrentMetadata();
|
if (!tag.getErrors().isEmpty()) {
|
||||||
// force recreate notification to ensure seek bar is shown when preparation finishes
|
final ErrorInfo errorInfo = new ErrorInfo(
|
||||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true);
|
tag.getErrors().get(0),
|
||||||
|
UserAction.PLAY_STREAM,
|
||||||
|
"Loading failed for [" + tag.getTitle() + "]: " + tag.getStreamUrl(),
|
||||||
|
tag.getServiceId());
|
||||||
|
ErrorUtil.createNotification(context, errorInfo);
|
||||||
|
}
|
||||||
|
tag.getMaybeStreamInfo().ifPresent(info -> {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "ExoPlayer - onEvents() update stream info: " + info.getName());
|
||||||
|
}
|
||||||
|
updateMetadataWith(info);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTracksChanged(@NonNull final TrackGroupArray trackGroups,
|
public void onTracksInfoChanged(@NonNull final TracksInfo tracksInfo) {
|
||||||
@NonNull final TrackSelectionArray trackSelections) {
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "ExoPlayer - onTracksChanged(), "
|
Log.d(TAG, "ExoPlayer - onTracksChanged(), "
|
||||||
+ "track group size = " + trackGroups.length);
|
+ "track group size = " + tracksInfo.getTrackGroupInfos().size());
|
||||||
}
|
}
|
||||||
maybeUpdateCurrentMetadata();
|
|
||||||
onTextTracksChanged();
|
onTextTracksChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2499,20 +2536,32 @@ public final class Player implements
|
||||||
@DiscontinuityReason final int discontinuityReason) {
|
@DiscontinuityReason final int discontinuityReason) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with "
|
Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with "
|
||||||
|
+ "oldPositionIndex = [" + oldPosition.mediaItemIndex + "], "
|
||||||
|
+ "oldPositionMs = [" + oldPosition.positionMs + "], "
|
||||||
|
+ "newPositionIndex = [" + newPosition.mediaItemIndex + "], "
|
||||||
|
+ "newPositionMs = [" + newPosition.positionMs + "], "
|
||||||
+ "discontinuityReason = [" + discontinuityReason + "]");
|
+ "discontinuityReason = [" + discontinuityReason + "]");
|
||||||
}
|
}
|
||||||
if (playQueue == null) {
|
if (playQueue == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newPosition.contentPositionMs == 0 &&
|
||||||
|
simpleExoPlayer.getTotalBufferedDuration() < 500L) {
|
||||||
|
Log.d(TAG, "Playback - skipping to initial keyframe.");
|
||||||
|
simpleExoPlayer.setSeekParameters(SeekParameters.CLOSEST_SYNC);
|
||||||
|
simpleExoPlayer.seekTo(1L);
|
||||||
|
simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context));
|
||||||
|
}
|
||||||
|
|
||||||
// Refresh the playback if there is a transition to the next video
|
// Refresh the playback if there is a transition to the next video
|
||||||
final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
|
final int newIndex = newPosition.mediaItemIndex;
|
||||||
switch (discontinuityReason) {
|
switch (discontinuityReason) {
|
||||||
case DISCONTINUITY_REASON_AUTO_TRANSITION:
|
case DISCONTINUITY_REASON_AUTO_TRANSITION:
|
||||||
case DISCONTINUITY_REASON_REMOVE:
|
case DISCONTINUITY_REASON_REMOVE:
|
||||||
// When player is in single repeat mode and a period transition occurs,
|
// When player is in single repeat mode and a period transition occurs,
|
||||||
// we need to register a view count here since no metadata has changed
|
// we need to register a view count here since no metadata has changed
|
||||||
if (getRepeatMode() == REPEAT_MODE_ONE && newWindowIndex == playQueue.getIndex()) {
|
if (getRepeatMode() == REPEAT_MODE_ONE && newIndex == playQueue.getIndex()) {
|
||||||
registerStreamViewed();
|
registerStreamViewed();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -2525,16 +2574,15 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
|
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
|
||||||
case DISCONTINUITY_REASON_INTERNAL:
|
case DISCONTINUITY_REASON_INTERNAL:
|
||||||
if (playQueue.getIndex() != newWindowIndex) {
|
// Player index may be invalid when playback is blocked
|
||||||
|
if (getCurrentState() != STATE_BLOCKED && newIndex != playQueue.getIndex()) {
|
||||||
saveStreamProgressStateCompleted(); // current stream has ended
|
saveStreamProgressStateCompleted(); // current stream has ended
|
||||||
playQueue.setIndex(newWindowIndex);
|
playQueue.setIndex(newIndex);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case DISCONTINUITY_REASON_SKIP:
|
case DISCONTINUITY_REASON_SKIP:
|
||||||
break; // only makes Android Studio linter happy, as there are no ads
|
break; // only makes Android Studio linter happy, as there are no ads
|
||||||
}
|
}
|
||||||
|
|
||||||
maybeUpdateCurrentMetadata();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -2557,96 +2605,83 @@ public final class Player implements
|
||||||
//region Errors
|
//region Errors
|
||||||
/**
|
/**
|
||||||
* Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
|
* Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
|
||||||
* <p>There are multiple types of errors:</p>
|
|
||||||
* <ul>
|
|
||||||
* <li>{@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}</li>
|
|
||||||
* <li>{@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}:
|
|
||||||
* If a runtime error occurred, then we can try to recover it by restarting the playback
|
|
||||||
* after setting the timestamp recovery.</li>
|
|
||||||
* <li>{@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}:
|
|
||||||
* If the renderer failed, treat the error as unrecoverable.</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
*
|
||||||
* @see #processSourceError(IOException)
|
* @see com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException)
|
||||||
* @see com.google.android.exoplayer2.Player.Listener#onPlayerError(ExoPlaybackException)
|
* */
|
||||||
*/
|
@SuppressLint("SwitchIntDef")
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerError(@NonNull final ExoPlaybackException error) {
|
public void onPlayerError(@NonNull final PlaybackException error) {
|
||||||
Log.e(TAG, "ExoPlayer - onPlayerError() called with:", error);
|
Log.e(TAG, "ExoPlayer - onPlayerError() called with:", error);
|
||||||
|
|
||||||
|
setRecovery();
|
||||||
saveStreamProgressState();
|
saveStreamProgressState();
|
||||||
boolean isCatchableException = false;
|
boolean isCatchableException = false;
|
||||||
|
|
||||||
switch (error.type) {
|
switch (error.errorCode) {
|
||||||
case ExoPlaybackException.TYPE_SOURCE:
|
case ERROR_CODE_BEHIND_LIVE_WINDOW:
|
||||||
isCatchableException = processSourceError(error.getSourceException());
|
isCatchableException = true;
|
||||||
|
simpleExoPlayer.seekToDefaultPosition();
|
||||||
|
simpleExoPlayer.prepare();
|
||||||
|
// Inform the user that we are reloading the stream by
|
||||||
|
// switching to the buffering state
|
||||||
|
onBuffering();
|
||||||
break;
|
break;
|
||||||
case ExoPlaybackException.TYPE_UNEXPECTED:
|
case ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE:
|
||||||
|
case ERROR_CODE_IO_BAD_HTTP_STATUS:
|
||||||
|
case ERROR_CODE_IO_FILE_NOT_FOUND:
|
||||||
|
case ERROR_CODE_IO_NO_PERMISSION:
|
||||||
|
case ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED:
|
||||||
|
case ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE:
|
||||||
|
case ERROR_CODE_PARSING_CONTAINER_MALFORMED:
|
||||||
|
case ERROR_CODE_PARSING_MANIFEST_MALFORMED:
|
||||||
|
case ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED:
|
||||||
|
case ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED:
|
||||||
|
// Source errors, signal on playQueue and move on:
|
||||||
|
if (!exoPlayerIsNull() && playQueue != null) {
|
||||||
|
isCatchableException = true;
|
||||||
|
playQueue.error();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ERROR_CODE_TIMEOUT:
|
||||||
|
case ERROR_CODE_IO_UNSPECIFIED:
|
||||||
|
case ERROR_CODE_IO_NETWORK_CONNECTION_FAILED:
|
||||||
|
case ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT:
|
||||||
|
// Don't create notification on timeout/networking errors:
|
||||||
|
isCatchableException = true;
|
||||||
|
case ERROR_CODE_UNSPECIFIED:
|
||||||
|
// Reload playback on unexpected errors:
|
||||||
setRecovery();
|
setRecovery();
|
||||||
reloadPlayQueueManager();
|
reloadPlayQueueManager();
|
||||||
break;
|
break;
|
||||||
case ExoPlaybackException.TYPE_REMOTE:
|
|
||||||
case ExoPlaybackException.TYPE_RENDERER:
|
|
||||||
default:
|
default:
|
||||||
|
// API, remote and renderer errors belong here:
|
||||||
onPlaybackShutdown();
|
onPlaybackShutdown();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCatchableException) {
|
if (!isCatchableException) {
|
||||||
return;
|
createErrorNotification(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
createErrorNotification(error);
|
|
||||||
|
|
||||||
if (fragmentListener != null) {
|
if (fragmentListener != null) {
|
||||||
fragmentListener.onPlayerError(error);
|
fragmentListener.onPlayerError(error, isCatchableException);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createErrorNotification(@NonNull final ExoPlaybackException error) {
|
private void createErrorNotification(@NonNull final PlaybackException error) {
|
||||||
final ErrorInfo errorInfo;
|
final ErrorInfo errorInfo;
|
||||||
if (currentMetadata == null) {
|
if (currentMetadata == null) {
|
||||||
errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM,
|
errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM,
|
||||||
"Player error[type=" + error.type + "] occurred, currentMetadata is null");
|
"Player error[type=" + error.getErrorCodeName()
|
||||||
|
+ "] occurred, currentMetadata is null");
|
||||||
} else {
|
} else {
|
||||||
errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM,
|
errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM,
|
||||||
"Player error[type=" + error.type + "] occurred while playing "
|
"Player error[type=" + error.getErrorCodeName()
|
||||||
+ currentMetadata.getMetadata().getUrl(),
|
+ "] occurred while playing " + currentMetadata.getStreamUrl(),
|
||||||
currentMetadata.getMetadata());
|
currentMetadata.getServiceId());
|
||||||
}
|
}
|
||||||
ErrorUtil.createNotification(context, errorInfo);
|
ErrorUtil.createNotification(context, errorInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Process an {@link IOException} returned by {@link ExoPlaybackException#getSourceException()}
|
|
||||||
* for {@link ExoPlaybackException#TYPE_SOURCE} exceptions.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* This method sets the recovery position and sends an error message to the play queue if the
|
|
||||||
* exception is not a {@link BehindLiveWindowException}.
|
|
||||||
* </p>
|
|
||||||
* @param error the source error which was thrown by ExoPlayer
|
|
||||||
* @return whether the exception thrown is a {@link BehindLiveWindowException} ({@code false}
|
|
||||||
* is always returned if ExoPlayer or the play queue is null)
|
|
||||||
*/
|
|
||||||
private boolean processSourceError(final IOException error) {
|
|
||||||
if (exoPlayerIsNull() || playQueue == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
setRecovery();
|
|
||||||
|
|
||||||
if (error instanceof BehindLiveWindowException) {
|
|
||||||
simpleExoPlayer.seekToDefaultPosition();
|
|
||||||
simpleExoPlayer.prepare();
|
|
||||||
// Inform the user that we are reloading the stream by switching to the buffering state
|
|
||||||
onBuffering();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
playQueue.error();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
@ -2693,7 +2728,7 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
|
|
||||||
final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline();
|
final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline();
|
||||||
final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
|
final int currentWindowIndex = simpleExoPlayer.getCurrentMediaItemIndex();
|
||||||
if (currentTimeline.isEmpty() || currentWindowIndex < 0
|
if (currentTimeline.isEmpty() || currentWindowIndex < 0
|
||||||
|| currentWindowIndex >= currentTimeline.getWindowCount()) {
|
|| currentWindowIndex >= currentTimeline.getWindowCount()) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -2705,10 +2740,10 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override // own playback listener
|
@Override // own playback listener
|
||||||
public void onPlaybackSynchronize(@NonNull final PlayQueueItem item) {
|
public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, final boolean wasBlocked) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Playback - onPlaybackSynchronize() called with "
|
Log.d(TAG, "Playback - onPlaybackSynchronize(was blocked: " + wasBlocked
|
||||||
+ "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]");
|
+ ") called with item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]");
|
||||||
}
|
}
|
||||||
if (exoPlayerIsNull() || playQueue == null) {
|
if (exoPlayerIsNull() || playQueue == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -2718,7 +2753,7 @@ public final class Player implements
|
||||||
final boolean hasPlayQueueItemChanged = currentItem != item;
|
final boolean hasPlayQueueItemChanged = currentItem != item;
|
||||||
|
|
||||||
final int currentPlayQueueIndex = playQueue.indexOf(item);
|
final int currentPlayQueueIndex = playQueue.indexOf(item);
|
||||||
final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex();
|
final int currentPlaylistIndex = simpleExoPlayer.getCurrentMediaItemIndex();
|
||||||
final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount();
|
final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount();
|
||||||
|
|
||||||
// If nothing to synchronize
|
// If nothing to synchronize
|
||||||
|
@ -2740,8 +2775,7 @@ public final class Player implements
|
||||||
+ "index=[" + currentPlayQueueIndex + "] with "
|
+ "index=[" + currentPlayQueueIndex + "] with "
|
||||||
+ "playlist length=[" + currentPlaylistSize + "]");
|
+ "playlist length=[" + currentPlaylistSize + "]");
|
||||||
|
|
||||||
} else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial
|
} 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 + "], "
|
||||||
|
@ -2758,28 +2792,6 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeCorrectSeekPosition() {
|
|
||||||
if (playQueue == null || exoPlayerIsNull() || currentMetadata == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final PlayQueueItem currentSourceItem = playQueue.getItem();
|
|
||||||
if (currentSourceItem == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final StreamInfo currentInfo = currentMetadata.getMetadata();
|
|
||||||
final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000;
|
|
||||||
if (presetStartPositionMillis > 0L) {
|
|
||||||
// Has another start position?
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "Playback - Seeking to preset start "
|
|
||||||
+ "position=[" + presetStartPositionMillis + "]");
|
|
||||||
}
|
|
||||||
seekTo(presetStartPositionMillis);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void seekTo(final long positionMillis) {
|
public void seekTo(final long positionMillis) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]");
|
Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]");
|
||||||
|
@ -2941,24 +2953,24 @@ public final class Player implements
|
||||||
//region StreamInfo history: views and progress
|
//region StreamInfo history: views and progress
|
||||||
|
|
||||||
private void registerStreamViewed() {
|
private void registerStreamViewed() {
|
||||||
if (currentMetadata != null) {
|
getCurrentStreamInfo().ifPresent(info -> {
|
||||||
databaseUpdateDisposable.add(recordManager.onViewed(currentMetadata.getMetadata())
|
databaseUpdateDisposable
|
||||||
.onErrorComplete().subscribe());
|
.add(recordManager.onViewed(info).onErrorComplete().subscribe());
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveStreamProgressState(final long progressMillis) {
|
private void saveStreamProgressState(final long progressMillis) {
|
||||||
if (currentMetadata == null
|
if (!getCurrentStreamInfo().isPresent()
|
||||||
|| !prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
|
|| !prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "saveStreamProgressState() called with: progressMillis=" + progressMillis
|
Log.d(TAG, "saveStreamProgressState() called with: progressMillis=" + progressMillis
|
||||||
+ ", currentMetadata=[" + currentMetadata.getMetadata().getName() + "]");
|
+ ", currentMetadata=[" + getCurrentStreamInfo().get().getName() + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
databaseUpdateDisposable
|
databaseUpdateDisposable
|
||||||
.add(recordManager.saveStreamState(currentMetadata.getMetadata(), progressMillis)
|
.add(recordManager.saveStreamState(getCurrentStreamInfo().get(), progressMillis)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doOnError(e -> {
|
.doOnError(e -> {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
@ -2971,7 +2983,7 @@ public final class Player implements
|
||||||
|
|
||||||
public void saveStreamProgressState() {
|
public void saveStreamProgressState() {
|
||||||
if (exoPlayerIsNull() || currentMetadata == null || playQueue == null
|
if (exoPlayerIsNull() || currentMetadata == null || playQueue == null
|
||||||
|| playQueue.getIndex() != simpleExoPlayer.getCurrentWindowIndex()) {
|
|| playQueue.getIndex() != simpleExoPlayer.getCurrentMediaItemIndex()) {
|
||||||
// Make sure play queue and current window index are equal, to prevent saving state for
|
// Make sure play queue and current window index are equal, to prevent saving state for
|
||||||
// the wrong stream on discontinuity (e.g. when the stream just changed but the
|
// the wrong stream on discontinuity (e.g. when the stream just changed but the
|
||||||
// playQueue index and currentMetadata still haven't updated)
|
// playQueue index and currentMetadata still haven't updated)
|
||||||
|
@ -2984,10 +2996,10 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveStreamProgressStateCompleted() {
|
public void saveStreamProgressStateCompleted() {
|
||||||
if (currentMetadata != null) {
|
getCurrentStreamInfo().ifPresent(info -> {
|
||||||
// current stream has ended, so the progress is its duration (+1 to overcome rounding)
|
// current stream has ended, so the progress is its duration (+1 to overcome rounding)
|
||||||
saveStreamProgressState((currentMetadata.getMetadata().getDuration() + 1) * 1000);
|
saveStreamProgressState((info.getDuration() + 1) * 1000);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
@ -2998,8 +3010,7 @@ public final class Player implements
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region Metadata
|
//region Metadata
|
||||||
|
|
||||||
private void onMetadataChanged(@NonNull final MediaSourceTag tag) {
|
private void onMetadataChanged(@NonNull final StreamInfo info) {
|
||||||
final StreamInfo info = tag.getMetadata();
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName());
|
Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName());
|
||||||
}
|
}
|
||||||
|
@ -3009,12 +3020,10 @@ public final class Player implements
|
||||||
updateStreamRelatedViews();
|
updateStreamRelatedViews();
|
||||||
showHideKodiButton();
|
showHideKodiButton();
|
||||||
|
|
||||||
binding.titleTextView.setText(tag.getMetadata().getName());
|
binding.titleTextView.setText(info.getName());
|
||||||
binding.channelTextView.setText(tag.getMetadata().getUploaderName());
|
binding.channelTextView.setText(info.getUploaderName());
|
||||||
|
|
||||||
this.seekbarPreviewThumbnailHolder.resetFrom(
|
this.seekbarPreviewThumbnailHolder.resetFrom(this.getContext(), info.getPreviewFrames());
|
||||||
this.getContext(),
|
|
||||||
tag.getMetadata().getPreviewFrames());
|
|
||||||
|
|
||||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||||
|
|
||||||
|
@ -3024,9 +3033,7 @@ public final class Player implements
|
||||||
getVideoTitle(),
|
getVideoTitle(),
|
||||||
getUploaderName(),
|
getUploaderName(),
|
||||||
showThumbnail ? Optional.ofNullable(getThumbnail()) : Optional.empty(),
|
showThumbnail ? Optional.ofNullable(getThumbnail()) : Optional.empty(),
|
||||||
StreamTypeUtil.isLiveStream(tag.getMetadata().getStreamType())
|
StreamTypeUtil.isLiveStream(info.getStreamType()) ? -1 : info.getDuration()
|
||||||
? -1
|
|
||||||
: tag.getMetadata().getDuration()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
notifyMetadataUpdateToListeners();
|
notifyMetadataUpdateToListeners();
|
||||||
|
@ -3043,40 +3050,21 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeUpdateCurrentMetadata() {
|
private void updateMetadataWith(@NonNull final StreamInfo streamInfo) {
|
||||||
if (exoPlayerIsNull()) {
|
if (exoPlayerIsNull()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final MediaSourceTag metadata;
|
maybeAutoQueueNextStream(streamInfo);
|
||||||
try {
|
onMetadataChanged(streamInfo);
|
||||||
final MediaItem currentMediaItem = simpleExoPlayer.getCurrentMediaItem();
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true);
|
||||||
if (currentMediaItem == null || currentMediaItem.playbackProperties == null
|
|
||||||
|| currentMediaItem.playbackProperties.tag == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
metadata = (MediaSourceTag) currentMediaItem.playbackProperties.tag;
|
|
||||||
} catch (final IndexOutOfBoundsException | ClassCastException ex) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "Could not update metadata", ex);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
maybeAutoQueueNextStream(metadata);
|
|
||||||
|
|
||||||
if (currentMetadata == metadata) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
currentMetadata = metadata;
|
|
||||||
onMetadataChanged(metadata);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private String getVideoUrl() {
|
private String getVideoUrl() {
|
||||||
return currentMetadata == null
|
return currentMetadata == null
|
||||||
? context.getString(R.string.unknown_content)
|
? context.getString(R.string.unknown_content)
|
||||||
: currentMetadata.getMetadata().getUrl();
|
: currentMetadata.getStreamUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -3084,7 +3072,7 @@ public final class Player implements
|
||||||
final int timeSeconds = binding.playbackSeekBar.getProgress() / 1000;
|
final int timeSeconds = binding.playbackSeekBar.getProgress() / 1000;
|
||||||
String videoUrl = getVideoUrl();
|
String videoUrl = getVideoUrl();
|
||||||
if (!isLive() && timeSeconds >= 0 && currentMetadata != null
|
if (!isLive() && timeSeconds >= 0 && currentMetadata != null
|
||||||
&& currentMetadata.getMetadata().getServiceId() == YouTube.getServiceId()) {
|
&& currentMetadata.getServiceId() == YouTube.getServiceId()) {
|
||||||
// Timestamp doesn't make sense in a live stream so drop it
|
// Timestamp doesn't make sense in a live stream so drop it
|
||||||
videoUrl += ("&t=" + timeSeconds);
|
videoUrl += ("&t=" + timeSeconds);
|
||||||
}
|
}
|
||||||
|
@ -3095,14 +3083,14 @@ public final class Player implements
|
||||||
public String getVideoTitle() {
|
public String getVideoTitle() {
|
||||||
return currentMetadata == null
|
return currentMetadata == null
|
||||||
? context.getString(R.string.unknown_content)
|
? context.getString(R.string.unknown_content)
|
||||||
: currentMetadata.getMetadata().getName();
|
: currentMetadata.getTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public String getUploaderName() {
|
public String getUploaderName() {
|
||||||
return currentMetadata == null
|
return currentMetadata == null
|
||||||
? context.getString(R.string.unknown_content)
|
? context.getString(R.string.unknown_content)
|
||||||
: currentMetadata.getMetadata().getUploaderName();
|
: currentMetadata.getUploaderName();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -3122,14 +3110,14 @@ public final class Player implements
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region Play queue, segments and streams
|
//region Play queue, segments and streams
|
||||||
|
|
||||||
private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag metadata) {
|
private void maybeAutoQueueNextStream(@NonNull final StreamInfo info) {
|
||||||
if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1
|
if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1
|
||||||
|| getRepeatMode() != REPEAT_MODE_OFF
|
|| getRepeatMode() != REPEAT_MODE_OFF
|
||||||
|| !PlayerHelper.isAutoQueueEnabled(context)) {
|
|| !PlayerHelper.isAutoQueueEnabled(context)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// auto queue when starting playback on the last item when not repeating
|
// auto queue when starting playback on the last item when not repeating
|
||||||
final PlayQueue autoQueue = PlayerHelper.autoQueueOf(metadata.getMetadata(),
|
final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info,
|
||||||
playQueue.getStreams());
|
playQueue.getStreams());
|
||||||
if (autoQueue != null) {
|
if (autoQueue != null) {
|
||||||
playQueue.append(autoQueue.getStreams());
|
playQueue.append(autoQueue.getStreams());
|
||||||
|
@ -3232,9 +3220,7 @@ public final class Player implements
|
||||||
itemTouchHelper.attachToRecyclerView(null);
|
itemTouchHelper.attachToRecyclerView(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentMetadata != null) {
|
getCurrentStreamInfo().ifPresent(segmentAdapter::setItems);
|
||||||
segmentAdapter.setItems(currentMetadata.getMetadata());
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.shuffleButton.setVisibility(View.GONE);
|
binding.shuffleButton.setVisibility(View.GONE);
|
||||||
binding.repeatButton.setVisibility(View.GONE);
|
binding.repeatButton.setVisibility(View.GONE);
|
||||||
|
@ -3288,7 +3274,9 @@ public final class Player implements
|
||||||
|
|
||||||
private int getNearestStreamSegmentPosition(final long playbackPosition) {
|
private int getNearestStreamSegmentPosition(final long playbackPosition) {
|
||||||
int nearestPosition = 0;
|
int nearestPosition = 0;
|
||||||
final List<StreamSegment> segments = currentMetadata.getMetadata().getStreamSegments();
|
final List<StreamSegment> segments = getCurrentStreamInfo()
|
||||||
|
.map(StreamInfo::getStreamSegments)
|
||||||
|
.orElse(Collections.emptyList());
|
||||||
|
|
||||||
for (int i = 0; i < segments.size(); i++) {
|
for (int i = 0; i < segments.size(); i++) {
|
||||||
if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) {
|
if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) {
|
||||||
|
@ -3379,10 +3367,10 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateStreamRelatedViews() {
|
private void updateStreamRelatedViews() {
|
||||||
if (currentMetadata == null) {
|
if (!getCurrentStreamInfo().isPresent()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final StreamInfo info = currentMetadata.getMetadata();
|
final StreamInfo info = getCurrentStreamInfo().get();
|
||||||
|
|
||||||
binding.qualityTextView.setVisibility(View.GONE);
|
binding.qualityTextView.setVisibility(View.GONE);
|
||||||
binding.playbackSpeed.setVisibility(View.GONE);
|
binding.playbackSpeed.setVisibility(View.GONE);
|
||||||
|
@ -3410,12 +3398,15 @@ public final class Player implements
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case VIDEO_STREAM:
|
case VIDEO_STREAM:
|
||||||
if (info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) {
|
if (currentMetadata == null
|
||||||
|
|| !currentMetadata.getMaybeQuality().isPresent()
|
||||||
|
|| info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
availableStreams = currentMetadata.getSortedAvailableVideoStreams();
|
availableStreams = currentMetadata.getMaybeQuality().get().getSortedVideoStreams();
|
||||||
selectedStreamIndex = currentMetadata.getSelectedVideoStreamIndex();
|
selectedStreamIndex =
|
||||||
|
currentMetadata.getMaybeQuality().get().getSelectedVideoStreamIndex();
|
||||||
buildQualityMenu();
|
buildQualityMenu();
|
||||||
|
|
||||||
binding.qualityTextView.setVisibility(View.VISIBLE);
|
binding.qualityTextView.setVisibility(View.VISIBLE);
|
||||||
|
@ -3535,8 +3526,8 @@ public final class Player implements
|
||||||
captionItem.setOnMenuItemClickListener(menuItem -> {
|
captionItem.setOnMenuItemClickListener(menuItem -> {
|
||||||
final int textRendererIndex = getCaptionRendererIndex();
|
final int textRendererIndex = getCaptionRendererIndex();
|
||||||
if (textRendererIndex != RENDERER_UNAVAILABLE) {
|
if (textRendererIndex != RENDERER_UNAVAILABLE) {
|
||||||
trackSelector.setPreferredTextLanguage(captionLanguage);
|
|
||||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||||
|
.setPreferredTextLanguage(captionLanguage)
|
||||||
.setRendererDisabled(textRendererIndex, false));
|
.setRendererDisabled(textRendererIndex, false));
|
||||||
prefs.edit().putString(context.getString(R.string.caption_user_set_key),
|
prefs.edit().putString(context.getString(R.string.caption_user_set_key),
|
||||||
captionLanguage).apply();
|
captionLanguage).apply();
|
||||||
|
@ -3551,8 +3542,8 @@ public final class Player implements
|
||||||
userPreferredLanguage.substring(0, userPreferredLanguage.indexOf('(')))))) {
|
userPreferredLanguage.substring(0, userPreferredLanguage.indexOf('(')))))) {
|
||||||
final int textRendererIndex = getCaptionRendererIndex();
|
final int textRendererIndex = getCaptionRendererIndex();
|
||||||
if (textRendererIndex != RENDERER_UNAVAILABLE) {
|
if (textRendererIndex != RENDERER_UNAVAILABLE) {
|
||||||
trackSelector.setPreferredTextLanguage(captionLanguage);
|
|
||||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||||
|
.setPreferredTextLanguage(captionLanguage)
|
||||||
.setRendererDisabled(textRendererIndex, false));
|
.setRendererDisabled(textRendererIndex, false));
|
||||||
}
|
}
|
||||||
searchForAutogenerated = false;
|
searchForAutogenerated = false;
|
||||||
|
@ -3679,7 +3670,10 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize mismatching language strings
|
// Normalize mismatching language strings
|
||||||
final String preferredLanguage = trackSelector.getPreferredTextLanguage();
|
final List<String> preferredLanguages =
|
||||||
|
trackSelector.getParameters().preferredTextLanguages;
|
||||||
|
final String preferredLanguage =
|
||||||
|
preferredLanguages.isEmpty() ? null : preferredLanguages.get(0);
|
||||||
// Build UI
|
// Build UI
|
||||||
buildCaptionMenu(availableLanguages);
|
buildCaptionMenu(availableLanguages);
|
||||||
if (trackSelector.getParameters().getRendererDisabled(textRenderer)
|
if (trackSelector.getParameters().getRendererDisabled(textRenderer)
|
||||||
|
@ -3886,10 +3880,9 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onOpenInBrowserClicked() {
|
private void onOpenInBrowserClicked() {
|
||||||
if (currentMetadata != null) {
|
getCurrentStreamInfo().map(Info::getOriginalUrl).ifPresent(originalUrl -> {
|
||||||
ShareUtils.openUrlInBrowser(getParentActivity(),
|
ShareUtils.openUrlInBrowser(Objects.requireNonNull(getParentActivity()), originalUrl);
|
||||||
currentMetadata.getMetadata().getOriginalUrl());
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
@ -4145,12 +4138,14 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyMetadataUpdateToListeners() {
|
private void notifyMetadataUpdateToListeners() {
|
||||||
if (fragmentListener != null && currentMetadata != null) {
|
getCurrentStreamInfo().ifPresent(info -> {
|
||||||
fragmentListener.onMetadataUpdate(currentMetadata.getMetadata(), playQueue);
|
if (fragmentListener != null) {
|
||||||
}
|
fragmentListener.onMetadataUpdate(info, playQueue);
|
||||||
if (activityListener != null && currentMetadata != null) {
|
}
|
||||||
activityListener.onMetadataUpdate(currentMetadata.getMetadata(), playQueue);
|
if (activityListener != null) {
|
||||||
}
|
activityListener.onMetadataUpdate(info, playQueue);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyPlaybackUpdateToListeners() {
|
private void notifyPlaybackUpdateToListeners() {
|
||||||
|
@ -4201,14 +4196,14 @@ public final class Player implements
|
||||||
// in livestreams) so we will be not able to execute the block below.
|
// in livestreams) so we will be not able to execute the block below.
|
||||||
// Reload the play queue manager in this case, which is the behavior when we don't know the
|
// Reload the play queue manager in this case, which is the behavior when we don't know the
|
||||||
// index of the video renderer or playQueueManagerReloadingNeeded returns true.
|
// index of the video renderer or playQueueManagerReloadingNeeded returns true.
|
||||||
if (currentMetadata == null) {
|
if (!getCurrentStreamInfo().isPresent()) {
|
||||||
reloadPlayQueueManager();
|
reloadPlayQueueManager();
|
||||||
setRecovery();
|
setRecovery();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int videoRenderIndex = getVideoRendererIndex();
|
final int videoRenderIndex = getVideoRendererIndex();
|
||||||
final StreamInfo info = currentMetadata.getMetadata();
|
final StreamInfo info = getCurrentStreamInfo().get();
|
||||||
|
|
||||||
// In the case we don't know the source type, fallback to the one with video with audio or
|
// In the case we don't know the source type, fallback to the one with video with audio or
|
||||||
// audio-only source.
|
// audio-only source.
|
||||||
|
@ -4313,6 +4308,10 @@ public final class Player implements
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region Getters
|
//region Getters
|
||||||
|
|
||||||
|
private Optional<StreamInfo> getCurrentStreamInfo() {
|
||||||
|
return Optional.ofNullable(currentMetadata).flatMap(MediaItemTag::getMaybeStreamInfo);
|
||||||
|
}
|
||||||
|
|
||||||
public int getCurrentState() {
|
public int getCurrentState() {
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
|
@ -4322,8 +4321,7 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isStopped() {
|
public boolean isStopped() {
|
||||||
return exoPlayerIsNull()
|
return exoPlayerIsNull() || simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_IDLE;
|
||||||
|| simpleExoPlayer.getPlaybackState() == SimpleExoPlayer.STATE_IDLE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPlaying() {
|
public boolean isPlaying() {
|
||||||
|
@ -4340,7 +4338,7 @@ public final class Player implements
|
||||||
|
|
||||||
private boolean isLive() {
|
private boolean isLive() {
|
||||||
try {
|
try {
|
||||||
return !exoPlayerIsNull() && simpleExoPlayer.isCurrentWindowDynamic();
|
return !exoPlayerIsNull() && simpleExoPlayer.isCurrentMediaItemDynamic();
|
||||||
} catch (final IndexOutOfBoundsException e) {
|
} catch (final IndexOutOfBoundsException e) {
|
||||||
// Why would this even happen =(... but lets log it anyway, better safe than sorry
|
// Why would this even happen =(... but lets log it anyway, better safe than sorry
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
@ -4519,9 +4517,13 @@ public final class Player implements
|
||||||
surfaceHolderCallback = new SurfaceHolderCallback(context, simpleExoPlayer);
|
surfaceHolderCallback = new SurfaceHolderCallback(context, simpleExoPlayer);
|
||||||
binding.surfaceView.getHolder().addCallback(surfaceHolderCallback);
|
binding.surfaceView.getHolder().addCallback(surfaceHolderCallback);
|
||||||
final Surface surface = binding.surfaceView.getHolder().getSurface();
|
final Surface surface = binding.surfaceView.getHolder().getSurface();
|
||||||
// initially set the surface manually otherwise
|
// ensure player is using an unreleased surface, which the surfaceView might not be
|
||||||
// onRenderedFirstFrame() will not be called
|
// when starting playback on background or during player switching
|
||||||
simpleExoPlayer.setVideoSurface(surface);
|
if (surface.isValid()) {
|
||||||
|
// initially set the surface manually otherwise
|
||||||
|
// onRenderedFirstFrame() will not be called
|
||||||
|
simpleExoPlayer.setVideoSurface(surface);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
simpleExoPlayer.setVideoSurfaceView(binding.surfaceView);
|
simpleExoPlayer.setVideoSurfaceView(binding.surfaceView);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package org.schabi.newpipe.player.event;
|
package org.schabi.newpipe.player.event;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.PlaybackException;
|
||||||
|
|
||||||
public interface PlayerServiceEventListener extends PlayerEventListener {
|
public interface PlayerServiceEventListener extends PlayerEventListener {
|
||||||
void onFullscreenStateChanged(boolean fullscreen);
|
void onFullscreenStateChanged(boolean fullscreen);
|
||||||
|
@ -9,7 +9,7 @@ public interface PlayerServiceEventListener extends PlayerEventListener {
|
||||||
|
|
||||||
void onMoreOptionsLongClicked();
|
void onMoreOptionsLongClicked();
|
||||||
|
|
||||||
void onPlayerError(ExoPlaybackException error);
|
void onPlayerError(PlaybackException error, boolean isCatchableException);
|
||||||
|
|
||||||
void hideSystemUiIfNeeded();
|
void hideSystemUiIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import androidx.core.content.ContextCompat;
|
||||||
import androidx.media.AudioFocusRequestCompat;
|
import androidx.media.AudioFocusRequestCompat;
|
||||||
import androidx.media.AudioManagerCompat;
|
import androidx.media.AudioManagerCompat;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
import com.google.android.exoplayer2.analytics.AnalyticsListener;
|
import com.google.android.exoplayer2.analytics.AnalyticsListener;
|
||||||
|
|
||||||
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener {
|
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener {
|
||||||
|
@ -27,14 +27,14 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
|
||||||
private static final int FOCUS_GAIN_TYPE = AudioManagerCompat.AUDIOFOCUS_GAIN;
|
private static final int FOCUS_GAIN_TYPE = AudioManagerCompat.AUDIOFOCUS_GAIN;
|
||||||
private static final int STREAM_TYPE = AudioManager.STREAM_MUSIC;
|
private static final int STREAM_TYPE = AudioManager.STREAM_MUSIC;
|
||||||
|
|
||||||
private final SimpleExoPlayer player;
|
private final ExoPlayer player;
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final AudioManager audioManager;
|
private final AudioManager audioManager;
|
||||||
|
|
||||||
private final AudioFocusRequestCompat request;
|
private final AudioFocusRequestCompat request;
|
||||||
|
|
||||||
public AudioReactor(@NonNull final Context context,
|
public AudioReactor(@NonNull final Context context,
|
||||||
@NonNull final SimpleExoPlayer player) {
|
@NonNull final ExoPlayer player) {
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.audioManager = ContextCompat.getSystemService(context, AudioManager.class);
|
this.audioManager = ContextCompat.getSystemService(context, AudioManager.class);
|
||||||
|
|
|
@ -3,12 +3,10 @@ package org.schabi.newpipe.player.helper;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.FileDataSource;
|
import com.google.android.exoplayer2.upstream.FileDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
|
||||||
|
@ -18,6 +16,8 @@ import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
/* package-private */ class CacheFactory implements DataSource.Factory {
|
/* package-private */ class CacheFactory implements DataSource.Factory {
|
||||||
private static final String TAG = "CacheFactory";
|
private static final String TAG = "CacheFactory";
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ import java.io.File;
|
||||||
private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE
|
private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE
|
||||||
| CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
|
| CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
|
||||||
|
|
||||||
private final DefaultDataSourceFactory dataSourceFactory;
|
private final DataSource.Factory dataSourceFactory;
|
||||||
private final File cacheDir;
|
private final File cacheDir;
|
||||||
private final long maxFileSize;
|
private final long maxFileSize;
|
||||||
|
|
||||||
|
@ -49,7 +49,9 @@ import java.io.File;
|
||||||
final long maxFileSize) {
|
final long maxFileSize) {
|
||||||
this.maxFileSize = maxFileSize;
|
this.maxFileSize = maxFileSize;
|
||||||
|
|
||||||
dataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener);
|
dataSourceFactory = new DefaultDataSource
|
||||||
|
.Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
|
||||||
|
.setTransferListener(transferListener);
|
||||||
cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
|
cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
|
||||||
if (!cacheDir.exists()) {
|
if (!cacheDir.exists()) {
|
||||||
//noinspection ResultOfMethodCallIgnored
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
@ -59,7 +61,7 @@ import java.io.File;
|
||||||
if (cache == null) {
|
if (cache == null) {
|
||||||
final LeastRecentlyUsedCacheEvictor evictor
|
final LeastRecentlyUsedCacheEvictor evictor
|
||||||
= new LeastRecentlyUsedCacheEvictor(maxCacheSize);
|
= new LeastRecentlyUsedCacheEvictor(maxCacheSize);
|
||||||
cache = new SimpleCache(cacheDir, evictor, new ExoDatabaseProvider(context));
|
cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +70,7 @@ import java.io.File;
|
||||||
public DataSource createDataSource() {
|
public DataSource createDataSource() {
|
||||||
Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath());
|
Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath());
|
||||||
|
|
||||||
final DefaultDataSource dataSource = dataSourceFactory.createDataSource();
|
final DataSource dataSource = dataSourceFactory.createDataSource();
|
||||||
final FileDataSource fileSource = new FileDataSource();
|
final FileDataSource fileSource = new FileDataSource();
|
||||||
final CacheDataSink dataSink = new CacheDataSink(cache, maxFileSize);
|
final CacheDataSink dataSink = new CacheDataSink(cache, maxFileSize);
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@ import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
||||||
import org.schabi.newpipe.player.mediasession.PlayQueueNavigator;
|
import org.schabi.newpipe.player.mediasession.PlayQueueNavigator;
|
||||||
import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ -55,7 +54,6 @@ public class MediaSessionManager {
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
sessionConnector = new MediaSessionConnector(mediaSession);
|
sessionConnector = new MediaSessionConnector(mediaSession);
|
||||||
sessionConnector.setControlDispatcher(new PlayQueuePlaybackController(callback));
|
|
||||||
sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback));
|
sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback));
|
||||||
sessionConnector.setPlayer(player);
|
sessionConnector.setPlayer(player);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@ package org.schabi.newpipe.player.helper;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
|
@ -13,10 +11,13 @@ import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTrack
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
public class PlayerDataSource {
|
public class PlayerDataSource {
|
||||||
|
|
||||||
public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
|
public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
|
||||||
|
@ -35,12 +36,14 @@ public class PlayerDataSource {
|
||||||
private final DataSource.Factory cacheDataSourceFactory;
|
private final DataSource.Factory cacheDataSourceFactory;
|
||||||
private final DataSource.Factory cachelessDataSourceFactory;
|
private final DataSource.Factory cachelessDataSourceFactory;
|
||||||
|
|
||||||
public PlayerDataSource(@NonNull final Context context, @NonNull final String userAgent,
|
public PlayerDataSource(@NonNull final Context context,
|
||||||
|
@NonNull final String userAgent,
|
||||||
@NonNull final TransferListener transferListener) {
|
@NonNull final TransferListener transferListener) {
|
||||||
continueLoadingCheckIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context);
|
continueLoadingCheckIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context);
|
||||||
cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener);
|
cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener);
|
||||||
cachelessDataSourceFactory
|
cachelessDataSourceFactory = new DefaultDataSource
|
||||||
= new DefaultDataSourceFactory(context, userAgent, transferListener);
|
.Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
|
||||||
|
.setTransferListener(transferListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
|
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import android.util.Log;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.PlaybackException;
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
|
|
||||||
import org.schabi.newpipe.App;
|
import org.schabi.newpipe.App;
|
||||||
|
@ -233,9 +233,10 @@ public final class PlayerHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerError(final ExoPlaybackException error) {
|
public void onPlayerError(final PlaybackException error,
|
||||||
|
final boolean isCatchableException) {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.onPlayerError(error);
|
listener.onPlayerError(error, isCatchableException);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
package org.schabi.newpipe.player.mediaitem;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
public final class ExceptionTag implements MediaItemTag {
|
||||||
|
@NonNull
|
||||||
|
private final PlayQueueItem item;
|
||||||
|
@NonNull
|
||||||
|
private final List<Throwable> errors;
|
||||||
|
@Nullable
|
||||||
|
private final Object extras;
|
||||||
|
|
||||||
|
private ExceptionTag(@NonNull final PlayQueueItem item,
|
||||||
|
@NonNull final List<Throwable> errors,
|
||||||
|
@Nullable final Object extras) {
|
||||||
|
this.item = item;
|
||||||
|
this.errors = errors;
|
||||||
|
this.extras = extras;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExceptionTag of(@NonNull final PlayQueueItem playQueueItem,
|
||||||
|
@NonNull final List<Throwable> errors) {
|
||||||
|
return new ExceptionTag(playQueueItem, errors, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public List<Throwable> getErrors() {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getServiceId() {
|
||||||
|
return item.getServiceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTitle() {
|
||||||
|
return item.getTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderName() {
|
||||||
|
return item.getUploader();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDurationSeconds() {
|
||||||
|
return item.getDuration();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getStreamUrl() {
|
||||||
|
return item.getUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getThumbnailUrl() {
|
||||||
|
return item.getThumbnailUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderUrl() {
|
||||||
|
return item.getUploaderUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StreamType getStreamType() {
|
||||||
|
return item.getStreamType();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<StreamInfo> getMaybeStreamInfo() {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Quality> getMaybeQuality() {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> Optional<T> getMaybeExtras(@NonNull final Class<T> type) {
|
||||||
|
return Optional.ofNullable(extras).map(type::cast);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> MediaItemTag withExtras(@NonNull final T extra) {
|
||||||
|
return new ExceptionTag(item, errors, extra);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
package org.schabi.newpipe.player.mediaitem;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
|
import com.google.android.exoplayer2.MediaMetadata;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
public interface MediaItemTag {
|
||||||
|
|
||||||
|
List<Throwable> getErrors();
|
||||||
|
|
||||||
|
int getServiceId();
|
||||||
|
|
||||||
|
String getTitle();
|
||||||
|
|
||||||
|
String getUploaderName();
|
||||||
|
|
||||||
|
long getDurationSeconds();
|
||||||
|
|
||||||
|
String getStreamUrl();
|
||||||
|
|
||||||
|
String getThumbnailUrl();
|
||||||
|
|
||||||
|
String getUploaderUrl();
|
||||||
|
|
||||||
|
StreamType getStreamType();
|
||||||
|
|
||||||
|
Optional<StreamInfo> getMaybeStreamInfo();
|
||||||
|
|
||||||
|
Optional<Quality> getMaybeQuality();
|
||||||
|
|
||||||
|
<T> Optional<T> getMaybeExtras(@NonNull Class<T> type);
|
||||||
|
|
||||||
|
<T> MediaItemTag withExtras(@NonNull T extra);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
static Optional<MediaItemTag> from(@Nullable final MediaItem mediaItem) {
|
||||||
|
if (mediaItem == null || mediaItem.localConfiguration == null
|
||||||
|
|| !(mediaItem.localConfiguration.tag instanceof MediaItemTag)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.of((MediaItemTag) mediaItem.localConfiguration.tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
default String makeMediaId() {
|
||||||
|
return UUID.randomUUID().toString() + "[" + getTitle() + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
default MediaItem asMediaItem() {
|
||||||
|
final MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||||
|
.setMediaUri(Uri.parse(getStreamUrl()))
|
||||||
|
.setArtworkUri(Uri.parse(getThumbnailUrl()))
|
||||||
|
.setArtist(getUploaderName())
|
||||||
|
.setDescription(getTitle())
|
||||||
|
.setDisplayTitle(getTitle())
|
||||||
|
.setTitle(getTitle())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return MediaItem.fromUri(getStreamUrl())
|
||||||
|
.buildUpon()
|
||||||
|
.setMediaId(makeMediaId())
|
||||||
|
.setMediaMetadata(mediaMetadata)
|
||||||
|
.setTag(this)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Quality {
|
||||||
|
@NonNull
|
||||||
|
private final List<VideoStream> sortedVideoStreams;
|
||||||
|
private final int selectedVideoStreamIndex;
|
||||||
|
|
||||||
|
private Quality(@NonNull final List<VideoStream> sortedVideoStreams,
|
||||||
|
final int selectedVideoStreamIndex) {
|
||||||
|
this.sortedVideoStreams = sortedVideoStreams;
|
||||||
|
this.selectedVideoStreamIndex = selectedVideoStreamIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Quality of(@NonNull final List<VideoStream> sortedVideoStreams,
|
||||||
|
final int selectedVideoStreamIndex) {
|
||||||
|
return new Quality(sortedVideoStreams, selectedVideoStreamIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public List<VideoStream> getSortedVideoStreams() {
|
||||||
|
return sortedVideoStreams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSelectedVideoStreamIndex() {
|
||||||
|
return selectedVideoStreamIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public VideoStream getSelectedVideoStream() {
|
||||||
|
return selectedVideoStreamIndex < 0
|
||||||
|
|| selectedVideoStreamIndex >= sortedVideoStreams.size()
|
||||||
|
? null : sortedVideoStreams.get(selectedVideoStreamIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
package org.schabi.newpipe.player.mediaitem;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
public final class PlaceholderTag implements MediaItemTag {
|
||||||
|
public static final PlaceholderTag EMPTY = new PlaceholderTag(null);
|
||||||
|
private static final String UNKNOWN_VALUE_INTERNAL = "Placeholder";
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final Object extras;
|
||||||
|
|
||||||
|
private PlaceholderTag(@Nullable final Object extras) {
|
||||||
|
this.extras = extras;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public List<Throwable> getErrors() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getServiceId() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTitle() {
|
||||||
|
return UNKNOWN_VALUE_INTERNAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderName() {
|
||||||
|
return UNKNOWN_VALUE_INTERNAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDurationSeconds() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getStreamUrl() {
|
||||||
|
return UNKNOWN_VALUE_INTERNAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getThumbnailUrl() {
|
||||||
|
return UNKNOWN_VALUE_INTERNAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderUrl() {
|
||||||
|
return UNKNOWN_VALUE_INTERNAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StreamType getStreamType() {
|
||||||
|
return StreamType.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<StreamInfo> getMaybeStreamInfo() {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Quality> getMaybeQuality() {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> Optional<T> getMaybeExtras(@NonNull final Class<T> type) {
|
||||||
|
return Optional.ofNullable(extras).map(type::cast);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> MediaItemTag withExtras(@NonNull final T extra) {
|
||||||
|
return new PlaceholderTag(extra);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
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.VideoStream;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
public final class StreamInfoTag implements MediaItemTag {
|
||||||
|
@NonNull
|
||||||
|
private final StreamInfo streamInfo;
|
||||||
|
@Nullable
|
||||||
|
private final MediaItemTag.Quality quality;
|
||||||
|
@Nullable
|
||||||
|
private final Object extras;
|
||||||
|
|
||||||
|
private StreamInfoTag(@NonNull final StreamInfo streamInfo,
|
||||||
|
@Nullable final MediaItemTag.Quality quality,
|
||||||
|
@Nullable final Object extras) {
|
||||||
|
this.streamInfo = streamInfo;
|
||||||
|
this.quality = quality;
|
||||||
|
this.extras = extras;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo,
|
||||||
|
@NonNull final List<VideoStream> sortedVideoStreams,
|
||||||
|
final int selectedVideoStreamIndex) {
|
||||||
|
final Quality quality = Quality.of(sortedVideoStreams, selectedVideoStreamIndex);
|
||||||
|
return new StreamInfoTag(streamInfo, quality, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo) {
|
||||||
|
return new StreamInfoTag(streamInfo, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Throwable> getErrors() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getServiceId() {
|
||||||
|
return streamInfo.getServiceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTitle() {
|
||||||
|
return streamInfo.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderName() {
|
||||||
|
return streamInfo.getUploaderName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDurationSeconds() {
|
||||||
|
return streamInfo.getDuration();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getStreamUrl() {
|
||||||
|
return streamInfo.getUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getThumbnailUrl() {
|
||||||
|
return streamInfo.getThumbnailUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderUrl() {
|
||||||
|
return streamInfo.getUploaderUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StreamType getStreamType() {
|
||||||
|
return streamInfo.getStreamType();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<StreamInfo> getMaybeStreamInfo() {
|
||||||
|
return Optional.of(streamInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Quality> getMaybeQuality() {
|
||||||
|
return Optional.ofNullable(quality);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> Optional<T> getMaybeExtras(@NonNull final Class<T> type) {
|
||||||
|
return Optional.ofNullable(extras).map(type::cast);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StreamInfoTag withExtras(@NonNull final Object extra) {
|
||||||
|
return new StreamInfoTag(streamInfo, quality, extra);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,6 @@ import android.support.v4.media.session.MediaSessionCompat;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.ControlDispatcher;
|
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
@ -44,17 +43,17 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(final Player player) {
|
public void onTimelineChanged(@NonNull final Player player) {
|
||||||
publishFloatingQueueWindow();
|
publishFloatingQueueWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCurrentWindowIndexChanged(final Player player) {
|
public void onCurrentMediaItemIndexChanged(@NonNull final Player player) {
|
||||||
if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID
|
if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID
|
||||||
|| player.getCurrentTimeline().getWindowCount() > maxQueueSize) {
|
|| player.getCurrentTimeline().getWindowCount() > maxQueueSize) {
|
||||||
publishFloatingQueueWindow();
|
publishFloatingQueueWindow();
|
||||||
} else if (!player.getCurrentTimeline().isEmpty()) {
|
} else if (!player.getCurrentTimeline().isEmpty()) {
|
||||||
activeQueueItemId = player.getCurrentWindowIndex();
|
activeQueueItemId = player.getCurrentMediaItemIndex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,18 +63,17 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSkipToPrevious(final Player player, final ControlDispatcher controlDispatcher) {
|
public void onSkipToPrevious(@NonNull final Player player) {
|
||||||
callback.playPrevious();
|
callback.playPrevious();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSkipToQueueItem(final Player player, final ControlDispatcher controlDispatcher,
|
public void onSkipToQueueItem(@NonNull final Player player, final long id) {
|
||||||
final long id) {
|
|
||||||
callback.playItemAtIndex((int) id);
|
callback.playItemAtIndex((int) id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSkipToNext(final Player player, final ControlDispatcher controlDispatcher) {
|
public void onSkipToNext(@NonNull final Player player) {
|
||||||
callback.playNext();
|
callback.playNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,8 +100,10 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCommand(final Player player, final ControlDispatcher controlDispatcher,
|
public boolean onCommand(@NonNull final Player player,
|
||||||
final String command, final Bundle extras, final ResultReceiver cb) {
|
@NonNull final String command,
|
||||||
|
@Nullable final Bundle extras,
|
||||||
|
@Nullable final ResultReceiver cb) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
package org.schabi.newpipe.player.mediasession;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.DefaultControlDispatcher;
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
|
|
||||||
public class PlayQueuePlaybackController extends DefaultControlDispatcher {
|
|
||||||
private final MediaSessionCallback callback;
|
|
||||||
|
|
||||||
public PlayQueuePlaybackController(final MediaSessionCallback callback) {
|
|
||||||
super();
|
|
||||||
this.callback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean dispatchSetPlayWhenReady(final Player player, final boolean playWhenReady) {
|
|
||||||
if (playWhenReady) {
|
|
||||||
callback.play();
|
|
||||||
} else {
|
|
||||||
callback.pause();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,52 +2,80 @@ package org.schabi.newpipe.player.mediasource;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.source.BaseMediaSource;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
|
import com.google.android.exoplayer2.source.CompositeMediaSource;
|
||||||
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.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.MediaItemTag;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
public class FailedMediaSource extends CompositeMediaSource<Void> implements ManagedMediaSource {
|
||||||
|
private static final long SILENCE_DURATION_US = TimeUnit.SECONDS.toMicros(2);
|
||||||
|
|
||||||
public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSource {
|
|
||||||
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 FailedMediaSourceException error;
|
private final Throwable error;
|
||||||
private final long retryTimestamp;
|
private final long retryTimestamp;
|
||||||
|
private final MediaSource source;
|
||||||
|
private final MediaItem mediaItem;
|
||||||
|
/**
|
||||||
|
* Permanently fail the play queue item associated with this source, with no hope of retrying.
|
||||||
|
*
|
||||||
|
* The error will be propagated if the cause for load exception is unspecified.
|
||||||
|
* This means the error might be caused by reasons outside of extraction (e.g. no network).
|
||||||
|
* Otherwise, a silenced stream will play instead.
|
||||||
|
*
|
||||||
|
* @param playQueueItem play queue item
|
||||||
|
* @param error exception that was the reason to fail
|
||||||
|
* @param retryTimestamp epoch timestamp when this MediaSource can be refreshed
|
||||||
|
*/
|
||||||
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
|
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
|
||||||
@NonNull final FailedMediaSourceException error,
|
@NonNull final Throwable error,
|
||||||
final long retryTimestamp) {
|
final long retryTimestamp) {
|
||||||
this.playQueueItem = playQueueItem;
|
this.playQueueItem = playQueueItem;
|
||||||
this.error = error;
|
this.error = error;
|
||||||
this.retryTimestamp = retryTimestamp;
|
this.retryTimestamp = retryTimestamp;
|
||||||
|
|
||||||
|
final MediaItemTag tag = ExceptionTag
|
||||||
|
.of(playQueueItem, Collections.singletonList(error))
|
||||||
|
.withExtras(this);
|
||||||
|
this.mediaItem = tag.asMediaItem();
|
||||||
|
this.source = new SilenceMediaSource.Factory()
|
||||||
|
.setDurationUs(SILENCE_DURATION_US)
|
||||||
|
.setTag(tag)
|
||||||
|
.createMediaSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static FailedMediaSource of(@NonNull final PlayQueueItem playQueueItem,
|
||||||
* Permanently fail the play queue item associated with this source, with no hope of retrying.
|
@NonNull final FailedMediaSourceException error) {
|
||||||
* The error will always be propagated to ExoPlayer.
|
return new FailedMediaSource(playQueueItem, error, Long.MAX_VALUE);
|
||||||
*
|
}
|
||||||
* @param playQueueItem play queue item
|
|
||||||
* @param error exception that was the reason to fail
|
public static FailedMediaSource of(@NonNull final PlayQueueItem playQueueItem,
|
||||||
*/
|
@NonNull final Throwable error,
|
||||||
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
|
final long retryWaitMillis) {
|
||||||
@NonNull final FailedMediaSourceException error) {
|
return new FailedMediaSource(playQueueItem, error,
|
||||||
this.playQueueItem = playQueueItem;
|
System.currentTimeMillis() + retryWaitMillis);
|
||||||
this.error = error;
|
|
||||||
this.retryTimestamp = Long.MAX_VALUE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlayQueueItem getStream() {
|
public PlayQueueItem getStream() {
|
||||||
return playQueueItem;
|
return playQueueItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FailedMediaSourceException getError() {
|
public Throwable getError() {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,31 +88,46 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public MediaItem getMediaItem() {
|
public MediaItem getMediaItem() {
|
||||||
return MediaItem.fromUri(playQueueItem.getUrl());
|
return mediaItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) {
|
||||||
throw new IOException(error);
|
super.prepareSourceInternal(mediaTransferListener);
|
||||||
|
Log.e(TAG, "Loading failed source: ", error);
|
||||||
|
if (error instanceof FailedMediaSourceException) {
|
||||||
|
prepareChildSource(null, source);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
||||||
|
if (!(error instanceof FailedMediaSourceException)) {
|
||||||
|
throw new IOException(error);
|
||||||
|
}
|
||||||
|
super.maybeThrowSourceInfoRefreshError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onChildSourceInfoRefreshed(final Void id,
|
||||||
|
final MediaSource mediaSource,
|
||||||
|
final Timeline timeline) {
|
||||||
|
refreshSourceInfo(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator,
|
public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator,
|
||||||
final long startPositionUs) {
|
final long startPositionUs) {
|
||||||
return null;
|
return source.createPeriod(id, allocator, startPositionUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void releasePeriod(final MediaPeriod mediaPeriod) { }
|
public void releasePeriod(final MediaPeriod mediaPeriod) {
|
||||||
|
source.releasePeriod(mediaPeriod);
|
||||||
@Override
|
|
||||||
protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) {
|
|
||||||
Log.e(TAG, "Loading failed source: ", error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void releaseSourceInternal() { }
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
|
public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
|
||||||
final boolean isInterruptable) {
|
final boolean isInterruptable) {
|
||||||
|
|
|
@ -1,32 +1,34 @@
|
||||||
package org.schabi.newpipe.player.mediasource;
|
package org.schabi.newpipe.player.mediasource;
|
||||||
|
|
||||||
import android.os.Handler;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
|
import com.google.android.exoplayer2.Timeline;
|
||||||
|
import com.google.android.exoplayer2.source.CompositeMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.Allocator;
|
||||||
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
public class LoadedMediaSource extends CompositeMediaSource<Void> implements ManagedMediaSource {
|
||||||
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
|
||||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
|
||||||
import com.google.android.exoplayer2.upstream.Allocator;
|
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class LoadedMediaSource implements ManagedMediaSource {
|
|
||||||
private final MediaSource source;
|
private final MediaSource source;
|
||||||
private final PlayQueueItem stream;
|
private final PlayQueueItem stream;
|
||||||
|
private final MediaItem mediaItem;
|
||||||
private final long expireTimestamp;
|
private final long expireTimestamp;
|
||||||
|
|
||||||
public LoadedMediaSource(@NonNull final MediaSource source, @NonNull final PlayQueueItem stream,
|
public LoadedMediaSource(@NonNull final MediaSource source,
|
||||||
|
@NonNull final MediaItemTag tag,
|
||||||
|
@NonNull final PlayQueueItem stream,
|
||||||
final long expireTimestamp) {
|
final long expireTimestamp) {
|
||||||
this.source = source;
|
this.source = source;
|
||||||
this.stream = stream;
|
this.stream = stream;
|
||||||
this.expireTimestamp = expireTimestamp;
|
this.expireTimestamp = expireTimestamp;
|
||||||
|
|
||||||
|
this.mediaItem = tag.withExtras(this).asMediaItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlayQueueItem getStream() {
|
public PlayQueueItem getStream() {
|
||||||
|
@ -38,19 +40,16 @@ public class LoadedMediaSource implements ManagedMediaSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void prepareSource(final MediaSourceCaller mediaSourceCaller,
|
protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) {
|
||||||
@Nullable final TransferListener mediaTransferListener) {
|
super.prepareSourceInternal(mediaTransferListener);
|
||||||
source.prepareSource(mediaSourceCaller, mediaTransferListener);
|
prepareChildSource(null, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
protected void onChildSourceInfoRefreshed(final Void id,
|
||||||
source.maybeThrowSourceInfoRefreshError();
|
final MediaSource mediaSource,
|
||||||
}
|
final Timeline timeline) {
|
||||||
|
refreshSourceInfo(timeline);
|
||||||
@Override
|
|
||||||
public void enable(final MediaSourceCaller caller) {
|
|
||||||
source.enable(caller);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -64,57 +63,10 @@ public class LoadedMediaSource implements ManagedMediaSource {
|
||||||
source.releasePeriod(mediaPeriod);
|
source.releasePeriod(mediaPeriod);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@NonNull
|
||||||
public void disable(final MediaSourceCaller caller) {
|
|
||||||
source.disable(caller);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void releaseSource(final MediaSourceCaller mediaSourceCaller) {
|
|
||||||
source.releaseSource(mediaSourceCaller);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addEventListener(final Handler handler,
|
|
||||||
final MediaSourceEventListener eventListener) {
|
|
||||||
source.addEventListener(handler, eventListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeEventListener(final MediaSourceEventListener eventListener) {
|
|
||||||
source.removeEventListener(eventListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a {@link DrmSessionEventListener} to the list of listeners which are notified of DRM
|
|
||||||
* events for this media source.
|
|
||||||
*
|
|
||||||
* @param handler A handler on the which listener events will be posted.
|
|
||||||
* @param eventListener The listener to be added.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void addDrmEventListener(final Handler handler,
|
|
||||||
final DrmSessionEventListener eventListener) {
|
|
||||||
source.addDrmEventListener(handler, eventListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a {@link DrmSessionEventListener} from the list of listeners which are notified of
|
|
||||||
* DRM events for this media source.
|
|
||||||
*
|
|
||||||
* @param eventListener The listener to be removed.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void removeDrmEventListener(final DrmSessionEventListener eventListener) {
|
|
||||||
source.removeDrmEventListener(eventListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the {@link MediaItem} whose media is provided by the source.
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public MediaItem getMediaItem() {
|
public MediaItem getMediaItem() {
|
||||||
return source.getMediaItem();
|
return mediaItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package org.schabi.newpipe.player.mediasource;
|
package org.schabi.newpipe.player.mediasource;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
|
||||||
|
@ -28,10 +27,4 @@ public interface ManagedMediaSource extends MediaSource {
|
||||||
* @return whether this source is for the specified stream
|
* @return whether this source is for the specified stream
|
||||||
*/
|
*/
|
||||||
boolean isStreamEqual(@NonNull PlayQueueItem stream);
|
boolean isStreamEqual(@NonNull PlayQueueItem stream);
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
default Object getTag() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
||||||
|
|
||||||
public class ManagedMediaSourcePlaylist {
|
public class ManagedMediaSourcePlaylist {
|
||||||
@NonNull
|
@NonNull
|
||||||
private final ConcatenatingMediaSource internalSource;
|
private final ConcatenatingMediaSource internalSource;
|
||||||
|
@ -34,8 +36,14 @@ public class ManagedMediaSourcePlaylist {
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public ManagedMediaSource get(final int index) {
|
public ManagedMediaSource get(final int index) {
|
||||||
return (index < 0 || index >= size())
|
if (index < 0 || index >= size()) {
|
||||||
? null : (ManagedMediaSource) internalSource.getMediaSource(index).getTag();
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MediaItemTag
|
||||||
|
.from(internalSource.getMediaSource(index).getMediaItem())
|
||||||
|
.flatMap(tag -> tag.getMaybeExtras(ManagedMediaSource.class))
|
||||||
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -54,7 +62,7 @@ public class ManagedMediaSourcePlaylist {
|
||||||
* @see #append(ManagedMediaSource)
|
* @see #append(ManagedMediaSource)
|
||||||
*/
|
*/
|
||||||
public synchronized void expand() {
|
public synchronized void expand() {
|
||||||
append(new PlaceholderMediaSource());
|
append(PlaceholderMediaSource.COPY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -115,10 +123,10 @@ public class ManagedMediaSourcePlaylist {
|
||||||
public synchronized void invalidate(final int index,
|
public synchronized void invalidate(final int index,
|
||||||
@Nullable final Handler handler,
|
@Nullable final Handler handler,
|
||||||
@Nullable final Runnable finalizingAction) {
|
@Nullable final Runnable finalizingAction) {
|
||||||
if (get(index) instanceof PlaceholderMediaSource) {
|
if (get(index) == PlaceholderMediaSource.COPY) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
update(index, new PlaceholderMediaSource(), handler, finalizingAction);
|
update(index, PlaceholderMediaSource.COPY, handler, finalizingAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,28 +1,37 @@
|
||||||
package org.schabi.newpipe.player.mediasource;
|
package org.schabi.newpipe.player.mediasource;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.source.BaseMediaSource;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
|
import com.google.android.exoplayer2.source.CompositeMediaSource;
|
||||||
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.upstream.Allocator;
|
import com.google.android.exoplayer2.upstream.Allocator;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
|
||||||
|
|
||||||
|
import org.schabi.newpipe.player.mediaitem.PlaceholderTag;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
|
||||||
public class PlaceholderMediaSource extends BaseMediaSource implements ManagedMediaSource {
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
final class PlaceholderMediaSource
|
||||||
|
extends CompositeMediaSource<Void> implements ManagedMediaSource {
|
||||||
|
public static final PlaceholderMediaSource COPY = new 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.
|
* Returns the {@link MediaItem} whose media is provided by the source.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public MediaItem getMediaItem() {
|
public MediaItem getMediaItem() {
|
||||||
return null;
|
return MEDIA_ITEM;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do nothing, so this will stall the playback
|
|
||||||
@Override
|
@Override
|
||||||
public void maybeThrowSourceInfoRefreshError() { }
|
protected void onChildSourceInfoRefreshed(final Void id,
|
||||||
|
final MediaSource mediaSource,
|
||||||
|
final Timeline timeline) {
|
||||||
|
/* Do nothing, no timeline updates will stall playback */
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator,
|
public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator,
|
||||||
|
@ -33,12 +42,6 @@ public class PlaceholderMediaSource extends BaseMediaSource implements ManagedMe
|
||||||
@Override
|
@Override
|
||||||
public void releasePeriod(final MediaPeriod mediaPeriod) { }
|
public void releasePeriod(final MediaPeriod mediaPeriod) { }
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void releaseSourceInternal() { }
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
|
public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
|
||||||
final boolean isInterruptable) {
|
final boolean isInterruptable) {
|
||||||
|
|
|
@ -1,92 +0,0 @@
|
||||||
package org.schabi.newpipe.player.playback;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Pair;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.Format;
|
|
||||||
import com.google.android.exoplayer2.RendererCapabilities.Capabilities;
|
|
||||||
import com.google.android.exoplayer2.source.TrackGroup;
|
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
|
||||||
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
|
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class allows irregular text language labels for use when selecting text captions and
|
|
||||||
* is mostly a copy-paste from {@link DefaultTrackSelector}.
|
|
||||||
* <p>
|
|
||||||
* This is a hack and should be removed once ExoPlayer fixes language normalization to accept
|
|
||||||
* a broader set of languages.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
public class CustomTrackSelector extends DefaultTrackSelector {
|
|
||||||
private String preferredTextLanguage;
|
|
||||||
|
|
||||||
public CustomTrackSelector(final Context context,
|
|
||||||
final ExoTrackSelection.Factory adaptiveTrackSelectionFactory) {
|
|
||||||
super(context, adaptiveTrackSelectionFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean formatHasLanguage(final Format format, final String language) {
|
|
||||||
return language != null && TextUtils.equals(language, format.language);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPreferredTextLanguage() {
|
|
||||||
return preferredTextLanguage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPreferredTextLanguage(@NonNull final String label) {
|
|
||||||
Assertions.checkNotNull(label);
|
|
||||||
if (!label.equals(preferredTextLanguage)) {
|
|
||||||
preferredTextLanguage = label;
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nullable
|
|
||||||
protected Pair<ExoTrackSelection.Definition, TextTrackScore> selectTextTrack(
|
|
||||||
final TrackGroupArray groups,
|
|
||||||
@NonNull final int[][] formatSupport,
|
|
||||||
@NonNull final Parameters params,
|
|
||||||
@Nullable final String selectedAudioLanguage) {
|
|
||||||
TrackGroup selectedGroup = null;
|
|
||||||
int selectedTrackIndex = C.INDEX_UNSET;
|
|
||||||
TextTrackScore selectedTrackScore = null;
|
|
||||||
|
|
||||||
for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|
|
||||||
final TrackGroup trackGroup = groups.get(groupIndex);
|
|
||||||
@Capabilities final int[] trackFormatSupport = formatSupport[groupIndex];
|
|
||||||
|
|
||||||
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
|
|
||||||
if (isSupported(trackFormatSupport[trackIndex],
|
|
||||||
params.exceedRendererCapabilitiesIfNecessary)) {
|
|
||||||
final Format format = trackGroup.getFormat(trackIndex);
|
|
||||||
final TextTrackScore trackScore = new TextTrackScore(format, params,
|
|
||||||
trackFormatSupport[trackIndex], selectedAudioLanguage);
|
|
||||||
|
|
||||||
if (formatHasLanguage(format, preferredTextLanguage)) {
|
|
||||||
selectedGroup = trackGroup;
|
|
||||||
selectedTrackIndex = trackIndex;
|
|
||||||
selectedTrackScore = trackScore;
|
|
||||||
break; // found user selected match (perfect!)
|
|
||||||
|
|
||||||
} else if (trackScore.isWithinConstraints && (selectedTrackScore == null
|
|
||||||
|| trackScore.compareTo(selectedTrackScore) > 0)) {
|
|
||||||
selectedGroup = trackGroup;
|
|
||||||
selectedTrackIndex = trackIndex;
|
|
||||||
selectedTrackScore = trackScore;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return selectedGroup == null ? null
|
|
||||||
: Pair.create(new ExoTrackSelection.Definition(selectedGroup, selectedTrackIndex),
|
|
||||||
Assertions.checkNotNull(selectedTrackScore));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,11 +11,12 @@ import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
import org.reactivestreams.Subscription;
|
import org.reactivestreams.Subscription;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
||||||
import org.schabi.newpipe.player.mediasource.FailedMediaSource;
|
import org.schabi.newpipe.player.mediasource.FailedMediaSource;
|
||||||
import org.schabi.newpipe.player.mediasource.LoadedMediaSource;
|
import org.schabi.newpipe.player.mediasource.LoadedMediaSource;
|
||||||
import org.schabi.newpipe.player.mediasource.ManagedMediaSource;
|
import org.schabi.newpipe.player.mediasource.ManagedMediaSource;
|
||||||
import org.schabi.newpipe.player.mediasource.ManagedMediaSourcePlaylist;
|
import org.schabi.newpipe.player.mediasource.ManagedMediaSourcePlaylist;
|
||||||
import org.schabi.newpipe.player.mediasource.PlaceholderMediaSource;
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
import org.schabi.newpipe.player.playqueue.events.MoveEvent;
|
import org.schabi.newpipe.player.playqueue.events.MoveEvent;
|
||||||
|
@ -195,7 +196,7 @@ public class MediaSourceManager {
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private Subscriber<PlayQueueEvent> getReactor() {
|
private Subscriber<PlayQueueEvent> getReactor() {
|
||||||
return new Subscriber<PlayQueueEvent>() {
|
return new Subscriber<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(@NonNull final Subscription d) {
|
public void onSubscribe(@NonNull final Subscription d) {
|
||||||
playQueueReactor.cancel();
|
playQueueReactor.cancel();
|
||||||
|
@ -209,10 +210,12 @@ public class MediaSourceManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(@NonNull final Throwable e) { }
|
public void onError(@NonNull final Throwable e) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onComplete() { }
|
public void onComplete() {
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,11 +295,11 @@ public class MediaSourceManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
final ManagedMediaSource mediaSource = playlist.get(playQueue.getIndex());
|
final ManagedMediaSource mediaSource = playlist.get(playQueue.getIndex());
|
||||||
if (mediaSource == null) {
|
final PlayQueueItem playQueueItem = playQueue.getItem();
|
||||||
|
if (mediaSource == null || playQueueItem == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final PlayQueueItem playQueueItem = playQueue.getItem();
|
|
||||||
return mediaSource.isStreamEqual(playQueueItem);
|
return mediaSource.isStreamEqual(playQueueItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,7 +318,7 @@ public class MediaSourceManager {
|
||||||
isBlocked.set(true);
|
isBlocked.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeUnblock() {
|
private boolean maybeUnblock() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "maybeUnblock() called.");
|
Log.d(TAG, "maybeUnblock() called.");
|
||||||
}
|
}
|
||||||
|
@ -323,14 +326,17 @@ public class MediaSourceManager {
|
||||||
if (isBlocked.get()) {
|
if (isBlocked.get()) {
|
||||||
isBlocked.set(false);
|
isBlocked.set(false);
|
||||||
playbackListener.onPlaybackUnblock(playlist.getParentMediaSource());
|
playbackListener.onPlaybackUnblock(playlist.getParentMediaSource());
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Metadata Synchronization
|
// Metadata Synchronization
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private void maybeSync() {
|
private void maybeSync(final boolean wasBlocked) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "maybeSync() called.");
|
Log.d(TAG, "maybeSync() called.");
|
||||||
}
|
}
|
||||||
|
@ -340,13 +346,13 @@ public class MediaSourceManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackListener.onPlaybackSynchronize(currentItem);
|
playbackListener.onPlaybackSynchronize(currentItem, wasBlocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void maybeSynchronizePlayer() {
|
private synchronized void maybeSynchronizePlayer() {
|
||||||
if (isPlayQueueReady() && isPlaybackReady()) {
|
if (isPlayQueueReady() && isPlaybackReady()) {
|
||||||
maybeUnblock();
|
final boolean isBlockReleased = maybeUnblock();
|
||||||
maybeSync();
|
maybeSync(isBlockReleased);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -417,20 +423,26 @@ public class MediaSourceManager {
|
||||||
private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) {
|
private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) {
|
||||||
return stream.getStream().map(streamInfo -> {
|
return stream.getStream().map(streamInfo -> {
|
||||||
final MediaSource source = playbackListener.sourceOf(stream, streamInfo);
|
final MediaSource source = playbackListener.sourceOf(stream, streamInfo);
|
||||||
if (source == null) {
|
if (source == null || !MediaItemTag.from(source.getMediaItem()).isPresent()) {
|
||||||
final String message = "Unable to resolve source from stream info. "
|
final String message = "Unable to resolve source from stream info. "
|
||||||
+ "URL: " + stream.getUrl() + ", "
|
+ "URL: " + stream.getUrl() + ", "
|
||||||
+ "audio count: " + streamInfo.getAudioStreams().size() + ", "
|
+ "audio count: " + streamInfo.getAudioStreams().size() + ", "
|
||||||
+ "video count: " + streamInfo.getVideoOnlyStreams().size() + ", "
|
+ "video count: " + streamInfo.getVideoOnlyStreams().size() + ", "
|
||||||
+ streamInfo.getVideoStreams().size();
|
+ streamInfo.getVideoStreams().size();
|
||||||
return new FailedMediaSource(stream, new MediaSourceResolutionException(message));
|
return (ManagedMediaSource)
|
||||||
|
FailedMediaSource.of(stream, new MediaSourceResolutionException(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final MediaItemTag tag = MediaItemTag.from(source.getMediaItem()).get();
|
||||||
final long expiration = System.currentTimeMillis()
|
final long expiration = System.currentTimeMillis()
|
||||||
+ ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId());
|
+ ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId());
|
||||||
return new LoadedMediaSource(source, stream, expiration);
|
return new LoadedMediaSource(source, tag, stream, expiration);
|
||||||
}).onErrorReturn(throwable -> new FailedMediaSource(stream,
|
}).onErrorReturn(throwable -> {
|
||||||
new StreamInfoLoadException(throwable)));
|
if (throwable instanceof ExtractionException) {
|
||||||
|
return FailedMediaSource.of(stream, new StreamInfoLoadException(throwable));
|
||||||
|
}
|
||||||
|
return FailedMediaSource.of(stream, throwable, /*immediatelyRetryable=*/0L);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onMediaSourceReceived(@NonNull final PlayQueueItem item,
|
private void onMediaSourceReceived(@NonNull final PlayQueueItem item,
|
||||||
|
@ -478,23 +490,23 @@ public class MediaSourceManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the current playing index contains an expired {@link ManagedMediaSource}.
|
* Checks if the current playing index contains an expired {@link ManagedMediaSource}.
|
||||||
* If so, the expired source is replaced by a {@link PlaceholderMediaSource} and
|
* If so, the expired source is replaced by a dummy {@link ManagedMediaSource} and
|
||||||
* {@link #loadImmediate()} is called to reload the current item.
|
* {@link #loadImmediate()} is called to reload the current item.
|
||||||
* <br><br>
|
* <br><br>
|
||||||
* If not, then the media source at the current index is ready for playback, and
|
* If not, then the media source at the current index is ready for playback, and
|
||||||
* {@link #maybeSynchronizePlayer()} is called.
|
* {@link #maybeSynchronizePlayer()} is called.
|
||||||
* <br><br>
|
* <br><br>
|
||||||
* Under both cases, {@link #maybeSync()} will be called to ensure the listener
|
* Under both cases, {@link #maybeSync(boolean)} will be called to ensure the listener
|
||||||
* is up-to-date.
|
* is up-to-date.
|
||||||
*/
|
*/
|
||||||
private void maybeRenewCurrentIndex() {
|
private void maybeRenewCurrentIndex() {
|
||||||
final int currentIndex = playQueue.getIndex();
|
final int currentIndex = playQueue.getIndex();
|
||||||
|
final PlayQueueItem currentItem = playQueue.getItem();
|
||||||
final ManagedMediaSource currentSource = playlist.get(currentIndex);
|
final ManagedMediaSource currentSource = playlist.get(currentIndex);
|
||||||
if (currentSource == null) {
|
if (currentItem == null || currentSource == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final PlayQueueItem currentItem = playQueue.getItem();
|
|
||||||
if (!currentSource.shouldBeReplacedWith(currentItem, true)) {
|
if (!currentSource.shouldBeReplacedWith(currentItem, true)) {
|
||||||
maybeSynchronizePlayer();
|
maybeSynchronizePlayer();
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -51,9 +51,10 @@ public interface PlaybackListener {
|
||||||
* May be called anytime at any amount once unblock is called.
|
* May be called anytime at any amount once unblock is called.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param item
|
* @param item item the player should be playing/synchronized to
|
||||||
|
* @param wasBlocked was the player recently released from blocking state
|
||||||
*/
|
*/
|
||||||
void onPlaybackSynchronize(@NonNull PlayQueueItem item);
|
void onPlaybackSynchronize(@NonNull PlayQueueItem item, boolean wasBlocked);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests the listener to resolve a stream info into a media source
|
* Requests the listener to resolve a stream info into a media source
|
||||||
|
|
|
@ -3,7 +3,7 @@ package org.schabi.newpipe.player.playback;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.video.DummySurface;
|
import com.google.android.exoplayer2.video.DummySurface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,10 +25,10 @@ import com.google.android.exoplayer2.video.DummySurface;
|
||||||
public final class SurfaceHolderCallback implements SurfaceHolder.Callback {
|
public final class SurfaceHolderCallback implements SurfaceHolder.Callback {
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final SimpleExoPlayer player;
|
private final Player player;
|
||||||
private DummySurface dummySurface;
|
private DummySurface dummySurface;
|
||||||
|
|
||||||
public SurfaceHolderCallback(final Context context, final SimpleExoPlayer player) {
|
public SurfaceHolderCallback(final Context context, final Player player) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.player = player;
|
this.player = player;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
|
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
||||||
|
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
|
|
||||||
public class AudioPlaybackResolver implements PlaybackResolver {
|
public class AudioPlaybackResolver implements PlaybackResolver {
|
||||||
|
@ -40,7 +42,7 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
final AudioStream audio = info.getAudioStreams().get(index);
|
final AudioStream audio = info.getAudioStreams().get(index);
|
||||||
final MediaSourceTag tag = new MediaSourceTag(info);
|
final MediaItemTag tag = StreamInfoTag.of(info);
|
||||||
return buildMediaSource(dataSource, audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio),
|
return buildMediaSource(dataSource, audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio),
|
||||||
MediaFormat.getSuffixById(audio.getFormatId()), tag);
|
MediaFormat.getSuffixById(audio.getFormatId()), tag);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
package org.schabi.newpipe.player.resolver;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class MediaSourceTag implements Serializable {
|
|
||||||
@NonNull
|
|
||||||
private final StreamInfo metadata;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final List<VideoStream> sortedAvailableVideoStreams;
|
|
||||||
private final int selectedVideoStreamIndex;
|
|
||||||
|
|
||||||
public MediaSourceTag(@NonNull final StreamInfo metadata,
|
|
||||||
@NonNull final List<VideoStream> sortedAvailableVideoStreams,
|
|
||||||
final int selectedVideoStreamIndex) {
|
|
||||||
this.metadata = metadata;
|
|
||||||
this.sortedAvailableVideoStreams = sortedAvailableVideoStreams;
|
|
||||||
this.selectedVideoStreamIndex = selectedVideoStreamIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MediaSourceTag(@NonNull final StreamInfo metadata) {
|
|
||||||
this(metadata, Collections.emptyList(), /*indexNotAvailable=*/-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public StreamInfo getMetadata() {
|
|
||||||
return metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public List<VideoStream> getSortedAvailableVideoStreams() {
|
|
||||||
return sortedAvailableVideoStreams;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSelectedVideoStreamIndex() {
|
|
||||||
return selectedVideoStreamIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public VideoStream getSelectedVideoStream() {
|
|
||||||
return selectedVideoStreamIndex < 0
|
|
||||||
|| selectedVideoStreamIndex >= sortedAvailableVideoStreams.size()
|
|
||||||
? null : sortedAvailableVideoStreams.get(selectedVideoStreamIndex);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,20 +3,23 @@ package org.schabi.newpipe.player.resolver;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
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.player.helper.PlayerDataSource;
|
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
||||||
|
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
||||||
|
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.player.helper.PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS;
|
||||||
|
|
||||||
public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -27,7 +30,7 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final MediaSourceTag tag = new MediaSourceTag(info);
|
final StreamInfoTag tag = StreamInfoTag.of(info);
|
||||||
if (!info.getHlsUrl().isEmpty()) {
|
if (!info.getHlsUrl().isEmpty()) {
|
||||||
return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag);
|
return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag);
|
||||||
} else if (!info.getDashMpdUrl().isEmpty()) {
|
} else if (!info.getDashMpdUrl().isEmpty()) {
|
||||||
|
@ -41,8 +44,8 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||||
default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
|
default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
|
||||||
@NonNull final String sourceUrl,
|
@NonNull final String sourceUrl,
|
||||||
@C.ContentType final int type,
|
@C.ContentType final int type,
|
||||||
@NonNull final MediaSourceTag metadata) {
|
@NonNull final MediaItemTag metadata) {
|
||||||
final MediaSourceFactory factory;
|
final MediaSource.Factory factory;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
factory = dataSource.getLiveSsMediaSourceFactory();
|
factory = dataSource.getLiveSsMediaSourceFactory();
|
||||||
|
@ -61,7 +64,11 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||||
new MediaItem.Builder()
|
new MediaItem.Builder()
|
||||||
.setTag(metadata)
|
.setTag(metadata)
|
||||||
.setUri(Uri.parse(sourceUrl))
|
.setUri(Uri.parse(sourceUrl))
|
||||||
.setLiveTargetOffsetMs(PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS)
|
.setLiveConfiguration(
|
||||||
|
new MediaItem.LiveConfiguration.Builder()
|
||||||
|
.setTargetOffsetMs(LIVE_STREAM_EDGE_GAP_MILLIS)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -71,12 +78,12 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||||
@NonNull final String sourceUrl,
|
@NonNull final String sourceUrl,
|
||||||
@NonNull final String cacheKey,
|
@NonNull final String cacheKey,
|
||||||
@NonNull final String overrideExtension,
|
@NonNull final String overrideExtension,
|
||||||
@NonNull final MediaSourceTag metadata) {
|
@NonNull final MediaItemTag metadata) {
|
||||||
final Uri uri = Uri.parse(sourceUrl);
|
final Uri uri = Uri.parse(sourceUrl);
|
||||||
@C.ContentType final int type = TextUtils.isEmpty(overrideExtension)
|
@C.ContentType final int type = TextUtils.isEmpty(overrideExtension)
|
||||||
? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
|
? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
|
||||||
|
|
||||||
final MediaSourceFactory factory;
|
final MediaSource.Factory factory;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
factory = dataSource.getLiveSsMediaSourceFactory();
|
factory = dataSource.getLiveSsMediaSourceFactory();
|
||||||
|
|
|
@ -17,6 +17,8 @@ import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
|
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
||||||
|
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -73,8 +75,10 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
} else {
|
} else {
|
||||||
index = qualityResolver.getOverrideResolutionIndex(videos, getPlaybackQuality());
|
index = qualityResolver.getOverrideResolutionIndex(videos, getPlaybackQuality());
|
||||||
}
|
}
|
||||||
final MediaSourceTag tag = new MediaSourceTag(info, videos, index);
|
final MediaItemTag tag = StreamInfoTag.of(info, videos, index);
|
||||||
@Nullable final VideoStream video = tag.getSelectedVideoStream();
|
@Nullable final VideoStream video = tag.getMaybeQuality()
|
||||||
|
.map(MediaItemTag.Quality::getSelectedVideoStream)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
if (video != null) {
|
if (video != null) {
|
||||||
final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(),
|
final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(),
|
||||||
|
@ -112,12 +116,14 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
if (mimeType == null) {
|
if (mimeType == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
|
final MediaItem.SubtitleConfiguration textMediaItem =
|
||||||
.createMediaSource(
|
new MediaItem.SubtitleConfiguration.Builder(Uri.parse(subtitle.getUrl()))
|
||||||
new MediaItem.Subtitle(Uri.parse(subtitle.getUrl()),
|
.setMimeType(mimeType)
|
||||||
mimeType,
|
.setLanguage(PlayerHelper.captionLanguageOf(context, subtitle))
|
||||||
PlayerHelper.captionLanguageOf(context, subtitle)),
|
.build();
|
||||||
TIME_UNSET);
|
final MediaSource textSource = dataSource
|
||||||
|
.getSampleMediaSourceFactory()
|
||||||
|
.createMediaSource(textMediaItem, TIME_UNSET);
|
||||||
mediaSources.add(textSource);
|
mediaSources.add(textSource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue