-Fixed incorrect stream from being played after consecutive player errors.
-Fixed MediaSource reuse due to MediaSourceManager not resetting source on block.
This commit is contained in:
parent
2e414cfd63
commit
f1e52b8b92
|
@ -32,6 +32,7 @@ import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.support.annotation.IntRange;
|
import android.support.annotation.IntRange;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
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;
|
||||||
|
@ -49,6 +50,7 @@ import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
@ -346,7 +348,7 @@ public final class BackgroundPlayer extends Service {
|
||||||
public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) {
|
public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) {
|
||||||
resetNotification();
|
resetNotification();
|
||||||
if (bigNotRemoteView != null) {
|
if (bigNotRemoteView != null) {
|
||||||
if (currentInfo != null) {
|
if (currentItem != null) {
|
||||||
bigNotRemoteView.setTextViewText(R.id.notificationSongName, getVideoTitle());
|
bigNotRemoteView.setTextViewText(R.id.notificationSongName, getVideoTitle());
|
||||||
bigNotRemoteView.setTextViewText(R.id.notificationArtist, getUploaderName());
|
bigNotRemoteView.setTextViewText(R.id.notificationArtist, getUploaderName());
|
||||||
}
|
}
|
||||||
|
@ -354,7 +356,7 @@ public final class BackgroundPlayer extends Service {
|
||||||
bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + getTimeString(duration));
|
bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + getTimeString(duration));
|
||||||
}
|
}
|
||||||
if (notRemoteView != null) {
|
if (notRemoteView != null) {
|
||||||
if (currentInfo != null) {
|
if (currentItem != null) {
|
||||||
notRemoteView.setTextViewText(R.id.notificationSongName, getVideoTitle());
|
notRemoteView.setTextViewText(R.id.notificationSongName, getVideoTitle());
|
||||||
notRemoteView.setTextViewText(R.id.notificationArtist, getUploaderName());
|
notRemoteView.setTextViewText(R.id.notificationArtist, getUploaderName());
|
||||||
}
|
}
|
||||||
|
@ -442,8 +444,8 @@ public final class BackgroundPlayer extends Service {
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sync(@Nullable final StreamInfo info) {
|
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
|
||||||
super.sync(info);
|
super.sync(item, info);
|
||||||
|
|
||||||
resetNotification();
|
resetNotification();
|
||||||
notRemoteView.setTextViewText(R.id.notificationSongName, getVideoTitle());
|
notRemoteView.setTextViewText(R.id.notificationSongName, getVideoTitle());
|
||||||
|
|
|
@ -79,6 +79,7 @@ import org.schabi.newpipe.player.playback.MediaSourceManager;
|
||||||
import org.schabi.newpipe.player.playback.PlaybackListener;
|
import org.schabi.newpipe.player.playback.PlaybackListener;
|
||||||
import org.schabi.newpipe.playlist.PlayQueue;
|
import org.schabi.newpipe.playlist.PlayQueue;
|
||||||
import org.schabi.newpipe.playlist.PlayQueueAdapter;
|
import org.schabi.newpipe.playlist.PlayQueueAdapter;
|
||||||
|
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
@ -149,6 +150,7 @@ public abstract class BasePlayer implements Player.EventListener,
|
||||||
private long videoPos = -1;
|
private long videoPos = -1;
|
||||||
|
|
||||||
protected StreamInfo currentInfo;
|
protected StreamInfo currentInfo;
|
||||||
|
protected PlayQueueItem currentItem;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Player
|
// Player
|
||||||
|
@ -729,23 +731,27 @@ public abstract class BasePlayer implements Player.EventListener,
|
||||||
if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING);
|
if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING);
|
||||||
|
|
||||||
simpleExoPlayer.prepare(mediaSource);
|
simpleExoPlayer.prepare(mediaSource);
|
||||||
|
simpleExoPlayer.seekToDefaultPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sync(@Nullable final StreamInfo info) {
|
public void sync(@android.support.annotation.NonNull final PlayQueueItem item,
|
||||||
if (info == null || simpleExoPlayer == null) return;
|
@Nullable final StreamInfo info) {
|
||||||
|
if (simpleExoPlayer == null) return;
|
||||||
if (DEBUG) Log.d(TAG, "Syncing...");
|
if (DEBUG) Log.d(TAG, "Syncing...");
|
||||||
|
|
||||||
|
currentItem = item;
|
||||||
|
currentInfo = info;
|
||||||
|
|
||||||
// Check if on wrong window
|
// Check if on wrong window
|
||||||
final int currentSourceIndex = playQueue.getIndex();
|
final int currentSourceIndex = playQueue.getIndex();
|
||||||
if (!(simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex)) {
|
if (simpleExoPlayer.getCurrentWindowIndex() != currentSourceIndex) {
|
||||||
final long startPos = currentInfo != null ? currentInfo.start_position : 0;
|
final long startPos = info != null ? info.start_position : 0;
|
||||||
if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + " at: " + getTimeString((int)startPos));
|
if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + " at: " + getTimeString((int)startPos));
|
||||||
simpleExoPlayer.seekTo(currentSourceIndex, startPos);
|
simpleExoPlayer.seekTo(currentSourceIndex, startPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentInfo = info;
|
initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url);
|
||||||
initThumbnail(info.thumbnail_url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -797,13 +803,14 @@ public abstract class BasePlayer implements Player.EventListener,
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onPlayPrevious() {
|
public void onPlayPrevious() {
|
||||||
if (simpleExoPlayer == null || playQueue == null || currentInfo == null) return;
|
if (simpleExoPlayer == null || playQueue == null) return;
|
||||||
if (DEBUG) Log.d(TAG, "onPlayPrevious() called");
|
if (DEBUG) Log.d(TAG, "onPlayPrevious() called");
|
||||||
|
|
||||||
/* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, restart current track.
|
/* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, restart current track.
|
||||||
* Also restart the track if the current track is the first in a queue.*/
|
* Also restart the track if the current track is the first in a queue.*/
|
||||||
if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || playQueue.getIndex() == 0) {
|
if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || playQueue.getIndex() == 0) {
|
||||||
simpleExoPlayer.seekTo(currentInfo.start_position);
|
final long startPos = currentInfo == null ? 0 : currentInfo.start_position;
|
||||||
|
simpleExoPlayer.seekTo(startPos);
|
||||||
} else {
|
} else {
|
||||||
playQueue.offsetIndex(-1);
|
playQueue.offsetIndex(-1);
|
||||||
}
|
}
|
||||||
|
@ -947,15 +954,15 @@ public abstract class BasePlayer implements Player.EventListener,
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getVideoUrl() {
|
public String getVideoUrl() {
|
||||||
return currentInfo == null ? null : currentInfo.url;
|
return currentItem == null ? null : currentItem.getUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getVideoTitle() {
|
public String getVideoTitle() {
|
||||||
return currentInfo == null ? null : currentInfo.name;
|
return currentItem == null ? null : currentItem.getTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUploaderName() {
|
public String getUploaderName() {
|
||||||
return currentInfo == null ? null : currentInfo.uploader_name;
|
return currentItem == null ? null : currentItem.getUploader();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isCompleted() {
|
public boolean isCompleted() {
|
||||||
|
|
|
@ -26,6 +26,7 @@ import android.graphics.Color;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.GestureDetector;
|
import android.view.GestureDetector;
|
||||||
|
@ -43,6 +44,7 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||||
import org.schabi.newpipe.util.AnimationUtils;
|
import org.schabi.newpipe.util.AnimationUtils;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
|
@ -250,8 +252,8 @@ public final class MainVideoPlayer extends Activity {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sync(@Nullable final StreamInfo info) {
|
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
|
||||||
super.sync(info);
|
super.sync(item, info);
|
||||||
titleTextView.setText(getVideoTitle());
|
titleTextView.setText(getVideoTitle());
|
||||||
channelTextView.setText(getUploaderName());
|
channelTextView.setText(getUploaderName());
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ import android.graphics.Color;
|
||||||
import android.graphics.PorterDuff;
|
import android.graphics.PorterDuff;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
@ -64,6 +65,7 @@ import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
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.PlayQueueItem;
|
||||||
import org.schabi.newpipe.util.AnimationUtils;
|
import org.schabi.newpipe.util.AnimationUtils;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
|
|
||||||
|
@ -304,8 +306,8 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sync(@Nullable final StreamInfo info) {
|
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
|
||||||
super.sync(info);
|
super.sync(item, info);
|
||||||
|
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false);
|
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false);
|
||||||
|
|
|
@ -29,7 +29,7 @@ import io.reactivex.functions.Consumer;
|
||||||
public class MediaSourceManager implements DeferredMediaSource.Callback {
|
public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
private final String TAG = "MediaSourceManager@" + Integer.toHexString(hashCode());
|
private final String TAG = "MediaSourceManager@" + Integer.toHexString(hashCode());
|
||||||
// One-side rolling window size for default loading
|
// One-side rolling window size for default loading
|
||||||
// Effectively loads WINDOW_SIZE * 2 + 1 streams, should be at least 1 to ensure gapless playback
|
// Effectively loads WINDOW_SIZE * 2 + 1 streams, must be greater than 0
|
||||||
// todo: inject this parameter, allow user settings perhaps
|
// todo: inject this parameter, allow user settings perhaps
|
||||||
private static final int WINDOW_SIZE = 1;
|
private static final int WINDOW_SIZE = 1;
|
||||||
|
|
||||||
|
@ -116,7 +116,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
* */
|
* */
|
||||||
public void reset() {
|
public void reset() {
|
||||||
tryBlock();
|
tryBlock();
|
||||||
resetSources();
|
|
||||||
populateSources();
|
populateSources();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,6 +148,10 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
private void onPlayQueueChanged(final PlayQueueMessage event) {
|
private void onPlayQueueChanged(final PlayQueueMessage event) {
|
||||||
// why no pattern matching in Java =(
|
// why no pattern matching in Java =(
|
||||||
switch (event.type()) {
|
switch (event.type()) {
|
||||||
|
case INIT:
|
||||||
|
case REORDER:
|
||||||
|
reset();
|
||||||
|
break;
|
||||||
case APPEND:
|
case APPEND:
|
||||||
populateSources();
|
populateSources();
|
||||||
break;
|
break;
|
||||||
|
@ -159,10 +162,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
final RemoveEvent removeEvent = (RemoveEvent) event;
|
final RemoveEvent removeEvent = (RemoveEvent) event;
|
||||||
remove(removeEvent.index());
|
remove(removeEvent.index());
|
||||||
break;
|
break;
|
||||||
case INIT:
|
|
||||||
case REORDER:
|
|
||||||
reset();
|
|
||||||
break;
|
|
||||||
case MOVE:
|
case MOVE:
|
||||||
final MoveEvent moveEvent = (MoveEvent) event;
|
final MoveEvent moveEvent = (MoveEvent) event;
|
||||||
move(moveEvent.getFromIndex(), moveEvent.getToIndex());
|
move(moveEvent.getFromIndex(), moveEvent.getToIndex());
|
||||||
|
@ -195,6 +194,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
private boolean tryBlock() {
|
private boolean tryBlock() {
|
||||||
if (!isBlocked) {
|
if (!isBlocked) {
|
||||||
playbackListener.block();
|
playbackListener.block();
|
||||||
|
resetSources();
|
||||||
isBlocked = true;
|
isBlocked = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -202,7 +202,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean tryUnblock() {
|
private boolean tryUnblock() {
|
||||||
if (isPlayQueueReady() && isBlocked) {
|
if (isPlayQueueReady() && isBlocked && sources != null) {
|
||||||
isBlocked = false;
|
isBlocked = false;
|
||||||
playbackListener.unblock(sources);
|
playbackListener.unblock(sources);
|
||||||
return true;
|
return true;
|
||||||
|
@ -216,7 +216,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
final Consumer<StreamInfo> syncPlayback = new Consumer<StreamInfo>() {
|
final Consumer<StreamInfo> syncPlayback = new Consumer<StreamInfo>() {
|
||||||
@Override
|
@Override
|
||||||
public void accept(StreamInfo streamInfo) throws Exception {
|
public void accept(StreamInfo streamInfo) throws Exception {
|
||||||
playbackListener.sync(streamInfo);
|
playbackListener.sync(currentItem, streamInfo);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -224,7 +224,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
@Override
|
@Override
|
||||||
public void accept(Throwable throwable) throws Exception {
|
public void accept(Throwable throwable) throws Exception {
|
||||||
Log.e(TAG, "Sync error:", throwable);
|
Log.e(TAG, "Sync error:", throwable);
|
||||||
playbackListener.sync(null);
|
playbackListener.sync(currentItem,null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -244,11 +244,12 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
|
|
||||||
private void resetSources() {
|
private void resetSources() {
|
||||||
if (this.sources != null) this.sources.releaseSource();
|
if (this.sources != null) this.sources.releaseSource();
|
||||||
|
|
||||||
this.sources = new DynamicConcatenatingMediaSource();
|
this.sources = new DynamicConcatenatingMediaSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void populateSources() {
|
private void populateSources() {
|
||||||
|
if (sources == null) return;
|
||||||
|
|
||||||
for (final PlayQueueItem item : playQueue.getStreams()) {
|
for (final PlayQueueItem item : playQueue.getStreams()) {
|
||||||
insert(playQueue.indexOf(item), new DeferredMediaSource(item, this));
|
insert(playQueue.indexOf(item), new DeferredMediaSource(item, this));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +1,43 @@
|
||||||
package org.schabi.newpipe.player.playback;
|
package org.schabi.newpipe.player.playback;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface PlaybackListener {
|
public interface PlaybackListener {
|
||||||
/*
|
/**
|
||||||
* Called when the stream at the current queue index is not ready yet.
|
* Called when the stream at the current queue index is not ready yet.
|
||||||
* Signals to the listener to block the player from playing anything.
|
* Signals to the listener to block the player from playing anything and notify the source
|
||||||
|
* is now invalid.
|
||||||
*
|
*
|
||||||
* May be called at any time.
|
* May be called at any time.
|
||||||
* */
|
* */
|
||||||
void block();
|
void block();
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Called when the stream at the current queue index is ready.
|
* Called when the stream at the current queue index is ready.
|
||||||
* Signals to the listener to resume the player.
|
* Signals to the listener to resume the player by preparing a new source.
|
||||||
*
|
*
|
||||||
* May be called only when the player is blocked.
|
* May be called only when the player is blocked.
|
||||||
* */
|
* */
|
||||||
void unblock(final MediaSource mediaSource);
|
void unblock(final MediaSource mediaSource);
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Called when the queue index is refreshed.
|
* Called when the queue index is refreshed.
|
||||||
* Signals to the listener to synchronize the player's window to the manager's
|
* Signals to the listener to synchronize the player's window to the manager's
|
||||||
* window.
|
* window.
|
||||||
*
|
*
|
||||||
* May be null.
|
* May be called only after unblock is called.
|
||||||
* May be called only after playback is unblocked.
|
|
||||||
* */
|
* */
|
||||||
void sync(@Nullable final StreamInfo info);
|
void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info);
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Requests the listener to resolve a stream info into a media source
|
* Requests the listener to resolve a stream info into a media source
|
||||||
* according to the listener's implementation (background, popup or main video player).
|
* according to the listener's implementation (background, popup or main video player).
|
||||||
*
|
*
|
||||||
|
@ -43,7 +45,7 @@ public interface PlaybackListener {
|
||||||
* */
|
* */
|
||||||
MediaSource sourceOf(final StreamInfo info);
|
MediaSource sourceOf(final StreamInfo info);
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Called when the play queue can no longer to played or used.
|
* Called when the play queue can no longer to played or used.
|
||||||
* Currently, this means the play queue is empty and complete.
|
* Currently, this means the play queue is empty and complete.
|
||||||
* Signals to the listener that it should shutdown.
|
* Signals to the listener that it should shutdown.
|
||||||
|
|
Loading…
Reference in New Issue