-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:
parent
a9aee21e58
commit
eebd83d6ac
13 changed files with 304 additions and 171 deletions
|
|
@ -36,6 +36,7 @@ import android.support.annotation.Nullable;
|
|||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
import android.widget.RemoteViews;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
|
|
@ -402,6 +403,7 @@ public final class BackgroundPlayer extends Service {
|
|||
@Override
|
||||
public void onError(Exception exception) {
|
||||
exception.printStackTrace();
|
||||
Toast.makeText(context, "Failed to play this audio", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -33,12 +33,12 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
|||
|
||||
private static final String TAG = "BGPlayerActivity";
|
||||
|
||||
private boolean isServiceBound;
|
||||
private boolean serviceBound;
|
||||
private ServiceConnection serviceConnection;
|
||||
|
||||
private BackgroundPlayer.BasePlayerImpl player;
|
||||
|
||||
private boolean isSeeking;
|
||||
private boolean seeking;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
|
|
@ -104,9 +104,9 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
|||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
if(isServiceBound) {
|
||||
if(serviceBound) {
|
||||
unbindService(serviceConnection);
|
||||
isServiceBound = false;
|
||||
serviceBound = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -119,7 +119,7 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
|||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
Log.d(TAG, "Background player service is disconnected");
|
||||
isServiceBound = false;
|
||||
serviceBound = false;
|
||||
player = null;
|
||||
finish();
|
||||
}
|
||||
|
|
@ -132,7 +132,7 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
|||
if (player == null) {
|
||||
finish();
|
||||
} else {
|
||||
isServiceBound = true;
|
||||
serviceBound = true;
|
||||
buildComponents();
|
||||
|
||||
player.setActivityListener(BackgroundPlayerActivity.this);
|
||||
|
|
@ -220,13 +220,13 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
|||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
isSeeking = true;
|
||||
seeking = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
player.simpleExoPlayer.seekTo(seekBar.getProgress());
|
||||
isSeeking = false;
|
||||
seeking = false;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -284,7 +284,7 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
|||
progressEndTime.setText(Localization.getDurationString(duration / 1000));
|
||||
|
||||
// Set current time if not seeking
|
||||
if (!isSeeking) {
|
||||
if (!seeking) {
|
||||
progressSeekBar.setProgress(currentProgress);
|
||||
progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -664,8 +664,22 @@ public abstract class BasePlayer implements Player.EventListener,
|
|||
@Override
|
||||
public void onPlayerError(ExoPlaybackException 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
|
||||
|
|
|
|||
|
|
@ -30,27 +30,37 @@ import io.reactivex.schedulers.Schedulers;
|
|||
public final class DeferredMediaSource implements MediaSource {
|
||||
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;
|
||||
/**
|
||||
* This state indicates the {@link DeferredMediaSource} has been prepared and is ready to load.
|
||||
* */
|
||||
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_DISPOSED = 3;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private PlayQueueItem stream;
|
||||
private Callback callback;
|
||||
private int state;
|
||||
|
||||
private MediaSource mediaSource;
|
||||
|
||||
/* Custom internal objects */
|
||||
private Disposable loader;
|
||||
|
||||
private ExoPlayer exoPlayer;
|
||||
private Listener listener;
|
||||
private Throwable error;
|
||||
|
|
@ -62,6 +72,17 @@ public final class DeferredMediaSource implements MediaSource {
|
|||
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.
|
||||
* */
|
||||
|
|
@ -72,54 +93,37 @@ public final class DeferredMediaSource implements MediaSource {
|
|||
this.state = STATE_PREPARED;
|
||||
}
|
||||
|
||||
public int state() {
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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
|
||||
* If loading fails here, an error will be propagated out and result in an
|
||||
* {@link com.google.android.exoplayer2.ExoPlaybackException ExoPlaybackException}, which is delegated
|
||||
* to the player.
|
||||
* */
|
||||
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());
|
||||
|
||||
final Consumer<StreamInfo> onSuccess = new Consumer<StreamInfo>() {
|
||||
@Override
|
||||
public void accept(StreamInfo streamInfo) throws Exception {
|
||||
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);
|
||||
onStreamInfoReceived(streamInfo);
|
||||
}
|
||||
};
|
||||
|
||||
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(Throwable throwable) throws Exception {
|
||||
Log.e(TAG, "Loading error:", throwable);
|
||||
error = throwable;
|
||||
state = STATE_LOADED;
|
||||
onStreamInfoError(throwable);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -129,6 +133,38 @@ public final class DeferredMediaSource implements MediaSource {
|
|||
.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
|
||||
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
||||
if (error != null) {
|
||||
|
|
@ -145,19 +181,27 @@ public final class DeferredMediaSource implements MediaSource {
|
|||
return mediaSource.createPeriod(mediaPeriodId, allocator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the media period (buffers).
|
||||
*
|
||||
* This may be called after {@link #releaseSource releaseSource}.
|
||||
* */
|
||||
@Override
|
||||
public void releasePeriod(MediaPeriod mediaPeriod) {
|
||||
if (mediaSource == null) {
|
||||
Log.e(TAG, "releasePeriod() called when media source is null, memory leak may have occurred.");
|
||||
} else {
|
||||
mediaSource.releasePeriod(mediaPeriod);
|
||||
}
|
||||
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
|
||||
public void releaseSource() {
|
||||
state = STATE_DISPOSED;
|
||||
|
||||
if (mediaSource != null) {
|
||||
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 */
|
||||
stream = null;
|
||||
callback = null;
|
||||
loader = null;
|
||||
exoPlayer = null;
|
||||
listener = null;
|
||||
error = null;
|
||||
|
||||
state = STATE_INIT;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,11 +107,13 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
|||
|
||||
/**
|
||||
* 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() {
|
||||
// The current item has higher priority
|
||||
final int currentIndex = playQueue.getIndex();
|
||||
final PlayQueueItem currentItem = playQueue.get(currentIndex);
|
||||
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
|
||||
if (currentItem == null) return;
|
||||
load(currentItem);
|
||||
|
||||
|
|
@ -121,12 +123,24 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
|||
final int rightBound = Math.min(playQueue.size(), rightLimit);
|
||||
final List<PlayQueueItem> items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound));
|
||||
|
||||
// Do a round robin
|
||||
final int excess = rightLimit - playQueue.size();
|
||||
if (excess >= 0) items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
|
||||
|
||||
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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
|
@ -141,44 +155,8 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onNext(@NonNull PlayQueueMessage event) {
|
||||
// why no pattern matching in Java =(
|
||||
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);
|
||||
public void onNext(@NonNull PlayQueueMessage playQueueMessage) {
|
||||
onPlayQueueChanged(playQueueMessage);
|
||||
}
|
||||
|
||||
@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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
|
@ -220,7 +237,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
|||
}
|
||||
|
||||
private void sync() {
|
||||
final PlayQueueItem currentItem = playQueue.getCurrent();
|
||||
final PlayQueueItem currentItem = playQueue.getItem();
|
||||
|
||||
final Consumer<StreamInfo> syncPlayback = new Consumer<StreamInfo>() {
|
||||
@Override
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue