1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-06-26 15:13:20 +00:00

updated: ExoPlayer to 2.17.1.

added: MediaItemTag for ManagedMediaSources.
added: silent track for FailedMediaSource.
added: keyframe fast forward at initial playback buffer.
added: error notification on silently skipped streams.
This commit is contained in:
karyogamy 2022-03-13 00:22:47 -05:00
parent 8c5e8bdf78
commit 4e459b3383
29 changed files with 892 additions and 613 deletions

View File

@ -104,7 +104,7 @@ ext {
androidxRoomVersion = '2.4.2' androidxRoomVersion = '2.4.2'
icepickVersion = '3.2.0' icepickVersion = '3.2.0'
exoPlayerVersion = '2.14.2' exoPlayerVersion = '2.17.1'
googleAutoServiceVersion = '1.0.1' googleAutoServiceVersion = '1.0.1'
groupieVersion = '2.10.0' groupieVersion = '2.10.0'
markwonVersion = '4.6.2' markwonVersion = '4.6.2'

View File

@ -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();

View File

@ -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
*/ */

View File

@ -1,5 +1,21 @@
package org.schabi.newpipe.player; package org.schabi.newpipe.player;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_NO_PERMISSION;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_UNSPECIFIED;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_TIMEOUT;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECIFIED;
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AUTO_TRANSITION; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AUTO_TRANSITION;
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL;
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_REMOVE; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_REMOVE;
@ -112,20 +128,20 @@ import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player.PositionInfo; import com.google.android.exoplayer2.Player.PositionInfo;
import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.TracksInfo;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.CaptionStyleCompat; import com.google.android.exoplayer2.ui.CaptionStyleCompat;
import com.google.android.exoplayer2.ui.SubtitleView; import com.google.android.exoplayer2.ui.SubtitleView;
@ -145,6 +161,7 @@ import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding;
import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamSegment; import org.schabi.newpipe.extractor.stream.StreamSegment;
@ -168,7 +185,7 @@ import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.listeners.view.PlaybackSpeedClickListener; import org.schabi.newpipe.player.listeners.view.PlaybackSpeedClickListener;
import org.schabi.newpipe.player.listeners.view.QualityClickListener; import org.schabi.newpipe.player.listeners.view.QualityClickListener;
import org.schabi.newpipe.player.playback.CustomTrackSelector; import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.playback.MediaSourceManager; import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.player.playback.PlaybackListener;
import org.schabi.newpipe.player.playback.PlayerMediaSession; import org.schabi.newpipe.player.playback.PlayerMediaSession;
@ -180,7 +197,6 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder;
import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder;
import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
import org.schabi.newpipe.player.resolver.MediaSourceTag;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType;
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper; import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
@ -196,8 +212,8 @@ import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.views.ExpandableSurfaceView; import org.schabi.newpipe.views.ExpandableSurfaceView;
import org.schabi.newpipe.views.player.PlayerFastSeekOverlay; import org.schabi.newpipe.views.player.PlayerFastSeekOverlay;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -278,19 +294,19 @@ public final class Player implements
@Nullable private MediaSourceManager playQueueManager; @Nullable private MediaSourceManager playQueueManager;
@Nullable private PlayQueueItem currentItem; @Nullable private PlayQueueItem currentItem;
@Nullable private MediaSourceTag currentMetadata; @Nullable private MediaItemTag currentMetadata;
@Nullable private Bitmap currentThumbnail; @Nullable private Bitmap currentThumbnail;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Player // Player
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private SimpleExoPlayer simpleExoPlayer; private ExoPlayer simpleExoPlayer;
private AudioReactor audioReactor; private AudioReactor audioReactor;
private MediaSessionManager mediaSessionManager; private MediaSessionManager mediaSessionManager;
@Nullable private SurfaceHolderCallback surfaceHolderCallback; @Nullable private SurfaceHolderCallback surfaceHolderCallback;
@NonNull private final CustomTrackSelector trackSelector; @NonNull private final DefaultTrackSelector trackSelector;
@NonNull private final LoadController loadController; @NonNull private final LoadController loadController;
@NonNull private final RenderersFactory renderFactory; @NonNull private final RenderersFactory renderFactory;
@ -415,7 +431,7 @@ public final class Player implements
setupBroadcastReceiver(); setupBroadcastReceiver();
trackSelector = new CustomTrackSelector(context, PlayerHelper.getQualitySelector()); trackSelector = new DefaultTrackSelector(context, PlayerHelper.getQualitySelector());
final PlayerDataSource dataSource = new PlayerDataSource(context, DownloaderImpl.USER_AGENT, final PlayerDataSource dataSource = new PlayerDataSource(context, DownloaderImpl.USER_AGENT,
new DefaultBandwidthMeter.Builder(context).build()); new DefaultBandwidthMeter.Builder(context).build());
loadController = new LoadController(); loadController = new LoadController();
@ -498,7 +514,7 @@ public final class Player implements
Log.d(TAG, "initPlayer() called with: playOnReady = [" + playOnReady + "]"); Log.d(TAG, "initPlayer() called with: playOnReady = [" + playOnReady + "]");
} }
simpleExoPlayer = new SimpleExoPlayer.Builder(context, renderFactory) simpleExoPlayer = new ExoPlayer.Builder(context, renderFactory)
.setTrackSelector(trackSelector) .setTrackSelector(trackSelector)
.setLoadControl(loadController) .setLoadControl(loadController)
.build(); .build();
@ -1642,8 +1658,7 @@ public final class Player implements
} }
public boolean getPlaybackSkipSilence() { public boolean getPlaybackSkipSilence() {
return !exoPlayerIsNull() && simpleExoPlayer.getAudioComponent() != null return !exoPlayerIsNull() && simpleExoPlayer.getSkipSilenceEnabled();
&& simpleExoPlayer.getAudioComponent().getSkipSilenceEnabled();
} }
public PlaybackParameters getPlaybackParameters() { public PlaybackParameters getPlaybackParameters() {
@ -1669,9 +1684,7 @@ public final class Player implements
savePlaybackParametersToPrefs(this, roundedSpeed, roundedPitch, skipSilence); savePlaybackParametersToPrefs(this, roundedSpeed, roundedPitch, skipSilence);
simpleExoPlayer.setPlaybackParameters( simpleExoPlayer.setPlaybackParameters(
new PlaybackParameters(roundedSpeed, roundedPitch)); new PlaybackParameters(roundedSpeed, roundedPitch));
if (simpleExoPlayer.getAudioComponent() != null) { simpleExoPlayer.setSkipSilenceEnabled(skipSilence);
simpleExoPlayer.getAudioComponent().setSkipSilenceEnabled(skipSilence);
}
} }
//endregion //endregion
@ -1950,10 +1963,12 @@ public final class Player implements
final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size(); final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size();
final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected(); final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected();
boolean showSegment = false; boolean showSegment = false;
if (currentMetadata != null) { showSegment = /*only when stream has segment and playing in fullscreen player*/
showSegment = !currentMetadata.getMetadata().getStreamSegments().isEmpty() !popupPlayerSelected()
&& !popupPlayerSelected(); && !getCurrentStreamInfo()
} .map(StreamInfo::getStreamSegments)
.map(List::isEmpty)
.orElse(/*no stream info=*/true);
binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE); binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE);
binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f); binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f);
@ -1993,9 +2008,30 @@ public final class Player implements
// Playback states // Playback states
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region Playback states //region Playback states
@Override
public void onPlayWhenReadyChanged(final boolean playWhenReady, final int reason) {
if (DEBUG) {
Log.d(TAG, "ExoPlayer - onPlayWhenReadyChanged() called with: "
+ "playWhenReady = [" + playWhenReady + "], "
+ "reason = [" + reason + "]");
}
final int playbackState = simpleExoPlayer == null
? com.google.android.exoplayer2.Player.STATE_IDLE
: simpleExoPlayer.getPlaybackState();
updatePlaybackState(playWhenReady, playbackState);
}
@Override // exoplayer listener @Override
public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) { public void onPlaybackStateChanged(final int playbackState) {
if (DEBUG) {
Log.d(TAG, "ExoPlayer - onPlaybackStateChanged() called with: "
+ "playbackState = [" + playbackState + "]");
}
final boolean playWhenReady = simpleExoPlayer != null && simpleExoPlayer.getPlayWhenReady();
updatePlaybackState(playWhenReady, playbackState);
}
private void updatePlaybackState(final boolean playWhenReady, final int playbackState) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: " Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: "
+ "playWhenReady = [" + playWhenReady + "], " + "playWhenReady = [" + playWhenReady + "], "
@ -2004,7 +2040,7 @@ public final class Player implements
if (currentState == STATE_PAUSED_SEEK) { if (currentState == STATE_PAUSED_SEEK) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "ExoPlayer - onPlayerStateChanged() is currently blocked"); Log.d(TAG, "updatePlaybackState() is currently blocked");
} }
return; return;
} }
@ -2019,8 +2055,6 @@ public final class Player implements
} }
break; break;
case com.google.android.exoplayer2.Player.STATE_READY: //3 case com.google.android.exoplayer2.Player.STATE_READY: //3
maybeUpdateCurrentMetadata();
maybeCorrectSeekPosition();
if (!isPrepared) { if (!isPrepared) {
isPrepared = true; isPrepared = true;
onPrepared(playWhenReady); onPrepared(playWhenReady);
@ -2037,18 +2071,11 @@ public final class Player implements
@Override // exoplayer listener @Override // exoplayer listener
public void onIsLoadingChanged(final boolean isLoading) { public void onIsLoadingChanged(final boolean isLoading) {
if (DEBUG) {
Log.d(TAG, "ExoPlayer - onLoadingChanged() called with: "
+ "isLoading = [" + isLoading + "]");
}
if (!isLoading && currentState == STATE_PAUSED && isProgressLoopRunning()) { if (!isLoading && currentState == STATE_PAUSED && isProgressLoopRunning()) {
stopProgressLoop(); stopProgressLoop();
} else if (isLoading && !isProgressLoopRunning()) { } else if (isLoading && !isProgressLoopRunning()) {
startProgressLoop(); startProgressLoop();
} }
maybeUpdateCurrentMetadata();
} }
@Override // own playback listener @Override // own playback listener
@ -2460,27 +2487,37 @@ public final class Player implements
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region ExoPlayer listeners (that didn't fit in other categories) //region ExoPlayer listeners (that didn't fit in other categories)
@Override public void onEvents(@NonNull final com.google.android.exoplayer2.Player player,
public void onTimelineChanged(@NonNull final Timeline timeline, final int reason) { @NonNull final com.google.android.exoplayer2.Player.Events events) {
if (DEBUG) { Listener.super.onEvents(player, events);
Log.d(TAG, "ExoPlayer - onTimelineChanged() called with " MediaItemTag.from(player.getCurrentMediaItem()).ifPresent(tag -> {
+ "timeline size = [" + timeline.getWindowCount() + "], " if (tag == currentMetadata) {
+ "reason = [" + reason + "]"); return;
} }
currentMetadata = tag;
maybeUpdateCurrentMetadata(); if (!tag.getErrors().isEmpty()) {
// force recreate notification to ensure seek bar is shown when preparation finishes final ErrorInfo errorInfo = new ErrorInfo(
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); tag.getErrors().get(0),
UserAction.PLAY_STREAM,
"Loading failed for [" + tag.getTitle() + "]: " + tag.getStreamUrl(),
tag.getServiceId());
ErrorUtil.createNotification(context, errorInfo);
}
tag.getMaybeStreamInfo().ifPresent(info -> {
if (DEBUG) {
Log.d(TAG, "ExoPlayer - onEvents() update stream info: " + info.getName());
}
updateMetadataWith(info);
});
});
} }
@Override @Override
public void onTracksChanged(@NonNull final TrackGroupArray trackGroups, public void onTracksInfoChanged(@NonNull final TracksInfo tracksInfo) {
@NonNull final TrackSelectionArray trackSelections) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "ExoPlayer - onTracksChanged(), " Log.d(TAG, "ExoPlayer - onTracksChanged(), "
+ "track group size = " + trackGroups.length); + "track group size = " + tracksInfo.getTrackGroupInfos().size());
} }
maybeUpdateCurrentMetadata();
onTextTracksChanged(); onTextTracksChanged();
} }
@ -2499,20 +2536,32 @@ public final class Player implements
@DiscontinuityReason final int discontinuityReason) { @DiscontinuityReason final int discontinuityReason) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with "
+ "oldPositionIndex = [" + oldPosition.mediaItemIndex + "], "
+ "oldPositionMs = [" + oldPosition.positionMs + "], "
+ "newPositionIndex = [" + newPosition.mediaItemIndex + "], "
+ "newPositionMs = [" + newPosition.positionMs + "], "
+ "discontinuityReason = [" + discontinuityReason + "]"); + "discontinuityReason = [" + discontinuityReason + "]");
} }
if (playQueue == null) { if (playQueue == null) {
return; return;
} }
if (newPosition.contentPositionMs == 0 &&
simpleExoPlayer.getTotalBufferedDuration() < 500L) {
Log.d(TAG, "Playback - skipping to initial keyframe.");
simpleExoPlayer.setSeekParameters(SeekParameters.CLOSEST_SYNC);
simpleExoPlayer.seekTo(1L);
simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context));
}
// Refresh the playback if there is a transition to the next video // Refresh the playback if there is a transition to the next video
final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); final int newIndex = newPosition.mediaItemIndex;
switch (discontinuityReason) { switch (discontinuityReason) {
case DISCONTINUITY_REASON_AUTO_TRANSITION: case DISCONTINUITY_REASON_AUTO_TRANSITION:
case DISCONTINUITY_REASON_REMOVE: case DISCONTINUITY_REASON_REMOVE:
// When player is in single repeat mode and a period transition occurs, // When player is in single repeat mode and a period transition occurs,
// we need to register a view count here since no metadata has changed // we need to register a view count here since no metadata has changed
if (getRepeatMode() == REPEAT_MODE_ONE && newWindowIndex == playQueue.getIndex()) { if (getRepeatMode() == REPEAT_MODE_ONE && newIndex == playQueue.getIndex()) {
registerStreamViewed(); registerStreamViewed();
break; break;
} }
@ -2525,16 +2574,15 @@ public final class Player implements
} }
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
case DISCONTINUITY_REASON_INTERNAL: case DISCONTINUITY_REASON_INTERNAL:
if (playQueue.getIndex() != newWindowIndex) { // Player index may be invalid when playback is blocked
if (getCurrentState() != STATE_BLOCKED && newIndex != playQueue.getIndex()) {
saveStreamProgressStateCompleted(); // current stream has ended saveStreamProgressStateCompleted(); // current stream has ended
playQueue.setIndex(newWindowIndex); playQueue.setIndex(newIndex);
} }
break; break;
case DISCONTINUITY_REASON_SKIP: case DISCONTINUITY_REASON_SKIP:
break; // only makes Android Studio linter happy, as there are no ads break; // only makes Android Studio linter happy, as there are no ads
} }
maybeUpdateCurrentMetadata();
} }
@Override @Override
@ -2557,96 +2605,83 @@ public final class Player implements
//region Errors //region Errors
/** /**
* Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. * Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
* <p>There are multiple types of errors:</p>
* <ul>
* <li>{@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}</li>
* <li>{@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}:
* If a runtime error occurred, then we can try to recover it by restarting the playback
* after setting the timestamp recovery.</li>
* <li>{@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}:
* If the renderer failed, treat the error as unrecoverable.</li>
* </ul>
* *
* @see #processSourceError(IOException) * @see com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException)
* @see com.google.android.exoplayer2.Player.Listener#onPlayerError(ExoPlaybackException) * */
*/ @SuppressLint("SwitchIntDef")
@Override @Override
public void onPlayerError(@NonNull final ExoPlaybackException error) { public void onPlayerError(@NonNull final PlaybackException error) {
Log.e(TAG, "ExoPlayer - onPlayerError() called with:", error); Log.e(TAG, "ExoPlayer - onPlayerError() called with:", error);
setRecovery();
saveStreamProgressState(); saveStreamProgressState();
boolean isCatchableException = false; boolean isCatchableException = false;
switch (error.type) { switch (error.errorCode) {
case ExoPlaybackException.TYPE_SOURCE: case ERROR_CODE_BEHIND_LIVE_WINDOW:
isCatchableException = processSourceError(error.getSourceException()); isCatchableException = true;
simpleExoPlayer.seekToDefaultPosition();
simpleExoPlayer.prepare();
// Inform the user that we are reloading the stream by
// switching to the buffering state
onBuffering();
break; break;
case ExoPlaybackException.TYPE_UNEXPECTED: case ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE:
case ERROR_CODE_IO_BAD_HTTP_STATUS:
case ERROR_CODE_IO_FILE_NOT_FOUND:
case ERROR_CODE_IO_NO_PERMISSION:
case ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED:
case ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE:
case ERROR_CODE_PARSING_CONTAINER_MALFORMED:
case ERROR_CODE_PARSING_MANIFEST_MALFORMED:
case ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED:
case ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED:
// Source errors, signal on playQueue and move on:
if (!exoPlayerIsNull() && playQueue != null) {
isCatchableException = true;
playQueue.error();
}
break;
case ERROR_CODE_TIMEOUT:
case ERROR_CODE_IO_UNSPECIFIED:
case ERROR_CODE_IO_NETWORK_CONNECTION_FAILED:
case ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT:
// Don't create notification on timeout/networking errors:
isCatchableException = true;
case ERROR_CODE_UNSPECIFIED:
// Reload playback on unexpected errors:
setRecovery(); setRecovery();
reloadPlayQueueManager(); reloadPlayQueueManager();
break; break;
case ExoPlaybackException.TYPE_REMOTE:
case ExoPlaybackException.TYPE_RENDERER:
default: default:
// API, remote and renderer errors belong here:
onPlaybackShutdown(); onPlaybackShutdown();
break; break;
} }
if (isCatchableException) { if (!isCatchableException) {
return; createErrorNotification(error);
} }
createErrorNotification(error);
if (fragmentListener != null) { if (fragmentListener != null) {
fragmentListener.onPlayerError(error); fragmentListener.onPlayerError(error, isCatchableException);
} }
} }
private void createErrorNotification(@NonNull final ExoPlaybackException error) { private void createErrorNotification(@NonNull final PlaybackException error) {
final ErrorInfo errorInfo; final ErrorInfo errorInfo;
if (currentMetadata == null) { if (currentMetadata == null) {
errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM, errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM,
"Player error[type=" + error.type + "] occurred, currentMetadata is null"); "Player error[type=" + error.getErrorCodeName()
+ "] occurred, currentMetadata is null");
} else { } else {
errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM, errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM,
"Player error[type=" + error.type + "] occurred while playing " "Player error[type=" + error.getErrorCodeName()
+ currentMetadata.getMetadata().getUrl(), + "] occurred while playing " + currentMetadata.getStreamUrl(),
currentMetadata.getMetadata()); currentMetadata.getServiceId());
} }
ErrorUtil.createNotification(context, errorInfo); ErrorUtil.createNotification(context, errorInfo);
} }
/**
* Process an {@link IOException} returned by {@link ExoPlaybackException#getSourceException()}
* for {@link ExoPlaybackException#TYPE_SOURCE} exceptions.
*
* <p>
* This method sets the recovery position and sends an error message to the play queue if the
* exception is not a {@link BehindLiveWindowException}.
* </p>
* @param error the source error which was thrown by ExoPlayer
* @return whether the exception thrown is a {@link BehindLiveWindowException} ({@code false}
* is always returned if ExoPlayer or the play queue is null)
*/
private boolean processSourceError(final IOException error) {
if (exoPlayerIsNull() || playQueue == null) {
return false;
}
setRecovery();
if (error instanceof BehindLiveWindowException) {
simpleExoPlayer.seekToDefaultPosition();
simpleExoPlayer.prepare();
// Inform the user that we are reloading the stream by switching to the buffering state
onBuffering();
return true;
}
playQueue.error();
return false;
}
//endregion //endregion
@ -2693,7 +2728,7 @@ public final class Player implements
} }
final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline(); final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline();
final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); final int currentWindowIndex = simpleExoPlayer.getCurrentMediaItemIndex();
if (currentTimeline.isEmpty() || currentWindowIndex < 0 if (currentTimeline.isEmpty() || currentWindowIndex < 0
|| currentWindowIndex >= currentTimeline.getWindowCount()) { || currentWindowIndex >= currentTimeline.getWindowCount()) {
return false; return false;
@ -2705,10 +2740,10 @@ public final class Player implements
} }
@Override // own playback listener @Override // own playback listener
public void onPlaybackSynchronize(@NonNull final PlayQueueItem item) { public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, final boolean wasBlocked) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Playback - onPlaybackSynchronize() called with " Log.d(TAG, "Playback - onPlaybackSynchronize(was blocked: " + wasBlocked
+ "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); + ") called with item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]");
} }
if (exoPlayerIsNull() || playQueue == null) { if (exoPlayerIsNull() || playQueue == null) {
return; return;
@ -2718,7 +2753,7 @@ public final class Player implements
final boolean hasPlayQueueItemChanged = currentItem != item; final boolean hasPlayQueueItemChanged = currentItem != item;
final int currentPlayQueueIndex = playQueue.indexOf(item); final int currentPlayQueueIndex = playQueue.indexOf(item);
final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); final int currentPlaylistIndex = simpleExoPlayer.getCurrentMediaItemIndex();
final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount();
// If nothing to synchronize // If nothing to synchronize
@ -2740,8 +2775,7 @@ public final class Player implements
+ "index=[" + currentPlayQueueIndex + "] with " + "index=[" + currentPlayQueueIndex + "] with "
+ "playlist length=[" + currentPlaylistSize + "]"); + "playlist length=[" + currentPlaylistSize + "]");
} else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial } else if (wasBlocked || currentPlaylistIndex != currentPlayQueueIndex) {
|| !isPlaying()) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Playback - Rewinding to correct " Log.d(TAG, "Playback - Rewinding to correct "
+ "index=[" + currentPlayQueueIndex + "], " + "index=[" + currentPlayQueueIndex + "], "
@ -2758,28 +2792,6 @@ public final class Player implements
} }
} }
private void maybeCorrectSeekPosition() {
if (playQueue == null || exoPlayerIsNull() || currentMetadata == null) {
return;
}
final PlayQueueItem currentSourceItem = playQueue.getItem();
if (currentSourceItem == null) {
return;
}
final StreamInfo currentInfo = currentMetadata.getMetadata();
final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000;
if (presetStartPositionMillis > 0L) {
// Has another start position?
if (DEBUG) {
Log.d(TAG, "Playback - Seeking to preset start "
+ "position=[" + presetStartPositionMillis + "]");
}
seekTo(presetStartPositionMillis);
}
}
public void seekTo(final long positionMillis) { public void seekTo(final long positionMillis) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]"); Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]");
@ -2941,24 +2953,24 @@ public final class Player implements
//region StreamInfo history: views and progress //region StreamInfo history: views and progress
private void registerStreamViewed() { private void registerStreamViewed() {
if (currentMetadata != null) { getCurrentStreamInfo().ifPresent(info -> {
databaseUpdateDisposable.add(recordManager.onViewed(currentMetadata.getMetadata()) databaseUpdateDisposable
.onErrorComplete().subscribe()); .add(recordManager.onViewed(info).onErrorComplete().subscribe());
} });
} }
private void saveStreamProgressState(final long progressMillis) { private void saveStreamProgressState(final long progressMillis) {
if (currentMetadata == null if (!getCurrentStreamInfo().isPresent()
|| !prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { || !prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
return; return;
} }
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "saveStreamProgressState() called with: progressMillis=" + progressMillis Log.d(TAG, "saveStreamProgressState() called with: progressMillis=" + progressMillis
+ ", currentMetadata=[" + currentMetadata.getMetadata().getName() + "]"); + ", currentMetadata=[" + getCurrentStreamInfo().get().getName() + "]");
} }
databaseUpdateDisposable databaseUpdateDisposable
.add(recordManager.saveStreamState(currentMetadata.getMetadata(), progressMillis) .add(recordManager.saveStreamState(getCurrentStreamInfo().get(), progressMillis)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnError(e -> { .doOnError(e -> {
if (DEBUG) { if (DEBUG) {
@ -2971,7 +2983,7 @@ public final class Player implements
public void saveStreamProgressState() { public void saveStreamProgressState() {
if (exoPlayerIsNull() || currentMetadata == null || playQueue == null if (exoPlayerIsNull() || currentMetadata == null || playQueue == null
|| playQueue.getIndex() != simpleExoPlayer.getCurrentWindowIndex()) { || playQueue.getIndex() != simpleExoPlayer.getCurrentMediaItemIndex()) {
// Make sure play queue and current window index are equal, to prevent saving state for // Make sure play queue and current window index are equal, to prevent saving state for
// the wrong stream on discontinuity (e.g. when the stream just changed but the // the wrong stream on discontinuity (e.g. when the stream just changed but the
// playQueue index and currentMetadata still haven't updated) // playQueue index and currentMetadata still haven't updated)
@ -2984,10 +2996,10 @@ public final class Player implements
} }
public void saveStreamProgressStateCompleted() { public void saveStreamProgressStateCompleted() {
if (currentMetadata != null) { getCurrentStreamInfo().ifPresent(info -> {
// current stream has ended, so the progress is its duration (+1 to overcome rounding) // current stream has ended, so the progress is its duration (+1 to overcome rounding)
saveStreamProgressState((currentMetadata.getMetadata().getDuration() + 1) * 1000); saveStreamProgressState((info.getDuration() + 1) * 1000);
} });
} }
//endregion //endregion
@ -2998,8 +3010,7 @@ public final class Player implements
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region Metadata //region Metadata
private void onMetadataChanged(@NonNull final MediaSourceTag tag) { private void onMetadataChanged(@NonNull final StreamInfo info) {
final StreamInfo info = tag.getMetadata();
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName()); Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName());
} }
@ -3009,12 +3020,10 @@ public final class Player implements
updateStreamRelatedViews(); updateStreamRelatedViews();
showHideKodiButton(); showHideKodiButton();
binding.titleTextView.setText(tag.getMetadata().getName()); binding.titleTextView.setText(info.getName());
binding.channelTextView.setText(tag.getMetadata().getUploaderName()); binding.channelTextView.setText(info.getUploaderName());
this.seekbarPreviewThumbnailHolder.resetFrom( this.seekbarPreviewThumbnailHolder.resetFrom(this.getContext(), info.getPreviewFrames());
this.getContext(),
tag.getMetadata().getPreviewFrames());
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
@ -3024,9 +3033,7 @@ public final class Player implements
getVideoTitle(), getVideoTitle(),
getUploaderName(), getUploaderName(),
showThumbnail ? Optional.ofNullable(getThumbnail()) : Optional.empty(), showThumbnail ? Optional.ofNullable(getThumbnail()) : Optional.empty(),
StreamTypeUtil.isLiveStream(tag.getMetadata().getStreamType()) StreamTypeUtil.isLiveStream(info.getStreamType()) ? -1 : info.getDuration()
? -1
: tag.getMetadata().getDuration()
); );
notifyMetadataUpdateToListeners(); notifyMetadataUpdateToListeners();
@ -3043,40 +3050,21 @@ public final class Player implements
} }
} }
private void maybeUpdateCurrentMetadata() { private void updateMetadataWith(@NonNull final StreamInfo streamInfo) {
if (exoPlayerIsNull()) { if (exoPlayerIsNull()) {
return; return;
} }
final MediaSourceTag metadata; maybeAutoQueueNextStream(streamInfo);
try { onMetadataChanged(streamInfo);
final MediaItem currentMediaItem = simpleExoPlayer.getCurrentMediaItem(); NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true);
if (currentMediaItem == null || currentMediaItem.playbackProperties == null
|| currentMediaItem.playbackProperties.tag == null) {
return;
}
metadata = (MediaSourceTag) currentMediaItem.playbackProperties.tag;
} catch (final IndexOutOfBoundsException | ClassCastException ex) {
if (DEBUG) {
Log.d(TAG, "Could not update metadata", ex);
}
return;
}
maybeAutoQueueNextStream(metadata);
if (currentMetadata == metadata) {
return;
}
currentMetadata = metadata;
onMetadataChanged(metadata);
} }
@NonNull @NonNull
private String getVideoUrl() { private String getVideoUrl() {
return currentMetadata == null return currentMetadata == null
? context.getString(R.string.unknown_content) ? context.getString(R.string.unknown_content)
: currentMetadata.getMetadata().getUrl(); : currentMetadata.getStreamUrl();
} }
@NonNull @NonNull
@ -3084,7 +3072,7 @@ public final class Player implements
final int timeSeconds = binding.playbackSeekBar.getProgress() / 1000; final int timeSeconds = binding.playbackSeekBar.getProgress() / 1000;
String videoUrl = getVideoUrl(); String videoUrl = getVideoUrl();
if (!isLive() && timeSeconds >= 0 && currentMetadata != null if (!isLive() && timeSeconds >= 0 && currentMetadata != null
&& currentMetadata.getMetadata().getServiceId() == YouTube.getServiceId()) { && currentMetadata.getServiceId() == YouTube.getServiceId()) {
// Timestamp doesn't make sense in a live stream so drop it // Timestamp doesn't make sense in a live stream so drop it
videoUrl += ("&t=" + timeSeconds); videoUrl += ("&t=" + timeSeconds);
} }
@ -3095,14 +3083,14 @@ public final class Player implements
public String getVideoTitle() { public String getVideoTitle() {
return currentMetadata == null return currentMetadata == null
? context.getString(R.string.unknown_content) ? context.getString(R.string.unknown_content)
: currentMetadata.getMetadata().getName(); : currentMetadata.getTitle();
} }
@NonNull @NonNull
public String getUploaderName() { public String getUploaderName() {
return currentMetadata == null return currentMetadata == null
? context.getString(R.string.unknown_content) ? context.getString(R.string.unknown_content)
: currentMetadata.getMetadata().getUploaderName(); : currentMetadata.getUploaderName();
} }
@Nullable @Nullable
@ -3122,14 +3110,14 @@ public final class Player implements
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region Play queue, segments and streams //region Play queue, segments and streams
private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag metadata) { private void maybeAutoQueueNextStream(@NonNull final StreamInfo info) {
if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1 if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1
|| getRepeatMode() != REPEAT_MODE_OFF || getRepeatMode() != REPEAT_MODE_OFF
|| !PlayerHelper.isAutoQueueEnabled(context)) { || !PlayerHelper.isAutoQueueEnabled(context)) {
return; return;
} }
// auto queue when starting playback on the last item when not repeating // auto queue when starting playback on the last item when not repeating
final PlayQueue autoQueue = PlayerHelper.autoQueueOf(metadata.getMetadata(), final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info,
playQueue.getStreams()); playQueue.getStreams());
if (autoQueue != null) { if (autoQueue != null) {
playQueue.append(autoQueue.getStreams()); playQueue.append(autoQueue.getStreams());
@ -3232,9 +3220,7 @@ public final class Player implements
itemTouchHelper.attachToRecyclerView(null); itemTouchHelper.attachToRecyclerView(null);
} }
if (currentMetadata != null) { getCurrentStreamInfo().ifPresent(segmentAdapter::setItems);
segmentAdapter.setItems(currentMetadata.getMetadata());
}
binding.shuffleButton.setVisibility(View.GONE); binding.shuffleButton.setVisibility(View.GONE);
binding.repeatButton.setVisibility(View.GONE); binding.repeatButton.setVisibility(View.GONE);
@ -3288,7 +3274,9 @@ public final class Player implements
private int getNearestStreamSegmentPosition(final long playbackPosition) { private int getNearestStreamSegmentPosition(final long playbackPosition) {
int nearestPosition = 0; int nearestPosition = 0;
final List<StreamSegment> segments = currentMetadata.getMetadata().getStreamSegments(); final List<StreamSegment> segments = getCurrentStreamInfo()
.map(StreamInfo::getStreamSegments)
.orElse(Collections.emptyList());
for (int i = 0; i < segments.size(); i++) { for (int i = 0; i < segments.size(); i++) {
if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) { if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) {
@ -3379,10 +3367,10 @@ public final class Player implements
} }
private void updateStreamRelatedViews() { private void updateStreamRelatedViews() {
if (currentMetadata == null) { if (!getCurrentStreamInfo().isPresent()) {
return; return;
} }
final StreamInfo info = currentMetadata.getMetadata(); final StreamInfo info = getCurrentStreamInfo().get();
binding.qualityTextView.setVisibility(View.GONE); binding.qualityTextView.setVisibility(View.GONE);
binding.playbackSpeed.setVisibility(View.GONE); binding.playbackSpeed.setVisibility(View.GONE);
@ -3410,12 +3398,15 @@ public final class Player implements
break; break;
case VIDEO_STREAM: case VIDEO_STREAM:
if (info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) { if (currentMetadata == null
|| !currentMetadata.getMaybeQuality().isPresent()
|| info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) {
break; break;
} }
availableStreams = currentMetadata.getSortedAvailableVideoStreams(); availableStreams = currentMetadata.getMaybeQuality().get().getSortedVideoStreams();
selectedStreamIndex = currentMetadata.getSelectedVideoStreamIndex(); selectedStreamIndex =
currentMetadata.getMaybeQuality().get().getSelectedVideoStreamIndex();
buildQualityMenu(); buildQualityMenu();
binding.qualityTextView.setVisibility(View.VISIBLE); binding.qualityTextView.setVisibility(View.VISIBLE);
@ -3535,8 +3526,8 @@ public final class Player implements
captionItem.setOnMenuItemClickListener(menuItem -> { captionItem.setOnMenuItemClickListener(menuItem -> {
final int textRendererIndex = getCaptionRendererIndex(); final int textRendererIndex = getCaptionRendererIndex();
if (textRendererIndex != RENDERER_UNAVAILABLE) { if (textRendererIndex != RENDERER_UNAVAILABLE) {
trackSelector.setPreferredTextLanguage(captionLanguage);
trackSelector.setParameters(trackSelector.buildUponParameters() trackSelector.setParameters(trackSelector.buildUponParameters()
.setPreferredTextLanguage(captionLanguage)
.setRendererDisabled(textRendererIndex, false)); .setRendererDisabled(textRendererIndex, false));
prefs.edit().putString(context.getString(R.string.caption_user_set_key), prefs.edit().putString(context.getString(R.string.caption_user_set_key),
captionLanguage).apply(); captionLanguage).apply();
@ -3551,8 +3542,8 @@ public final class Player implements
userPreferredLanguage.substring(0, userPreferredLanguage.indexOf('(')))))) { userPreferredLanguage.substring(0, userPreferredLanguage.indexOf('(')))))) {
final int textRendererIndex = getCaptionRendererIndex(); final int textRendererIndex = getCaptionRendererIndex();
if (textRendererIndex != RENDERER_UNAVAILABLE) { if (textRendererIndex != RENDERER_UNAVAILABLE) {
trackSelector.setPreferredTextLanguage(captionLanguage);
trackSelector.setParameters(trackSelector.buildUponParameters() trackSelector.setParameters(trackSelector.buildUponParameters()
.setPreferredTextLanguage(captionLanguage)
.setRendererDisabled(textRendererIndex, false)); .setRendererDisabled(textRendererIndex, false));
} }
searchForAutogenerated = false; searchForAutogenerated = false;
@ -3679,7 +3670,10 @@ public final class Player implements
} }
// Normalize mismatching language strings // Normalize mismatching language strings
final String preferredLanguage = trackSelector.getPreferredTextLanguage(); final List<String> preferredLanguages =
trackSelector.getParameters().preferredTextLanguages;
final String preferredLanguage =
preferredLanguages.isEmpty() ? null : preferredLanguages.get(0);
// Build UI // Build UI
buildCaptionMenu(availableLanguages); buildCaptionMenu(availableLanguages);
if (trackSelector.getParameters().getRendererDisabled(textRenderer) if (trackSelector.getParameters().getRendererDisabled(textRenderer)
@ -3886,10 +3880,9 @@ public final class Player implements
} }
private void onOpenInBrowserClicked() { private void onOpenInBrowserClicked() {
if (currentMetadata != null) { getCurrentStreamInfo().map(Info::getOriginalUrl).ifPresent(originalUrl -> {
ShareUtils.openUrlInBrowser(getParentActivity(), ShareUtils.openUrlInBrowser(Objects.requireNonNull(getParentActivity()), originalUrl);
currentMetadata.getMetadata().getOriginalUrl()); });
}
} }
//endregion //endregion
@ -4145,12 +4138,14 @@ public final class Player implements
} }
private void notifyMetadataUpdateToListeners() { private void notifyMetadataUpdateToListeners() {
if (fragmentListener != null && currentMetadata != null) { getCurrentStreamInfo().ifPresent(info -> {
fragmentListener.onMetadataUpdate(currentMetadata.getMetadata(), playQueue); if (fragmentListener != null) {
} fragmentListener.onMetadataUpdate(info, playQueue);
if (activityListener != null && currentMetadata != null) { }
activityListener.onMetadataUpdate(currentMetadata.getMetadata(), playQueue); if (activityListener != null) {
} activityListener.onMetadataUpdate(info, playQueue);
}
});
} }
private void notifyPlaybackUpdateToListeners() { private void notifyPlaybackUpdateToListeners() {
@ -4201,14 +4196,14 @@ public final class Player implements
// in livestreams) so we will be not able to execute the block below. // in livestreams) so we will be not able to execute the block below.
// Reload the play queue manager in this case, which is the behavior when we don't know the // Reload the play queue manager in this case, which is the behavior when we don't know the
// index of the video renderer or playQueueManagerReloadingNeeded returns true. // index of the video renderer or playQueueManagerReloadingNeeded returns true.
if (currentMetadata == null) { if (!getCurrentStreamInfo().isPresent()) {
reloadPlayQueueManager(); reloadPlayQueueManager();
setRecovery(); setRecovery();
return; return;
} }
final int videoRenderIndex = getVideoRendererIndex(); final int videoRenderIndex = getVideoRendererIndex();
final StreamInfo info = currentMetadata.getMetadata(); final StreamInfo info = getCurrentStreamInfo().get();
// In the case we don't know the source type, fallback to the one with video with audio or // In the case we don't know the source type, fallback to the one with video with audio or
// audio-only source. // audio-only source.
@ -4313,6 +4308,10 @@ public final class Player implements
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region Getters //region Getters
private Optional<StreamInfo> getCurrentStreamInfo() {
return Optional.ofNullable(currentMetadata).flatMap(MediaItemTag::getMaybeStreamInfo);
}
public int getCurrentState() { public int getCurrentState() {
return currentState; return currentState;
} }
@ -4322,8 +4321,7 @@ public final class Player implements
} }
public boolean isStopped() { public boolean isStopped() {
return exoPlayerIsNull() return exoPlayerIsNull() || simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_IDLE;
|| simpleExoPlayer.getPlaybackState() == SimpleExoPlayer.STATE_IDLE;
} }
public boolean isPlaying() { public boolean isPlaying() {
@ -4340,7 +4338,7 @@ public final class Player implements
private boolean isLive() { private boolean isLive() {
try { try {
return !exoPlayerIsNull() && simpleExoPlayer.isCurrentWindowDynamic(); return !exoPlayerIsNull() && simpleExoPlayer.isCurrentMediaItemDynamic();
} catch (final IndexOutOfBoundsException e) { } catch (final IndexOutOfBoundsException e) {
// Why would this even happen =(... but lets log it anyway, better safe than sorry // Why would this even happen =(... but lets log it anyway, better safe than sorry
if (DEBUG) { if (DEBUG) {
@ -4519,9 +4517,13 @@ public final class Player implements
surfaceHolderCallback = new SurfaceHolderCallback(context, simpleExoPlayer); surfaceHolderCallback = new SurfaceHolderCallback(context, simpleExoPlayer);
binding.surfaceView.getHolder().addCallback(surfaceHolderCallback); binding.surfaceView.getHolder().addCallback(surfaceHolderCallback);
final Surface surface = binding.surfaceView.getHolder().getSurface(); final Surface surface = binding.surfaceView.getHolder().getSurface();
// initially set the surface manually otherwise // ensure player is using an unreleased surface, which the surfaceView might not be
// onRenderedFirstFrame() will not be called // when starting playback on background or during player switching
simpleExoPlayer.setVideoSurface(surface); if (surface.isValid()) {
// initially set the surface manually otherwise
// onRenderedFirstFrame() will not be called
simpleExoPlayer.setVideoSurface(surface);
}
} else { } else {
simpleExoPlayer.setVideoSurfaceView(binding.surfaceView); simpleExoPlayer.setVideoSurfaceView(binding.surfaceView);
} }

View File

@ -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();
} }

View File

@ -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);

View File

@ -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);

View File

@ -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);
} }

