diff --git a/app/build.gradle b/app/build.gradle index 0244ae4b9..ea4d5384d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -73,7 +73,7 @@ dependencies { implementation 'de.hdodenhof:circleimageview:2.2.0' implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1' implementation 'com.nononsenseapps:filepicker:3.0.1' - implementation 'com.google.android.exoplayer:exoplayer:r2.5.4' + implementation 'com.google.android.exoplayer:exoplayer:2.6.0' debugImplementation 'com.facebook.stetho:stetho:1.5.0' debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0' 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 7558f1375..07d567d57 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -81,6 +81,10 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; /** @@ -279,6 +283,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen if (playbackManager != null) playbackManager.dispose(); if (audioReactor != null) audioReactor.abandonAudioFocus(); if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); + + if (playQueueAdapter != null) { + playQueueAdapter.unsetSelectedListener(); + playQueueAdapter.dispose(); + } } public void destroy() { @@ -460,11 +469,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen final PlayQueueItem currentSourceItem = playQueue.getItem(); // Check if already playing correct window - final boolean isCurrentWindowCorrect = - simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex; + final boolean isCurrentPeriodCorrect = + simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex; // Check if recovering - if (isCurrentWindowCorrect && currentSourceItem != null) { + if (isCurrentPeriodCorrect && currentSourceItem != null) { /* Recovering with sub-second position may cause a long buffer delay in ExoPlayer, * rounding this position to the nearest second will help alleviate this.*/ final long position = currentSourceItem.getRecoveryPosition(); @@ -605,17 +614,25 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } @Override - public void onPositionDiscontinuity() { + public void onPositionDiscontinuity(int reason) { + if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with reason = [" + reason + "]"); // Refresh the playback if there is a transition to the next video - final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); - if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with window index = [" + newWindowIndex + "]"); + final int newWindowIndex = simpleExoPlayer.getCurrentPeriodIndex(); - // 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 gapless transition, - // which can only offset the current track by +1. - if (newWindowIndex == playQueue.getIndex() + 1 || - (newWindowIndex == 0 && playQueue.getIndex() == playQueue.size() - 1)) { - playQueue.offsetIndex(+1); + /* Discontinuity reasons!! Thank you ExoPlayer lords */ + switch (reason) { + case DISCONTINUITY_REASON_PERIOD_TRANSITION: + if (newWindowIndex == playQueue.getIndex()) { + registerView(); + } else { + playQueue.offsetIndex(+1); + } + break; + case DISCONTINUITY_REASON_SEEK: + case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: + case DISCONTINUITY_REASON_INTERNAL: + default: + break; } playbackManager.load(); } @@ -625,6 +642,16 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen if (DEBUG) Log.d(TAG, "onRepeatModeChanged() called with: mode = [" + i + "]"); } + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + if (DEBUG) Log.d(TAG, "onShuffleModeEnabledChanged() called with: " + + "mode = [" + shuffleModeEnabled + "]"); + } + + @Override + public void onSeekProcessed() { + if (DEBUG) Log.d(TAG, "onSeekProcessed() called"); + } /*////////////////////////////////////////////////////////////////////////// // Playback Listener //////////////////////////////////////////////////////////////////////////*/ @@ -668,19 +695,14 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen if (currentSourceIndex != playQueue.getIndex()) { Log.e(TAG, "Play Queue may be desynchronized: item index=[" + currentSourceIndex + "], queue index=[" + playQueue.getIndex() + "]"); - } else if (simpleExoPlayer.getCurrentWindowIndex() != currentSourceIndex || !isPlaying()) { + } else if (simpleExoPlayer.getCurrentPeriodIndex() != currentSourceIndex || !isPlaying()) { final long startPos = info != null ? info.start_position : 0; if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + " at: " + getTimeString((int)startPos)); simpleExoPlayer.seekTo(currentSourceIndex, startPos); } - // TODO: update exoplayer to 2.6.x in order to register view count on repeated streams - databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete() - .subscribe( - ignored -> {/* successful */}, - error -> Log.e(TAG, "Player onViewed() failure: ", error) - )); + registerView(); initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); } @@ -814,6 +836,15 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen // Utils //////////////////////////////////////////////////////////////////////////*/ + private void registerView() { + if (databaseUpdateReactor == null || recordManager == null || currentInfo == null) return; + databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete() + .subscribe( + ignored -> {/* successful */}, + error -> Log.e(TAG, "Player onViewed() failure: ", error) + )); + } + protected void reload() { if (playbackManager != null) { playbackManager.reset(); 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 5518357a8..1378d9a80 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -61,6 +61,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; + private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10; + private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25; + private View rootView; private RecyclerView itemsList; @@ -211,6 +214,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity unbindService(serviceConnection); serviceBound = false; stopPlayerListener(); + + if (player != null && player.getPlayQueueAdapter() != null) { + player.getPlayQueueAdapter().unsetSelectedListener(); + } + if (itemsList != null) itemsList.setAdapter(null); + if (itemTouchHelper != null) itemTouchHelper.attachToRecyclerView(null); + + itemsList = null; + itemTouchHelper = null; player = null; } } @@ -385,7 +397,19 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private ItemTouchHelper.SimpleCallback getItemTouchCallback() { return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) { + public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, + int viewSizeOutOfBounds, int totalSize, + long msSinceStartScroll) { + final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, + viewSizeOutOfBounds, totalSize, msSinceStartScroll); + final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY, + Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY)); + return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); + } + + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, + RecyclerView.ViewHolder target) { if (source.getItemViewType() != target.getItemViewType()) { return false; } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index 4e031a0dd..2c85cfc34 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -181,7 +181,9 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au public void onAudioInputFormatChanged(Format format) {} @Override - public void onAudioTrackUnderrun(int i, long l, long l1) {} + public void onAudioSinkUnderrun(int bufferSize, + long bufferSizeMs, + long elapsedSinceLastFeedMs) {} @Override public void onAudioDisabled(DecoderCounters decoderCounters) {} diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java index acc20f5b0..be7b8efde 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java @@ -2,6 +2,7 @@ package org.schabi.newpipe.player.helper; import android.content.Context; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.Renderer; @@ -10,6 +11,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DefaultAllocator; +import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; + public class LoadController implements LoadControl { public static final String TAG = "LoadController"; @@ -23,16 +26,17 @@ public class LoadController implements LoadControl { public LoadController(final Context context) { this(PlayerHelper.getMinBufferMs(context), PlayerHelper.getMaxBufferMs(context), - PlayerHelper.getBufferForPlaybackMs(context), - PlayerHelper.getBufferForPlaybackAfterRebufferMs(context)); + PlayerHelper.getBufferForPlaybackMs(context)); } public LoadController(final int minBufferMs, final int maxBufferMs, - final long bufferForPlaybackMs, - final long bufferForPlaybackAfterRebufferMs) { - final DefaultAllocator allocator = new DefaultAllocator(true, 65536); - internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs); + final int bufferForPlaybackMs) { + final DefaultAllocator allocator = new DefaultAllocator(true, + C.DEFAULT_BUFFER_SEGMENT_SIZE); + + internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs, + bufferForPlaybackMs, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS); } /*////////////////////////////////////////////////////////////////////////// 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 476838f13..4929be9a3 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 @@ -113,12 +113,8 @@ public class PlayerHelper { return 30000; } - public static long getBufferForPlaybackMs(@NonNull final Context context) { - return 2500L; - } - - public static long getBufferForPlaybackAfterRebufferMs(@NonNull final Context context) { - return 5000L; + public static int getBufferForPlaybackMs(@NonNull final Context context) { + return 2500; } public static boolean isUsingDSP(@NonNull final Context context) { diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java index b0990f56a..3ae744d18 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java @@ -114,32 +114,10 @@ public final class DeferredMediaSource implements MediaSource { Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl()); - final Function onReceive = new Function() { - @Override - public MediaSource apply(StreamInfo streamInfo) throws Exception { - return onStreamInfoReceived(stream, streamInfo); - } - }; - - final Consumer onSuccess = new Consumer() { - @Override - public void accept(MediaSource mediaSource) throws Exception { - onMediaSourceReceived(mediaSource); - } - }; - - final Consumer onError = new Consumer() { - @Override - public void accept(Throwable throwable) throws Exception { - onStreamInfoError(throwable); - } - }; - loader = stream.getStream() - .observeOn(Schedulers.io()) - .map(onReceive) + .map(streamInfo -> onStreamInfoReceived(stream, streamInfo)) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(onSuccess, onError); + .subscribe(this::onMediaSourceReceived, this::onStreamInfoError); } private MediaSource onStreamInfoReceived(@NonNull final PlayQueueItem item, 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 04f1606fa..baf2b9c29 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 @@ -4,7 +4,6 @@ import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; -import com.google.android.exoplayer2.source.MediaSource; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -21,8 +20,8 @@ import java.util.concurrent.TimeUnit; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; -import io.reactivex.disposables.SerialDisposable; import io.reactivex.functions.Consumer; import io.reactivex.subjects.PublishSubject; @@ -46,7 +45,9 @@ public class MediaSourceManager { private DynamicConcatenatingMediaSource sources; private Subscription playQueueReactor; - private SerialDisposable syncReactor; + private CompositeDisposable syncReactor; + + private PlayQueueItem syncedItem; private boolean isBlocked; @@ -68,7 +69,7 @@ public class MediaSourceManager { this.windowSize = windowSize; this.loadDebounceMillis = loadDebounceMillis; - this.syncReactor = new SerialDisposable(); + this.syncReactor = new CompositeDisposable(); this.debouncedLoadSignal = PublishSubject.create(); this.debouncedLoader = getDebouncedLoader(); @@ -86,12 +87,7 @@ public class MediaSourceManager { //////////////////////////////////////////////////////////////////////////*/ private DeferredMediaSource.Callback getSourceBuilder() { - return new DeferredMediaSource.Callback() { - @Override - public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { - return playbackListener.sourceOf(item, info); - } - }; + return playbackListener::sourceOf; } /*////////////////////////////////////////////////////////////////////////// @@ -241,22 +237,28 @@ public class MediaSourceManager { final PlayQueueItem currentItem = playQueue.getItem(); if (currentItem == null) return; - final Consumer syncPlayback = new Consumer() { - @Override - public void accept(StreamInfo streamInfo) throws Exception { - playbackListener.sync(currentItem, streamInfo); - } + final Consumer onSuccess = info -> syncInternal(currentItem, info); + final Consumer onError = throwable -> { + Log.e(TAG, "Sync error:", throwable); + syncInternal(currentItem, null); }; - final Consumer onError = new Consumer() { - @Override - public void accept(Throwable throwable) throws Exception { - Log.e(TAG, "Sync error:", throwable); - playbackListener.sync(currentItem,null); - } - }; + final Disposable sync = currentItem.getStream() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(onSuccess, onError); + syncReactor.add(sync); + } - syncReactor.set(currentItem.getStream().subscribe(syncPlayback, onError)); + private void syncInternal(@android.support.annotation.NonNull final PlayQueueItem item, + @Nullable final StreamInfo info) { + if (playQueue == null || playbackListener == null) return; + + // Sync each new item once only and ensure the current item is up to date + // with the play queue + if (playQueue.getItem() != syncedItem && playQueue.getItem() == item) { + syncedItem = item; + playbackListener.sync(syncedItem,info); + } } private void loadDebounced() { @@ -313,12 +315,7 @@ public class MediaSourceManager { return debouncedLoadSignal .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer() { - @Override - public void accept(Long timestamp) throws Exception { - loadImmediate(); - } - }); + .subscribe(timestamp -> loadImmediate()); } /*////////////////////////////////////////////////////////////////////////// // Media Source List Manipulation diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java index dfed04c01..c6fdde656 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java @@ -33,6 +33,8 @@ public interface PlaybackListener { * Signals to the listener to synchronize the player's window to the manager's * window. * + * Occurs once only per play queue item change. + * * May be called only after unblock is called. * */ void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info); diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java index e16693ec6..cd833c1ab 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java @@ -73,6 +73,10 @@ public class PlayQueueAdapter extends RecyclerView.Adapter observer = new Observer() { @Override diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java index f8e7b8655..752dc223d 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java @@ -104,17 +104,9 @@ public class PlayQueueItem implements Serializable { @NonNull private Single getInfo() { - final Consumer onError = new Consumer() { - @Override - public void accept(Throwable throwable) throws Exception { - error = throwable; - } - }; - return ExtractorHelper.getStreamInfo(this.serviceId, this.url, false) .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError(onError); + .doOnError(throwable -> error = throwable); } //////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java index 82277a4e7..73cdf1113 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java @@ -53,24 +53,18 @@ public class PlayQueueItemBuilder { ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, imageOptions); - holder.itemRoot.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (onItemClickListener != null) { - onItemClickListener.selected(item, view); - } + holder.itemRoot.setOnClickListener(view -> { + if (onItemClickListener != null) { + onItemClickListener.selected(item, view); } }); - holder.itemRoot.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View view) { - if (onItemClickListener != null) { - onItemClickListener.held(item, view); - return true; - } - return false; + holder.itemRoot.setOnLongClickListener(view -> { + if (onItemClickListener != null) { + onItemClickListener.held(item, view); + return true; } + return false; }); holder.itemThumbnailView.setOnTouchListener(getOnTouchListener(holder)); @@ -78,26 +72,21 @@ public class PlayQueueItemBuilder { } private View.OnTouchListener getOnTouchListener(final PlayQueueItemHolder holder) { - return new View.OnTouchListener() { - @Override - public boolean onTouch(View view, MotionEvent motionEvent) { - view.performClick(); - if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { - onItemClickListener.onStartDrag(holder); - } - return false; + return (view, motionEvent) -> { + view.performClick(); + if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN + && onItemClickListener != null) { + onItemClickListener.onStartDrag(holder); } + return false; }; } private DisplayImageOptions buildImageOptions(final int widthPx, final int heightPx) { - final BitmapProcessor bitmapProcessor = new BitmapProcessor() { - @Override - public Bitmap process(Bitmap bitmap) { - final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, widthPx, heightPx, false); - bitmap.recycle(); - return resizedBitmap; - } + final BitmapProcessor bitmapProcessor = bitmap -> { + final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, widthPx, heightPx, false); + bitmap.recycle(); + return resizedBitmap; }; return new DisplayImageOptions.Builder()