-Added documentations for play queue components.

This commit is contained in:
John Zhen M 2017-09-28 19:37:04 -07:00 committed by John Zhen Mo
parent 80f3e3c3b6
commit c75c2d0f21
8 changed files with 179 additions and 34 deletions

View File

@ -27,6 +27,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.audiofx.AudioEffect; import android.media.audiofx.AudioEffect;
@ -34,6 +35,7 @@ import android.net.Uri;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.view.View; 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.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageSize;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
import org.schabi.newpipe.Downloader; import org.schabi.newpipe.Downloader;

View File

@ -64,7 +64,6 @@ import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.old.PlayVideoActivity; import org.schabi.newpipe.player.old.PlayVideoActivity;
import org.schabi.newpipe.player.playback.MediaSourceManager; import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;

View File

@ -65,6 +65,7 @@ import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue;
import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ListHelper; 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 Format format = group.getFormat(trackIndex);
final boolean isSetCurrent = selectedTrackGroup.indexOf(format) != -1; 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()) { 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); 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 { } 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 // thus have no inherent quality format
if (isSetCurrent) qualityTextView.setText(resolutionStringOf(format));
final MediaFormat mediaFormat = MediaFormat.getFromMimeType(format.sampleMimeType); final MediaFormat mediaFormat = MediaFormat.getFromMimeType(format.sampleMimeType);
final String mediaName = mediaFormat == null ? format.sampleMimeType : mediaFormat.name; final String mediaName = mediaFormat == null ? format.sampleMimeType : mediaFormat.name;
final String resolution = resolutionStringOf(format); final String menuItem = mediaName + " " + format.width + "x" + format.height;
popupMenu.getMenu().add(qualityPopupMenuGroupId, acc, Menu.NONE, mediaName + " " + resolution); popupMenu.getMenu().add(qualityPopupMenuGroupId, acc, Menu.NONE, menuItem);
if (isSetCurrent) qualityTextView.setText(resolution);
} }
trackGroupInfos.add(new TrackGroupInfo(trackIndex, groupIndex, MediaFormat.getNameById(stream.format), stream.resolution, format)); trackGroupInfos.add(new TrackGroupInfo(trackIndex, groupIndex, MediaFormat.getNameById(stream.format), stream.resolution, format));
acc++; acc++;
} }

View File

@ -19,6 +19,15 @@ import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer; import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers; 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 { public final class DeferredMediaSource implements MediaSource {
private final String TAG = "DeferredMediaSource@" + Integer.toHexString(hashCode()); 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 final static int STATE_DISPOSED = 3;
public interface Callback { public interface Callback {
/**
* Player-specific MediaSource resolution from given StreamInfo.
* */
MediaSource sourceOf(final StreamInfo info); MediaSource sourceOf(final StreamInfo info);
} }
@ -51,6 +63,9 @@ public final class DeferredMediaSource implements MediaSource {
this.state = STATE_INIT; this.state = STATE_INIT;
} }
/**
* Parameters are kept in the class for delayed preparation.
* */
@Override @Override
public void prepareSource(ExoPlayer exoPlayer, boolean isTopLevelSource, Listener listener) { public void prepareSource(ExoPlayer exoPlayer, boolean isTopLevelSource, Listener listener) {
this.exoPlayer = exoPlayer; this.exoPlayer = exoPlayer;
@ -62,6 +77,17 @@ public final class DeferredMediaSource implements MediaSource {
return state; 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() { public synchronized void load() {
if (state != STATE_PREPARED || stream == null || loader != null) return; if (state != STATE_PREPARED || stream == null || loader != null) return;
Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl()); Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl());

View File

@ -60,22 +60,37 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
.subscribe(getReactor()); .subscribe(getReactor());
} }
/*//////////////////////////////////////////////////////////////////////////
// DeferredMediaSource listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public MediaSource sourceOf(StreamInfo info) {
return playbackListener.sourceOf(info);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Exposed Methods // 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() { public int getCurrentSourceIndex() {
return sourceToQueueIndex.indexOf(playQueue.getIndex()); return sourceToQueueIndex.indexOf(playQueue.getIndex());
} }
/**
* Returns the play queue index of a given media source playlist index.
* */
public int getQueueIndexOf(final int sourceIndex) { public int getQueueIndexOf(final int sourceIndex) {
if (sourceIndex < 0 || sourceIndex >= sourceToQueueIndex.size()) return -1; if (sourceIndex < 0 || sourceIndex >= sourceToQueueIndex.size()) return -1;
return sourceToQueueIndex.get(sourceIndex); return sourceToQueueIndex.get(sourceIndex);
} }
/**
* Dispose the manager and releases all message buses and loaders.
* */
public void dispose() { public void dispose() {
if (playQueueReactor != null) playQueueReactor.cancel(); if (playQueueReactor != null) playQueueReactor.cancel();
if (syncReactor != null) syncReactor.dispose(); if (syncReactor != null) syncReactor.dispose();
@ -90,6 +105,9 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
playQueue = null; playQueue = null;
} }
/**
* Loads the current playing stream and the streams within its WINDOW_SIZE bound.
* */
public void load() { public void load() {
// The current item has higher priority // The current item has higher priority
final int currentIndex = playQueue.getIndex(); final int currentIndex = playQueue.getIndex();
@ -140,6 +158,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
remove(removeEvent.index()); remove(removeEvent.index());
break; break;
} }
// Reset the sources if the index to remove is the current playing index
case INIT: case INIT:
case REORDER: case REORDER:
tryBlock(); tryBlock();
@ -249,8 +268,12 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
// Media Source List Manipulation // 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) { private void insert(final int queueIndex, final DeferredMediaSource source) {
if (queueIndex < 0) return; 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) { private void remove(final int queueIndex) {
if (queueIndex < 0) return; if (queueIndex < 0) return;
@ -276,9 +304,4 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
sourceToQueueIndex.set(i, sourceToQueueIndex.get(i) - 1); sourceToQueueIndex.set(i, sourceToQueueIndex.get(i) - 1);
} }
} }
@Override
public MediaSource sourceOf(StreamInfo info) {
return playbackListener.sourceOf(info);
}
} }