View File

@ -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() {

View File

@ -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);
} }
} }

View File

@ -0,0 +1,99 @@
package org.schabi.newpipe.player.mediaitem;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import java.util.List;
import java.util.Optional;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public final class ExceptionTag implements MediaItemTag {
@NonNull
private final PlayQueueItem item;
@NonNull
private final List<Throwable> errors;
@Nullable
private final Object extras;
private ExceptionTag(@NonNull final PlayQueueItem item,
@NonNull final List<Throwable> errors,
@Nullable final Object extras) {
this.item = item;
this.errors = errors;
this.extras = extras;
}
public static ExceptionTag of(@NonNull final PlayQueueItem playQueueItem,
@NonNull final List<Throwable> errors) {
return new ExceptionTag(playQueueItem, errors, null);
}
@NonNull
@Override
public List<Throwable> getErrors() {
return errors;
}
@Override
public int getServiceId() {
return item.getServiceId();
}
@Override
public String getTitle() {
return item.getTitle();
}
@Override
public String getUploaderName() {
return item.getUploader();
}
@Override
public long getDurationSeconds() {
return item.getDuration();
}
@Override
public String getStreamUrl() {
return item.getUrl();
}
@Override
public String getThumbnailUrl() {
return item.getThumbnailUrl();
}
@Override
public String getUploaderUrl() {
return item.getUploaderUrl();
}
@Override
public StreamType getStreamType() {
return item.getStreamType();
}
@Override
public Optional<StreamInfo> getMaybeStreamInfo() {
return Optional.empty();
}
@Override
public Optional<Quality> getMaybeQuality() {
return Optional.empty();
}
@Override
public <T> Optional<T> getMaybeExtras(@NonNull final Class<T> type) {
return Optional.ofNullable(extras).map(type::cast);
}
@Override
public <T> MediaItemTag withExtras(@NonNull final T extra) {
return new ExceptionTag(item, errors, extra);
}
}

