diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index a2c5ba142..f3450f59f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -27,6 +27,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.res.Resources; import android.graphics.Bitmap; import android.media.AudioManager; import android.media.audiofx.AudioEffect; @@ -34,6 +35,7 @@ import android.net.Uri; import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.text.TextUtils; +import android.util.DisplayMetrics; import android.util.Log; import android.view.View; @@ -70,6 +72,7 @@ import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvicto import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.Util; import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.assist.ImageSize; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; import org.schabi.newpipe.Downloader; diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 74e71e30e..9706fade5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -64,7 +64,6 @@ import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.old.PlayVideoActivity; import org.schabi.newpipe.player.playback.MediaSourceManager; -import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index ab4b5bd64..e0ef71e3f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -65,6 +65,7 @@ import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.playlist.PlayQueue; +import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ListHelper; @@ -395,20 +396,25 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. final Format format = group.getFormat(trackIndex); final boolean isSetCurrent = selectedTrackGroup.indexOf(format) != -1; - // If the source is extracted (e.g. mp4), then we use the resolution contained in the stream if (group.length == 1 && videoTrackGroups.length == availableStreams.size()) { - popupMenu.getMenu().add(qualityPopupMenuGroupId, acc, Menu.NONE, MediaFormat.getNameById(stream.format) + " " + stream.resolution + " (" + format.width + "x" + format.height + ")"); + // If the source is non-adaptive (extractor source), then we use the resolution contained in the stream if (isSetCurrent) qualityTextView.setText(stream.resolution); + + final String menuItem = MediaFormat.getNameById(stream.format) + " " + + stream.resolution + " (" + format.width + "x" + format.height + ")"; + popupMenu.getMenu().add(qualityPopupMenuGroupId, acc, Menu.NONE, menuItem); } else { - // Otherwise, we have a DASH source, which contains multiple formats and + // Otherwise, we have an adaptive source, which contains multiple formats and // thus have no inherent quality format + if (isSetCurrent) qualityTextView.setText(resolutionStringOf(format)); + final MediaFormat mediaFormat = MediaFormat.getFromMimeType(format.sampleMimeType); final String mediaName = mediaFormat == null ? format.sampleMimeType : mediaFormat.name; - final String resolution = resolutionStringOf(format); - popupMenu.getMenu().add(qualityPopupMenuGroupId, acc, Menu.NONE, mediaName + " " + resolution); - if (isSetCurrent) qualityTextView.setText(resolution); + final String menuItem = mediaName + " " + format.width + "x" + format.height; + popupMenu.getMenu().add(qualityPopupMenuGroupId, acc, Menu.NONE, menuItem); } + trackGroupInfos.add(new TrackGroupInfo(trackIndex, groupIndex, MediaFormat.getNameById(stream.format), stream.resolution, format)); acc++; } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java index c85161e60..fccd064c3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java @@ -19,6 +19,15 @@ import io.reactivex.disposables.Disposable; import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; +/** + * DeferredMediaSource is specifically designed to allow external control over when + * the source metadata are loaded while being compatible with ExoPlayer's playlists. + * + * This media source follows the structure of how NewPipeExtractor's + * {@link org.schabi.newpipe.extractor.stream.StreamInfoItem} is converted into + * {@link org.schabi.newpipe.extractor.stream.StreamInfo}. Once conversion is complete, + * this media source behaves identically as any other native media sources. + * */ public final class DeferredMediaSource implements MediaSource { private final String TAG = "DeferredMediaSource@" + Integer.toHexString(hashCode()); @@ -30,6 +39,9 @@ public final class DeferredMediaSource implements MediaSource { public final static int STATE_DISPOSED = 3; public interface Callback { + /** + * Player-specific MediaSource resolution from given StreamInfo. + * */ MediaSource sourceOf(final StreamInfo info); } @@ -51,6 +63,9 @@ public final class DeferredMediaSource implements MediaSource { this.state = STATE_INIT; } + /** + * Parameters are kept in the class for delayed preparation. + * */ @Override public void prepareSource(ExoPlayer exoPlayer, boolean isTopLevelSource, Listener listener) { this.exoPlayer = exoPlayer; @@ -62,6 +77,17 @@ public final class DeferredMediaSource implements MediaSource { return state; } + /** + * Externally controlled loading. This method fully prepares the source to be used + * like any other native MediaSource. + * + * Ideally, this should be called after this source has entered PREPARED state and + * called once only. + * + * If loading fails here, an error will be propagated out and result in a + * {@link com.google.android.exoplayer2.ExoPlaybackException}, which is delegated + * out to the player. + * */ public synchronized void load() { if (state != STATE_PREPARED || stream == null || loader != null) return; Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl()); 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 fa94e8b4e..2c87a7b0f 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 @@ -60,22 +60,37 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { .subscribe(getReactor()); } + /*////////////////////////////////////////////////////////////////////////// + // DeferredMediaSource listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public MediaSource sourceOf(StreamInfo info) { + return playbackListener.sourceOf(info); + } + /*////////////////////////////////////////////////////////////////////////// // Exposed Methods //////////////////////////////////////////////////////////////////////////*/ - /* - * Returns the media source index of the currently playing stream. - * */ + /** + * Returns the media source index of the currently playing stream. + * */ public int getCurrentSourceIndex() { return sourceToQueueIndex.indexOf(playQueue.getIndex()); } + /** + * Returns the play queue index of a given media source playlist index. + * */ public int getQueueIndexOf(final int sourceIndex) { if (sourceIndex < 0 || sourceIndex >= sourceToQueueIndex.size()) return -1; return sourceToQueueIndex.get(sourceIndex); } + /** + * Dispose the manager and releases all message buses and loaders. + * */ public void dispose() { if (playQueueReactor != null) playQueueReactor.cancel(); if (syncReactor != null) syncReactor.dispose(); @@ -90,6 +105,9 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { playQueue = null; } + /** + * Loads the current playing stream and the streams within its WINDOW_SIZE bound. + * */ public void load() { // The current item has higher priority final int currentIndex = playQueue.getIndex(); @@ -140,6 +158,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { remove(removeEvent.index()); break; } + // Reset the sources if the index to remove is the current playing index case INIT: case REORDER: tryBlock(); @@ -249,8 +268,12 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { // Media Source List Manipulation //////////////////////////////////////////////////////////////////////////*/ - // Insert source into playlist with position in respect to the play queue - // If the play queue index already exists, then the insert is ignored + /** + * Inserts a source into {@link DynamicConcatenatingMediaSource} with position + * in respect to the play queue. + * + * If the play queue index already exists, then the insert is ignored. + * */ private void insert(final int queueIndex, final DeferredMediaSource source) { if (queueIndex < 0) return; @@ -262,6 +285,11 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { } } + /** + * Removes a source from {@link DynamicConcatenatingMediaSource} with the given play queue index. + * + * If the play queue index does not exist, the removal is ignored. + * */ private void remove(final int queueIndex) { if (queueIndex < 0) return; @@ -276,9 +304,4 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { sourceToQueueIndex.set(i, sourceToQueueIndex.get(i) - 1); } } - - @Override - public MediaSource sourceOf(StreamInfo info) { - return playbackListener.sourceOf(info); - } } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java index 45d456ecf..65eecc62a 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java @@ -20,12 +20,6 @@ import io.reactivex.schedulers.Schedulers; public final class ExternalPlayQueue extends PlayQueue { private final String TAG = "ExternalPlayQueue@" + Integer.toHexString(hashCode()); - public static final String SERVICE_ID = "service_id"; - public static final String INDEX = "index"; - public static final String STREAMS = "streams"; - public static final String URL = "url"; - public static final String NEXT_PAGE_URL = "next_page_url"; - private static final int RETRY_COUNT = 2; private boolean isComplete; @@ -87,7 +81,7 @@ public final class ExternalPlayQueue extends PlayQueue { public void onError(@NonNull Throwable e) { Log.e(TAG, "Error fetching more playlist, marking playlist as complete.", e); isComplete = true; - append(Collections.emptyList()); + append(); // Notify change } }; } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java index f15acabe8..5b32ddfb2 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java @@ -25,6 +25,16 @@ import io.reactivex.Flowable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.subjects.BehaviorSubject; +/** + * 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 { private final String TAG = "PlayQueue@" + Integer.toHexString(hashCode()); @@ -54,6 +64,11 @@ public abstract class PlayQueue implements Serializable { // Playlist actions //////////////////////////////////////////////////////////////////////////*/ + /** + * Initializes the play queue message buses. + * + * Also starts a self reporter for logging if debug mode is enabled. + * */ public void init() { streamsEventBroadcast = BehaviorSubject.create(); indexEventBroadcast = BehaviorSubject.create(); @@ -66,6 +81,9 @@ public abstract class PlayQueue implements Serializable { if (DEBUG) broadcastReceiver.subscribe(getSelfReporter()); } + /** + * Dispose this play queue by stopping all message buses and clearing the playlist. + * */ public void dispose() { if (backup != null) backup.clear(); if (streams != null) streams.clear(); @@ -78,49 +96,82 @@ public abstract class PlayQueue implements Serializable { reportingReactor = null; } - // a queue is complete if it has loaded all items in an external playlist - // single stream or local queues are always complete + /** + * 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. + * */ public abstract boolean isComplete(); - // load partial queue in the background, does nothing if the queue is complete + /** + * Load partial queue in the background, does nothing if the queue is complete. + * */ public abstract void fetch(); /*////////////////////////////////////////////////////////////////////////// // Readonly ops //////////////////////////////////////////////////////////////////////////*/ + /** + * Returns the current index that should be played. + * */ public int getIndex() { return queueIndex.get(); } + /** + * Returns the current item that should be played. + * */ public PlayQueueItem getCurrent() { return get(getIndex()); } + /** + * Returns the item at the given index. + * May throw {@link IndexOutOfBoundsException}. + * */ public PlayQueueItem get(int index) { if (index >= streams.size() || streams.get(index) == null) 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. + * */ public int indexOf(final PlayQueueItem item) { // referential equality, can't think of a better way to do this // todo: better than this return streams.indexOf(item); } + /** + * Returns the current size of play queue. + * */ public int size() { return streams.size(); } + /** + * Checks if the play queue is empty. + * */ public boolean isEmpty() { return streams.isEmpty(); } + /** + * Returns an immutable view of the play queue. + * */ @NonNull public List getStreams() { return Collections.unmodifiableList(streams); } + /** + * Returns the play queue's update broadcast. + * May be null if the play queue message bus is not initialized. + * */ @NonNull public Flowable getBroadcastReceiver() { return broadcastReceiver; @@ -130,6 +181,13 @@ public abstract class PlayQueue implements Serializable { // Write ops //////////////////////////////////////////////////////////////////////////*/ + /** + * 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. + * */ public synchronized void setIndex(final int index) { if (index == getIndex()) return; @@ -141,34 +199,65 @@ public abstract class PlayQueue implements Serializable { indexEventBroadcast.onNext(new SelectEvent(newIndex)); } + /** + * Changes the current playing index by an offset amount. + * + * Will emit a {@link SelectEvent} if offset is non-zero. + * */ public synchronized void offsetIndex(final int offset) { setIndex(getIndex() + offset); } + /** + * Appends the given {@link PlayQueueItem}s to the current play queue. + * + * Will emit a {@link AppendEvent} on any given context. + * */ protected synchronized void append(final PlayQueueItem... items) { streams.addAll(Arrays.asList(items)); broadcast(new AppendEvent(items.length)); } + /** + * Appends the given {@link PlayQueueItem}s to the current play queue. + * + * Will emit a {@link AppendEvent} on any given context. + * */ protected synchronized void append(final Collection items) { streams.addAll(items); broadcast(new AppendEvent(items.size())); } + /** + * Removes the item at the given index from the play queue. + * + * The current playing index will decrement if greater than or equal to the index being removed. + * + * Will emit a {@link RemoveEvent} if the index is within the play queue index range. + * + * */ public synchronized void remove(final int index) { if (index >= streams.size() || index < 0) return; final boolean isCurrent = index == getIndex(); - streams.remove(index); - // Nudge the index if it becomes larger than the queue size - if (queueIndex.get() > size()) { - queueIndex.set(size() - 1); + if (queueIndex.get() >= index) { + queueIndex.decrementAndGet(); } + streams.remove(index); broadcast(new RemoveEvent(index, isCurrent)); } + /** + * 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 the index of + * the previously playing item. + * + * Will emit a {@link ReorderEvent} in any context. + * */ public synchronized void shuffle() { backup = new ArrayList<>(streams); final PlayQueueItem current = getCurrent(); @@ -178,6 +267,13 @@ public abstract class PlayQueue implements Serializable { broadcast(new ReorderEvent(true)); } + /** + * 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. + * + * Will emit a {@link ReorderEvent} if a backup exists. + * */ public synchronized void unshuffle() { if (backup == null) return; final PlayQueueItem current = getCurrent(); @@ -218,7 +314,7 @@ public abstract class PlayQueue implements Serializable { @Override public void onComplete() { - Log.d(TAG, "Broadcast is shut down."); + Log.d(TAG, "Broadcast is shutting down."); } }; } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/SinglePlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/SinglePlayQueue.java index 91156da5e..fc68e931a 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/SinglePlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/SinglePlayQueue.java @@ -5,8 +5,6 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; import java.util.Collections; public final class SinglePlayQueue extends PlayQueue { - public static final String STREAM = "stream"; - public SinglePlayQueue(final StreamInfo info) { super(0, Collections.singletonList(new PlayQueueItem(info))); }