diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index d3e3ff1df..094032a06 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -397,7 +397,7 @@ public final class Player implements PlaybackListener, Listener { && newQueue.size() == 1 && newQueue.getItem() != null && playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null && 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 // and we should retry in this case if (simpleExoPlayer.getPlaybackState() @@ -425,7 +425,7 @@ public final class Player implements PlaybackListener, Listener { && !samePlayQueue && !newQueue.isEmpty() && newQueue.getItem() != null - && newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) { + && newQueue.getItem().getRecoveryPosition() == Long.MIN_VALUE) { databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem()) .observeOn(AndroidSchedulers.mainThread()) // 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 - if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { + if (item.getRecoveryPosition() != Long.MIN_VALUE) { simpleExoPlayer.seekTo(playQueueIndex, item.getRecoveryPosition()); playQueue.unsetRecovery(playQueueIndex); } else { 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 88d7145bc..9092906fa 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 @@ -38,9 +38,9 @@ import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription; import io.reactivex.rxjava3.schedulers.Schedulers; 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.StreamInfoLoadException; -import static org.schabi.newpipe.player.playqueue.PlayQueue.DEBUG; import static org.schabi.newpipe.util.ServiceHelper.getCacheExpirationMillis; public class MediaSourceManager { diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt index a474b624b..1ae7e5cdb 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt @@ -1,189 +1,173 @@ -package org.schabi.newpipe.player.playqueue; +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; +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. - *
*/ -public abstract class PlayQueue implements Serializable { - public static final boolean DEBUG = MainActivity.DEBUG; - @NonNull - private final AtomicInteger queueIndex; - private final List+ * * Also starts a self reporter for logging if debug mode is enabled. - *
*/ - public void init() { - eventBroadcast = BehaviorSubject.create(); + fun init() { + eventBroadcast = BehaviorSubject.create() - broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER) + broadcastReceiver = + eventBroadcast!! + .toFlowable(BackpressureStrategy.BUFFER) .observeOn(AndroidSchedulers.mainThread()) - .startWithItem(new InitEvent()); + .startWithItem(InitEvent()) } /** * Dispose the play queue by stopping all message buses. */ - public void dispose() { - if (eventBroadcast != null) { - eventBroadcast.onComplete(); - } - - eventBroadcast = null; - broadcastReceiver = null; - disposed = true; + 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 */ - public abstract boolean isComplete(); + abstract val isComplete: Boolean /** * Load partial queue in the background, does nothing if the queue is complete. */ - public abstract void fetch(); + abstract fun 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. - *- * This method is guarded using in a circular manner for index exceeding the play queue size. - *
- *- * Will emit a {@link SelectEvent} if the index is not the current playing index. - *
- * - * @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 + ////////////////////////////////////////////////////////////////////////// */ + @set:Synchronized + var index: Int = 0 + /** + * @return the current index that should be played */ - broadcast(new SelectEvent(oldIndex, newIndex)); - } + 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 */ - @Nullable - public PlayQueueItem getItem() { - return getItem(getIndex()); - } + 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 */ - @Nullable - public PlayQueueItem getItem(final int index) { - if (index < 0 || index >= streams.size()) { - return null; - } - return streams.get(index); - } + fun getItem(index: Int) = streams.getOrNull(index) /** * Returns the index of the given item using referential equality. @@ -192,303 +176,280 @@ public abstract class PlayQueue implements Serializable { * @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); - } + fun indexOf(item: PlayQueueItem): Int = streams.indexOf(item) /** * @return the current size of play queue. */ - public int size() { - return streams.size(); - } + fun size(): Int = streams.size /** * Checks if the play queue is empty. * * @return whether the play queue is empty */ - public boolean isEmpty() { - return streams.isEmpty(); - } + val isEmpty: Boolean + get() = streams.isEmpty() /** * Determines if the current play queue is shuffled. * * @return whether the play queue is shuffled */ - public boolean isShuffled() { - return backup != null; - } + val isShuffled: Boolean + get() = backup.isNotEmpty() /** * @return an immutable view of the play queue */ - @NonNull - public List- * Will emit a {@link SelectEvent} if offset is non-zero. - *
+ * + * Will emit a [SelectEvent] if offset is non-zero. * * @param offset the offset relative to the current index */ - public synchronized void offsetIndex(final int offset) { - setIndex(getIndex() + offset); + @Synchronized + fun offsetIndex(offset: Int) { + this.index += offset } /** * Notifies that a change has occurred. */ - public synchronized void notifyChange() { - broadcast(new AppendEvent(0)); + @Synchronized + fun notifyChange() { + broadcast(AppendEvent(0)) } /** - * Appends the given {@link PlayQueueItem}s to the current play queue. - *+ * 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 {@link AppendEvent} on any given context. - *
* - * @param items {@link PlayQueueItem}s to append + * Will emit a [AppendEvent] on any given context. + * + * @param items [PlayQueueItem]s to append */ - public synchronized void append(@NonNull final List+ * * 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 {@link RemoveEvent} if the index is within the play queue index range. - *
+ * + * Will emit a [RemoveEvent] if the index is within the play queue index range. * * @param index the index of the item to remove */ - public synchronized void remove(final int index) { - if (index >= streams.size() || index < 0) { - return; + @Synchronized + fun remove(index: Int) { + if (index >= streams.size || index < 0) { + return } - removeInternal(index); - broadcast(new RemoveEvent(index, getIndex())); + 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. - *
*/ - public synchronized void error() { - final int oldIndex = getIndex(); - queueIndex.incrementAndGet(); - if (streams.size() > queueIndex.get()) { - history.add(streams.get(queueIndex.get())); + @Synchronized + fun error() { + val oldIndex = this.index + queueIndex.incrementAndGet() + if (streams.size > queueIndex.get()) { + history.add(streams[queueIndex.get()]) } - broadcast(new ErrorEvent(oldIndex, getIndex())); + broadcast(ErrorEvent(oldIndex, this.index)) } - private synchronized void removeInternal(final int removeIndex) { - final int currentIndex = queueIndex.get(); - final int size = size(); + @Synchronized + private fun removeInternal(removeIndex: Int) { + val currentIndex = queueIndex.get() + val size = size() if (currentIndex > removeIndex) { - queueIndex.decrementAndGet(); - + queueIndex.decrementAndGet() } else if (currentIndex >= size) { - queueIndex.set(currentIndex % (size - 1)); - + queueIndex.set(currentIndex % (size - 1)) } else if (currentIndex == removeIndex && currentIndex == size - 1) { - queueIndex.set(0); + queueIndex.set(0) } - if (backup != null) { - backup.remove(getItem(removeIndex)); - } + backup.remove(getItem(removeIndex)!!) - history.remove(streams.remove(removeIndex)); - if (streams.size() > queueIndex.get()) { - history.add(streams.get(queueIndex.get())); + 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 + * 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. - *
+ * 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 */ - public synchronized void move(final int source, final int target) { + @Synchronized + fun move( + source: Int, + target: Int, + ) { if (source < 0 || target < 0) { - return; + return } - if (source >= streams.size() || target >= streams.size()) { - return; + if (source >= streams.size || target >= streams.size) { + return } - final int current = getIndex(); + val current = this.index if (source == current) { - queueIndex.set(target); + queueIndex.set(target) } else if (source < current && target >= current) { - queueIndex.decrementAndGet(); + queueIndex.decrementAndGet() } else if (source > current && target <= current) { - queueIndex.incrementAndGet(); + queueIndex.incrementAndGet() } - final PlayQueueItem playQueueItem = streams.remove(source); - playQueueItem.setAutoQueued(false); - streams.add(target, playQueueItem); - broadcast(new MoveEvent(source, target)); + 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 */ - public synchronized void setRecovery(final int index, final long position) { - if (index < 0 || index >= streams.size()) { - return; + @Synchronized + fun setRecovery( + index: Int, + position: Long, + ) { + streams.getOrNull(index)?.let { + it.recoveryPosition = position + broadcast(RecoveryEvent(index, position)) } - - streams.get(index).setRecoveryPosition(position); - broadcast(new RecoveryEvent(index, position)); } /** * Revoke the recovery record of the item at the index. - *+ * * Broadcasts a recovery event. - *
* * @param index index of the item */ - public synchronized void unsetRecovery(final int index) { - setRecovery(index, PlayQueueItem.RECOVERY_UNSET); + @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 {@link ReorderEvent} if shuffled. - *
+ * + * 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) */ - public synchronized void shuffle() { + @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 == null) { - backup = new ArrayList<>(streams); + if (backup.isEmpty()) { + backup = streams.toMutableList() } // Can't shuffle a list that's empty or only has one element if (size() <= 2) { - return; + return } - final int originalIndex = getIndex(); - final PlayQueueItem currentItem = getItem(); + val originalIndex = this.index + val currentItem = this.item - Collections.shuffle(streams); + streams.shuffle() // Move currentItem to the head of the queue - streams.remove(currentItem); - streams.add(0, currentItem); - queueIndex.set(0); + streams.remove(currentItem!!) + streams.add(0, currentItem) + queueIndex.set(0) - history.add(currentItem); + history.add(currentItem) - broadcast(new ReorderEvent(originalIndex, 0)); + 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 {@link ReorderEvent} if a backup exists. - *
+ * + * Will emit a [ReorderEvent] if a backup exists. */ - public synchronized void unshuffle() { - if (backup == null) { - return; + @Synchronized + fun unshuffle() { + if (backup.isEmpty()) { + return } - final int originIndex = getIndex(); - final PlayQueueItem current = getItem(); + val originIndex = this.index + val current = this.item - streams = backup; - backup = null; + streams = backup + backup = mutableListOf() - final int newIndex = streams.indexOf(current); + val newIndex = streams.indexOf(current!!) if (newIndex != -1) { - queueIndex.set(newIndex); + queueIndex.set(newIndex) } else { - queueIndex.set(0); + queueIndex.set(0) } - if (streams.size() > queueIndex.get()) { - history.add(streams.get(queueIndex.get())); + if (streams.size > queueIndex.get()) { + history.add(streams[queueIndex.get()]) } - broadcast(new ReorderEvent(originIndex, queueIndex.get())); + broadcast(ReorderEvent(originIndex, queueIndex.get())) } /** @@ -498,18 +459,19 @@ public abstract class PlayQueue implements Serializable { * 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; + */ + @Synchronized + fun previous(): Boolean { + if (history.size <= 1) { + return false } - history.remove(history.size() - 1); + history.removeAt(history.size - 1) - final PlayQueueItem last = history.remove(history.size() - 1); - setIndex(indexOf(last)); + val last = history.removeAt(history.size - 1) + this.index = indexOf(last) - return true; + return true } /* @@ -518,31 +480,18 @@ public abstract class PlayQueue implements Serializable { * This method also gives a chance to track history of items in a queue in * VideoDetailFragment without duplicating items from two identical queues */ - @Override - public boolean equals(final Object o) { - return o instanceof PlayQueue playQueue && streams.equals(playQueue.streams); + 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 } - @Override - public int hashCode() { - return streams.hashCode(); - } - - public boolean equalStreamsAndIndex(@Nullable final PlayQueue other) { - return equals(other) && other.getIndex() == getIndex(); //NOSONAR: other is not null - } - - public boolean isDisposed() { - return disposed; - } /*////////////////////////////////////////////////////////////////////////// - // Rx Broadcast - //////////////////////////////////////////////////////////////////////////*/ - - private void broadcast(@NonNull final PlayQueueEvent event) { - if (eventBroadcast != null) { - eventBroadcast.onNext(event); - } + // Rx Broadcast + ////////////////////////////////////////////////////////////////////////// */ + private fun broadcast(event: PlayQueueEvent) { + eventBroadcast?.onNext(event) } } - diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.kt b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.kt index 8f41ceb60..d6b4b0402 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.kt +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.kt @@ -1,155 +1,71 @@ -package org.schabi.newpipe.player.playqueue; +package org.schabi.newpipe.player.playqueue -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; +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 -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; +class PlayQueueItem private constructor( + val title: String, + val url: String, + val serviceId: Int, + val duration: Long, + val thumbnails: List