View File

@ -0,0 +1,113 @@
package org.schabi.newpipe.player.mediaitem;
import android.net.Uri;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.MediaMetadata;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public interface MediaItemTag {
List<Throwable> getErrors();
int getServiceId();
String getTitle();
String getUploaderName();
long getDurationSeconds();
String getStreamUrl();
String getThumbnailUrl();
String getUploaderUrl();
StreamType getStreamType();
Optional<StreamInfo> getMaybeStreamInfo();
Optional<Quality> getMaybeQuality();
<T> Optional<T> getMaybeExtras(@NonNull Class<T> type);
<T> MediaItemTag withExtras(@NonNull T extra);
@NonNull
static Optional<MediaItemTag> from(@Nullable final MediaItem mediaItem) {
if (mediaItem == null || mediaItem.localConfiguration == null
|| !(mediaItem.localConfiguration.tag instanceof MediaItemTag)) {
return Optional.empty();
}
return Optional.of((MediaItemTag) mediaItem.localConfiguration.tag);
}
@NonNull
default String makeMediaId() {
return UUID.randomUUID().toString() + "[" + getTitle() + "]";
}
@NonNull
default MediaItem asMediaItem() {
final MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setMediaUri(Uri.parse(getStreamUrl()))
.setArtworkUri(Uri.parse(getThumbnailUrl()))
.setArtist(getUploaderName())
.setDescription(getTitle())
.setDisplayTitle(getTitle())
.setTitle(getTitle())
.build();
return MediaItem.fromUri(getStreamUrl())
.buildUpon()
.setMediaId(makeMediaId())
.setMediaMetadata(mediaMetadata)
.setTag(this)
.build();
}
class Quality {
@NonNull
private final List<VideoStream> sortedVideoStreams;
private final int selectedVideoStreamIndex;
private Quality(@NonNull final List<VideoStream> sortedVideoStreams,
final int selectedVideoStreamIndex) {
this.sortedVideoStreams = sortedVideoStreams;
this.selectedVideoStreamIndex = selectedVideoStreamIndex;
}
static Quality of(@NonNull final List<VideoStream> sortedVideoStreams,
final int selectedVideoStreamIndex) {
return new Quality(sortedVideoStreams, selectedVideoStreamIndex);
}
@NonNull
public List<VideoStream> getSortedVideoStreams() {
return sortedVideoStreams;
}
public int getSelectedVideoStreamIndex() {
return selectedVideoStreamIndex;
}
@Nullable
public VideoStream getSelectedVideoStream() {
return selectedVideoStreamIndex < 0
|| selectedVideoStreamIndex >= sortedVideoStreams.size()
? null : sortedVideoStreams.get(selectedVideoStreamIndex);
}
}
}

