-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.
This commit is contained in:
John Zhen Mo 2017-10-28 12:57:09 -07:00
parent 1fb3774e03
commit b4fdbdeb1b
6 changed files with 103 additions and 90 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"/>
</LinearLayout>