mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 07:13:00 +00:00 
			
		
		
		
	Merge pull request #8020 from karyogamy/exo-update-v17
ExoPlayer 2.17.1 update and MediaSource management rework
This commit is contained in:
		| @@ -105,7 +105,7 @@ ext { | |||||||
|     androidxWorkVersion = '2.7.1' |     androidxWorkVersion = '2.7.1' | ||||||
|  |  | ||||||
|     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,19 @@ 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.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 +160,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 +184,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 +196,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 +211,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 +293,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 +430,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 +513,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 +1657,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 +1683,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 | ||||||
|  |  | ||||||
| @@ -1949,11 +1961,12 @@ public final class Player implements | |||||||
|         final boolean showPrev = playQueue.getIndex() != 0; |         final boolean showPrev = playQueue.getIndex() != 0; | ||||||
|         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; |         /* only when stream has segments and is not playing in popup player */ | ||||||
|         if (currentMetadata != null) { |         final boolean showSegment = !popupPlayerSelected() | ||||||
|             showSegment = !currentMetadata.getMetadata().getStreamSegments().isEmpty() |                 && !getCurrentStreamInfo() | ||||||
|                     && !popupPlayerSelected(); |                 .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 +2006,29 @@ 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 = exoPlayerIsNull() | ||||||
|  |                 ? 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 + "]"); | ||||||
|  |         } | ||||||
|  |         updatePlaybackState(getPlayWhenReady(), 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 +2037,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 +2052,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 +2068,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 +2484,51 @@ 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) | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * <p>Listens for event or state changes on ExoPlayer. When any event happens, we check for | ||||||
|  |      * changes in the currently-playing metadata and update the encapsulating | ||||||
|  |      * {@link Player}. Downstream listeners are also informed.</p> | ||||||
|  |      * | ||||||
|  |      * <p>When the renewed metadata contains any error, it is reported as a notification. | ||||||
|  |      * This is done because not all source resolution errors are {@link PlaybackException}, which | ||||||
|  |      * are also captured by {@link ExoPlayer} and stops the playback.</p> | ||||||
|  |      * | ||||||
|  |      * @param player The {@link com.google.android.exoplayer2.Player} whose state changed. | ||||||
|  |      * @param events The {@link com.google.android.exoplayer2.Player.Events} that has triggered | ||||||
|  |      *               the player state changes. | ||||||
|  |      **/ | ||||||
|     @Override |     @Override | ||||||
|     public void onTimelineChanged(@NonNull final Timeline timeline, final int reason) { |     public void onEvents(@NonNull final com.google.android.exoplayer2.Player player, | ||||||
|         if (DEBUG) { |                          @NonNull final com.google.android.exoplayer2.Player.Events events) { | ||||||
|             Log.d(TAG, "ExoPlayer - onTimelineChanged() called with " |         Listener.super.onEvents(player, events); | ||||||
|                     + "timeline size = [" + timeline.getWindowCount() + "], " |         MediaItemTag.from(player.getCurrentMediaItem()).ifPresent(tag -> { | ||||||
|                     + "reason = [" + reason + "]"); |             if (tag == currentMetadata) { | ||||||
|         } |                 return; | ||||||
|  |             } | ||||||
|         maybeUpdateCurrentMetadata(); |             currentMetadata = tag; | ||||||
|         // force recreate notification to ensure seek bar is shown when preparation finishes |             if (!tag.getErrors().isEmpty()) { | ||||||
|         NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); |                 final ErrorInfo errorInfo = new ErrorInfo( | ||||||
|  |                         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,6 +2547,10 @@ 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) { | ||||||
| @@ -2506,13 +2558,13 @@ public final class Player implements | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         // 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 +2577,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 | ||||||
| @@ -2559,94 +2610,103 @@ public final class Player implements | |||||||
|      * 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> |      * <p>There are multiple types of errors:</p> | ||||||
|      * <ul> |      * <ul> | ||||||
|      * <li>{@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}</li> |      * <li>{@link PlaybackException#ERROR_CODE_BEHIND_LIVE_WINDOW BEHIND_LIVE_WINDOW}: | ||||||
|      * <li>{@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}: |      * If the playback on livestreams are lagged too far behind the current playable | ||||||
|      * If a runtime error occurred, then we can try to recover it by restarting the playback |      * window. Then we seek to the latest timestamp and restart the playback. | ||||||
|      * after setting the timestamp recovery.</li> |      * This error is <b>catchable</b>. | ||||||
|      * <li>{@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}: |      * </li> | ||||||
|      * If the renderer failed, treat the error as unrecoverable.</li> |      * <li>From {@link PlaybackException#ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE BAD_IO} to | ||||||
|  |      * {@link PlaybackException#ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED UNSUPPORTED_FORMATS}: | ||||||
|  |      * If the stream source is validated by the extractor but not recognized by the player, | ||||||
|  |      * then we can try to recover playback by signalling an error on the {@link PlayQueue}.</li> | ||||||
|  |      * <li>For {@link PlaybackException#ERROR_CODE_TIMEOUT PLAYER_TIMEOUT}, | ||||||
|  |      * {@link PlaybackException#ERROR_CODE_IO_UNSPECIFIED MEDIA_SOURCE_RESOLVER_TIMEOUT} and | ||||||
|  |      * {@link PlaybackException#ERROR_CODE_IO_NETWORK_CONNECTION_FAILED NO_NETWORK}: | ||||||
|  |      * We can keep set the recovery record and keep to player at the current state until | ||||||
|  |      * it is ready to play by restarting the {@link MediaSourceManager}.</li> | ||||||
|  |      * <li>On any ExoPlayer specific issue internal to its device interaction, such as | ||||||
|  |      * {@link PlaybackException#ERROR_CODE_DECODER_INIT_FAILED DECODER_ERROR}: | ||||||
|  |      * We terminate the playback.</li> | ||||||
|  |      * <li>For any other unspecified issue internal: We set a recovery and try to restart | ||||||
|  |      * the playback.</li> | ||||||
|  |      * For any error above that is <b>not</b> explicitly <b>catchable</b>, the player will | ||||||
|  |      * create a notification so users are aware. | ||||||
|      * </ul> |      * </ul> | ||||||
|      * |      * @see com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException) | ||||||
|      * @see #processSourceError(IOException) |      * */ | ||||||
|      * @see com.google.android.exoplayer2.Player.Listener#onPlayerError(ExoPlaybackException) |     // Any error code not explicitly covered here are either unrelated to NewPipe use case | ||||||
|      */ |     // (e.g. DRM) or not recoverable (e.g. Decoder error). In both cases, the player should | ||||||
|  |     // shutdown. | ||||||
|  |     @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); | ||||||
|  |  | ||||||
|         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) { | ||||||
|  |                     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: | ||||||
|  |             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 +2753,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,20 +2765,19 @@ 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; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         final boolean onPlaybackInitial = currentItem == null; |  | ||||||
|         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 +2799,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()) { | ||||||
|                 || !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 +2816,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 +2977,22 @@ 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 | ||||||
|             databaseUpdateDisposable.add(recordManager.onViewed(currentMetadata.getMetadata()) |                 .add(recordManager.onViewed(info).onErrorComplete().subscribe())); | ||||||
|                     .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 +3005,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 +3018,9 @@ public final class Player implements | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void saveStreamProgressStateCompleted() { |     public void saveStreamProgressStateCompleted() { | ||||||
|         if (currentMetadata != null) { |         // 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) |         getCurrentStreamInfo().ifPresent(info -> | ||||||
|             saveStreamProgressState((currentMetadata.getMetadata().getDuration() + 1) * 1000); |                 saveStreamProgressState((info.getDuration() + 1) * 1000)); | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|     //endregion |     //endregion | ||||||
|  |  | ||||||
| @@ -2998,8 +3031,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 +3041,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 +3054,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 +3071,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 +3093,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 +3104,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 +3131,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()); | ||||||
| @@ -3146,7 +3155,7 @@ public final class Player implements | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { |         if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentMediaItemIndex() == index) { | ||||||
|             seekToDefault(); |             seekToDefault(); | ||||||
|         } else { |         } else { | ||||||
|             saveStreamProgressState(); |             saveStreamProgressState(); | ||||||
| @@ -3232,9 +3241,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 +3295,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 +3388,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 +3419,16 @@ 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().isEmpty() | ||||||
|  |                         && info.getVideoOnlyStreams().isEmpty())) { | ||||||
|                     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 +3548,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 +3564,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 +3692,8 @@ public final class Player implements | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Normalize mismatching language strings |         // Normalize mismatching language strings | ||||||
|         final String preferredLanguage = trackSelector.getPreferredTextLanguage(); |         final String preferredLanguage = trackSelector.getParameters() | ||||||
|  |                 .preferredTextLanguages.stream().findFirst().orElse(null); | ||||||
|         // Build UI |         // Build UI | ||||||
|         buildCaptionMenu(availableLanguages); |         buildCaptionMenu(availableLanguages); | ||||||
|         if (trackSelector.getParameters().getRendererDisabled(textRenderer) |         if (trackSelector.getParameters().getRendererDisabled(textRenderer) | ||||||
| @@ -3886,10 +3900,10 @@ public final class Player implements | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void onOpenInBrowserClicked() { |     private void onOpenInBrowserClicked() { | ||||||
|         if (currentMetadata != null) { |         getCurrentStreamInfo() | ||||||
|             ShareUtils.openUrlInBrowser(getParentActivity(), |                 .map(Info::getOriginalUrl) | ||||||
|                     currentMetadata.getMetadata().getOriginalUrl()); |                 .ifPresent(originalUrl -> ShareUtils.openUrlInBrowser( | ||||||
|         } |                         Objects.requireNonNull(getParentActivity()), originalUrl)); | ||||||
|     } |     } | ||||||
|     //endregion |     //endregion | ||||||
|  |  | ||||||
| @@ -4145,12 +4159,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 +4217,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 +4329,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 +4342,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 +4359,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 +4538,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; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This {@link MediaItemTag} object is designed to contain metadata for a stream | ||||||
|  |  * that has failed to load. It supplies metadata from an underlying | ||||||
|  |  * {@link PlayQueueItem}, which is used by the internal players to resolve actual | ||||||
|  |  * playback info. | ||||||
|  |  * | ||||||
|  |  * This {@link MediaItemTag} does not contain any {@link StreamInfo} that can be | ||||||
|  |  * used to start playback and can be detected by checking {@link ExceptionTag#getErrors()} | ||||||
|  |  * when in generic form. | ||||||
|  |  **/ | ||||||
|  | public final class ExceptionTag implements MediaItemTag { | ||||||
|  |     @NonNull | ||||||
|  |     private final PlayQueueItem item; | ||||||
|  |     @NonNull | ||||||
|  |     private final List<Exception> errors; | ||||||
|  |     @Nullable | ||||||
|  |     private final Object extras; | ||||||
|  |  | ||||||
|  |     private ExceptionTag(@NonNull final PlayQueueItem item, | ||||||
|  |                          @NonNull final List<Exception> 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<Exception> errors) { | ||||||
|  |         return new ExceptionTag(playQueueItem, errors, null); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public List<Exception> 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 <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,127 @@ | |||||||
|  | package org.schabi.newpipe.player.mediaitem; | ||||||
|  |  | ||||||
|  | import android.net.Uri; | ||||||
|  |  | ||||||
|  | import com.google.android.exoplayer2.MediaItem; | ||||||
|  | import com.google.android.exoplayer2.MediaMetadata; | ||||||
|  | import com.google.android.exoplayer2.Player; | ||||||
|  |  | ||||||
|  | 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; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Metadata container and accessor used by player internals. | ||||||
|  |  * | ||||||
|  |  * This interface ensures consistency of fetching metadata on each stream, | ||||||
|  |  * which is encapsulated in a {@link MediaItem} and delivered via ExoPlayer's | ||||||
|  |  * {@link Player.Listener} on event triggers to the downstream users. | ||||||
|  |  **/ | ||||||
|  | public interface MediaItemTag { | ||||||
|  |  | ||||||
|  |     List<Exception> getErrors(); | ||||||
|  |  | ||||||
|  |     int getServiceId(); | ||||||
|  |  | ||||||
|  |     String getTitle(); | ||||||
|  |  | ||||||
|  |     String getUploaderName(); | ||||||
|  |  | ||||||
|  |     long getDurationSeconds(); | ||||||
|  |  | ||||||
|  |     String getStreamUrl(); | ||||||
|  |  | ||||||
|  |     String getThumbnailUrl(); | ||||||
|  |  | ||||||
|  |     String getUploaderUrl(); | ||||||
|  |  | ||||||
|  |     StreamType getStreamType(); | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     default Optional<StreamInfo> getMaybeStreamInfo() { | ||||||
|  |         return Optional.empty(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     default Optional<Quality> getMaybeQuality() { | ||||||
|  |         return Optional.empty(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     <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(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     final 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,85 @@ | |||||||
|  | package org.schabi.newpipe.player.mediaitem; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.extractor.stream.StreamType; | ||||||
|  | import org.schabi.newpipe.util.Constants; | ||||||
|  |  | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Optional; | ||||||
|  |  | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This is a Placeholding {@link MediaItemTag}, designed as a dummy metadata object for | ||||||
|  |  * any stream that has not been resolved. | ||||||
|  |  * | ||||||
|  |  * This object cannot be instantiated and does not hold real metadata of any form. | ||||||
|  |  * */ | ||||||
|  | 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<Exception> getErrors() { | ||||||
|  |         return Collections.emptyList(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public int getServiceId() { | ||||||
|  |         return Constants.NO_SERVICE_ID; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public String getTitle() { | ||||||
|  |         return UNKNOWN_VALUE_INTERNAL; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public String getUploaderName() { | ||||||
|  |         return UNKNOWN_VALUE_INTERNAL; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public long getDurationSeconds() { | ||||||
|  |         return 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @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 <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,115 @@ | |||||||
|  | package org.schabi.newpipe.player.mediaitem; | ||||||
|  |  | ||||||
|  | import com.google.android.exoplayer2.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; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This {@link MediaItemTag} object contains metadata for a resolved stream | ||||||
|  |  * that is ready for playback. This object guarantees the {@link StreamInfo} | ||||||
|  |  * is available and may provide the {@link Quality} of video stream used in | ||||||
|  |  * the {@link MediaItem}. | ||||||
|  |  **/ | ||||||
|  | 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<Exception> 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(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public Optional<StreamInfo> getMaybeStreamInfo() { | ||||||
|  |         return Optional.of(streamInfo); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     @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,83 @@ 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.PlaybackException; | ||||||
|  | import com.google.android.exoplayer2.Timeline; | ||||||
| import com.google.android.exoplayer2.source.BaseMediaSource; | import com.google.android.exoplayer2.source.BaseMediaSource; | ||||||
| import com.google.android.exoplayer2.source.MediaPeriod; | import com.google.android.exoplayer2.source.MediaPeriod; | ||||||
|  | import com.google.android.exoplayer2.source.SilenceMediaSource; | ||||||
|  | import com.google.android.exoplayer2.source.SinglePeriodTimeline; | ||||||
| import com.google.android.exoplayer2.upstream.Allocator; | import com.google.android.exoplayer2.upstream.Allocator; | ||||||
| import com.google.android.exoplayer2.upstream.TransferListener; | import com.google.android.exoplayer2.upstream.TransferListener; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.player.mediaitem.ExceptionTag; | ||||||
| import org.schabi.newpipe.player.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 BaseMediaSource implements ManagedMediaSource { | public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSource { | ||||||
|  |     /** | ||||||
|  |      * Play 2 seconds of silenced audio when a stream fails to resolve due to a known issue, | ||||||
|  |      * such as {@link org.schabi.newpipe.extractor.exceptions.ExtractionException}. | ||||||
|  |      * | ||||||
|  |      * This silence duration allows user to react and have time to jump to a previous stream, | ||||||
|  |      * while still provide a smooth playback experience. A duration lower than 1 second is | ||||||
|  |      * not recommended, it may cause ExoPlayer to buffer for a while. | ||||||
|  |      * */ | ||||||
|  |     public static final long SILENCE_DURATION_US = TimeUnit.SECONDS.toMicros(2); | ||||||
|  |     public static final MediaPeriod SILENT_MEDIA = makeSilentMediaPeriod(SILENCE_DURATION_US); | ||||||
|  |  | ||||||
|     private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); |     private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); | ||||||
|     private final PlayQueueItem playQueueItem; |     private final PlayQueueItem playQueueItem; | ||||||
|     private final FailedMediaSourceException error; |     private final Exception error; | ||||||
|     private final long retryTimestamp; |     private final long retryTimestamp; | ||||||
|  |     private final MediaItem mediaItem; | ||||||
|  |     /** | ||||||
|  |      * Fail the play queue item associated with this source, with potential future retries. | ||||||
|  |      * | ||||||
|  |      * 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 Exception 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; | ||||||
|  |         this.mediaItem = ExceptionTag | ||||||
|  |                 .of(playQueueItem, Collections.singletonList(error)) | ||||||
|  |                 .withExtras(this) | ||||||
|  |                 .asMediaItem(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     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 Exception 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 Exception getError() { | ||||||
|         return error; |         return error; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -55,35 +86,78 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo | |||||||
|         return System.currentTimeMillis() >= retryTimestamp; |         return System.currentTimeMillis() >= retryTimestamp; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Returns the {@link MediaItem} whose media is provided by the source. |  | ||||||
|      */ |  | ||||||
|     @Override |     @Override | ||||||
|     public MediaItem getMediaItem() { |     public MediaItem getMediaItem() { | ||||||
|         return MediaItem.fromUri(playQueueItem.getUrl()); |         return mediaItem; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     /** | ||||||
|     public void maybeThrowSourceInfoRefreshError() throws IOException { |      * Prepares the source with {@link Timeline} info on the silence playback when the error | ||||||
|         throw new IOException(error); |      * is classed as {@link FailedMediaSourceException}, for example, when the error is | ||||||
|     } |      * {@link org.schabi.newpipe.extractor.exceptions.ExtractionException ExtractionException}. | ||||||
|  |      * These types of error are swallowed by {@link FailedMediaSource}, and the underlying | ||||||
|     @Override |      * exception is carried to the {@link MediaItem} metadata during playback. | ||||||
|     public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator, |      * <br><br> | ||||||
|                                     final long startPositionUs) { |      * If the exception is not known, e.g. {@link java.net.UnknownHostException} or some | ||||||
|         return null; |      * other network issue, then no source info is refreshed and | ||||||
|     } |      * {@link #maybeThrowSourceInfoRefreshError()} be will triggered. | ||||||
|  |      * <br><br> | ||||||
|     @Override |      * Note that this method is called only once until {@link #releaseSourceInternal()} is called, | ||||||
|     public void releasePeriod(final MediaPeriod mediaPeriod) { } |      * so if no action is done in here, playback will stall unless | ||||||
|  |      * {@link #maybeThrowSourceInfoRefreshError()} is called. | ||||||
|  |      * | ||||||
|  |      * @param mediaTransferListener No data transfer listener needed, ignored here. | ||||||
|  |      */ | ||||||
|     @Override |     @Override | ||||||
|     protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) { |     protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) { | ||||||
|         Log.e(TAG, "Loading failed source: ", error); |         Log.e(TAG, "Loading failed source: ", error); | ||||||
|  |         if (error instanceof FailedMediaSourceException) { | ||||||
|  |             refreshSourceInfo(makeSilentMediaTimeline(SILENCE_DURATION_US, mediaItem)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * If the error is not known, e.g. network issue, then the exception is not swallowed here in | ||||||
|  |      * {@link FailedMediaSource}. The exception is then propagated to the player, which | ||||||
|  |      * {@link org.schabi.newpipe.player.Player Player} can react to inside | ||||||
|  |      * {@link com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException)}. | ||||||
|  |      * | ||||||
|  |      * @throws IOException An error which will always result in | ||||||
|  |      * {@link com.google.android.exoplayer2.PlaybackException#ERROR_CODE_IO_UNSPECIFIED}. | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void maybeThrowSourceInfoRefreshError() throws IOException { | ||||||
|  |         if (!(error instanceof FailedMediaSourceException)) { | ||||||
|  |             throw new IOException(error); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * This method is only called if {@link #prepareSourceInternal(TransferListener)} | ||||||
|  |      * refreshes the source info with no exception. All parameters are ignored as this | ||||||
|  |      * returns a static and reused piece of silent audio. | ||||||
|  |      * | ||||||
|  |      * @param id                The identifier of the period. | ||||||
|  |      * @param allocator         An {@link Allocator} from which to obtain media buffer allocations. | ||||||
|  |      * @param startPositionUs   The expected start position, in microseconds. | ||||||
|  |      * @return The common {@link MediaPeriod} holding the silence. | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public MediaPeriod createPeriod(final MediaPeriodId id, | ||||||
|  |                                     final Allocator allocator, | ||||||
|  |                                     final long startPositionUs) { | ||||||
|  |         return SILENT_MEDIA; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected void releaseSourceInternal() { } |     public void releasePeriod(final MediaPeriod mediaPeriod) { | ||||||
|  |         /* Do Nothing (we want to keep re-using the Silent MediaPeriod) */ | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void releaseSourceInternal() { | ||||||
|  |         /* Do Nothing, no clean-up for processing/extra thread is needed by this MediaSource */ | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, |     public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, | ||||||
| @@ -117,4 +191,22 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo | |||||||
|             super(cause); |             super(cause); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private static Timeline makeSilentMediaTimeline(final long durationUs, | ||||||
|  |                                                     @NonNull final MediaItem mediaItem) { | ||||||
|  |         return new SinglePeriodTimeline( | ||||||
|  |                 durationUs, | ||||||
|  |                 /* isSeekable= */ true, | ||||||
|  |                 /* isDynamic= */ false, | ||||||
|  |                 /* useLiveConfiguration= */ false, | ||||||
|  |                 /* manifest= */ null, | ||||||
|  |                 mediaItem); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static MediaPeriod makeSilentMediaPeriod(final long durationUs) { | ||||||
|  |         return new SilenceMediaSource.Factory() | ||||||
|  |                 .setDurationUs(durationUs) | ||||||
|  |                 .createMediaSource() | ||||||
|  |                 .createPeriod(null, null, 0); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,32 +1,46 @@ | |||||||
| 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<Integer> 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, |     /** | ||||||
|  |      * Uses a {@link CompositeMediaSource} to wrap one or more child {@link MediaSource}s | ||||||
|  |      * containing actual media. This wrapper {@link LoadedMediaSource} holds the expiration | ||||||
|  |      * timestamp as a {@link ManagedMediaSource} to allow explicit playlist management under | ||||||
|  |      * {@link ManagedMediaSourcePlaylist}. | ||||||
|  |      * | ||||||
|  |      * @param source            The child media source with actual media. | ||||||
|  |      * @param tag               Metadata for the child media source. | ||||||
|  |      * @param stream            The queue item associated with the media source. | ||||||
|  |      * @param expireTimestamp   The timestamp when the media source expires and might not be | ||||||
|  |      *                          available for playback. | ||||||
|  |      */ | ||||||
|  |     public LoadedMediaSource(@NonNull final MediaSource source, | ||||||
|  |                              @NonNull final MediaItemTag tag, | ||||||
|  |                              @NonNull final PlayQueueItem stream, | ||||||
|                              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() { | ||||||
| @@ -37,20 +51,38 @@ public class LoadedMediaSource implements ManagedMediaSource { | |||||||
|         return System.currentTimeMillis() >= expireTimestamp; |         return System.currentTimeMillis() >= expireTimestamp; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Delegates the preparation of child {@link MediaSource}s to the | ||||||
|  |      * {@link CompositeMediaSource} wrapper. Since all {@link LoadedMediaSource}s use only | ||||||
|  |      * a single child media, the child id of 0 is always used (sonar doesn't like null as id here). | ||||||
|  |      * | ||||||
|  |      * @param mediaTransferListener A data transfer listener that will be registered by the | ||||||
|  |      *                              {@link CompositeMediaSource} for child source preparation. | ||||||
|  |      */ | ||||||
|     @Override |     @Override | ||||||
|     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(0, source); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * When any child {@link MediaSource} is prepared, the refreshed {@link Timeline} can | ||||||
|  |      * be listened to here. But since {@link LoadedMediaSource} has only a single child source, | ||||||
|  |      * this method is called only once until {@link #releaseSourceInternal()} is called. | ||||||
|  |      * <br><br> | ||||||
|  |      * On refresh, the {@link CompositeMediaSource} delegate will be notified with the | ||||||
|  |      * new {@link Timeline}, otherwise {@link #createPeriod(MediaPeriodId, Allocator, long)} | ||||||
|  |      * will not be called and playback may be stalled. | ||||||
|  |      * | ||||||
|  |      * @param id            The unique id used to prepare the child source. | ||||||
|  |      * @param mediaSource   The child source whose source info has been refreshed. | ||||||
|  |      * @param timeline      The new timeline of the child source. | ||||||
|  |      */ | ||||||
|     @Override |     @Override | ||||||
|     public void maybeThrowSourceInfoRefreshError() throws IOException { |     protected void onChildSourceInfoRefreshed(final Integer 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 +96,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,35 @@ | |||||||
| 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; | ||||||
|     /** |  | ||||||
|      * Returns the {@link MediaItem} whose media is provided by the source. | 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() { } | ||||||
|  |  | ||||||
|     @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 or error 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 +40,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,29 @@ 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)); | ||||||
|  |             } | ||||||
|  |             // Non-source related error expected here (e.g. network), | ||||||
|  |             // should allow retry shortly after the error. | ||||||
|  |             return FailedMediaSource.of(stream, new Exception(throwable), | ||||||
|  |                     /*allowRetryIn=*/TimeUnit.MILLISECONDS.convert(3, TimeUnit.SECONDS)); | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void onMediaSourceReceived(@NonNull final PlayQueueItem item, |     private void onMediaSourceReceived(@NonNull final PlayQueueItem item, | ||||||
| @@ -478,23 +493,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); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 litetex
					litetex