View File

@ -0,0 +1,89 @@
package org.schabi.newpipe.player.mediaitem;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public final class PlaceholderTag implements MediaItemTag {
public static final PlaceholderTag EMPTY = new PlaceholderTag(null);
private static final String UNKNOWN_VALUE_INTERNAL = "Placeholder";
@Nullable
private final Object extras;
private PlaceholderTag(@Nullable final Object extras) {
this.extras = extras;
}
@NonNull
@Override
public List<Throwable> getErrors() {
return Collections.emptyList();
}
@Override
public int getServiceId() {
return -1;
}
@Override
public String getTitle() {
return UNKNOWN_VALUE_INTERNAL;
}
@Override
public String getUploaderName() {
return UNKNOWN_VALUE_INTERNAL;
}
@Override
public long getDurationSeconds() {
return -1;
}
@Override
public String getStreamUrl() {
return UNKNOWN_VALUE_INTERNAL;
}
@Override
public String getThumbnailUrl() {
return UNKNOWN_VALUE_INTERNAL;
}
@Override
public String getUploaderUrl() {
return UNKNOWN_VALUE_INTERNAL;
}
@Override
public StreamType getStreamType() {
return StreamType.NONE;
}
@Override
public Optional<StreamInfo> getMaybeStreamInfo() {
return Optional.empty();
}
@Override
public Optional<Quality> getMaybeQuality() {
return Optional.empty();
}
@Override
public <T> Optional<T> getMaybeExtras(@NonNull final Class<T> type) {
return Optional.ofNullable(extras).map(type::cast);
}
@Override
public <T> MediaItemTag withExtras(@NonNull final T extra) {
return new PlaceholderTag(extra);
}
}

