-Fixed deferred media source from releasing reused resources.

-Fixed external play queue to load more than once.
-Fixed wrong item removal due to player error.
-Added new event to indicate error to play queue.
-Changed player error to skip item instead of removing.
-Modified play queue adapter to update changed items only.
-Removed headers from play queue adapter.
-Merged event broadcast on play queue.
-Changed toast on player error.
-Modified remove event to no longer indicate current index status.
-Modified move event to no longer indicate randomization status.
-Added shuffle check to play queue.
This commit is contained in:
John Zhen M 2017-10-07 21:39:34 -07:00 committed by John Zhen Mo
parent a9aee21e58
commit eebd83d6ac
13 changed files with 304 additions and 171 deletions

View File

@ -36,6 +36,7 @@ import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.util.Log; import android.util.Log;
import android.widget.RemoteViews; import android.widget.RemoteViews;
import android.widget.Toast;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
@ -402,6 +403,7 @@ public final class BackgroundPlayer extends Service {
@Override @Override
public void onError(Exception exception) { public void onError(Exception exception) {
exception.printStackTrace(); exception.printStackTrace();
Toast.makeText(context, "Failed to play this audio", Toast.LENGTH_SHORT).show();
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////

View File

@ -33,12 +33,12 @@ public class BackgroundPlayerActivity extends AppCompatActivity
private static final String TAG = "BGPlayerActivity"; private static final String TAG = "BGPlayerActivity";
private boolean isServiceBound; private boolean serviceBound;
private ServiceConnection serviceConnection; private ServiceConnection serviceConnection;
private BackgroundPlayer.BasePlayerImpl player; private BackgroundPlayer.BasePlayerImpl player;
private boolean isSeeking; private boolean seeking;
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Views // Views
@ -104,9 +104,9 @@ public class BackgroundPlayerActivity extends AppCompatActivity
@Override @Override
protected void onStop() { protected void onStop() {
super.onStop(); super.onStop();
if(isServiceBound) { if(serviceBound) {
unbindService(serviceConnection); unbindService(serviceConnection);
isServiceBound = false; serviceBound = false;
} }
} }
@ -119,7 +119,7 @@ public class BackgroundPlayerActivity extends AppCompatActivity
@Override @Override
public void onServiceDisconnected(ComponentName name) { public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "Background player service is disconnected"); Log.d(TAG, "Background player service is disconnected");
isServiceBound = false; serviceBound = false;
player = null; player = null;
finish(); finish();
} }
@ -132,7 +132,7 @@ public class BackgroundPlayerActivity extends AppCompatActivity
if (player == null) { if (player == null) {
finish(); finish();
} else { } else {
isServiceBound = true; serviceBound = true;
buildComponents(); buildComponents();
player.setActivityListener(BackgroundPlayerActivity.this); player.setActivityListener(BackgroundPlayerActivity.this);
@ -220,13 +220,13 @@ public class BackgroundPlayerActivity extends AppCompatActivity
@Override @Override
public void onStartTrackingTouch(SeekBar seekBar) { public void onStartTrackingTouch(SeekBar seekBar) {
isSeeking = true; seeking = true;
} }
@Override @Override
public void onStopTrackingTouch(SeekBar seekBar) { public void onStopTrackingTouch(SeekBar seekBar) {
player.simpleExoPlayer.seekTo(seekBar.getProgress()); player.simpleExoPlayer.seekTo(seekBar.getProgress());
isSeeking = false; seeking = false;
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@ -284,7 +284,7 @@ public class BackgroundPlayerActivity extends AppCompatActivity
progressEndTime.setText(Localization.getDurationString(duration / 1000)); progressEndTime.setText(Localization.getDurationString(duration / 1000));
// Set current time if not seeking // Set current time if not seeking
if (!isSeeking) { if (!seeking) {
progressSeekBar.setProgress(currentProgress); progressSeekBar.setProgress(currentProgress);
progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000)); progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000));
} }

View File

@ -664,8 +664,22 @@ public abstract class BasePlayer implements Player.EventListener,
@Override @Override
public void onPlayerError(ExoPlaybackException error) { public void onPlayerError(ExoPlaybackException error) {
if (DEBUG) Log.d(TAG, "onPlayerError() called with: error = [" + error + "]"); if (DEBUG) Log.d(TAG, "onPlayerError() called with: error = [" + error + "]");
playQueue.remove(playQueue.getIndex());
onError(error); // If the current window is seekable, then the error is produced by transitioning into
// bad window, therefore we simply increment the current index.
// This is done because ExoPlayer reports the exception before window is
// transitioned due to seamless playback.
if (!simpleExoPlayer.isCurrentWindowSeekable()) {
playQueue.error();
onError(error);
} else {
playQueue.offsetIndex(+1);
}
// Player error causes ExoPlayer to go back to IDLE state, which requires resetting
// preparing a new media source.
playbackManager.reset();
playbackManager.load();
} }
@Override @Override

View File

@ -30,27 +30,37 @@ import io.reactivex.schedulers.Schedulers;
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());
private int state = -1; /**
* This state indicates the {@link DeferredMediaSource} has just been initialized or reset.
* The source must be prepared and loaded again before playback.
* */
public final static int STATE_INIT = 0; public final static int STATE_INIT = 0;
/**
* This state indicates the {@link DeferredMediaSource} has been prepared and is ready to load.
* */
public final static int STATE_PREPARED = 1; public final static int STATE_PREPARED = 1;
/**
* This state indicates the {@link DeferredMediaSource} has been loaded without errors and
* is ready for playback.
* */
public final static int STATE_LOADED = 2; public final static int STATE_LOADED = 2;
public final static int STATE_DISPOSED = 3;
public interface Callback { public interface Callback {
/** /**
* Player-specific MediaSource resolution from given StreamInfo. * Player-specific {@link com.google.android.exoplayer2.source.MediaSource} resolution
* from a given StreamInfo.
* */ * */
MediaSource sourceOf(final StreamInfo info); MediaSource sourceOf(final StreamInfo info);
} }
private PlayQueueItem stream; private PlayQueueItem stream;
private Callback callback; private Callback callback;
private int state;
private MediaSource mediaSource; private MediaSource mediaSource;
/* Custom internal objects */
private Disposable loader; private Disposable loader;
private ExoPlayer exoPlayer; private ExoPlayer exoPlayer;
private Listener listener; private Listener listener;
private Throwable error; private Throwable error;
@ -62,6 +72,17 @@ public final class DeferredMediaSource implements MediaSource {
this.state = STATE_INIT; this.state = STATE_INIT;
} }
/**
* Returns the current state of the {@link DeferredMediaSource}.
*
* @see DeferredMediaSource#STATE_INIT
* @see DeferredMediaSource#STATE_PREPARED
* @see DeferredMediaSource#STATE_LOADED
* */
public int state() {
return state;
}
/** /**
* Parameters are kept in the class for delayed preparation. * Parameters are kept in the class for delayed preparation.
* */ * */
@ -72,54 +93,37 @@ public final class DeferredMediaSource implements MediaSource {
this.state = STATE_PREPARED; this.state = STATE_PREPARED;
} }
public int state() {
return state;
}
/** /**
* Externally controlled loading. This method fully prepares the source to be used * Externally controlled loading. This method fully prepares the source to be used
* like any other native MediaSource. * like any other native {@link com.google.android.exoplayer2.source.MediaSource}.
* *
* Ideally, this should be called after this source has entered PREPARED state and * Ideally, this should be called after this source has entered PREPARED state and
* called once only. * called once only.
* *
* If loading fails here, an error will be propagated out and result in a * If loading fails here, an error will be propagated out and result in an
* {@link com.google.android.exoplayer2.ExoPlaybackException}, which is delegated * {@link com.google.android.exoplayer2.ExoPlaybackException ExoPlaybackException}, which is delegated
* to the player. * to the player.
* */ * */
public synchronized void load() { public synchronized void load() {
if (state != STATE_PREPARED || stream == null || loader != null) return; if (stream == null) {
Log.e(TAG, "Stream Info missing, media source loading terminated.");
return;
}
if (state != STATE_PREPARED || loader != null) return;
Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl()); Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl());
final Consumer<StreamInfo> onSuccess = new Consumer<StreamInfo>() { final Consumer<StreamInfo> onSuccess = new Consumer<StreamInfo>() {
@Override @Override
public void accept(StreamInfo streamInfo) throws Exception { public void accept(StreamInfo streamInfo) throws Exception {
Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl()); onStreamInfoReceived(streamInfo);
state = STATE_LOADED;
if (exoPlayer == null || listener == null || streamInfo == null) {
error = new Throwable("Stream info loading failed. URL: " + stream.getUrl());
return;
}
mediaSource = callback.sourceOf(streamInfo);
if (mediaSource == null) {
error = new Throwable("Unable to resolve source from stream info. URL: " + stream.getUrl() +
", audio count: " + streamInfo.audio_streams.size() +
", video count: " + streamInfo.video_only_streams.size() + streamInfo.video_streams.size());
return;
}
mediaSource.prepareSource(exoPlayer, false, listener);
} }
}; };
final Consumer<Throwable> onError = new Consumer<Throwable>() { final Consumer<Throwable> onError = new Consumer<Throwable>() {
@Override @Override
public void accept(Throwable throwable) throws Exception { public void accept(Throwable throwable) throws Exception {
Log.e(TAG, "Loading error:", throwable); onStreamInfoError(throwable);
error = throwable;
state = STATE_LOADED;
} }
}; };
@ -129,6 +133,38 @@ public final class DeferredMediaSource implements MediaSource {
.subscribe(onSuccess, onError); .subscribe(onSuccess, onError);
} }
private void onStreamInfoReceived(final StreamInfo streamInfo) {
Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl());
state = STATE_LOADED;
if (exoPlayer == null || listener == null || streamInfo == null) {
error = new Throwable("Stream info loading failed. URL: " + stream.getUrl());
return;
}
mediaSource = callback.sourceOf(streamInfo);
if (mediaSource == null) {
error = new Throwable("Unable to resolve source from stream info. URL: " + stream.getUrl() +
", audio count: " + streamInfo.audio_streams.size() +
", video count: " + streamInfo.video_only_streams.size() + streamInfo.video_streams.size());
return;
}
mediaSource.prepareSource(exoPlayer, false, listener);
}
private void onStreamInfoError(final Throwable throwable) {
Log.e(TAG, "Loading error:", throwable);
error = throwable;
state = STATE_LOADED;
}
/**
* Delegate all errors to the player after {@link #load() load} is complete.
*
* Specifically, this method is called after an exception has occurred during loading or
* {@link com.google.android.exoplayer2.source.MediaSource#prepareSource(ExoPlayer, boolean, Listener) prepareSource}.
* */
@Override @Override
public void maybeThrowSourceInfoRefreshError() throws IOException { public void maybeThrowSourceInfoRefreshError() throws IOException {
if (error != null) { if (error != null) {
@ -145,19 +181,27 @@ public final class DeferredMediaSource implements MediaSource {
return mediaSource.createPeriod(mediaPeriodId, allocator); return mediaSource.createPeriod(mediaPeriodId, allocator);
} }
/**
* Releases the media period (buffers).
*
* This may be called after {@link #releaseSource releaseSource}.
* */
@Override @Override
public void releasePeriod(MediaPeriod mediaPeriod) { public void releasePeriod(MediaPeriod mediaPeriod) {
if (mediaSource == null) { mediaSource.releasePeriod(mediaPeriod);
Log.e(TAG, "releasePeriod() called when media source is null, memory leak may have occurred.");
} else {
mediaSource.releasePeriod(mediaPeriod);
}
} }
/**
* Cleans up all internal custom objects creating during loading.
*
* This method is called when the parent {@link com.google.android.exoplayer2.source.MediaSource}
* is released or when the player is stopped.
*
* This method should not release or set null the resources passed in through the constructor.
* This method should not set null the internal {@link com.google.android.exoplayer2.source.MediaSource}.
* */
@Override @Override
public void releaseSource() { public void releaseSource() {
state = STATE_DISPOSED;
if (mediaSource != null) { if (mediaSource != null) {
mediaSource.releaseSource(); mediaSource.releaseSource();
} }
@ -166,9 +210,11 @@ public final class DeferredMediaSource implements MediaSource {
} }
/* Do not set mediaSource as null here as it may be called through releasePeriod */ /* Do not set mediaSource as null here as it may be called through releasePeriod */
stream = null; loader = null;
callback = null;
exoPlayer = null; exoPlayer = null;
listener = null; listener = null;
error = null;
state = STATE_INIT;
} }
} }

View File

@ -107,11 +107,13 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
/** /**
* Loads the current playing stream and the streams within its WINDOW_SIZE bound. * Loads the current playing stream and the streams within its WINDOW_SIZE bound.
*
* Unblocks the player once the item at the current index is loaded.
* */ * */
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();
final PlayQueueItem currentItem = playQueue.get(currentIndex); final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
if (currentItem == null) return; if (currentItem == null) return;
load(currentItem); load(currentItem);
@ -121,12 +123,24 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
final int rightBound = Math.min(playQueue.size(), rightLimit); final int rightBound = Math.min(playQueue.size(), rightLimit);
final List<PlayQueueItem> items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound)); final List<PlayQueueItem> items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound));
// Do a round robin
final int excess = rightLimit - playQueue.size(); final int excess = rightLimit - playQueue.size();
if (excess >= 0) items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); if (excess >= 0) items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
for (final PlayQueueItem item: items) load(item); for (final PlayQueueItem item: items) load(item);
} }
/**
* Blocks the player and repopulate the sources.
*
* Does not ensure the player is unblocked and should be done explicitly through {@link #load() load}.
* */
public void reset() {
tryBlock();
resetSources();
populateSources();
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Event Reactor // Event Reactor
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -141,44 +155,8 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
} }
@Override @Override
public void onNext(@NonNull PlayQueueMessage event) { public void onNext(@NonNull PlayQueueMessage playQueueMessage) {
// why no pattern matching in Java =( onPlayQueueChanged(playQueueMessage);
switch (event.type()) {
case APPEND:
populateSources();
break;
case SELECT:
if (isCurrentIndexLoaded()) {
sync();
}
break;
case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event;
if (!removeEvent.isCurrent()) {
remove(removeEvent.index());
break;
}
// Reset the sources if the index to remove is the current playing index
case INIT:
case REORDER:
tryBlock();
resetSources();
populateSources();
break;
default:
break;
}
if (!isPlayQueueReady()) {
tryBlock();
playQueue.fetch();
} else if (playQueue.isEmpty()) {
playbackListener.shutdown();
} else {
load(); // All event warrants a load
}
if (playQueueReactor != null) playQueueReactor.request(1);
} }
@Override @Override
@ -189,6 +167,45 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
}; };
} }
private void onPlayQueueChanged(final PlayQueueMessage event) {
// why no pattern matching in Java =(
switch (event.type()) {
case APPEND:
populateSources();
break;
case SELECT:
if (isCurrentIndexLoaded()) {
sync();
} else {
reset();
}
break;
case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event;
remove(removeEvent.index());
break;
case INIT:
case REORDER:
reset();
break;
case ERROR:
case MOVE:
default:
break;
}
if (!isPlayQueueReady()) {
tryBlock();
playQueue.fetch();
} else if (playQueue.isEmpty()) {
playbackListener.shutdown();
} else {
load(); // All event warrants a load
}
if (playQueueReactor != null) playQueueReactor.request(1);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Internal Helpers // Internal Helpers
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -220,7 +237,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
} }
private void sync() { private void sync() {
final PlayQueueItem currentItem = playQueue.getCurrent(); final PlayQueueItem currentItem = playQueue.getItem();
final Consumer<StreamInfo> syncPlayback = new Consumer<StreamInfo>() { final Consumer<StreamInfo> syncPlayback = new Consumer<StreamInfo>() {
@Override @Override

View File

@ -20,8 +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());
private static final int RETRY_COUNT = 2;
private boolean isComplete; private boolean isComplete;
private int serviceId; private int serviceId;
@ -54,7 +52,6 @@ public final class ExternalPlayQueue extends PlayQueue {
ExtractorHelper.getMorePlaylistItems(this.serviceId, this.baseUrl, this.nextUrl) ExtractorHelper.getMorePlaylistItems(this.serviceId, this.baseUrl, this.nextUrl)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.retry(RETRY_COUNT)
.subscribe(getPlaylistObserver()); .subscribe(getPlaylistObserver());
} }
@ -75,6 +72,9 @@ public final class ExternalPlayQueue extends PlayQueue {
nextUrl = result.nextItemsUrl; nextUrl = result.nextItemsUrl;
append(extractPlaylistItems(result.nextItemsList)); append(extractPlaylistItems(result.nextItemsList));
fetchReactor.dispose();
fetchReactor = null;
} }
@Override @Override

View File

@ -6,6 +6,7 @@ import android.util.Log;
import org.reactivestreams.Subscriber; import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
import org.schabi.newpipe.playlist.events.AppendEvent; import org.schabi.newpipe.playlist.events.AppendEvent;
import org.schabi.newpipe.playlist.events.ErrorEvent;
import org.schabi.newpipe.playlist.events.InitEvent; import org.schabi.newpipe.playlist.events.InitEvent;
import org.schabi.newpipe.playlist.events.PlayQueueMessage; import org.schabi.newpipe.playlist.events.PlayQueueMessage;
import org.schabi.newpipe.playlist.events.RemoveEvent; import org.schabi.newpipe.playlist.events.RemoveEvent;
@ -44,8 +45,7 @@ public abstract class PlayQueue implements Serializable {
private ArrayList<PlayQueueItem> streams; private ArrayList<PlayQueueItem> streams;
private final AtomicInteger queueIndex; private final AtomicInteger queueIndex;
private transient BehaviorSubject<PlayQueueMessage> streamsEventBroadcast; private transient BehaviorSubject<PlayQueueMessage> eventBroadcast;
private transient BehaviorSubject<PlayQueueMessage> indexEventBroadcast;
private transient Flowable<PlayQueueMessage> broadcastReceiver; private transient Flowable<PlayQueueMessage> broadcastReceiver;
private transient Subscription reportingReactor; private transient Subscription reportingReactor;
@ -70,13 +70,11 @@ public abstract class PlayQueue implements Serializable {
* Also starts a self reporter for logging if debug mode is enabled. * Also starts a self reporter for logging if debug mode is enabled.
* */ * */
public void init() { public void init() {
streamsEventBroadcast = BehaviorSubject.create(); eventBroadcast = BehaviorSubject.create();
indexEventBroadcast = BehaviorSubject.create();
broadcastReceiver = Flowable.merge( broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER)
streamsEventBroadcast.toFlowable(BackpressureStrategy.BUFFER), .observeOn(AndroidSchedulers.mainThread())
indexEventBroadcast.toFlowable(BackpressureStrategy.BUFFER) .startWith(new InitEvent());
).observeOn(AndroidSchedulers.mainThread()).startWith(new InitEvent());
if (DEBUG) broadcastReceiver.subscribe(getSelfReporter()); if (DEBUG) broadcastReceiver.subscribe(getSelfReporter());
} }
@ -88,8 +86,7 @@ public abstract class PlayQueue implements Serializable {
if (backup != null) backup.clear(); if (backup != null) backup.clear();
if (streams != null) streams.clear(); if (streams != null) streams.clear();
if (streamsEventBroadcast != null) streamsEventBroadcast.onComplete(); if (eventBroadcast != null) eventBroadcast.onComplete();
if (indexEventBroadcast != null) indexEventBroadcast.onComplete();
if (reportingReactor != null) reportingReactor.cancel(); if (reportingReactor != null) reportingReactor.cancel();
broadcastReceiver = null; broadcastReceiver = null;
@ -123,15 +120,15 @@ public abstract class PlayQueue implements Serializable {
/** /**
* Returns the current item that should be played. * Returns the current item that should be played.
* */ * */
public PlayQueueItem getCurrent() { public PlayQueueItem getItem() {
return get(getIndex()); return getItem(getIndex());
} }
/** /**
* Returns the item at the given index. * Returns the item at the given index.
* May throw {@link IndexOutOfBoundsException}. * May throw {@link IndexOutOfBoundsException}.
* */ * */
public PlayQueueItem get(int index) { public PlayQueueItem getItem(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);
} }
@ -160,6 +157,13 @@ public abstract class PlayQueue implements Serializable {
return streams.isEmpty(); return streams.isEmpty();
} }
/**
* Determines if the current play queue is shuffled.
* */
public boolean isShuffled() {
return backup != null;
}
/** /**
* Returns an immutable view of the play queue. * Returns an immutable view of the play queue.
* */ * */
@ -191,12 +195,14 @@ public abstract class PlayQueue implements Serializable {
public synchronized void setIndex(final int index) { public synchronized void setIndex(final int index) {
if (index == getIndex()) return; if (index == getIndex()) return;
final int oldIndex = getIndex();
int newIndex = index; int newIndex = index;
if (index < 0) newIndex = 0; if (index < 0) newIndex = 0;
if (index >= streams.size()) newIndex = isComplete() ? index % streams.size() : streams.size() - 1; if (index >= streams.size()) newIndex = isComplete() ? index % streams.size() : streams.size() - 1;
queueIndex.set(newIndex); queueIndex.set(newIndex);
indexEventBroadcast.onNext(new SelectEvent(newIndex)); broadcast(new SelectEvent(oldIndex, newIndex));
} }
/** /**
@ -213,7 +219,7 @@ public abstract class PlayQueue implements Serializable {
* *
* Will emit a {@link AppendEvent} on any given context. * Will emit a {@link AppendEvent} on any given context.
* */ * */
protected synchronized void append(final PlayQueueItem... items) { public 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));
} }
@ -223,7 +229,7 @@ public abstract class PlayQueue implements Serializable {
* *
* Will emit a {@link AppendEvent} on any given context. * Will emit a {@link AppendEvent} on any given context.
* */ * */
protected synchronized void append(final Collection<PlayQueueItem> items) { public synchronized void append(final Collection<PlayQueueItem> items) {
streams.addAll(items); streams.addAll(items);
broadcast(new AppendEvent(items.size())); broadcast(new AppendEvent(items.size()));
} }
@ -235,22 +241,35 @@ public abstract class PlayQueue implements Serializable {
* On cases where the current playing index exceeds the playlist range, it is set to 0. * 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 {@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;
removeInternal(index);
broadcast(new RemoveEvent(index));
}
/**
* Report an exception for the item at the current index in order to remove it.
*
* This is done as a separate event as the underlying manager may have
* different implementation regarding exceptions.
* */
public synchronized void error() {
final int index = getIndex();
removeInternal(index);
broadcast(new ErrorEvent(index));
}
private synchronized void removeInternal(final int index) {
final int currentIndex = queueIndex.get(); final int currentIndex = queueIndex.get();
final boolean isCurrent = index == getIndex();
if (currentIndex > index) { if (currentIndex > index) {
queueIndex.decrementAndGet(); queueIndex.decrementAndGet();
} else if (currentIndex >= size()) { } else if (currentIndex >= size()) {
queueIndex.set(0); queueIndex.set(0);
} }
streams.remove(index);
broadcast(new RemoveEvent(index, isCurrent)); streams.remove(index);
} }
/** /**
@ -264,11 +283,11 @@ public abstract class PlayQueue implements Serializable {
* */ * */
public synchronized void shuffle() { public synchronized void shuffle() {
backup = new ArrayList<>(streams); backup = new ArrayList<>(streams);
final PlayQueueItem current = getCurrent(); final PlayQueueItem current = getItem();
Collections.shuffle(streams); Collections.shuffle(streams);
queueIndex.set(streams.indexOf(current)); queueIndex.set(streams.indexOf(current));
broadcast(new ReorderEvent(true)); broadcast(new ReorderEvent());
} }
/** /**
@ -280,12 +299,13 @@ public abstract class PlayQueue implements Serializable {
* */ * */
public synchronized void unshuffle() { public synchronized void unshuffle() {
if (backup == null) return; if (backup == null) return;
final PlayQueueItem current = getCurrent(); final PlayQueueItem current = getItem();
streams.clear(); streams.clear();
streams = backup; streams = backup;
backup = null;
queueIndex.set(streams.indexOf(current)); queueIndex.set(streams.indexOf(current));
broadcast(new ReorderEvent(false)); broadcast(new ReorderEvent());
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -293,7 +313,9 @@ public abstract class PlayQueue implements Serializable {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void broadcast(final PlayQueueMessage event) { private void broadcast(final PlayQueueMessage event) {
streamsEventBroadcast.onNext(event); if (eventBroadcast != null) {
eventBroadcast.onNext(event);
}
} }
private Subscriber<PlayQueueMessage> getSelfReporter() { private Subscriber<PlayQueueMessage> getSelfReporter() {

View File

@ -7,7 +7,11 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.playlist.events.AppendEvent;
import org.schabi.newpipe.playlist.events.ErrorEvent;
import org.schabi.newpipe.playlist.events.PlayQueueMessage; import org.schabi.newpipe.playlist.events.PlayQueueMessage;
import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.SelectEvent;
import java.util.List; import java.util.List;
@ -38,10 +42,12 @@ import io.reactivex.disposables.Disposable;
public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = PlayQueueAdapter.class.toString(); private static final String TAG = PlayQueueAdapter.class.toString();
private static final int ITEM_VIEW_TYPE_ID = 0;
private static final int FOOTER_VIEW_TYPE_ID = 1;
private final PlayQueueItemBuilder playQueueItemBuilder; private final PlayQueueItemBuilder playQueueItemBuilder;
private final PlayQueue playQueue; private final PlayQueue playQueue;
private boolean showFooter = false; private boolean showFooter = false;
private View header = null;
private View footer = null; private View footer = null;
private Disposable playQueueReactor; private Disposable playQueueReactor;
@ -54,11 +60,6 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
public View view; public View view;
} }
public void showFooter(final boolean show) {
showFooter = show;
notifyDataSetChanged();
}
public PlayQueueAdapter(final PlayQueue playQueue) { public PlayQueueAdapter(final PlayQueue playQueue) {
this.playQueueItemBuilder = new PlayQueueItemBuilder(); this.playQueueItemBuilder = new PlayQueueItemBuilder();
this.playQueue = playQueue; this.playQueue = playQueue;
@ -92,7 +93,7 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
@Override @Override
public void onNext(@NonNull PlayQueueMessage playQueueMessage) { public void onNext(@NonNull PlayQueueMessage playQueueMessage) {
notifyDataSetChanged(); onPlayQueueChanged(playQueueMessage);
} }
@Override @Override
@ -109,19 +110,46 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
.subscribe(observer); .subscribe(observer);
} }
private void onPlayQueueChanged(final PlayQueueMessage message) {
switch (message.type()) {
case SELECT:
final SelectEvent selectEvent = (SelectEvent) message;
notifyItemChanged(selectEvent.getOldIndex());
notifyItemChanged(selectEvent.getNewIndex());
break;
case APPEND:
final AppendEvent appendEvent = (AppendEvent) message;
notifyItemRangeInserted(playQueue.size(), appendEvent.getAmount());
break;
case ERROR:
final ErrorEvent errorEvent = (ErrorEvent) message;
notifyItemRangeRemoved(errorEvent.index(), 1);
notifyItemChanged(errorEvent.index());
break;
case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) message;
notifyItemRangeRemoved(removeEvent.index(), 1);
notifyItemChanged(removeEvent.index());
break;
default:
notifyDataSetChanged();
break;
}
}
public void dispose() { public void dispose() {
if (playQueueReactor != null) playQueueReactor.dispose(); if (playQueueReactor != null) playQueueReactor.dispose();
playQueueReactor = null; playQueueReactor = null;
} }
public void setHeader(View header) {
this.header = header;
notifyDataSetChanged();
}
public void setFooter(View footer) { public void setFooter(View footer) {
this.footer = footer; this.footer = footer;
notifyDataSetChanged(); notifyItemChanged(playQueue.size());
}
public void showFooter(final boolean show) {
showFooter = show;
notifyItemChanged(playQueue.size());
} }
public List<PlayQueueItem> getItems() { public List<PlayQueueItem> getItems() {
@ -131,36 +159,28 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
@Override @Override
public int getItemCount() { public int getItemCount() {
int count = playQueue.getStreams().size(); int count = playQueue.getStreams().size();
if(header != null) count++;
if(footer != null && showFooter) count++; if(footer != null && showFooter) count++;
return count; return count;
} }
@Override @Override
public int getItemViewType(int position) { public int getItemViewType(int position) {
if(header != null && position == 0) {
return 0;
} else if(header != null) {
position--;
}
if(footer != null && position == playQueue.getStreams().size() && showFooter) { if(footer != null && position == playQueue.getStreams().size() && showFooter) {
return 1; return FOOTER_VIEW_TYPE_ID;
} }
return 2;
return ITEM_VIEW_TYPE_ID;
} }
@Override @Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) { public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
switch(type) { switch(type) {
case 0: case FOOTER_VIEW_TYPE_ID:
return new HFHolder(header);
case 1:
return new HFHolder(footer); return new HFHolder(footer);
case 2: case ITEM_VIEW_TYPE_ID:
return new PlayQueueItemHolder(LayoutInflater.from(parent.getContext()) return new PlayQueueItemHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.play_queue_item, parent, false));
.inflate(R.layout.play_queue_item, parent, false));
default: default:
Log.e(TAG, "Trollolo"); Log.e(TAG, "Attempting to create view holder with undefined type: " + type);
return null; return null;
} }
} }
@ -168,14 +188,10 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
@Override @Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(holder instanceof PlayQueueItemHolder) { if(holder instanceof PlayQueueItemHolder) {
// Ensure header does not interfere with list building
if (header != null) position--;
// Build the list item // Build the list item
playQueueItemBuilder.buildStreamInfoItem((PlayQueueItemHolder) holder, playQueue.getStreams().get(position)); playQueueItemBuilder.buildStreamInfoItem((PlayQueueItemHolder) holder, playQueue.getStreams().get(position));
// Check if the current item should be selected/highlighted // Check if the current item should be selected/highlighted
holder.itemView.setSelected(playQueue.getIndex() == position); holder.itemView.setSelected(playQueue.getIndex() == position);
} else if(holder instanceof HFHolder && position == 0 && header != null) {
((HFHolder) holder).view = header;
} else if(holder instanceof HFHolder && position == playQueue.getStreams().size() && footer != null && showFooter) { } else if(holder instanceof HFHolder && position == playQueue.getStreams().size() && footer != null && showFooter) {
((HFHolder) holder).view = footer; ((HFHolder) holder).view = footer;
} }

View File

@ -0,0 +1,19 @@
package org.schabi.newpipe.playlist.events;
public class ErrorEvent implements PlayQueueMessage {
final private int index;
@Override
public PlayQueueEvent type() {
return PlayQueueEvent.ERROR;
}
public ErrorEvent(final int index) {
this.index = index;
}
public int index() {
return index;
}
}

View File

@ -15,7 +15,10 @@ public enum PlayQueueEvent {
// sent when two streams swap place in the play queue // sent when two streams swap place in the play queue
MOVE, MOVE,
// send when queue is shuffled // sent when queue is shuffled
REORDER REORDER,
// sent when the item at index has caused an exception
ERROR
} }

