1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2026-04-25 00:01:23 +00:00

-[#1060] Added toggle to disable thumbnail loading.

-Added button to wipe metadata cache.
-Added more paddings on player buttons.
-Added new animations to main player secondary controls and play queue expand/collapse.
-Refactored play queue item touch callback for use in all players.
-Improved MediaSourceManager to better handle expired stream reloading.
-[#1186] Changed live sync button text to "LIVE".
-Removed MediaSourceManager loader coupling on main players.
-Moved service dependent expiry resolution to ServiceHelper.
-[#1186] Fixed livestream timeline updates causing negative time position.
-[#1186] Fixed livestream not starting from live-edge.
-Fixed main player system UI not retracting on playback start.
This commit is contained in:
John Zhen Mo
2018-03-13 20:25:22 -07:00
parent 1e57b5ea49
commit 61b422502b
18 changed files with 305 additions and 179 deletions

View File

@@ -21,15 +21,15 @@ import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueEvent;
import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.ReorderEvent;
import org.schabi.newpipe.util.ServiceHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
@@ -42,7 +42,7 @@ import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.playlist.PlayQueue.DEBUG;
public class MediaSourceManager {
@NonNull private final static String TAG = "MediaSourceManager";
@NonNull private final String TAG = "MediaSourceManager@" + hashCode();
/**
* Determines how many streams before and after the current stream should be loaded.
@@ -60,17 +60,18 @@ public class MediaSourceManager {
@NonNull private final PlayQueue playQueue;
/**
* Determines how long NEIGHBOURING {@link LoadedMediaSource} window of a currently playing
* {@link MediaSource} is allowed to stay in the playlist timeline. This is to ensure
* the {@link StreamInfo} used in subsequent playback is up-to-date.
* <br><br>
* Once a {@link LoadedMediaSource} has expired, a new source will be reloaded to
* replace the expired one on whereupon {@link #loadImmediate()} is called.
* Determines the gap time between the playback position and the playback duration which
* the {@link #getEdgeIntervalSignal()} begins to request loading.
*
* @see #loadImmediate()
* @see #isCorrectionNeeded(PlayQueueItem)
* @see #progressUpdateIntervalMillis
* */
private final long windowRefreshTimeMillis;
private final long playbackNearEndGapMillis;
/**
* Determines the interval which the {@link #getEdgeIntervalSignal()} waits for between
* each request for loading, once {@link #playbackNearEndGapMillis} has reached.
* */
private final long progressUpdateIntervalMillis;
@NonNull private final Observable<Long> nearEndIntervalSignal;
/**
* Process only the last load order when receiving a stream of load orders (lessens I/O).
@@ -106,23 +107,31 @@ public class MediaSourceManager {
public MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) {
this(listener, playQueue,
/*loadDebounceMillis=*/400L,
/*windowRefreshTimeMillis=*/TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES));
this(listener, playQueue, /*loadDebounceMillis=*/400L,
/*playbackNearEndGapMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS),
/*progressUpdateIntervalMillis*/TimeUnit.MILLISECONDS.convert(2, TimeUnit.SECONDS));
}
private MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue,
final long loadDebounceMillis,
final long windowRefreshTimeMillis) {
final long playbackNearEndGapMillis,
final long progressUpdateIntervalMillis) {
if (playQueue.getBroadcastReceiver() == null) {
throw new IllegalArgumentException("Play Queue has not been initialized.");
}
if (playbackNearEndGapMillis < progressUpdateIntervalMillis) {
throw new IllegalArgumentException("Playback end gap=[" + playbackNearEndGapMillis +
" ms] must be longer than update interval=[ " + progressUpdateIntervalMillis +
" ms] for them to be useful.");
}
this.playbackListener = listener;
this.playQueue = playQueue;
this.windowRefreshTimeMillis = windowRefreshTimeMillis;
this.playbackNearEndGapMillis = playbackNearEndGapMillis;
this.progressUpdateIntervalMillis = progressUpdateIntervalMillis;
this.nearEndIntervalSignal = getEdgeIntervalSignal();
this.loadDebounceMillis = loadDebounceMillis;
this.debouncedSignal = PublishSubject.create();
@@ -161,28 +170,6 @@ public class MediaSourceManager {
sources.releaseSource();
}
/**
* Loads the current playing stream and the streams within its windowSize bound.
*
* Unblocks the player once the item at the current index is loaded.
* */
public void load() {
if (DEBUG) Log.d(TAG, "load() called.");
loadDebounced();
}
/**
* Blocks the player and repopulate the sources.
*
* Does not ensure the player is unblocked and should be done explicitly
* through {@link #load() load}.
* */
public void reset() {
if (DEBUG) Log.d(TAG, "reset() called.");
maybeBlock();
populateSources();
}
/*//////////////////////////////////////////////////////////////////////////
// Event Reactor
//////////////////////////////////////////////////////////////////////////*/
@@ -219,11 +206,13 @@ public class MediaSourceManager {
switch (event.type()) {
case INIT:
case ERROR:
reset();
break;
maybeBlock();
case APPEND:
populateSources();
break;
case SELECT:
maybeRenewCurrentIndex();
break;
case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event;
remove(removeEvent.getRemoveIndex());
@@ -238,7 +227,6 @@ public class MediaSourceManager {
final ReorderEvent reorderEvent = (ReorderEvent) event;
move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex());
break;
case SELECT:
case RECOVERY:
default:
break;
@@ -347,8 +335,13 @@ public class MediaSourceManager {
// MediaSource Loading
//////////////////////////////////////////////////////////////////////////*/
private Observable<Long> getEdgeIntervalSignal() {
return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS)
.filter(ignored -> playbackListener.isNearPlaybackEdge(playbackNearEndGapMillis));
}
private Disposable getDebouncedLoader() {
return debouncedSignal
return debouncedSignal.mergeWith(nearEndIntervalSignal)
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(timestamp -> loadImmediate());
@@ -359,13 +352,14 @@ public class MediaSourceManager {
}
private void loadImmediate() {
if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called");
// The current item has higher priority
final int currentIndex = playQueue.getIndex();
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
if (currentItem == null) return;
// Evict the items being loaded to free up memory
if (!loadingItems.contains(currentItem) && loaderReactor.size() > MAXIMUM_LOADER_SIZE) {
if (loaderReactor.size() > MAXIMUM_LOADER_SIZE) {
loaderReactor.clear();
loadingItems.clear();
}
@@ -377,7 +371,7 @@ public class MediaSourceManager {
final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE);
final int rightLimit = currentIndex + WINDOW_SIZE + 1;
final int rightBound = Math.min(playQueue.size(), rightLimit);
final List<PlayQueueItem> items = new ArrayList<>(
final Set<PlayQueueItem> items = new HashSet<>(
playQueue.getStreams().subList(leftBound,rightBound));
// Do a round robin
@@ -385,6 +379,7 @@ public class MediaSourceManager {
if (excess >= 0) {
items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
}
items.remove(currentItem);
for (final PlayQueueItem item : items) {
maybeLoadItem(item);
@@ -405,9 +400,9 @@ public class MediaSourceManager {
/* No exception handling since getLoadedMediaSource guarantees nonnull return */
.subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource));
loaderReactor.add(loader);
} else {
maybeSynchronizePlayer();
}
maybeSynchronizePlayer();
}
private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) {
@@ -423,7 +418,8 @@ public class MediaSourceManager {
return new FailedMediaSource(stream, exception);
}
final long expiration = System.currentTimeMillis() + windowRefreshTimeMillis;
final long expiration = System.currentTimeMillis() +
ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId());
return new LoadedMediaSource(source, stream, expiration);
}).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable));
}
@@ -467,6 +463,24 @@ public class MediaSourceManager {
}
}
/**
* Checks if the current playing index contains an expired {@link ManagedMediaSource}.
* If so, the expired source is replaced by a {@link PlaceholderMediaSource} and
* {@link #loadImmediate()} is called to reload the current item.
* */
private void maybeRenewCurrentIndex() {
final int currentIndex = playQueue.getIndex();
if (sources.getSize() <= currentIndex) return;
final ManagedMediaSource currentSource =
(ManagedMediaSource) sources.getMediaSource(currentIndex);
final PlayQueueItem currentItem = playQueue.getItem();
if (!currentSource.canReplace(currentItem)) return;
if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " +
"index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]");
update(currentIndex, new PlaceholderMediaSource(), this::loadImmediate);
}
/*//////////////////////////////////////////////////////////////////////////
// MediaSource Playlist Helpers
//////////////////////////////////////////////////////////////////////////*/
@@ -476,6 +490,7 @@ public class MediaSourceManager {
this.sources.releaseSource();
this.sources = new DynamicConcatenatingMediaSource(false,
// Shuffling is done on PlayQueue, thus no need to use ExoPlayer's shuffle order
new ShuffleOrder.UnshuffledShuffleOrder(0));
}