View File

@ -0,0 +1,105 @@
package org.schabi.newpipe.player.mediaitem;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public final class StreamInfoTag implements MediaItemTag {
@NonNull
private final StreamInfo streamInfo;
@Nullable
private final MediaItemTag.Quality quality;
@Nullable
private final Object extras;
private StreamInfoTag(@NonNull final StreamInfo streamInfo,
@Nullable final MediaItemTag.Quality quality,
@Nullable final Object extras) {
this.streamInfo = streamInfo;
this.quality = quality;
this.extras = extras;
}
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo,
@NonNull final List<VideoStream> sortedVideoStreams,
final int selectedVideoStreamIndex) {
final Quality quality = Quality.of(sortedVideoStreams, selectedVideoStreamIndex);
return new StreamInfoTag(streamInfo, quality, null);
}
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo) {
return new StreamInfoTag(streamInfo, null, null);
}
@Override
public List<Throwable> getErrors() {
return Collections.emptyList();
}
@Override
public int getServiceId() {
return streamInfo.getServiceId();
}
@Override
public String getTitle() {
return streamInfo.getName();
}
@Override
public String getUploaderName() {
return streamInfo.getUploaderName();
}
@Override
public long getDurationSeconds() {
return streamInfo.getDuration();
}
@Override
public String getStreamUrl() {
return streamInfo.getUrl();
}
@Override
public String getThumbnailUrl() {
return streamInfo.getThumbnailUrl();
}
@Override
public String getUploaderUrl() {
return streamInfo.getUploaderUrl();
}
@Override
public StreamType getStreamType() {
return streamInfo.getStreamType();
}
@Override
public Optional<StreamInfo> getMaybeStreamInfo() {
return Optional.of(streamInfo);
}
@Override
public Optional<Quality> getMaybeQuality() {
return Optional.ofNullable(quality);
}
@Override
public <T> Optional<T> getMaybeExtras(@NonNull final Class<T> type) {
return Optional.ofNullable(extras).map(type::cast);
}
@Override
public StreamInfoTag withExtras(@NonNull final Object extra) {
return new StreamInfoTag(streamInfo, quality, extra);
}
}

