-Added documentations for play queue components.
This commit is contained in:
parent
80f3e3c3b6
commit
c75c2d0f21
8 changed files with 179 additions and 34 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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.
|
||||
* */
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.<PlayQueueItem>emptyList());
|
||||
append(); // Notify change
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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<PlayQueueItem> 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<PlayQueueMessage> 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<PlayQueueItem> 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.");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue