From b4fdbdeb1bcc035eea60a389593b8e1efca5e387 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 28 Oct 2017 12:57:09 -0700 Subject: [PATCH] -Added load debouncing to MediaSourceManager to prevent mass loading due to rapid timeline change. -Added marquee title to main video player. -Modified destroyPlayer to always dispose play queue and media source manager. -Remove unused code from players. --- .../org/schabi/newpipe/player/BasePlayer.java | 65 ++---------- .../newpipe/player/MainVideoPlayer.java | 7 +- .../newpipe/player/PopupVideoPlayer.java | 2 +- .../schabi/newpipe/player/VideoPlayer.java | 1 - .../player/playback/MediaSourceManager.java | 100 +++++++++++++----- .../main/res/layout/activity_main_player.xml | 18 +++- 6 files changed, 103 insertions(+), 90 deletions(-) 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 e9bd60373..9beff48a8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -19,18 +19,13 @@ package org.schabi.newpipe.player; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; import android.graphics.Bitmap; import android.media.AudioManager; import android.net.Uri; -import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; @@ -67,11 +62,11 @@ import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListene import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.player.playback.MediaSourceManager; -import org.schabi.newpipe.player.playback.PlaybackListener; 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.playback.MediaSourceManager; +import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueAdapter; import org.schabi.newpipe.playlist.PlayQueueItem; @@ -93,14 +88,13 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; * * @author mauriciocolli */ -@SuppressWarnings({"WeakerAccess", "unused"}) +@SuppressWarnings({"WeakerAccess"}) public abstract class BasePlayer implements Player.EventListener, PlaybackListener { public static final boolean DEBUG = true; public static final String TAG = "BasePlayer"; protected Context context; - protected SharedPreferences sharedPreferences; protected BroadcastReceiver broadcastReceiver; protected IntentFilter intentFilter; @@ -156,7 +150,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen public BasePlayer(Context context) { this.context = context; - this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); this.broadcastReceiver = new BroadcastReceiver() { @Override @@ -234,17 +227,15 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen // Re-initialization destroyPlayer(); - if (playQueue != null) playQueue.dispose(); - if (playbackManager != null) playbackManager.dispose(); initPlayer(); setRepeatMode(repeatMode); setPlaybackParameters(playbackSpeed, playbackPitch); // Good to go... - initPlayback(this, queue); + initPlayback(queue); } - protected void initPlayback(@NonNull final PlaybackListener listener, @NonNull final PlayQueue queue) { + protected void initPlayback(@NonNull final PlayQueue queue) { playQueue = queue; playQueue.init(); playbackManager = new MediaSourceManager(this, playQueue); @@ -279,6 +270,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen simpleExoPlayer.release(); } if (isProgressLoopRunning()) stopProgressLoop(); + if (playQueue != null) playQueue.dispose(); + if (playbackManager != null) playbackManager.dispose(); if (audioReactor != null) audioReactor.abandonAudioFocus(); } @@ -288,9 +281,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen clearThumbnailCache(); unregisterBroadcastReceiver(); - if (playQueue != null) playQueue.dispose(); - if (playbackManager != null) playbackManager.dispose(); - trackSelector = null; simpleExoPlayer = null; } @@ -602,7 +592,10 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen // If the user selects a new track, then the discontinuity occurs after the index is changed. // Therefore, the only source that causes a discrepancy would be autoplay, // which can only offset the current track by +1. - if (newWindowIndex != playQueue.getIndex()) playQueue.offsetIndex(+1); + if (newWindowIndex != playQueue.getIndex() && playbackManager != null) { + playQueue.offsetIndex(+1); + playbackManager.load(); + } } @Override @@ -659,9 +652,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen @Override public void shutdown() { if (DEBUG) Log.d(TAG, "Shutting down..."); - - playbackManager.dispose(); - playQueue.dispose(); destroy(); } @@ -817,35 +807,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen ); } - public void animateAudio(final float from, final float to, int duration) { - ValueAnimator valueAnimator = new ValueAnimator(); - valueAnimator.setFloatValues(from, to); - valueAnimator.setDuration(duration); - valueAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - if (simpleExoPlayer != null) simpleExoPlayer.setVolume(from); - } - - @Override - public void onAnimationCancel(Animator animation) { - if (simpleExoPlayer != null) simpleExoPlayer.setVolume(to); - } - - @Override - public void onAnimationEnd(Animator animation) { - if (simpleExoPlayer != null) simpleExoPlayer.setVolume(to); - } - }); - valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - if (simpleExoPlayer != null) simpleExoPlayer.setVolume(((float) animation.getAnimatedValue())); - } - }); - valueAnimator.start(); - } - /*////////////////////////////////////////////////////////////////////////// // Getters and Setters //////////////////////////////////////////////////////////////////////////*/ @@ -854,10 +815,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen return simpleExoPlayer; } - public SharedPreferences getSharedPreferences() { - return sharedPreferences; - } - public AudioReactor getAudioReactor() { return audioReactor; } diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index f3b05b66a..cf4421720 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -144,7 +144,7 @@ public final class MainVideoPlayer extends Activity { playerImpl.getPlayPauseButton().setImageResource(R.drawable.ic_play_arrow_white); playerImpl.getPlayer().setPlayWhenReady(playerImpl.wasPlaying); - playerImpl.initPlayback(playerImpl, playerImpl.playQueue); + playerImpl.initPlayback(playerImpl.playQueue); if (playerImpl.trackSelector != null && parameters != null) { playerImpl.trackSelector.setParameters(parameters); @@ -265,6 +265,9 @@ public final class MainVideoPlayer extends Activity { this.playPreviousButton = rootView.findViewById(R.id.playPreviousButton); this.playNextButton = rootView.findViewById(R.id.playNextButton); + titleTextView.setSelected(true); + channelTextView.setSelected(true); + getRootView().setKeepScreenOn(true); } @@ -350,9 +353,9 @@ public final class MainVideoPlayer extends Activity { this.getPlaybackQuality() ); context.startService(intent); - destroyPlayer(); ((View) getControlAnimationView().getParent()).setVisibility(View.GONE); + destroy(); finish(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 4c13afa82..71ce4726a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -884,7 +884,7 @@ public final class PopupVideoPlayer extends Service { mainHandler.post(new Runnable() { @Override public void run() { - playerImpl.initPlayback(playerImpl, new SinglePlayQueue(info)); + playerImpl.initPlayback(new SinglePlayQueue(info)); } }); } 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 816c8d0d2..1a386d45d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -475,7 +475,6 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. if (selectedStreamIndex == menuItemIndex || availableStreams == null || availableStreams.size() <= menuItemIndex) return true; - final String oldResolution = getPlaybackQuality(); final String newResolution = availableStreams.get(menuItemIndex).resolution; setRecovery(); setPlaybackQuality(newResolution); 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 9d778155c..206f953c0 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 @@ -17,13 +17,16 @@ import org.schabi.newpipe.playlist.events.RemoveEvent; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.Disposable; import io.reactivex.disposables.SerialDisposable; import io.reactivex.functions.Consumer; +import io.reactivex.subjects.PublishSubject; -public class MediaSourceManager implements DeferredMediaSource.Callback { +public class MediaSourceManager { private final String TAG = "MediaSourceManager@" + Integer.toHexString(hashCode()); // One-side rolling window size for default loading // Effectively loads windowSize * 2 + 1 streams, must be greater than 0 @@ -31,6 +34,16 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { private final PlaybackListener playbackListener; private final PlayQueue playQueue; + // Process only the last load order when receiving a stream of load orders (lessens IO) + // The lower it is, the faster the error processing during loading + // The higher it is, the less loading occurs during rapid timeline changes + // Not recommended to go below 50ms or above 500ms + private final long loadDebounceMillis; + private final PublishSubject loadSignal; + private final Disposable debouncedLoader; + + private final DeferredMediaSource.Callback sourceBuilder; + private DynamicConcatenatingMediaSource sources; private Subscription playQueueReactor; @@ -40,17 +53,27 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { public MediaSourceManager(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue) { - this(listener, playQueue, 1); + this(listener, playQueue, 1, 200L); } - public MediaSourceManager(@NonNull final PlaybackListener listener, - @NonNull final PlayQueue playQueue, - final int windowSize) { + private MediaSourceManager(@NonNull final PlaybackListener listener, + @NonNull final PlayQueue playQueue, + final int windowSize, + final long loadDebounceMillis) { + if (windowSize <= 0) { + throw new UnsupportedOperationException("MediaSourceManager window size must be greater than 0"); + } + this.playbackListener = listener; this.playQueue = playQueue; this.windowSize = windowSize; + this.loadDebounceMillis = loadDebounceMillis; this.syncReactor = new SerialDisposable(); + this.loadSignal = PublishSubject.create(); + this.debouncedLoader = getDebouncedLoader(); + + this.sourceBuilder = getSourceBuilder(); this.sources = new DynamicConcatenatingMediaSource(); @@ -63,9 +86,13 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { // DeferredMediaSource listener //////////////////////////////////////////////////////////////////////////*/ - @Override - public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { - return playbackListener.sourceOf(item, info); + private DeferredMediaSource.Callback getSourceBuilder() { + return new DeferredMediaSource.Callback() { + @Override + public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { + return playbackListener.sourceOf(item, info); + } + }; } /*////////////////////////////////////////////////////////////////////////// @@ -75,6 +102,8 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { * Dispose the manager and releases all message buses and loaders. * */ public void dispose() { + if (loadSignal != null) loadSignal.onComplete(); + if (debouncedLoader != null) debouncedLoader.dispose(); if (playQueueReactor != null) playQueueReactor.cancel(); if (syncReactor != null) syncReactor.dispose(); if (sources != null) sources.releaseSource(); @@ -90,23 +119,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { * Unblocks the player once the item at the current index is loaded. * */ public void load() { - // The current item has higher priority - final int currentIndex = playQueue.getIndex(); - final PlayQueueItem currentItem = playQueue.getItem(currentIndex); - if (currentItem == null) return; - load(currentItem); - - // The rest are just for seamless playback - final int leftBound = Math.max(0, currentIndex - windowSize); - final int rightLimit = currentIndex + windowSize + 1; - final int rightBound = Math.min(playQueue.size(), rightLimit); - final List items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound)); - - // Do a round robin - final int excess = rightLimit - playQueue.size(); - if (excess >= 0) items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); - - for (final PlayQueueItem item: items) load(item); + loadSignal.onNext(System.currentTimeMillis()); } /** @@ -241,7 +254,27 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { syncReactor.set(currentItem.getStream().subscribe(syncPlayback, onError)); } - private void load(@Nullable final PlayQueueItem item) { + private void loadInternal() { + // The current item has higher priority + final int currentIndex = playQueue.getIndex(); + final PlayQueueItem currentItem = playQueue.getItem(currentIndex); + if (currentItem == null) return; + loadItem(currentItem); + + // The rest are just for seamless playback + final int leftBound = Math.max(0, currentIndex - windowSize); + final int rightLimit = currentIndex + windowSize + 1; + final int rightBound = Math.min(playQueue.size(), rightLimit); + final List items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound)); + + // Do a round robin + final int excess = rightLimit - playQueue.size(); + if (excess >= 0) items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); + + for (final PlayQueueItem item: items) loadItem(item); + } + + private void loadItem(@Nullable final PlayQueueItem item) { if (item == null) return; final int index = playQueue.indexOf(item); @@ -261,10 +294,21 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { if (sources == null) return; for (final PlayQueueItem item : playQueue.getStreams()) { - insert(playQueue.indexOf(item), new DeferredMediaSource(item, this)); + insert(playQueue.indexOf(item), new DeferredMediaSource(item, sourceBuilder)); } } + private Disposable getDebouncedLoader() { + return loadSignal + .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + @Override + public void accept(Long timestamp) throws Exception { + loadInternal(); + } + }); + } /*////////////////////////////////////////////////////////////////////////// // Media Source List Manipulation //////////////////////////////////////////////////////////////////////////*/ @@ -287,7 +331,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { * If the play queue index does not exist, the removal is ignored. * */ private void remove(final int queueIndex) { - if (queueIndex < 0) return; + if (queueIndex < 0 || queueIndex > sources.getSize()) return; sources.removeMediaSource(queueIndex); } diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index 1c3e8eda6..0ee247373 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -158,11 +158,16 @@ android:id="@+id/titleTextView" android:layout_width="match_parent" android:layout_height="wrap_content" - android:ellipsize="end" - android:maxLines="1" + android:ellipsize="marquee" + android:fadingEdge="horizontal" + android:marqueeRepeatLimit="marquee_forever" + android:scrollHorizontally="true" + android:singleLine="true" android:textColor="@android:color/white" android:textSize="15sp" android:textStyle="bold" + android:clickable="true" + android:focusable="true" tools:ignore="RtlHardcoded" tools:text="The Video Title LONG very LONG"/> @@ -170,10 +175,15 @@ android:id="@+id/channelTextView" android:layout_width="match_parent" android:layout_height="wrap_content" - android:ellipsize="end" - android:maxLines="1" + android:ellipsize="marquee" + android:fadingEdge="horizontal" + android:marqueeRepeatLimit="marquee_forever" + android:scrollHorizontally="true" + android:singleLine="true" android:textColor="@android:color/white" android:textSize="12sp" + android:clickable="true" + android:focusable="true" tools:text="The Video Artist LONG very LONG very Long"/>