View File

@ -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;
} }
} }

View File

@ -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;
}
}

View File

@ -2,52 +2,80 @@ package org.schabi.newpipe.player.mediasource;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.CompositeMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.SilenceMediaSource;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import org.schabi.newpipe.player.mediaitem.ExceptionTag;
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class FailedMediaSource extends CompositeMediaSource<Void> implements ManagedMediaSource {
private static final long SILENCE_DURATION_US = TimeUnit.SECONDS.toMicros(2);
public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSource {
private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode());
private final PlayQueueItem playQueueItem; private final PlayQueueItem playQueueItem;
private final FailedMediaSourceException error; private final Throwable error;
private final long retryTimestamp; private final long retryTimestamp;
private final MediaSource source;
private final MediaItem mediaItem;
/**
* Permanently fail the play queue item associated with this source, with no hope of retrying.
*
* The error will be propagated if the cause for load exception is unspecified.
* This means the error might be caused by reasons outside of extraction (e.g. no network).
* Otherwise, a silenced stream will play instead.
*
* @param playQueueItem play queue item
* @param error exception that was the reason to fail
* @param retryTimestamp epoch timestamp when this MediaSource can be refreshed
*/
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem, public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
@NonNull final FailedMediaSourceException error, @NonNull final Throwable error,
final long retryTimestamp) { final long retryTimestamp) {
this.playQueueItem = playQueueItem; this.playQueueItem = playQueueItem;
this.error = error; this.error = error;
this.retryTimestamp = retryTimestamp; this.retryTimestamp = retryTimestamp;
final MediaItemTag tag = ExceptionTag
.of(playQueueItem, Collections.singletonList(error))
.withExtras(this);
this.mediaItem = tag.asMediaItem();
this.source = new SilenceMediaSource.Factory()
.setDurationUs(SILENCE_DURATION_US)
.setTag(tag)
.createMediaSource();
} }
/** public static FailedMediaSource of(@NonNull final PlayQueueItem playQueueItem,
* Permanently fail the play queue item associated with this source, with no hope of retrying. @NonNull final FailedMediaSourceException error) {
* The error will always be propagated to ExoPlayer. return new FailedMediaSource(playQueueItem, error, Long.MAX_VALUE);
* }
* @param playQueueItem play queue item
* @param error exception that was the reason to fail public static FailedMediaSource of(@NonNull final PlayQueueItem playQueueItem,
*/ @NonNull final Throwable error,
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem, final long retryWaitMillis) {
@NonNull final FailedMediaSourceException error) { return new FailedMediaSource(playQueueItem, error,
this.playQueueItem = playQueueItem; System.currentTimeMillis() + retryWaitMillis);
this.error = error;
this.retryTimestamp = Long.MAX_VALUE;
} }
public PlayQueueItem getStream() { public PlayQueueItem getStream() {
return playQueueItem; return playQueueItem;
} }
public FailedMediaSourceException getError() { public Throwable getError() {
return error; return error;
} }
@ -60,31 +88,46 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo
*/ */
@Override @Override
public MediaItem getMediaItem() { public MediaItem getMediaItem() {
return MediaItem.fromUri(playQueueItem.getUrl()); return mediaItem;
} }
@Override @Override
public void maybeThrowSourceInfoRefreshError() throws IOException { protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) {
throw new IOException(error); super.prepareSourceInternal(mediaTransferListener);
Log.e(TAG, "Loading failed source: ", error);
if (error instanceof FailedMediaSourceException) {
prepareChildSource(null, source);
}
} }
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
if (!(error instanceof FailedMediaSourceException)) {
throw new IOException(error);
}
super.maybeThrowSourceInfoRefreshError();
}
@Override
protected void onChildSourceInfoRefreshed(final Void id,
final MediaSource mediaSource,
final Timeline timeline) {
refreshSourceInfo(timeline);
}
@Override @Override
public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator, public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator,
final long startPositionUs) { final long startPositionUs) {
return null; return source.createPeriod(id, allocator, startPositionUs);
} }
@Override @Override
public void releasePeriod(final MediaPeriod mediaPeriod) { } public void releasePeriod(final MediaPeriod mediaPeriod) {
source.releasePeriod(mediaPeriod);
@Override
protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) {
Log.e(TAG, "Loading failed source: ", error);
} }
@Override
protected void releaseSourceInternal() { }
@Override @Override
public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
final boolean isInterruptable) { final boolean isInterruptable) {

View File

@ -1,32 +1,34 @@
package org.schabi.newpipe.player.mediasource; package org.schabi.newpipe.player.mediasource;
import android.os.Handler; import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.CompositeMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.TransferListener;
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.MediaItem; public class LoadedMediaSource extends CompositeMediaSource<Void> implements ManagedMediaSource {
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.TransferListener;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import java.io.IOException;
public class LoadedMediaSource implements ManagedMediaSource {
private final MediaSource source; private final MediaSource source;
private final PlayQueueItem stream; private final PlayQueueItem stream;
private final MediaItem mediaItem;
private final long expireTimestamp; private final long expireTimestamp;
public LoadedMediaSource(@NonNull final MediaSource source, @NonNull final PlayQueueItem stream, public LoadedMediaSource(@NonNull final MediaSource source,
@NonNull final MediaItemTag tag,
@NonNull final PlayQueueItem stream,
final long expireTimestamp) { final long expireTimestamp) {
this.source = source; this.source = source;
this.stream = stream; this.stream = stream;
this.expireTimestamp = expireTimestamp; this.expireTimestamp = expireTimestamp;
this.mediaItem = tag.withExtras(this).asMediaItem();
} }
public PlayQueueItem getStream() { public PlayQueueItem getStream() {
@ -38,19 +40,16 @@ public class LoadedMediaSource implements ManagedMediaSource {
} }
@Override @Override
public void prepareSource(final MediaSourceCaller mediaSourceCaller, protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) {
@Nullable final TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener);
source.prepareSource(mediaSourceCaller, mediaTransferListener); prepareChildSource(null, source);
} }
@Override @Override
public void maybeThrowSourceInfoRefreshError() throws IOException { protected void onChildSourceInfoRefreshed(final Void id,
source.maybeThrowSourceInfoRefreshError(); final MediaSource mediaSource,
} final Timeline timeline) {
refreshSourceInfo(timeline);
@Override
public void enable(final MediaSourceCaller caller) {
source.enable(caller);
} }
@Override @Override
@ -64,57 +63,10 @@ public class LoadedMediaSource implements ManagedMediaSource {
source.releasePeriod(mediaPeriod); source.releasePeriod(mediaPeriod);
} }
@Override @NonNull
public void disable(final MediaSourceCaller caller) {
source.disable(caller);
}
@Override
public void releaseSource(final MediaSourceCaller mediaSourceCaller) {
source.releaseSource(mediaSourceCaller);
}
@Override
public void addEventListener(final Handler handler,
final MediaSourceEventListener eventListener) {
source.addEventListener(handler, eventListener);
}
@Override
public void removeEventListener(final MediaSourceEventListener eventListener) {
source.removeEventListener(eventListener);
}
/**
* Adds a {@link DrmSessionEventListener} to the list of listeners which are notified of DRM
* events for this media source.
*
* @param handler A handler on the which listener events will be posted.
* @param eventListener The listener to be added.
*/
@Override
public void addDrmEventListener(final Handler handler,
final DrmSessionEventListener eventListener) {
source.addDrmEventListener(handler, eventListener);
}
/**
* Removes a {@link DrmSessionEventListener} from the list of listeners which are notified of
* DRM events for this media source.
*
* @param eventListener The listener to be removed.
*/
@Override
public void removeDrmEventListener(final DrmSessionEventListener eventListener) {
source.removeDrmEventListener(eventListener);
}
/**
* Returns the {@link MediaItem} whose media is provided by the source.
*/
@Override @Override
public MediaItem getMediaItem() { public MediaItem getMediaItem() {
return source.getMediaItem(); return mediaItem;
} }
@Override @Override

View File

@ -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;
}
} }

