mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-08-22 01:58:16 +00:00
Merge pull request #12347 from Isira-Seneviratne/PlayQueueItem-equals
Add PlayQueueItem equals and hashCode
This commit is contained in:
commit
52ff052d6a
@ -852,8 +852,7 @@ public final class VideoDetailFragment
|
|||||||
if (playQueue == null) {
|
if (playQueue == null) {
|
||||||
playQueue = new SinglePlayQueue(result);
|
playQueue = new SinglePlayQueue(result);
|
||||||
}
|
}
|
||||||
if (stack.isEmpty() || !stack.peek().getPlayQueue()
|
if (stack.isEmpty() || !stack.peek().getPlayQueue().equals(playQueue)) {
|
||||||
.equalStreams(playQueue)) {
|
|
||||||
stack.push(new StackItem(serviceId, url, title, playQueue));
|
stack.push(new StackItem(serviceId, url, title, playQueue));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1739,7 +1738,7 @@ public final class VideoDetailFragment
|
|||||||
// deleted/added items inside Channel/Playlist queue and makes possible to have
|
// deleted/added items inside Channel/Playlist queue and makes possible to have
|
||||||
// a history of played items
|
// a history of played items
|
||||||
@Nullable final StackItem stackPeek = stack.peek();
|
@Nullable final StackItem stackPeek = stack.peek();
|
||||||
if (stackPeek != null && !stackPeek.getPlayQueue().equalStreams(queue)) {
|
if (stackPeek != null && !stackPeek.getPlayQueue().equals(queue)) {
|
||||||
@Nullable final PlayQueueItem playQueueItem = queue.getItem();
|
@Nullable final PlayQueueItem playQueueItem = queue.getItem();
|
||||||
if (playQueueItem != null) {
|
if (playQueueItem != null) {
|
||||||
stack.push(new StackItem(playQueueItem.getServiceId(), playQueueItem.getUrl(),
|
stack.push(new StackItem(playQueueItem.getServiceId(), playQueueItem.getUrl(),
|
||||||
@ -1803,7 +1802,7 @@ public final class VideoDetailFragment
|
|||||||
// They are not equal when user watches something in popup while browsing in fragment and
|
// They are not equal when user watches something in popup while browsing in fragment and
|
||||||
// then changes screen orientation. In that case the fragment will set itself as
|
// then changes screen orientation. In that case the fragment will set itself as
|
||||||
// a service listener and will receive initial call to onMetadataUpdate()
|
// a service listener and will receive initial call to onMetadataUpdate()
|
||||||
if (!queue.equalStreams(playQueue)) {
|
if (!queue.equals(playQueue)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2075,7 +2074,7 @@ public final class VideoDetailFragment
|
|||||||
final Iterator<StackItem> iterator = stack.descendingIterator();
|
final Iterator<StackItem> iterator = stack.descendingIterator();
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
final StackItem next = iterator.next();
|
final StackItem next = iterator.next();
|
||||||
if (next.getPlayQueue().equalStreams(queue)) {
|
if (next.getPlayQueue().equals(queue)) {
|
||||||
item = next;
|
item = next;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -2089,8 +2088,7 @@ public final class VideoDetailFragment
|
|||||||
// Player will have STATE_IDLE when a user pressed back button
|
// Player will have STATE_IDLE when a user pressed back button
|
||||||
if (isClearingQueueConfirmationRequired(activity)
|
if (isClearingQueueConfirmationRequired(activity)
|
||||||
&& playerIsNotStopped()
|
&& playerIsNotStopped()
|
||||||
&& activeQueue != null
|
&& !Objects.equals(activeQueue, playQueue)) {
|
||||||
&& !activeQueue.equalStreams(playQueue)) {
|
|
||||||
showClearingQueueConfirmation(onAllow);
|
showClearingQueueConfirmation(onAllow);
|
||||||
} else {
|
} else {
|
||||||
onAllow.run();
|
onAllow.run();
|
||||||
|
@ -397,7 +397,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
&& newQueue.size() == 1 && newQueue.getItem() != null
|
&& newQueue.size() == 1 && newQueue.getItem() != null
|
||||||
&& playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null
|
&& playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null
|
||||||
&& newQueue.getItem().getUrl().equals(playQueue.getItem().getUrl())
|
&& newQueue.getItem().getUrl().equals(playQueue.getItem().getUrl())
|
||||||
&& newQueue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) {
|
&& newQueue.getItem().getRecoveryPosition() != Long.MIN_VALUE) {
|
||||||
// Player can have state = IDLE when playback is stopped or failed
|
// Player can have state = IDLE when playback is stopped or failed
|
||||||
// and we should retry in this case
|
// and we should retry in this case
|
||||||
if (simpleExoPlayer.getPlaybackState()
|
if (simpleExoPlayer.getPlaybackState()
|
||||||
@ -425,7 +425,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
&& !samePlayQueue
|
&& !samePlayQueue
|
||||||
&& !newQueue.isEmpty()
|
&& !newQueue.isEmpty()
|
||||||
&& newQueue.getItem() != null
|
&& newQueue.getItem() != null
|
||||||
&& newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
|
&& newQueue.getItem().getRecoveryPosition() == Long.MIN_VALUE) {
|
||||||
databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem())
|
databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
// Do not place initPlayback() in doFinally() because
|
// Do not place initPlayback() in doFinally() because
|
||||||
@ -1588,7 +1588,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sync the player index with the queue index, and seek to the correct position
|
// sync the player index with the queue index, and seek to the correct position
|
||||||
if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) {
|
if (item.getRecoveryPosition() != Long.MIN_VALUE) {
|
||||||
simpleExoPlayer.seekTo(playQueueIndex, item.getRecoveryPosition());
|
simpleExoPlayer.seekTo(playQueueIndex, item.getRecoveryPosition());
|
||||||
playQueue.unsetRecovery(playQueueIndex);
|
playQueue.unsetRecovery(playQueueIndex);
|
||||||
} else {
|
} else {
|
||||||
|
@ -38,9 +38,9 @@ import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription;
|
|||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||||
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException;
|
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException;
|
||||||
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException;
|
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException;
|
||||||
import static org.schabi.newpipe.player.playqueue.PlayQueue.DEBUG;
|
|
||||||
import static org.schabi.newpipe.util.ServiceHelper.getCacheExpirationMillis;
|
import static org.schabi.newpipe.util.ServiceHelper.getCacheExpirationMillis;
|
||||||
|
|
||||||
public class MediaSourceManager {
|
public class MediaSourceManager {
|
||||||
|
@ -1,561 +0,0 @@
|
|||||||
package org.schabi.newpipe.player.playqueue;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
|
||||||
import org.schabi.newpipe.player.playqueue.events.AppendEvent;
|
|
||||||
import org.schabi.newpipe.player.playqueue.events.ErrorEvent;
|
|
||||||
import org.schabi.newpipe.player.playqueue.events.InitEvent;
|
|
||||||
import org.schabi.newpipe.player.playqueue.events.MoveEvent;
|
|
||||||
import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent;
|
|
||||||
import org.schabi.newpipe.player.playqueue.events.RecoveryEvent;
|
|
||||||
import org.schabi.newpipe.player.playqueue.events.RemoveEvent;
|
|
||||||
import org.schabi.newpipe.player.playqueue.events.ReorderEvent;
|
|
||||||
import org.schabi.newpipe.player.playqueue.events.SelectEvent;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.rxjava3.core.BackpressureStrategy;
|
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
|
||||||
import io.reactivex.rxjava3.subjects.BehaviorSubject;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PlayQueue is responsible for keeping track of a list of streams and the index of
|
|
||||||
* the stream that should be currently playing.
|
|
||||||
* <p>
|
|
||||||
* This class contains basic manipulation of a playlist while also functions as a
|
|
||||||
* message bus, providing all listeners with new updates to the play queue.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* This class can be serialized for passing intents, but in order to start the
|
|
||||||
* message bus, it must be initialized.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
public abstract class PlayQueue implements Serializable {
|
|
||||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
|
||||||
@NonNull
|
|
||||||
private final AtomicInteger queueIndex;
|
|
||||||
private final List<PlayQueueItem> history = new ArrayList<>();
|
|
||||||
|
|
||||||
private List<PlayQueueItem> backup;
|
|
||||||
private List<PlayQueueItem> streams;
|
|
||||||
|
|
||||||
private transient BehaviorSubject<PlayQueueEvent> eventBroadcast;
|
|
||||||
private transient Flowable<PlayQueueEvent> broadcastReceiver;
|
|
||||||
private transient boolean disposed = false;
|
|
||||||
|
|
||||||
PlayQueue(final int index, final List<PlayQueueItem> startWith) {
|
|
||||||
streams = new ArrayList<>(startWith);
|
|
||||||
|
|
||||||
if (streams.size() > index) {
|
|
||||||
history.add(streams.get(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
queueIndex = new AtomicInteger(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Playlist actions
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the play queue message buses.
|
|
||||||
* <p>
|
|
||||||
* Also starts a self reporter for logging if debug mode is enabled.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
public void init() {
|
|
||||||
eventBroadcast = BehaviorSubject.create();
|
|
||||||
|
|
||||||
broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.startWithItem(new InitEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispose the play queue by stopping all message buses.
|
|
||||||
*/
|
|
||||||
public void dispose() {
|
|
||||||
if (eventBroadcast != null) {
|
|
||||||
eventBroadcast.onComplete();
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBroadcast = null;
|
|
||||||
broadcastReceiver = null;
|
|
||||||
disposed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the queue is complete.
|
|
||||||
* <p>
|
|
||||||
* A queue is complete if it has loaded all items in an external playlist
|
|
||||||
* single stream or local queues are always complete.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return whether the queue is complete
|
|
||||||
*/
|
|
||||||
public abstract boolean isComplete();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load partial queue in the background, does nothing if the queue is complete.
|
|
||||||
*/
|
|
||||||
public abstract void fetch();
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Readonly ops
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the current index that should be played
|
|
||||||
*/
|
|
||||||
public int getIndex() {
|
|
||||||
return queueIndex.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the current playing index to a new index.
|
|
||||||
* <p>
|
|
||||||
* This method is guarded using in a circular manner for index exceeding the play queue size.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Will emit a {@link SelectEvent} if the index is not the current playing index.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param index the index to be set
|
|
||||||
*/
|
|
||||||
public synchronized void setIndex(final int index) {
|
|
||||||
final int oldIndex = getIndex();
|
|
||||||
|
|
||||||
final int newIndex;
|
|
||||||
|
|
||||||
if (index < 0) {
|
|
||||||
newIndex = 0;
|
|
||||||
} else if (index < streams.size()) {
|
|
||||||
// Regular assignment for index in bounds
|
|
||||||
newIndex = index;
|
|
||||||
} else if (streams.isEmpty()) {
|
|
||||||
// Out of bounds from here on
|
|
||||||
// Need to check if stream is empty to prevent arithmetic error and negative index
|
|
||||||
newIndex = 0;
|
|
||||||
} else if (isComplete()) {
|
|
||||||
// Circular indexing
|
|
||||||
newIndex = index % streams.size();
|
|
||||||
} else {
|
|
||||||
// Index of last element
|
|
||||||
newIndex = streams.size() - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
queueIndex.set(newIndex);
|
|
||||||
|
|
||||||
if (oldIndex != newIndex) {
|
|
||||||
history.add(streams.get(newIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
TODO: Documentation states that a SelectEvent will only be emitted if the new index is...
|
|
||||||
different from the old one but this is emitted regardless? Not sure what this what it does
|
|
||||||
exactly so I won't touch it
|
|
||||||
*/
|
|
||||||
broadcast(new SelectEvent(oldIndex, newIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the current item that should be played, or null if the queue is empty
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public PlayQueueItem getItem() {
|
|
||||||
return getItem(getIndex());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param index the index of the item to return
|
|
||||||
* @return the item at the given index, or null if the index is out of bounds
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public PlayQueueItem getItem(final int index) {
|
|
||||||
if (index < 0 || index >= streams.size()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return streams.get(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the index of the given item using referential equality.
|
|
||||||
* May be null despite play queue contains identical item.
|
|
||||||
*
|
|
||||||
* @param item the item to find the index of
|
|
||||||
* @return the index of the given item
|
|
||||||
*/
|
|
||||||
public int indexOf(@NonNull final PlayQueueItem item) {
|
|
||||||
return streams.indexOf(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the current size of play queue.
|
|
||||||
*/
|
|
||||||
public int size() {
|
|
||||||
return streams.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the play queue is empty.
|
|
||||||
*
|
|
||||||
* @return whether the play queue is empty
|
|
||||||
*/
|
|
||||||
public boolean isEmpty() {
|
|
||||||
return streams.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if the current play queue is shuffled.
|
|
||||||
*
|
|
||||||
* @return whether the play queue is shuffled
|
|
||||||
*/
|
|
||||||
public boolean isShuffled() {
|
|
||||||
return backup != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return an immutable view of the play queue
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public List<PlayQueueItem> getStreams() {
|
|
||||||
return Collections.unmodifiableList(streams);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Write ops
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the play queue's update broadcast.
|
|
||||||
* May be null if the play queue message bus is not initialized.
|
|
||||||
*
|
|
||||||
* @return the play queue's update broadcast
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public Flowable<PlayQueueEvent> getBroadcastReceiver() {
|
|
||||||
return broadcastReceiver;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the current playing index by an offset amount.
|
|
||||||
* <p>
|
|
||||||
* Will emit a {@link SelectEvent} if offset is non-zero.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param offset the offset relative to the current index
|
|
||||||
*/
|
|
||||||
public synchronized void offsetIndex(final int offset) {
|
|
||||||
setIndex(getIndex() + offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies that a change has occurred.
|
|
||||||
*/
|
|
||||||
public synchronized void notifyChange() {
|
|
||||||
broadcast(new AppendEvent(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends the given {@link PlayQueueItem}s to the current play queue.
|
|
||||||
* <p>
|
|
||||||
* If the play queue is shuffled, then append the items to the backup queue as is and
|
|
||||||
* append the shuffle items to the play queue.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Will emit a {@link AppendEvent} on any given context.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param items {@link PlayQueueItem}s to append
|
|
||||||
*/
|
|
||||||
public synchronized void append(@NonNull final List<PlayQueueItem> items) {
|
|
||||||
final List<PlayQueueItem> itemList = new ArrayList<>(items);
|
|
||||||
|
|
||||||
if (isShuffled()) {
|
|
||||||
backup.addAll(itemList);
|
|
||||||
Collections.shuffle(itemList);
|
|
||||||
}
|
|
||||||
if (!streams.isEmpty() && streams.get(streams.size() - 1).isAutoQueued()
|
|
||||||
&& !itemList.get(0).isAutoQueued()) {
|
|
||||||
streams.remove(streams.size() - 1);
|
|
||||||
}
|
|
||||||
streams.addAll(itemList);
|
|
||||||
|
|
||||||
broadcast(new AppendEvent(itemList.size()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the item at the given index from the play queue.
|
|
||||||
* <p>
|
|
||||||
* The current playing index will decrement if it is greater than the index being removed.
|
|
||||||
* On cases where the current playing index exceeds the playlist range, it is set to 0.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Will emit a {@link RemoveEvent} if the index is within the play queue index range.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param index the index of the item to remove
|
|
||||||
*/
|
|
||||||
public synchronized void remove(final int index) {
|
|
||||||
if (index >= streams.size() || index < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
removeInternal(index);
|
|
||||||
broadcast(new RemoveEvent(index, getIndex()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Report an exception for the item at the current index in order and skip to the next one
|
|
||||||
* <p>
|
|
||||||
* This is done as a separate event as the underlying manager may have
|
|
||||||
* different implementation regarding exceptions.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
public synchronized void error() {
|
|
||||||
final int oldIndex = getIndex();
|
|
||||||
queueIndex.incrementAndGet();
|
|
||||||
if (streams.size() > queueIndex.get()) {
|
|
||||||
history.add(streams.get(queueIndex.get()));
|
|
||||||
}
|
|
||||||
broadcast(new ErrorEvent(oldIndex, getIndex()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void removeInternal(final int removeIndex) {
|
|
||||||
final int currentIndex = queueIndex.get();
|
|
||||||
final int size = size();
|
|
||||||
|
|
||||||
if (currentIndex > removeIndex) {
|
|
||||||
queueIndex.decrementAndGet();
|
|
||||||
|
|
||||||
} else if (currentIndex >= size) {
|
|
||||||
queueIndex.set(currentIndex % (size - 1));
|
|
||||||
|
|
||||||
} else if (currentIndex == removeIndex && currentIndex == size - 1) {
|
|
||||||
queueIndex.set(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (backup != null) {
|
|
||||||
backup.remove(getItem(removeIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
history.remove(streams.remove(removeIndex));
|
|
||||||
if (streams.size() > queueIndex.get()) {
|
|
||||||
history.add(streams.get(queueIndex.get()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Moves a queue item at the source index to the target index.
|
|
||||||
* <p>
|
|
||||||
* If the item being moved is the currently playing, then the current playing index is set
|
|
||||||
* to that of the target.
|
|
||||||
* If the moved item is not the currently playing and moves to an index <b>AFTER</b> the
|
|
||||||
* current playing index, then the current playing index is decremented.
|
|
||||||
* Vice versa if the an item after the currently playing is moved <b>BEFORE</b>.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param source the original index of the item
|
|
||||||
* @param target the new index of the item
|
|
||||||
*/
|
|
||||||
public synchronized void move(final int source, final int target) {
|
|
||||||
if (source < 0 || target < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (source >= streams.size() || target >= streams.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int current = getIndex();
|
|
||||||
if (source == current) {
|
|
||||||
queueIndex.set(target);
|
|
||||||
} else if (source < current && target >= current) {
|
|
||||||
queueIndex.decrementAndGet();
|
|
||||||
} else if (source > current && target <= current) {
|
|
||||||
queueIndex.incrementAndGet();
|
|
||||||
}
|
|
||||||
|
|
||||||
final PlayQueueItem playQueueItem = streams.remove(source);
|
|
||||||
playQueueItem.setAutoQueued(false);
|
|
||||||
streams.add(target, playQueueItem);
|
|
||||||
broadcast(new MoveEvent(source, target));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the recovery record of the item at the index.
|
|
||||||
* <p>
|
|
||||||
* Broadcasts a recovery event.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param index index of the item
|
|
||||||
* @param position the recovery position
|
|
||||||
*/
|
|
||||||
public synchronized void setRecovery(final int index, final long position) {
|
|
||||||
if (index < 0 || index >= streams.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
streams.get(index).setRecoveryPosition(position);
|
|
||||||
broadcast(new RecoveryEvent(index, position));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Revoke the recovery record of the item at the index.
|
|
||||||
* <p>
|
|
||||||
* Broadcasts a recovery event.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param index index of the item
|
|
||||||
*/
|
|
||||||
public synchronized void unsetRecovery(final int index) {
|
|
||||||
setRecovery(index, PlayQueueItem.RECOVERY_UNSET);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shuffles the current play queue
|
|
||||||
* <p>
|
|
||||||
* This method first backs up the existing play queue and item being played. Then a newly
|
|
||||||
* shuffled play queue will be generated along with currently playing item placed at the
|
|
||||||
* beginning of the queue. This item will also be added to the history.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Will emit a {@link ReorderEvent} if shuffled.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @implNote Does nothing if the queue has a size <= 2 (the currently playing video must stay on
|
|
||||||
* top, so shuffling a size-2 list does nothing)
|
|
||||||
*/
|
|
||||||
public synchronized void shuffle() {
|
|
||||||
// Create a backup if it doesn't already exist
|
|
||||||
// Note: The backup-list has to be created at all cost (even when size <= 2).
|
|
||||||
// Otherwise it's not possible to enter shuffle-mode!
|
|
||||||
if (backup == null) {
|
|
||||||
backup = new ArrayList<>(streams);
|
|
||||||
}
|
|
||||||
// Can't shuffle a list that's empty or only has one element
|
|
||||||
if (size() <= 2) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int originalIndex = getIndex();
|
|
||||||
final PlayQueueItem currentItem = getItem();
|
|
||||||
|
|
||||||
Collections.shuffle(streams);
|
|
||||||
|
|
||||||
// Move currentItem to the head of the queue
|
|
||||||
streams.remove(currentItem);
|
|
||||||
streams.add(0, currentItem);
|
|
||||||
queueIndex.set(0);
|
|
||||||
|
|
||||||
history.add(currentItem);
|
|
||||||
|
|
||||||
broadcast(new ReorderEvent(originalIndex, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unshuffles the current play queue if a backup play queue exists.
|
|
||||||
* <p>
|
|
||||||
* This method undoes shuffling and index will be set to the previously playing item if found,
|
|
||||||
* otherwise, the index will reset to 0.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Will emit a {@link ReorderEvent} if a backup exists.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
public synchronized void unshuffle() {
|
|
||||||
if (backup == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final int originIndex = getIndex();
|
|
||||||
final PlayQueueItem current = getItem();
|
|
||||||
|
|
||||||
streams = backup;
|
|
||||||
backup = null;
|
|
||||||
|
|
||||||
final int newIndex = streams.indexOf(current);
|
|
||||||
if (newIndex != -1) {
|
|
||||||
queueIndex.set(newIndex);
|
|
||||||
} else {
|
|
||||||
queueIndex.set(0);
|
|
||||||
}
|
|
||||||
if (streams.size() > queueIndex.get()) {
|
|
||||||
history.add(streams.get(queueIndex.get()));
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcast(new ReorderEvent(originIndex, queueIndex.get()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Selects previous played item.
|
|
||||||
*
|
|
||||||
* This method removes currently playing item from history and
|
|
||||||
* starts playing the last item from history if it exists
|
|
||||||
*
|
|
||||||
* @return true if history is not empty and the item can be played
|
|
||||||
* */
|
|
||||||
public synchronized boolean previous() {
|
|
||||||
if (history.size() <= 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
history.remove(history.size() - 1);
|
|
||||||
|
|
||||||
final PlayQueueItem last = history.remove(history.size() - 1);
|
|
||||||
setIndex(indexOf(last));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Compares two PlayQueues. Useful when a user switches players but queue is the same so
|
|
||||||
* we don't have to do anything with new queue.
|
|
||||||
* This method also gives a chance to track history of items in a queue in
|
|
||||||
* VideoDetailFragment without duplicating items from two identical queues
|
|
||||||
*/
|
|
||||||
public boolean equalStreams(@Nullable final PlayQueue other) {
|
|
||||||
if (other == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (size() != other.size()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < size(); i++) {
|
|
||||||
final PlayQueueItem stream = streams.get(i);
|
|
||||||
final PlayQueueItem otherStream = other.streams.get(i);
|
|
||||||
// Check is based on serviceId and URL
|
|
||||||
if (stream.getServiceId() != otherStream.getServiceId()
|
|
||||||
|| !stream.getUrl().equals(otherStream.getUrl())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean equalStreamsAndIndex(@Nullable final PlayQueue other) {
|
|
||||||
if (equalStreams(other)) {
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
return other.getIndex() == getIndex(); //NOSONAR: other is not null
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDisposed() {
|
|
||||||
return disposed;
|
|
||||||
}
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Rx Broadcast
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
private void broadcast(@NonNull final PlayQueueEvent event) {
|
|
||||||
if (eventBroadcast != null) {
|
|
||||||
eventBroadcast.onNext(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,497 @@
|
|||||||
|
package org.schabi.newpipe.player.playqueue
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.core.BackpressureStrategy
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||||
|
import org.schabi.newpipe.player.playqueue.events.AppendEvent
|
||||||
|
import org.schabi.newpipe.player.playqueue.events.ErrorEvent
|
||||||
|
import org.schabi.newpipe.player.playqueue.events.InitEvent
|
||||||
|
import org.schabi.newpipe.player.playqueue.events.MoveEvent
|
||||||
|
import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent
|
||||||
|
import org.schabi.newpipe.player.playqueue.events.RecoveryEvent
|
||||||
|
import org.schabi.newpipe.player.playqueue.events.RemoveEvent
|
||||||
|
import org.schabi.newpipe.player.playqueue.events.ReorderEvent
|
||||||
|
import org.schabi.newpipe.player.playqueue.events.SelectEvent
|
||||||
|
import java.io.Serializable
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PlayQueue is responsible for keeping track of a list of streams and the index of
|
||||||
|
* the stream that should be currently playing.
|
||||||
|
*
|
||||||
|
* This class contains basic manipulation of a playlist while also functions as a
|
||||||
|
* message bus, providing all listeners with new updates to the play queue.
|
||||||
|
*
|
||||||
|
* This class can be serialized for passing intents, but in order to start the
|
||||||
|
* message bus, it must be initialized.
|
||||||
|
*/
|
||||||
|
abstract class PlayQueue internal constructor(
|
||||||
|
index: Int,
|
||||||
|
startWith: List<PlayQueueItem>,
|
||||||
|
) : Serializable {
|
||||||
|
private val queueIndex = AtomicInteger(index)
|
||||||
|
private val history = mutableListOf<PlayQueueItem>()
|
||||||
|
private var backup = mutableListOf<PlayQueueItem>()
|
||||||
|
private var streams = startWith.toMutableList()
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
private var eventBroadcast: BehaviorSubject<PlayQueueEvent>? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the play queue's update broadcast.
|
||||||
|
* May be null if the play queue message bus is not initialized.
|
||||||
|
*
|
||||||
|
* @return the play queue's update broadcast
|
||||||
|
*/
|
||||||
|
@Transient
|
||||||
|
var broadcastReceiver: Flowable<PlayQueueEvent>? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
var isDisposed: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (streams.size > index) {
|
||||||
|
history.add(streams[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Playlist actions
|
||||||
|
////////////////////////////////////////////////////////////////////////// */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the play queue message buses.
|
||||||
|
*
|
||||||
|
* Also starts a self reporter for logging if debug mode is enabled.
|
||||||
|
*/
|
||||||
|
fun init() {
|
||||||
|
eventBroadcast = BehaviorSubject.create()
|
||||||
|
|
||||||
|
broadcastReceiver =
|
||||||
|
eventBroadcast!!
|
||||||
|
.toFlowable(BackpressureStrategy.BUFFER)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.startWithItem(InitEvent())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose the play queue by stopping all message buses.
|
||||||
|
*/
|
||||||
|
open fun dispose() {
|
||||||
|
eventBroadcast?.onComplete()
|
||||||
|
eventBroadcast = null
|
||||||
|
broadcastReceiver = null
|
||||||
|
this.isDisposed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the queue is complete.
|
||||||
|
*
|
||||||
|
* A queue is complete if it has loaded all items in an external playlist
|
||||||
|
* single stream or local queues are always complete.
|
||||||
|
*
|
||||||
|
* @return whether the queue is complete
|
||||||
|
*/
|
||||||
|
abstract val isComplete: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load partial queue in the background, does nothing if the queue is complete.
|
||||||
|
*/
|
||||||
|
abstract fun fetch()
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Readonly ops
|
||||||
|
////////////////////////////////////////////////////////////////////////// */
|
||||||
|
@set:Synchronized
|
||||||
|
var index: Int = 0
|
||||||
|
/**
|
||||||
|
* @return the current index that should be played
|
||||||
|
*/
|
||||||
|
get() = queueIndex.get()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the current playing index to a new index.
|
||||||
|
*
|
||||||
|
* This method is guarded using in a circular manner for index exceeding the play queue size.
|
||||||
|
*
|
||||||
|
* Will emit a [SelectEvent] if the index is not the current playing index.
|
||||||
|
*
|
||||||
|
* @param index the index to be set
|
||||||
|
*/
|
||||||
|
set(index) {
|
||||||
|
val oldIndex = field
|
||||||
|
|
||||||
|
val newIndex: Int
|
||||||
|
|
||||||
|
if (index < 0) {
|
||||||
|
newIndex = 0
|
||||||
|
} else if (index < streams.size) {
|
||||||
|
// Regular assignment for index in bounds
|
||||||
|
newIndex = index
|
||||||
|
} else if (streams.isEmpty()) {
|
||||||
|
// Out of bounds from here on
|
||||||
|
// Need to check if stream is empty to prevent arithmetic error and negative index
|
||||||
|
newIndex = 0
|
||||||
|
} else if (this.isComplete) {
|
||||||
|
// Circular indexing
|
||||||
|
newIndex = index % streams.size
|
||||||
|
} else {
|
||||||
|
// Index of last element
|
||||||
|
newIndex = streams.size - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
queueIndex.set(newIndex)
|
||||||
|
|
||||||
|
if (oldIndex != newIndex) {
|
||||||
|
history.add(streams[newIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO: Documentation states that a SelectEvent will only be emitted if the new index is...
|
||||||
|
different from the old one but this is emitted regardless? Not sure what this what it does
|
||||||
|
exactly so I won't touch it
|
||||||
|
*/
|
||||||
|
broadcast(SelectEvent(oldIndex, newIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the current item that should be played, or null if the queue is empty
|
||||||
|
*/
|
||||||
|
val item get() = getItem(this.index)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param index the index of the item to return
|
||||||
|
* @return the item at the given index, or null if the index is out of bounds
|
||||||
|
*/
|
||||||
|
fun getItem(index: Int) = streams.getOrNull(index)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of the given item using referential equality.
|
||||||
|
* May be null despite play queue contains identical item.
|
||||||
|
*
|
||||||
|
* @param item the item to find the index of
|
||||||
|
* @return the index of the given item
|
||||||
|
*/
|
||||||
|
fun indexOf(item: PlayQueueItem): Int = streams.indexOf(item)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the current size of play queue.
|
||||||
|
*/
|
||||||
|
fun size(): Int = streams.size
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the play queue is empty.
|
||||||
|
*
|
||||||
|
* @return whether the play queue is empty
|
||||||
|
*/
|
||||||
|
val isEmpty: Boolean
|
||||||
|
get() = streams.isEmpty()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the current play queue is shuffled.
|
||||||
|
*
|
||||||
|
* @return whether the play queue is shuffled
|
||||||
|
*/
|
||||||
|
val isShuffled: Boolean
|
||||||
|
get() = backup.isNotEmpty()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return an immutable view of the play queue
|
||||||
|
*/
|
||||||
|
fun getStreams(): List<PlayQueueItem> = Collections.unmodifiableList(streams)
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Write ops
|
||||||
|
////////////////////////////////////////////////////////////////////////// */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the current playing index by an offset amount.
|
||||||
|
*
|
||||||
|
* Will emit a [SelectEvent] if offset is non-zero.
|
||||||
|
*
|
||||||
|
* @param offset the offset relative to the current index
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun offsetIndex(offset: Int) {
|
||||||
|
this.index += offset
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies that a change has occurred.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun notifyChange() {
|
||||||
|
broadcast(AppendEvent(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends the given [PlayQueueItem]s to the current play queue.
|
||||||
|
*
|
||||||
|
* If the play queue is shuffled, then append the items to the backup queue as is and
|
||||||
|
* append the shuffle items to the play queue.
|
||||||
|
*
|
||||||
|
* Will emit a [AppendEvent] on any given context.
|
||||||
|
*
|
||||||
|
* @param items [PlayQueueItem]s to append
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun append(items: List<PlayQueueItem>) {
|
||||||
|
val itemList = items.toMutableList()
|
||||||
|
|
||||||
|
if (this.isShuffled) {
|
||||||
|
backup.addAll(itemList)
|
||||||
|
itemList.shuffle()
|
||||||
|
}
|
||||||
|
if (!streams.isEmpty() && streams.last().isAutoQueued && !itemList[0].isAutoQueued) {
|
||||||
|
streams.removeAt(streams.lastIndex)
|
||||||
|
}
|
||||||
|
streams.addAll(itemList)
|
||||||
|
|
||||||
|
broadcast(AppendEvent(itemList.size))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the item at the given index from the play queue.
|
||||||
|
*
|
||||||
|
* The current playing index will decrement if it is greater than the index being removed.
|
||||||
|
* On cases where the current playing index exceeds the playlist range, it is set to 0.
|
||||||
|
*
|
||||||
|
* Will emit a [RemoveEvent] if the index is within the play queue index range.
|
||||||
|
*
|
||||||
|
* @param index the index of the item to remove
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun remove(index: Int) {
|
||||||
|
if (index >= streams.size || index < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
removeInternal(index)
|
||||||
|
broadcast(RemoveEvent(index, this.index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report an exception for the item at the current index in order and skip to the next one
|
||||||
|
*
|
||||||
|
* This is done as a separate event as the underlying manager may have
|
||||||
|
* different implementation regarding exceptions.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun error() {
|
||||||
|
val oldIndex = this.index
|
||||||
|
queueIndex.incrementAndGet()
|
||||||
|
if (streams.size > queueIndex.get()) {
|
||||||
|
history.add(streams[queueIndex.get()])
|
||||||
|
}
|
||||||
|
broadcast(ErrorEvent(oldIndex, this.index))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun removeInternal(removeIndex: Int) {
|
||||||
|
val currentIndex = queueIndex.get()
|
||||||
|
val size = size()
|
||||||
|
|
||||||
|
if (currentIndex > removeIndex) {
|
||||||
|
queueIndex.decrementAndGet()
|
||||||
|
} else if (currentIndex >= size) {
|
||||||
|
queueIndex.set(currentIndex % (size - 1))
|
||||||
|
} else if (currentIndex == removeIndex && currentIndex == size - 1) {
|
||||||
|
queueIndex.set(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
backup.remove(getItem(removeIndex)!!)
|
||||||
|
|
||||||
|
history.remove(streams.removeAt(removeIndex))
|
||||||
|
if (streams.size > queueIndex.get()) {
|
||||||
|
history.add(streams[queueIndex.get()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves a queue item at the source index to the target index.
|
||||||
|
*
|
||||||
|
* If the item being moved is the currently playing, then the current playing index is set
|
||||||
|
* to that of the target.
|
||||||
|
* If the moved item is not the currently playing and moves to an index **AFTER** the
|
||||||
|
* current playing index, then the current playing index is decremented.
|
||||||
|
* Vice versa if the an item after the currently playing is moved **BEFORE**.
|
||||||
|
*
|
||||||
|
* @param source the original index of the item
|
||||||
|
* @param target the new index of the item
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun move(
|
||||||
|
source: Int,
|
||||||
|
target: Int,
|
||||||
|
) {
|
||||||
|
if (source < 0 || target < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (source >= streams.size || target >= streams.size) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val current = this.index
|
||||||
|
if (source == current) {
|
||||||
|
queueIndex.set(target)
|
||||||
|
} else if (source < current && target >= current) {
|
||||||
|
queueIndex.decrementAndGet()
|
||||||
|
} else if (source > current && target <= current) {
|
||||||
|
queueIndex.incrementAndGet()
|
||||||
|
}
|
||||||
|
|
||||||
|
val playQueueItem = streams.removeAt(source)
|
||||||
|
playQueueItem.isAutoQueued = false
|
||||||
|
streams.add(target, playQueueItem)
|
||||||
|
broadcast(MoveEvent(source, target))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the recovery record of the item at the index.
|
||||||
|
*
|
||||||
|
* Broadcasts a recovery event.
|
||||||
|
*
|
||||||
|
* @param index index of the item
|
||||||
|
* @param position the recovery position
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun setRecovery(
|
||||||
|
index: Int,
|
||||||
|
position: Long,
|
||||||
|
) {
|
||||||
|
streams.getOrNull(index)?.let {
|
||||||
|
it.recoveryPosition = position
|
||||||
|
broadcast(RecoveryEvent(index, position))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke the recovery record of the item at the index.
|
||||||
|
*
|
||||||
|
* Broadcasts a recovery event.
|
||||||
|
*
|
||||||
|
* @param index index of the item
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun unsetRecovery(index: Int) {
|
||||||
|
setRecovery(index, Long.Companion.MIN_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuffles the current play queue
|
||||||
|
*
|
||||||
|
* This method first backs up the existing play queue and item being played. Then a newly
|
||||||
|
* shuffled play queue will be generated along with currently playing item placed at the
|
||||||
|
* beginning of the queue. This item will also be added to the history.
|
||||||
|
*
|
||||||
|
* Will emit a [ReorderEvent] if shuffled.
|
||||||
|
*
|
||||||
|
* @implNote Does nothing if the queue has a size <= 2 (the currently playing video must stay on
|
||||||
|
* top, so shuffling a size-2 list does nothing)
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun shuffle() {
|
||||||
|
// Create a backup if it doesn't already exist
|
||||||
|
// Note: The backup-list has to be created at all cost (even when size <= 2).
|
||||||
|
// Otherwise it's not possible to enter shuffle-mode!
|
||||||
|
if (backup.isEmpty()) {
|
||||||
|
backup = streams.toMutableList()
|
||||||
|
}
|
||||||
|
// Can't shuffle a list that's empty or only has one element
|
||||||
|
if (size() <= 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val originalIndex = this.index
|
||||||
|
val currentItem = this.item
|
||||||
|
|
||||||
|
streams.shuffle()
|
||||||
|
|
||||||
|
// Move currentItem to the head of the queue
|
||||||
|
streams.remove(currentItem!!)
|
||||||
|
streams.add(0, currentItem)
|
||||||
|
queueIndex.set(0)
|
||||||
|
|
||||||
|
history.add(currentItem)
|
||||||
|
|
||||||
|
broadcast(ReorderEvent(originalIndex, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unshuffles the current play queue if a backup play queue exists.
|
||||||
|
*
|
||||||
|
* This method undoes shuffling and index will be set to the previously playing item if found,
|
||||||
|
* otherwise, the index will reset to 0.
|
||||||
|
*
|
||||||
|
* Will emit a [ReorderEvent] if a backup exists.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun unshuffle() {
|
||||||
|
if (backup.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val originIndex = this.index
|
||||||
|
val current = this.item
|
||||||
|
|
||||||
|
streams = backup
|
||||||
|
backup = mutableListOf()
|
||||||
|
|
||||||
|
val newIndex = streams.indexOf(current!!)
|
||||||
|
if (newIndex != -1) {
|
||||||
|
queueIndex.set(newIndex)
|
||||||
|
} else {
|
||||||
|
queueIndex.set(0)
|
||||||
|
}
|
||||||
|
if (streams.size > queueIndex.get()) {
|
||||||
|
history.add(streams[queueIndex.get()])
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast(ReorderEvent(originIndex, queueIndex.get()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects previous played item.
|
||||||
|
*
|
||||||
|
* This method removes currently playing item from history and
|
||||||
|
* starts playing the last item from history if it exists
|
||||||
|
*
|
||||||
|
* @return true if history is not empty and the item can be played
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun previous(): Boolean {
|
||||||
|
if (history.size <= 1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
history.removeAt(history.size - 1)
|
||||||
|
|
||||||
|
val last = history.removeAt(history.size - 1)
|
||||||
|
this.index = indexOf(last)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Compares two PlayQueues. Useful when a user switches players but queue is the same so
|
||||||
|
* we don't have to do anything with new queue.
|
||||||
|
* This method also gives a chance to track history of items in a queue in
|
||||||
|
* VideoDetailFragment without duplicating items from two identical queues
|
||||||
|
*/
|
||||||
|
override fun equals(o: Any?): Boolean = o is PlayQueue && streams == o.streams
|
||||||
|
|
||||||
|
override fun hashCode(): Int = streams.hashCode()
|
||||||
|
|
||||||
|
fun equalStreamsAndIndex(other: PlayQueue?): Boolean {
|
||||||
|
return equals(other) && other!!.index == this.index // NOSONAR: other is not null
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Rx Broadcast
|
||||||
|
////////////////////////////////////////////////////////////////////////// */
|
||||||
|
private fun broadcast(event: PlayQueueEvent) {
|
||||||
|
eventBroadcast?.onNext(event)
|
||||||
|
}
|
||||||
|
}
|
@ -1,142 +0,0 @@
|
|||||||
package org.schabi.newpipe.player.playqueue;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.Image;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
|
||||||
|
|
||||||
public class PlayQueueItem implements Serializable {
|
|
||||||
public static final long RECOVERY_UNSET = Long.MIN_VALUE;
|
|
||||||
private static final String EMPTY_STRING = "";
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final String title;
|
|
||||||
@NonNull
|
|
||||||
private final String url;
|
|
||||||
private final int serviceId;
|
|
||||||
private final long duration;
|
|
||||||
@NonNull
|
|
||||||
private final List<Image> thumbnails;
|
|
||||||
@NonNull
|
|
||||||
private final String uploader;
|
|
||||||
private final String uploaderUrl;
|
|
||||||
@NonNull
|
|
||||||
private final StreamType streamType;
|
|
||||||
|
|
||||||
private boolean isAutoQueued;
|
|
||||||
|
|
||||||
private long recoveryPosition;
|
|
||||||
private Throwable error;
|
|
||||||
|
|
||||||
PlayQueueItem(@NonNull final StreamInfo info) {
|
|
||||||
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
|
|
||||||
info.getThumbnails(), info.getUploaderName(),
|
|
||||||
info.getUploaderUrl(), info.getStreamType());
|
|
||||||
|
|
||||||
if (info.getStartPosition() > 0) {
|
|
||||||
setRecoveryPosition(info.getStartPosition() * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayQueueItem(@NonNull final StreamInfoItem item) {
|
|
||||||
this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
|
|
||||||
item.getThumbnails(), item.getUploaderName(),
|
|
||||||
item.getUploaderUrl(), item.getStreamType());
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("ParameterNumber")
|
|
||||||
private PlayQueueItem(@Nullable final String name, @Nullable final String url,
|
|
||||||
final int serviceId, final long duration,
|
|
||||||
final List<Image> thumbnails, @Nullable final String uploader,
|
|
||||||
final String uploaderUrl, @NonNull final StreamType streamType) {
|
|
||||||
this.title = name != null ? name : EMPTY_STRING;
|
|
||||||
this.url = url != null ? url : EMPTY_STRING;
|
|
||||||
this.serviceId = serviceId;
|
|
||||||
this.duration = duration;
|
|
||||||
this.thumbnails = thumbnails;
|
|
||||||
this.uploader = uploader != null ? uploader : EMPTY_STRING;
|
|
||||||
this.uploaderUrl = uploaderUrl;
|
|
||||||
this.streamType = streamType;
|
|
||||||
|
|
||||||
this.recoveryPosition = RECOVERY_UNSET;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public String getTitle() {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public String getUrl() {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getServiceId() {
|
|
||||||
return serviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getDuration() {
|
|
||||||
return duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public List<Image> getThumbnails() {
|
|
||||||
return thumbnails;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public String getUploader() {
|
|
||||||
return uploader;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUploaderUrl() {
|
|
||||||
return uploaderUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public StreamType getStreamType() {
|
|
||||||
return streamType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getRecoveryPosition() {
|
|
||||||
return recoveryPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*package-private*/ void setRecoveryPosition(final long recoveryPosition) {
|
|
||||||
this.recoveryPosition = recoveryPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public Throwable getError() {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public Single<StreamInfo> getStream() {
|
|
||||||
return ExtractorHelper.getStreamInfo(this.serviceId, this.url, false)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.doOnError(throwable -> error = throwable);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAutoQueued() {
|
|
||||||
return isAutoQueued;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Item States, keep external access out
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
public void setAutoQueued(final boolean autoQueued) {
|
|
||||||
isAutoQueued = autoQueued;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,71 @@
|
|||||||
|
package org.schabi.newpipe.player.playqueue
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import org.schabi.newpipe.extractor.Image
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper
|
||||||
|
import java.io.Serializable
|
||||||
|
import java.util.Objects
|
||||||
|
|
||||||
|
class PlayQueueItem private constructor(
|
||||||
|
val title: String,
|
||||||
|
val url: String,
|
||||||
|
val serviceId: Int,
|
||||||
|
val duration: Long,
|
||||||
|
val thumbnails: List<Image>,
|
||||||
|
val uploader: String,
|
||||||
|
val uploaderUrl: String?,
|
||||||
|
val streamType: StreamType,
|
||||||
|
) : Serializable {
|
||||||
|
//
|
||||||
|
// ////////////////////////////////////////////////////////////////////// */
|
||||||
|
// Item States, keep external access out
|
||||||
|
//
|
||||||
|
// ////////////////////////////////////////////////////////////////////// */
|
||||||
|
var isAutoQueued: Boolean = false
|
||||||
|
|
||||||
|
// package-private
|
||||||
|
var recoveryPosition = Long.Companion.MIN_VALUE
|
||||||
|
var error: Throwable? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
constructor(info: StreamInfo) : this(
|
||||||
|
info.name.orEmpty(),
|
||||||
|
info.url.orEmpty(),
|
||||||
|
info.serviceId,
|
||||||
|
info.duration,
|
||||||
|
info.thumbnails,
|
||||||
|
info.uploaderName.orEmpty(),
|
||||||
|
info.uploaderUrl,
|
||||||
|
info.streamType,
|
||||||
|
) {
|
||||||
|
if (info.startPosition > 0) {
|
||||||
|
this.recoveryPosition = info.startPosition * 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(item: StreamInfoItem) : this(
|
||||||
|
item.name.orEmpty(),
|
||||||
|
item.url.orEmpty(),
|
||||||
|
item.serviceId,
|
||||||
|
item.duration,
|
||||||
|
item.thumbnails,
|
||||||
|
item.uploaderName.orEmpty(),
|
||||||
|
item.uploaderUrl,
|
||||||
|
item.streamType,
|
||||||
|
)
|
||||||
|
|
||||||
|
val stream: Single<StreamInfo>
|
||||||
|
get() =
|
||||||
|
ExtractorHelper
|
||||||
|
.getStreamInfo(serviceId, url, false)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.doOnError { throwable -> error = throwable }
|
||||||
|
|
||||||
|
override fun equals(o: Any?) = o is PlayQueueItem && serviceId == o.serviceId && url == o.url
|
||||||
|
|
||||||
|
override fun hashCode() = Objects.hash(url, serviceId)
|
||||||
|
}
|
@ -3,17 +3,15 @@ package org.schabi.newpipe.player.playqueue;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotEquals;
|
|
||||||
|
|
||||||
public class PlayQueueItemTest {
|
public class PlayQueueItemTest {
|
||||||
|
|
||||||
public static final String URL = "MY_URL";
|
public static final String URL = "MY_URL";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void equalsMustNotBeOverloaded() {
|
public void equalsMustWork() {
|
||||||
final PlayQueueItem a = PlayQueueTest.makeItemWithUrl(URL);
|
final PlayQueueItem a = PlayQueueTest.makeItemWithUrl(URL);
|
||||||
final PlayQueueItem b = PlayQueueTest.makeItemWithUrl(URL);
|
final PlayQueueItem b = PlayQueueTest.makeItemWithUrl(URL);
|
||||||
assertEquals(a, a);
|
assertEquals(a, b);
|
||||||
assertNotEquals(a, b); // they should compare different even if they have the same data
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ package org.schabi.newpipe.player.playqueue;
|
|||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.junit.experimental.runners.Enclosed;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
|
||||||
@ -13,12 +15,14 @@ import java.util.Objects;
|
|||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
import static org.mockito.Mockito.spy;
|
import static org.mockito.Mockito.spy;
|
||||||
|
|
||||||
|
@RunWith(Enclosed.class)
|
||||||
@SuppressWarnings("checkstyle:HideUtilityClassConstructor")
|
@SuppressWarnings("checkstyle:HideUtilityClassConstructor")
|
||||||
public class PlayQueueTest {
|
public class PlayQueueTest {
|
||||||
static PlayQueue makePlayQueue(final int index, final List<PlayQueueItem> streams) {
|
static PlayQueue makePlayQueue(final int index, final List<PlayQueueItem> streams) {
|
||||||
@ -168,7 +172,7 @@ public class PlayQueueTest {
|
|||||||
final List<PlayQueueItem> streams = Collections.nCopies(5, item1);
|
final List<PlayQueueItem> streams = Collections.nCopies(5, item1);
|
||||||
final PlayQueue queue1 = makePlayQueue(0, streams);
|
final PlayQueue queue1 = makePlayQueue(0, streams);
|
||||||
final PlayQueue queue2 = makePlayQueue(0, streams);
|
final PlayQueue queue2 = makePlayQueue(0, streams);
|
||||||
assertTrue(queue1.equalStreams(queue2));
|
assertEquals(queue1, queue2);
|
||||||
assertTrue(queue1.equalStreamsAndIndex(queue2));
|
assertTrue(queue1.equalStreamsAndIndex(queue2));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,7 +181,7 @@ public class PlayQueueTest {
|
|||||||
final List<PlayQueueItem> streams = Collections.nCopies(5, item1);
|
final List<PlayQueueItem> streams = Collections.nCopies(5, item1);
|
||||||
final PlayQueue queue1 = makePlayQueue(1, streams);
|
final PlayQueue queue1 = makePlayQueue(1, streams);
|
||||||
final PlayQueue queue2 = makePlayQueue(4, streams);
|
final PlayQueue queue2 = makePlayQueue(4, streams);
|
||||||
assertTrue(queue1.equalStreams(queue2));
|
assertEquals(queue1, queue2);
|
||||||
assertFalse(queue1.equalStreamsAndIndex(queue2));
|
assertFalse(queue1.equalStreamsAndIndex(queue2));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +191,7 @@ public class PlayQueueTest {
|
|||||||
final List<PlayQueueItem> streams2 = Collections.nCopies(5, item2);
|
final List<PlayQueueItem> streams2 = Collections.nCopies(5, item2);
|
||||||
final PlayQueue queue1 = makePlayQueue(0, streams1);
|
final PlayQueue queue1 = makePlayQueue(0, streams1);
|
||||||
final PlayQueue queue2 = makePlayQueue(0, streams2);
|
final PlayQueue queue2 = makePlayQueue(0, streams2);
|
||||||
assertFalse(queue1.equalStreams(queue2));
|
assertNotEquals(queue1, queue2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -196,7 +200,7 @@ public class PlayQueueTest {
|
|||||||
final List<PlayQueueItem> streams2 = Collections.nCopies(6, item2);
|
final List<PlayQueueItem> streams2 = Collections.nCopies(6, item2);
|
||||||
final PlayQueue queue1 = makePlayQueue(0, streams1);
|
final PlayQueue queue1 = makePlayQueue(0, streams1);
|
||||||
final PlayQueue queue2 = makePlayQueue(0, streams2);
|
final PlayQueue queue2 = makePlayQueue(0, streams2);
|
||||||
assertFalse(queue1.equalStreams(queue2));
|
assertNotEquals(queue1, queue2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user