View File

@ -20,12 +20,6 @@ import io.reactivex.schedulers.Schedulers;
public final class ExternalPlayQueue extends PlayQueue { public final class ExternalPlayQueue extends PlayQueue {
private final String TAG = "ExternalPlayQueue@" + Integer.toHexString(hashCode()); 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 static final int RETRY_COUNT = 2;
private boolean isComplete; private boolean isComplete;
@ -87,7 +81,7 @@ public final class ExternalPlayQueue extends PlayQueue {
public void onError(@NonNull Throwable e) { public void onError(@NonNull Throwable e) {
Log.e(TAG, "Error fetching more playlist, marking playlist as complete.", e); Log.e(TAG, "Error fetching more playlist, marking playlist as complete.", e);
isComplete = true; isComplete = true;
append(Collections.<PlayQueueItem>emptyList()); append(); // Notify change
} }
}; };
} }

View File

@ -25,6 +25,16 @@ import io.reactivex.Flowable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.subjects.BehaviorSubject; 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 { public abstract class PlayQueue implements Serializable {
private final String TAG = "PlayQueue@" + Integer.toHexString(hashCode()); private final String TAG = "PlayQueue@" + Integer.toHexString(hashCode());
@ -54,6 +64,11 @@ public abstract class PlayQueue implements Serializable {
// Playlist actions // Playlist actions
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
/**
* Initializes the play queue message buses.
*
* Also starts a self reporter for logging if debug mode is enabled.
* */
public void init() { public void init() {
streamsEventBroadcast = BehaviorSubject.create(); streamsEventBroadcast = BehaviorSubject.create();
indexEventBroadcast = BehaviorSubject.create(); indexEventBroadcast = BehaviorSubject.create();
@ -66,6 +81,9 @@ public abstract class PlayQueue implements Serializable {
if (DEBUG) broadcastReceiver.subscribe(getSelfReporter()); if (DEBUG) broadcastReceiver.subscribe(getSelfReporter());
} }
/**
* Dispose this play queue by stopping all message buses and clearing the playlist.
* */
public void dispose() { public void dispose() {
if (backup != null) backup.clear(); if (backup != null) backup.clear();
if (streams != null) streams.clear(); if (streams != null) streams.clear();
@ -78,49 +96,82 @@ public abstract class PlayQueue implements Serializable {
reportingReactor = null; 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(); 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(); public abstract void fetch();
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Readonly ops // Readonly ops
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
/**
* Returns the current index that should be played.
* */
public int getIndex() { public int getIndex() {
return queueIndex.get(); return queueIndex.get();
} }
/**
* Returns the current item that should be played.
* */
public PlayQueueItem getCurrent() { public PlayQueueItem getCurrent() {
return get(getIndex()); return get(getIndex());
} }
/**
* Returns the item at the given index.
* May throw {@link IndexOutOfBoundsException}.
* */
public PlayQueueItem get(int index) { public PlayQueueItem get(int index) {
if (index >= streams.size() || streams.get(index) == null) return null; if (index >= streams.size() || streams.get(index) == null) return null;
return streams.get(index); 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) { public int indexOf(final PlayQueueItem item) {
// referential equality, can't think of a better way to do this // referential equality, can't think of a better way to do this
// todo: better than this // todo: better than this
return streams.indexOf(item); return streams.indexOf(item);
} }
/**
* Returns the current size of play queue.
* */
public int size() { public int size() {
return streams.size(); return streams.size();
} }
/**
* Checks if the play queue is empty.
* */
public boolean isEmpty() { public boolean isEmpty() {
return streams.isEmpty(); return streams.isEmpty();
} }
/**
* Returns an immutable view of the play queue.
* */
@NonNull @NonNull
public List<PlayQueueItem> getStreams() { public List<PlayQueueItem> getStreams() {
return Collections.unmodifiableList(streams); return Collections.unmodifiableList(streams);
} }
/**
* Returns the play queue's update broadcast.
* May be null if the play queue message bus is not initialized.
* */
@NonNull @NonNull
public Flowable<PlayQueueMessage> getBroadcastReceiver() { public Flowable<PlayQueueMessage> getBroadcastReceiver() {
return broadcastReceiver; return broadcastReceiver;
@ -130,6 +181,13 @@ public abstract class PlayQueue implements Serializable {
// Write ops // 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) { public synchronized void setIndex(final int index) {
if (index == getIndex()) return; if (index == getIndex()) return;
@ -141,34 +199,65 @@ public abstract class PlayQueue implements Serializable {
indexEventBroadcast.onNext(new SelectEvent(newIndex)); 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) { public synchronized void offsetIndex(final int offset) {
setIndex(getIndex() + 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) { protected synchronized void append(final PlayQueueItem... items) {
streams.addAll(Arrays.asList(items)); streams.addAll(Arrays.asList(items));
broadcast(new AppendEvent(items.length)); 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<PlayQueueItem> items) { protected synchronized void append(final Collection<PlayQueueItem> items) {
streams.addAll(items); streams.addAll(items);
broadcast(new AppendEvent(items.size())); 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) { public synchronized void remove(final int index) {
if (index >= streams.size() || index < 0) return; if (index >= streams.size() || index < 0) return;
final boolean isCurrent = index == getIndex(); final boolean isCurrent = index == getIndex();
streams.remove(index); if (queueIndex.get() >= index) {
// Nudge the index if it becomes larger than the queue size queueIndex.decrementAndGet();
if (queueIndex.get() > size()) {
queueIndex.set(size() - 1);
} }
streams.remove(index);
broadcast(new RemoveEvent(index, isCurrent)); 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() { public synchronized void shuffle() {
backup = new ArrayList<>(streams); backup = new ArrayList<>(streams);
final PlayQueueItem current = getCurrent(); final PlayQueueItem current = getCurrent();
@ -178,6 +267,13 @@ public abstract class PlayQueue implements Serializable {
broadcast(new ReorderEvent(true)); 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() { public synchronized void unshuffle() {
if (backup == null) return; if (backup == null) return;
final PlayQueueItem current = getCurrent(); final PlayQueueItem current = getCurrent();
@ -218,7 +314,7 @@ public abstract class PlayQueue implements Serializable {
@Override @Override
public void onComplete() { public void onComplete() {
Log.d(TAG, "Broadcast is shut down."); Log.d(TAG, "Broadcast is shutting down.");
} }
}; };
} }

View File

@ -5,8 +5,6 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import java.util.Collections; import java.util.Collections;
public final class SinglePlayQueue extends PlayQueue { public final class SinglePlayQueue extends PlayQueue {
public static final String STREAM = "stream";
public SinglePlayQueue(final StreamInfo info) { public SinglePlayQueue(final StreamInfo info) {
super(0, Collections.singletonList(new PlayQueueItem(info))); super(0, Collections.singletonList(new PlayQueueItem(info)));
} }