View File

@ -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);
} }
/** /**

View File

@ -1,28 +1,37 @@
package org.schabi.newpipe.player.mediasource; package org.schabi.newpipe.player.mediasource;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.CompositeMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.TransferListener;
import org.schabi.newpipe.player.mediaitem.PlaceholderTag;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
public class PlaceholderMediaSource extends BaseMediaSource implements ManagedMediaSource { import androidx.annotation.NonNull;
final class PlaceholderMediaSource
extends CompositeMediaSource<Void> implements ManagedMediaSource {
public static final PlaceholderMediaSource COPY = new PlaceholderMediaSource();
private static final MediaItem MEDIA_ITEM = PlaceholderTag.EMPTY.withExtras(COPY).asMediaItem();
private PlaceholderMediaSource() { }
/** /**
* Returns the {@link MediaItem} whose media is provided by the source. * Returns the {@link MediaItem} whose media is provided by the source.
*/ */
@Override @Override
public MediaItem getMediaItem() { public MediaItem getMediaItem() {
return null; return MEDIA_ITEM;
} }
// Do nothing, so this will stall the playback
@Override @Override
public void maybeThrowSourceInfoRefreshError() { } protected void onChildSourceInfoRefreshed(final Void id,
final MediaSource mediaSource,
final Timeline timeline) {
/* Do nothing, no timeline updates will stall playback */
}
@Override @Override
public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator, public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator,
@ -33,12 +42,6 @@ public class PlaceholderMediaSource extends BaseMediaSource implements ManagedMe
@Override @Override
public void releasePeriod(final MediaPeriod mediaPeriod) { } public void releasePeriod(final MediaPeriod mediaPeriod) { }
@Override
protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) { }
@Override
protected void releaseSourceInternal() { }
@Override @Override
public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
final boolean isInterruptable) { final boolean isInterruptable) {

View File

@ -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));
}
}

View File

@ -11,11 +11,12 @@ import com.google.android.exoplayer2.source.MediaSource;
import org.reactivestreams.Subscriber; import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.mediasource.FailedMediaSource; import org.schabi.newpipe.player.mediasource.FailedMediaSource;
import org.schabi.newpipe.player.mediasource.LoadedMediaSource; import org.schabi.newpipe.player.mediasource.LoadedMediaSource;
import org.schabi.newpipe.player.mediasource.ManagedMediaSource; import org.schabi.newpipe.player.mediasource.ManagedMediaSource;
import org.schabi.newpipe.player.mediasource.ManagedMediaSourcePlaylist; import org.schabi.newpipe.player.mediasource.ManagedMediaSourcePlaylist;
import org.schabi.newpipe.player.mediasource.PlaceholderMediaSource;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.events.MoveEvent; import org.schabi.newpipe.player.playqueue.events.MoveEvent;
@ -195,7 +196,7 @@ public class MediaSourceManager {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private Subscriber<PlayQueueEvent> getReactor() { private Subscriber<PlayQueueEvent> getReactor() {
return new Subscriber<PlayQueueEvent>() { return new Subscriber<>() {
@Override @Override
public void onSubscribe(@NonNull final Subscription d) { public void onSubscribe(@NonNull final Subscription d) {
playQueueReactor.cancel(); playQueueReactor.cancel();
@ -209,10 +210,12 @@ public class MediaSourceManager {
} }
@Override @Override
public void onError(@NonNull final Throwable e) { } public void onError(@NonNull final Throwable e) {
}
@Override @Override
public void onComplete() { } public void onComplete() {
}
}; };
} }
@ -292,11 +295,11 @@ public class MediaSourceManager {
} }
final ManagedMediaSource mediaSource = playlist.get(playQueue.getIndex()); final ManagedMediaSource mediaSource = playlist.get(playQueue.getIndex());
if (mediaSource == null) { final PlayQueueItem playQueueItem = playQueue.getItem();
if (mediaSource == null || playQueueItem == null) {
return false; return false;
} }
final PlayQueueItem playQueueItem = playQueue.getItem();
return mediaSource.isStreamEqual(playQueueItem); return mediaSource.isStreamEqual(playQueueItem);
} }
@ -315,7 +318,7 @@ public class MediaSourceManager {
isBlocked.set(true); isBlocked.set(true);
} }
private void maybeUnblock() { private boolean maybeUnblock() {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "maybeUnblock() called."); Log.d(TAG, "maybeUnblock() called.");
} }
@ -323,14 +326,17 @@ public class MediaSourceManager {
if (isBlocked.get()) { if (isBlocked.get()) {
isBlocked.set(false); isBlocked.set(false);
playbackListener.onPlaybackUnblock(playlist.getParentMediaSource()); playbackListener.onPlaybackUnblock(playlist.getParentMediaSource());
return true;
} }
return false;
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Metadata Synchronization // Metadata Synchronization
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void maybeSync() { private void maybeSync(final boolean wasBlocked) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "maybeSync() called."); Log.d(TAG, "maybeSync() called.");
} }
@ -340,13 +346,13 @@ public class MediaSourceManager {
return; return;
} }
playbackListener.onPlaybackSynchronize(currentItem); playbackListener.onPlaybackSynchronize(currentItem, wasBlocked);
} }
private synchronized void maybeSynchronizePlayer() { private synchronized void maybeSynchronizePlayer() {
if (isPlayQueueReady() && isPlaybackReady()) { if (isPlayQueueReady() && isPlaybackReady()) {
maybeUnblock(); final boolean isBlockReleased = maybeUnblock();
maybeSync(); maybeSync(isBlockReleased);
} }
} }
@ -417,20 +423,26 @@ public class MediaSourceManager {
private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) { private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) {
return stream.getStream().map(streamInfo -> { return stream.getStream().map(streamInfo -> {
final MediaSource source = playbackListener.sourceOf(stream, streamInfo); final MediaSource source = playbackListener.sourceOf(stream, streamInfo);
if (source == null) { if (source == null || !MediaItemTag.from(source.getMediaItem()).isPresent()) {
final String message = "Unable to resolve source from stream info. " final String message = "Unable to resolve source from stream info. "
+ "URL: " + stream.getUrl() + ", " + "URL: " + stream.getUrl() + ", "
+ "audio count: " + streamInfo.getAudioStreams().size() + ", " + "audio count: " + streamInfo.getAudioStreams().size() + ", "
+ "video count: " + streamInfo.getVideoOnlyStreams().size() + ", " + "video count: " + streamInfo.getVideoOnlyStreams().size() + ", "
+ streamInfo.getVideoStreams().size(); + streamInfo.getVideoStreams().size();
return new FailedMediaSource(stream, new MediaSourceResolutionException(message)); return (ManagedMediaSource)
FailedMediaSource.of(stream, new MediaSourceResolutionException(message));
} }
final MediaItemTag tag = MediaItemTag.from(source.getMediaItem()).get();
final long expiration = System.currentTimeMillis() final long expiration = System.currentTimeMillis()
+ ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId()); + ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId());
return new LoadedMediaSource(source, stream, expiration); return new LoadedMediaSource(source, tag, stream, expiration);
}).onErrorReturn(throwable -> new FailedMediaSource(stream, }).onErrorReturn(throwable -> {
new StreamInfoLoadException(throwable))); if (throwable instanceof ExtractionException) {
return FailedMediaSource.of(stream, new StreamInfoLoadException(throwable));
}
return FailedMediaSource.of(stream, throwable, /*immediatelyRetryable=*/0L);
});
} }
private void onMediaSourceReceived(@NonNull final PlayQueueItem item, private void onMediaSourceReceived(@NonNull final PlayQueueItem item,
@ -478,23 +490,23 @@ public class MediaSourceManager {
/** /**
* Checks if the current playing index contains an expired {@link ManagedMediaSource}. * Checks if the current playing index contains an expired {@link ManagedMediaSource}.
* If so, the expired source is replaced by a {@link PlaceholderMediaSource} and * If so, the expired source is replaced by a dummy {@link ManagedMediaSource} and
* {@link #loadImmediate()} is called to reload the current item. * {@link #loadImmediate()} is called to reload the current item.
* <br><br> * <br><br>
* If not, then the media source at the current index is ready for playback, and * If not, then the media source at the current index is ready for playback, and
* {@link #maybeSynchronizePlayer()} is called. * {@link #maybeSynchronizePlayer()} is called.
* <br><br> * <br><br>
* Under both cases, {@link #maybeSync()} will be called to ensure the listener * Under both cases, {@link #maybeSync(boolean)} will be called to ensure the listener
* is up-to-date. * is up-to-date.
*/ */
private void maybeRenewCurrentIndex() { private void maybeRenewCurrentIndex() {
final int currentIndex = playQueue.getIndex(); final int currentIndex = playQueue.getIndex();
final PlayQueueItem currentItem = playQueue.getItem();
final ManagedMediaSource currentSource = playlist.get(currentIndex); final ManagedMediaSource currentSource = playlist.get(currentIndex);
if (currentSource == null) { if (currentItem == null || currentSource == null) {
return; return;
} }
final PlayQueueItem currentItem = playQueue.getItem();
if (!currentSource.shouldBeReplacedWith(currentItem, true)) { if (!currentSource.shouldBeReplacedWith(currentItem, true)) {
maybeSynchronizePlayer(); maybeSynchronizePlayer();
return; return;

View File

@ -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

View File

@ -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;
} }

View File

@ -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);
} }

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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);
} }
} }