View File

@ -3,23 +3,17 @@ package org.schabi.newpipe.playlist.events;
public class RemoveEvent implements PlayQueueMessage { public class RemoveEvent implements PlayQueueMessage {
final private int index; final private int index;
final private boolean isCurrent;
@Override @Override
public PlayQueueEvent type() { public PlayQueueEvent type() {
return PlayQueueEvent.REMOVE; return PlayQueueEvent.REMOVE;
} }
public RemoveEvent(final int index, final boolean isCurrent) { public RemoveEvent(final int index) {
this.index = index; this.index = index;
this.isCurrent = isCurrent;
} }
public int index() { public int index() {
return index; return index;
} }
public boolean isCurrent() {
return isCurrent;
}
} }

View File

@ -1,18 +1,12 @@
package org.schabi.newpipe.playlist.events; package org.schabi.newpipe.playlist.events;
public class ReorderEvent implements PlayQueueMessage { public class ReorderEvent implements PlayQueueMessage {
final private boolean randomize;
@Override @Override
public PlayQueueEvent type() { public PlayQueueEvent type() {
return PlayQueueEvent.REORDER; return PlayQueueEvent.REORDER;
} }
public ReorderEvent(final boolean randomize) { public ReorderEvent() {
this.randomize = randomize;
}
public boolean isRandomize() {
return randomize;
} }
} }

View File

@ -2,6 +2,7 @@ package org.schabi.newpipe.playlist.events;
public class SelectEvent implements PlayQueueMessage { public class SelectEvent implements PlayQueueMessage {
final private int oldIndex;
final private int newIndex; final private int newIndex;
@Override @Override
@ -9,11 +10,16 @@ public class SelectEvent implements PlayQueueMessage {
return PlayQueueEvent.SELECT; return PlayQueueEvent.SELECT;
} }
public SelectEvent(final int newIndex) { public SelectEvent(final int oldIndex, final int newIndex) {
this.oldIndex = oldIndex;
this.newIndex = newIndex; this.newIndex = newIndex;
} }
public int index() { public int getOldIndex() {
return oldIndex;
}
public int getNewIndex() {
return newIndex; return newIndex;
} }
} }