From b3b2748bb7c90d7a9362e969c77b33eb1a792347 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Mon, 26 Feb 2018 19:57:59 -0800 Subject: [PATCH] -Improved player queue stability by using more aggressive synchronization policy. -Added sync buttons on live streams to allow seeking to live edge. -Added custom cache key for extractor sources to allow more persistent reuse. -Refactored player data source factories into own class and separating live and non-live data sources. --- .../newpipe/player/BackgroundPlayer.java | 4 +- .../org/schabi/newpipe/player/BasePlayer.java | 108 ++++++++++-------- .../newpipe/player/ServicePlayerActivity.java | 19 +++ .../schabi/newpipe/player/VideoPlayer.java | 39 +++++-- .../newpipe/player/helper/CacheFactory.java | 2 +- .../player/helper/PlayerDataSource.java | 76 ++++++++++++ .../newpipe/player/helper/PlayerHelper.java | 14 ++- .../player/mediasource/FailedMediaSource.java | 14 ++- .../player/mediasource/LoadedMediaSource.java | 21 +++- .../mediasource/ManagedMediaSource.java | 6 +- .../mediasource/PlaceholderMediaSource.java | 7 +- .../player/playback/MediaSourceManager.java | 52 +++++++-- .../activity_player_queue_control.xml | 10 ++ .../main/res/layout/activity_main_player.xml | 11 ++ .../layout/activity_player_queue_control.xml | 10 ++ app/src/main/res/layout/player_popup.xml | 11 ++ app/src/main/res/values/strings.xml | 2 + 17 files changed, 320 insertions(+), 86 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index 2fc3252a7..7e5e612d6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -46,6 +46,7 @@ import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.helper.LockManager; +import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; @@ -398,7 +399,8 @@ public final class BackgroundPlayer extends Service { if (index < 0 || index >= info.audio_streams.size()) return null; final AudioStream audio = info.audio_streams.get(index); - return buildMediaSource(audio.getUrl(), MediaFormat.getSuffixById(audio.getFormatId())); + return buildMediaSource(audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio), + MediaFormat.getSuffixById(audio.getFormatId())); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 3e136dbde..1854e9a01 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -43,20 +43,11 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.util.Util; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; @@ -66,8 +57,8 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; -import org.schabi.newpipe.player.helper.CacheFactory; import org.schabi.newpipe.player.helper.LoadController; +import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.playback.CustomTrackSelector; import org.schabi.newpipe.player.playback.MediaSourceManager; import org.schabi.newpipe.player.playback.PlaybackListener; @@ -149,14 +140,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen protected boolean isPrepared = false; protected CustomTrackSelector trackSelector; - protected DataSource.Factory cacheDataSourceFactory; - protected DataSource.Factory cachelessDataSourceFactory; - protected SsMediaSource.Factory ssMediaSourceFactory; - protected HlsMediaSource.Factory hlsMediaSourceFactory; - protected DashMediaSource.Factory dashMediaSourceFactory; - protected ExtractorMediaSource.Factory extractorMediaSourceFactory; - protected SingleSampleMediaSource.Factory sampleMediaSourceFactory; + protected PlayerDataSource dataSource; protected Disposable progressUpdateReactor; protected CompositeDisposable databaseUpdateReactor; @@ -193,20 +178,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen final String userAgent = Downloader.USER_AGENT; final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter); + final AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter); - trackSelector = new CustomTrackSelector(trackSelectionFactory); - cacheDataSourceFactory = new CacheFactory(context, userAgent, bandwidthMeter); - cachelessDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter); - - ssMediaSourceFactory = new SsMediaSource.Factory( - new DefaultSsChunkSource.Factory(cachelessDataSourceFactory), cachelessDataSourceFactory); - hlsMediaSourceFactory = new HlsMediaSource.Factory(cachelessDataSourceFactory); - dashMediaSourceFactory = new DashMediaSource.Factory( - new DefaultDashChunkSource.Factory(cachelessDataSourceFactory), cachelessDataSourceFactory); - extractorMediaSourceFactory = new ExtractorMediaSource.Factory(cacheDataSourceFactory); - sampleMediaSourceFactory = new SingleSampleMediaSource.Factory(cacheDataSourceFactory); final LoadControl loadControl = new LoadController(context); final RenderersFactory renderFactory = new DefaultRenderersFactory(context); @@ -319,26 +295,56 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen recordManager = null; } - public MediaSource buildMediaSource(String url, String overrideExtension) { + /*////////////////////////////////////////////////////////////////////////// + // MediaSource Building + //////////////////////////////////////////////////////////////////////////*/ + + public MediaSource buildLiveMediaSource(@NonNull final String sourceUrl, + @C.ContentType final int type) { if (DEBUG) { - Log.d(TAG, "buildMediaSource() called with: url = [" + url + - "], overrideExtension = [" + overrideExtension + "]"); + Log.d(TAG, "buildLiveMediaSource() called with: url = [" + sourceUrl + + "], content type = [" + type + "]"); } - Uri uri = Uri.parse(url); - int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : - Util.inferContentType("." + overrideExtension); + if (dataSource == null) return null; + + final Uri uri = Uri.parse(sourceUrl); switch (type) { case C.TYPE_SS: - return ssMediaSourceFactory.createMediaSource(uri); + return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri); case C.TYPE_DASH: - return dashMediaSourceFactory.createMediaSource(uri); + return dataSource.getLiveDashMediaSourceFactory().createMediaSource(uri); case C.TYPE_HLS: - return hlsMediaSourceFactory.createMediaSource(uri); - case C.TYPE_OTHER: - return extractorMediaSourceFactory.createMediaSource(uri); - default: { + return dataSource.getLiveHlsMediaSourceFactory().createMediaSource(uri); + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } + + public MediaSource buildMediaSource(@NonNull final String sourceUrl, + @NonNull final String cacheKey, + @NonNull final String overrideExtension) { + if (DEBUG) { + Log.d(TAG, "buildMediaSource() called with: url = [" + sourceUrl + + "], cacheKey = [" + cacheKey + "]" + + "], overrideExtension = [" + overrideExtension + "]"); + } + if (dataSource == null) return null; + + final Uri uri = Uri.parse(sourceUrl); + @C.ContentType final int type = TextUtils.isEmpty(overrideExtension) ? + Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); + + switch (type) { + case C.TYPE_SS: + return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri); + case C.TYPE_DASH: + return dataSource.getDashMediaSourceFactory().createMediaSource(uri); + case C.TYPE_HLS: + return dataSource.getHlsMediaSourceFactory().createMediaSource(uri); + case C.TYPE_OTHER: + return dataSource.getExtractorMediaSourceFactory(cacheKey).createMediaSource(uri); + default: throw new IllegalStateException("Unsupported type: " + type); - } } } @@ -478,7 +484,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen // ExoPlayer Listener //////////////////////////////////////////////////////////////////////////*/ - private void recover() { + private void maybeRecover() { final int currentSourceIndex = playQueue.getIndex(); final PlayQueueItem currentSourceItem = playQueue.getItem(); @@ -554,7 +560,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } break; case Player.STATE_READY: //3 - recover(); + maybeRecover(); if (!isPrepared) { isPrepared = true; onPrepared(playWhenReady); @@ -566,7 +572,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen case Player.STATE_ENDED: // 4 // Ensure the current window has actually ended // since single windows that are still loading may produce an ended state - if (isCurrentWindowValid() && simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration()) { + if (isCurrentWindowValid() && + simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration()) { changeState(STATE_COMPLETED); isPrepared = false; } @@ -730,9 +737,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen @Override public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { if (!info.getHlsUrl().isEmpty()) { - return buildMediaSource(info.getHlsUrl(), "m3u8"); + return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS); } else if (!info.getDashMpdUrl().isEmpty()) { - return buildMediaSource(info.getDashMpdUrl(), "mpd"); + return buildLiveMediaSource(info.getDashMpdUrl(), C.TYPE_DASH); } return null; @@ -852,8 +859,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen public void seekBy(int milliSeconds) { if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]"); - if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) + if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || + ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) { return; + } + int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds); if (progress < 0) progress = 0; simpleExoPlayer.seekTo(progress); @@ -864,6 +874,10 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen && simpleExoPlayer.getCurrentPosition() >= 0; } + public void seekToDefault() { + if (simpleExoPlayer != null) simpleExoPlayer.seekToDefaultPosition(); + } + /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 1378d9a80..d9c04b796 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -76,6 +76,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private SeekBar progressSeekBar; private TextView progressCurrentTime; private TextView progressEndTime; + private TextView progressLiveSync; private TextView seekDisplay; private ImageButton repeatButton; @@ -294,9 +295,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity progressCurrentTime = rootView.findViewById(R.id.current_time); progressSeekBar = rootView.findViewById(R.id.seek_bar); progressEndTime = rootView.findViewById(R.id.end_time); + progressLiveSync = rootView.findViewById(R.id.live_sync); seekDisplay = rootView.findViewById(R.id.seek_display); progressSeekBar.setOnSeekBarChangeListener(this); + progressLiveSync.setOnClickListener(this); } private void buildControls() { @@ -513,6 +516,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } else if (view.getId() == metadata.getId()) { scrollToSelected(); + } else if (view.getId() == progressLiveSync.getId()) { + player.seekToDefault(); + } } @@ -574,6 +580,19 @@ public abstract class ServicePlayerActivity extends AppCompatActivity if (info != null) { metadataTitle.setText(info.getName()); metadataArtist.setText(info.uploader_name); + + progressEndTime.setVisibility(View.GONE); + progressLiveSync.setVisibility(View.GONE); + switch (info.getStreamType()) { + case LIVE_STREAM: + case AUDIO_LIVE_STREAM: + progressLiveSync.setVisibility(View.VISIBLE); + break; + default: + progressEndTime.setVisibility(View.VISIBLE); + break; + } + scrollToSelected(); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 3e03bc207..eca6415f6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -50,7 +50,6 @@ import android.widget.TextView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.TrackGroup; @@ -58,6 +57,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.SubtitleView; +import com.google.android.exoplayer2.video.VideoListener; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; @@ -87,7 +87,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView; */ @SuppressWarnings({"WeakerAccess", "unused"}) public abstract class VideoPlayer extends BasePlayer - implements SimpleExoPlayer.VideoListener, + implements VideoListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener, Player.EventListener, @@ -131,6 +131,7 @@ public abstract class VideoPlayer extends BasePlayer private SeekBar playbackSeekBar; private TextView playbackCurrentTime; private TextView playbackEndTime; + private TextView playbackLiveSync; private TextView playbackSpeedTextView; private View topControlsRoot; @@ -180,6 +181,7 @@ public abstract class VideoPlayer extends BasePlayer this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar); this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime); this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime); + this.playbackLiveSync = rootView.findViewById(R.id.playbackLiveSync); this.playbackSpeedTextView = rootView.findViewById(R.id.playbackSpeed); this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls); this.topControlsRoot = rootView.findViewById(R.id.topControls); @@ -221,6 +223,7 @@ public abstract class VideoPlayer extends BasePlayer qualityTextView.setOnClickListener(this); captionTextView.setOnClickListener(this); resizeView.setOnClickListener(this); + playbackLiveSync.setOnClickListener(this); } @Override @@ -261,7 +264,8 @@ public abstract class VideoPlayer extends BasePlayer qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId); for (int i = 0; i < availableStreams.size(); i++) { VideoStream videoStream = availableStreams.get(i); - qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution); + qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, + MediaFormat.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution); } if (getSelectedVideoStream() != null) { qualityTextView.setText(getSelectedVideoStream().resolution); @@ -327,9 +331,22 @@ public abstract class VideoPlayer extends BasePlayer qualityTextView.setVisibility(View.GONE); playbackSpeedTextView.setVisibility(View.GONE); + playbackEndTime.setVisibility(View.GONE); + playbackLiveSync.setVisibility(View.GONE); + final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType(); switch (streamType) { + case AUDIO_STREAM: + surfaceView.setVisibility(View.GONE); + break; + + case AUDIO_LIVE_STREAM: + surfaceView.setVisibility(View.GONE); + case LIVE_STREAM: + playbackLiveSync.setVisibility(View.VISIBLE); + break; + case VIDEO_STREAM: if (info.video_streams.size() + info.video_only_streams.size() == 0) break; @@ -344,14 +361,10 @@ public abstract class VideoPlayer extends BasePlayer buildQualityMenu(); qualityTextView.setVisibility(View.VISIBLE); - surfaceView.setVisibility(View.VISIBLE); - break; - case AUDIO_STREAM: - case AUDIO_LIVE_STREAM: - surfaceView.setVisibility(View.GONE); - break; + surfaceView.setVisibility(View.VISIBLE); default: + playbackEndTime.setVisibility(View.VISIBLE); break; } @@ -381,6 +394,7 @@ public abstract class VideoPlayer extends BasePlayer final VideoStream video = index >= 0 && index < videos.size() ? videos.get(index) : null; if (video != null) { final MediaSource streamSource = buildMediaSource(video.getUrl(), + PlayerHelper.cacheKeyOf(info, video), MediaFormat.getSuffixById(video.getFormatId())); mediaSources.add(streamSource); } @@ -393,6 +407,7 @@ public abstract class VideoPlayer extends BasePlayer // Merge with audio stream in case if video does not contain audio if (audio != null && ((video != null && video.isVideoOnly) || video == null)) { final MediaSource audioSource = buildMediaSource(audio.getUrl(), + PlayerHelper.cacheKeyOf(info, audio), MediaFormat.getSuffixById(audio.getFormatId())); mediaSources.add(audioSource); } @@ -408,8 +423,8 @@ public abstract class VideoPlayer extends BasePlayer final Format textFormat = Format.createTextSampleFormat(null, mimeType, SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); - final MediaSource textSource = sampleMediaSourceFactory.createMediaSource( - Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); + final MediaSource textSource = dataSource.getSampleMediaSourceFactory() + .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); mediaSources.add(textSource); } @@ -635,6 +650,8 @@ public abstract class VideoPlayer extends BasePlayer onResizeClicked(); } else if (v.getId() == captionTextView.getId()) { onCaptionClicked(); + } else if (v.getId() == playbackLiveSync.getId()) { + seekToDefault(); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java index 900f13ae9..ec7813056 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java @@ -21,7 +21,7 @@ import org.schabi.newpipe.Downloader; import java.io.File; -public class CacheFactory implements DataSource.Factory { +/* package-private */ class CacheFactory implements DataSource.Factory { private static final String TAG = "CacheFactory"; private static final String CACHE_FOLDER_NAME = "exoplayer"; private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR; diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java new file mode 100644 index 000000000..40548aa6c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java @@ -0,0 +1,76 @@ +package org.schabi.newpipe.player.helper; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.SingleSampleMediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.TransferListener; + +public class PlayerDataSource { + private static final int MANIFEST_MINIMUM_RETRY = 5; + private static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000; + + private final DataSource.Factory cacheDataSourceFactory; + private final DataSource.Factory cachelessDataSourceFactory; + + public PlayerDataSource(@NonNull final Context context, + @NonNull final String userAgent, + @NonNull final TransferListener transferListener) { + cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener); + cachelessDataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener); + } + + public SsMediaSource.Factory getLiveSsMediaSourceFactory() { + return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory( + cachelessDataSourceFactory), cachelessDataSourceFactory) + .setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY) + .setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS); + } + + public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() { + return new HlsMediaSource.Factory(cachelessDataSourceFactory) + .setAllowChunklessPreparation(true) + .setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY); + } + + public DashMediaSource.Factory getLiveDashMediaSourceFactory() { + return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( + cachelessDataSourceFactory), cachelessDataSourceFactory) + .setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY) + .setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS); + } + + public SsMediaSource.Factory getSsMediaSourceFactory() { + return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory( + cacheDataSourceFactory), cacheDataSourceFactory); + } + + public HlsMediaSource.Factory getHlsMediaSourceFactory() { + return new HlsMediaSource.Factory(cacheDataSourceFactory); + } + + public DashMediaSource.Factory getDashMediaSourceFactory() { + return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( + cacheDataSourceFactory), cacheDataSourceFactory); + } + + public ExtractorMediaSource.Factory getExtractorMediaSourceFactory() { + return new ExtractorMediaSource.Factory(cacheDataSourceFactory); + } + + public ExtractorMediaSource.Factory getExtractorMediaSourceFactory(@NonNull final String key) { + return new ExtractorMediaSource.Factory(cacheDataSourceFactory).setCustomCacheKey(key); + } + + public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() { + return new SingleSampleMediaSource.Factory(cacheDataSourceFactory); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index ea3f73a17..6e2ff0ac9 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -10,7 +10,10 @@ import com.google.android.exoplayer2.util.MimeTypes; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.Subtitles; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.SubtitlesFormat; +import org.schabi.newpipe.extractor.stream.VideoStream; import java.text.DecimalFormat; import java.text.NumberFormat; @@ -69,8 +72,7 @@ public class PlayerHelper { public static String captionLanguageOf(@NonNull final Context context, @NonNull final Subtitles subtitles) { final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale()); - return displayName + (subtitles.isAutoGenerated() ? - " (" + context.getString(R.string.caption_auto_generated)+ ")" : ""); + return displayName + (subtitles.isAutoGenerated() ? " (" + context.getString(R.string.caption_auto_generated)+ ")" : ""); } public static String resizeTypeOf(@NonNull final Context context, @@ -83,6 +85,14 @@ public class PlayerHelper { } } + public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull VideoStream video) { + return info.getUrl() + video.getResolution() + video.getFormat().getName(); + } + + public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull AudioStream audio) { + return info.getUrl() + audio.getAverageBitrate() + audio.getFormat().getName(); + } + public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) { return isResumeAfterAudioFocusGain(context, false); } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index a73c9975e..d88385d2d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.player.mediasource; import android.support.annotation.NonNull; +import android.util.Log; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.source.MediaPeriod; @@ -11,6 +12,7 @@ import org.schabi.newpipe.playlist.PlayQueueItem; import java.io.IOException; public class FailedMediaSource implements ManagedMediaSource { + private final String TAG = "ManagedMediaSource@" + Integer.toHexString(hashCode()); private final PlayQueueItem playQueueItem; private final Throwable error; @@ -36,7 +38,7 @@ public class FailedMediaSource implements ManagedMediaSource { this.retryTimestamp = Long.MAX_VALUE; } - public PlayQueueItem getPlayQueueItem() { + public PlayQueueItem getStream() { return playQueueItem; } @@ -44,12 +46,14 @@ public class FailedMediaSource implements ManagedMediaSource { return error; } - public boolean canRetry() { + private boolean canRetry() { return System.currentTimeMillis() >= retryTimestamp; } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {} + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Log.e(TAG, "Loading failed source: ", error); + } @Override public void maybeThrowSourceInfoRefreshError() throws IOException { @@ -68,7 +72,7 @@ public class FailedMediaSource implements ManagedMediaSource { public void releaseSource() {} @Override - public boolean canReplace() { - return canRetry(); + public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { + return newIdentity != playQueueItem || canRetry(); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java index ddc78bd77..f523667f9 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java @@ -7,7 +7,6 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.upstream.Allocator; -import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.playlist.PlayQueueItem; import java.io.IOException; @@ -15,15 +14,25 @@ import java.io.IOException; public class LoadedMediaSource implements ManagedMediaSource { private final MediaSource source; - + private final PlayQueueItem stream; private final long expireTimestamp; - public LoadedMediaSource(@NonNull final MediaSource source, final long expireTimestamp) { + public LoadedMediaSource(@NonNull final MediaSource source, + @NonNull final PlayQueueItem stream, + final long expireTimestamp) { this.source = source; - + this.stream = stream; this.expireTimestamp = expireTimestamp; } + public PlayQueueItem getStream() { + return stream; + } + + private boolean isExpired() { + return System.currentTimeMillis() >= expireTimestamp; + } + @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { source.prepareSource(player, isTopLevelSource, listener); @@ -50,7 +59,7 @@ public class LoadedMediaSource implements ManagedMediaSource { } @Override - public boolean canReplace() { - return System.currentTimeMillis() >= expireTimestamp; + public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { + return newIdentity != stream || isExpired(); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java index 5ac07c9f0..3bb7ca429 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java @@ -1,7 +1,11 @@ package org.schabi.newpipe.player.mediasource; +import android.support.annotation.NonNull; + import com.google.android.exoplayer2.source.MediaSource; +import org.schabi.newpipe.playlist.PlayQueueItem; + public interface ManagedMediaSource extends MediaSource { - boolean canReplace(); + boolean canReplace(@NonNull final PlayQueueItem newIdentity); } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java index 0a389a9d9..0d3436a01 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java @@ -1,10 +1,13 @@ package org.schabi.newpipe.player.mediasource; +import android.support.annotation.NonNull; + import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.upstream.Allocator; +import org.schabi.newpipe.playlist.PlayQueueItem; + import java.io.IOException; public class PlaceholderMediaSource implements ManagedMediaSource { @@ -16,7 +19,7 @@ public class PlaceholderMediaSource implements ManagedMediaSource { @Override public void releaseSource() {} @Override - public boolean canReplace() { + public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { return true; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 3b068f730..2a2a4ccb4 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.player.playback; import android.support.annotation.Nullable; +import android.util.Log; import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; @@ -34,7 +35,11 @@ import io.reactivex.disposables.SerialDisposable; import io.reactivex.functions.Consumer; import io.reactivex.subjects.PublishSubject; +import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; + public class MediaSourceManager { + private final String TAG = "MediaSourceManager"; + // One-side rolling window size for default loading // Effectively loads windowSize * 2 + 1 streams per call to load, must be greater than 0 private final int windowSize; @@ -233,10 +238,16 @@ public class MediaSourceManager { return playQueue.isComplete() || isWindowLoaded; } - // Checks if the current playback media source is a placeholder, if so, then it is not ready. private boolean isPlaybackReady() { - return sources != null && playQueue != null && sources.getSize() > playQueue.getIndex() && - !(sources.getMediaSource(playQueue.getIndex()) instanceof PlaceholderMediaSource); + if (sources == null || playQueue == null || sources.getSize() != playQueue.size()) { + return false; + } + + final MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex()); + if (!(mediaSource instanceof LoadedMediaSource)) return false; + + final PlayQueueItem playQueueItem = playQueue.getItem(); + return playQueueItem == ((LoadedMediaSource) mediaSource).getStream(); } private void tryBlock() { @@ -280,7 +291,7 @@ public class MediaSourceManager { if (playQueue == null || playbackListener == null) return; // Ensure the current item is up to date with the play queue if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) { - playbackListener.sync(syncedItem,info); + playbackListener.sync(syncedItem, info); } } @@ -329,14 +340,19 @@ public class MediaSourceManager { if (index > sources.getSize() - 1) return; final Consumer onDone = mediaSource -> { - update(playQueue.indexOf(item), mediaSource); + if (DEBUG) Log.d(TAG, " Loaded: [" + item.getTitle() + + "] with url: " + item.getUrl()); + + if (isCorrectionNeeded(item)) update(playQueue.indexOf(item), mediaSource); + loadingItems.remove(item); tryUnblock(); sync(); }; - if (!loadingItems.contains(item) && - ((ManagedMediaSource) sources.getMediaSource(index)).canReplace()) { + if (!loadingItems.contains(item) && isCorrectionNeeded(item)) { + if (DEBUG) Log.d(TAG, "Loading: [" + item.getTitle() + + "] with url: " + item.getUrl()); loadingItems.add(item); final Disposable loader = getLoadedMediaSource(item) @@ -358,16 +374,32 @@ public class MediaSourceManager { final MediaSource source = playbackListener.sourceOf(stream, streamInfo); if (source == null) { - return new FailedMediaSource(stream, new IllegalStateException( - "MediaSource cannot be resolved")); + final Exception exception = new IllegalStateException( + "Unable to resolve source from stream info." + + " URL: " + stream.getUrl() + + ", audio count: " + streamInfo.audio_streams.size() + + ", video count: " + streamInfo.video_only_streams.size() + + streamInfo.video_streams.size()); + return new FailedMediaSource(stream, new IllegalStateException(exception)); } final long expiration = System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(expirationTimeMillis, expirationTimeUnit); - return new LoadedMediaSource(source, expiration); + return new LoadedMediaSource(source, stream, expiration); }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); } + private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) { + if (playQueue == null || sources == null) return false; + + final int index = playQueue.indexOf(item); + if (index == -1 || index >= sources.getSize()) return false; + + final MediaSource mediaSource = sources.getMediaSource(index); + return !(mediaSource instanceof ManagedMediaSource) || + ((ManagedMediaSource) mediaSource).canReplace(item); + } + /*////////////////////////////////////////////////////////////////////////// // MediaSource Playlist Helpers //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml index a577b7fe0..c3480c547 100644 --- a/app/src/main/res/layout-land/activity_player_queue_control.xml +++ b/app/src/main/res/layout-land/activity_player_queue_control.xml @@ -296,5 +296,15 @@ android:textColor="?attr/colorAccent" tools:ignore="HardcodedText" tools:text="1:23:49"/> + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index 80c67974f..be778e7c8 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -397,6 +397,17 @@ android:textColor="@android:color/white" tools:ignore="HardcodedText" tools:text="1:23:49"/> + + diff --git a/app/src/main/res/layout/activity_player_queue_control.xml b/app/src/main/res/layout/activity_player_queue_control.xml index a59e5ba2e..639a8037c 100644 --- a/app/src/main/res/layout/activity_player_queue_control.xml +++ b/app/src/main/res/layout/activity_player_queue_control.xml @@ -146,6 +146,16 @@ android:textColor="?attr/colorAccent" tools:ignore="HardcodedText" tools:text="1:23:49"/> + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 495842092..9dd5dcddb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -413,6 +413,8 @@ Normal Font Larger Font + SYNC + Enable LeakCanary Memory leak monitoring may cause app to become unresponsive when heap dumping