package com.marverenic.music.player;
import android.content.Context;
import android.media.audiofx.Equalizer;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioTrack;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.LoopingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.marverenic.music.BuildConfig;
import com.marverenic.music.model.Song;
import com.marverenic.music.utils.Internal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import timber.log.Timber;
public class QueuedExoPlayer implements QueuedMediaPlayer {
private static final String USER_AGENT = "Jockey/" + BuildConfig.VERSION_NAME;
private Context mContext;
private EqualizedExoPlayer mExoPlayer;
private ExoPlayerState mState;
private boolean mRepeatAll;
private boolean mRepeatOne;
@Nullable
private PlaybackEventListener mEventListener;
private boolean mHasError;
private boolean mInvalid;
private List<Song> mQueue;
private int mQueueIndex;
private int mPrevDuration;
static {
AudioTrack.enablePreV21AudioSessionWorkaround = true;
}
public QueuedExoPlayer(Context context) {
mContext = context;
mState = ExoPlayerState.IDLE;
TrackSelector trackSelector = new DefaultTrackSelector(new FixedTrackSelection.Factory());
LoadControl loadControl = new DefaultLoadControl();
SimpleExoPlayer baseInstance = ExoPlayerFactory.newSimpleInstance(mContext,
trackSelector, loadControl);
mExoPlayer = new EqualizedExoPlayer(context, baseInstance);
mExoPlayer.addListener(new ExoPlayer.EventListener() {
@Override
public void onLoadingChanged(boolean isLoading) {
Timber.i("onLoadingChanged (%b)", isLoading);
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
Timber.i("onPlayerStateChanged");
QueuedExoPlayer.this.onPlayerStateChanged(playbackState);
}
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
Timber.i("onTimelineChanged");
QueuedExoPlayer.this.onTimelineChanged();
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups,
TrackSelectionArray trackSelections) {
Timber.i("onTracksChanged");
}
@Override
public void onPlayerError(ExoPlaybackException error) {
Timber.i("onPlayerError");
QueuedExoPlayer.this.onPlayerError(error);
}
@Override
public void onPositionDiscontinuity() {
Timber.i("onPositionDiscontinuity");
QueuedExoPlayer.this.onPositionDiscontinuity();
}
});
}
@Internal void onPlayerStateChanged(int playbackState) {
boolean stateDiff = mState != ExoPlayerState.fromInt(playbackState);
mState = ExoPlayerState.fromInt(playbackState);
mHasError = mHasError && (mState == ExoPlayerState.IDLE);
if (stateDiff && playbackState == ExoPlayer.STATE_ENDED) {
onCompletion();
pause();
setQueueIndex(0);
}
}
private void onCompletion() {
Song completed = getNowPlaying();
if (mInvalid) {
boolean ended = false;
if (!mRepeatOne) {
if (mRepeatAll && mQueueIndex == mQueue.size() - 1) {
mQueueIndex = 0;
} else if (mQueueIndex < mQueue.size() - 1) {
mQueueIndex++;
} else {
mQueueIndex = 0;
ended = true;
}
}
prepare(!ended, true);
if (!ended && mEventListener != null) {
mEventListener.onCompletion(completed);
}
} else if (mEventListener != null) {
mEventListener.onCompletion(completed);
}
}
private void onStart() {
if (mEventListener != null) {
mEventListener.onSongStart();
}
}
@Internal void onTimelineChanged() {
onStart();
}
@Internal void onPositionDiscontinuity() {
if (mQueue.size() == 0) {
return;
}
int currentQueueIndex = mExoPlayer.getCurrentWindowIndex() % mQueue.size();
boolean isRepeatOne = mRepeatOne;
if (mQueueIndex != currentQueueIndex || mInvalid) {
onCompletion();
if (!isRepeatOne && !mInvalid) {
mQueueIndex = currentQueueIndex;
onStart();
}
}
}
@Internal void onPlayerError(ExoPlaybackException error) {
mHasError = true;
mInvalid = true;
if (mExoPlayer.getCurrentPosition() >= mExoPlayer.getDuration()
&& mExoPlayer.getDuration() > 0) {
mQueueIndex = (mQueueIndex + 1) % mQueue.size();
}
if (mEventListener != null) {
mEventListener.onError(error.getCause());
}
}
@Override
public void setPlaybackEventListener(@Nullable PlaybackEventListener listener) {
mEventListener = listener;
}
@Override
public Song getNowPlaying() {
if (mQueue == null || mQueue.isEmpty()) {
return null;
}
return mQueue.get(mQueueIndex);
}
@Override
public List<Song> getQueue() {
return mQueue;
}
@Override
public int getQueueSize() {
return mQueue.size();
}
@Override
public void setQueue(@NonNull List<Song> queue, int index) {
if (index < 0) {
throw new IllegalArgumentException("index cannot be negative");
}
if (!queue.isEmpty() && index >= queue.size()) {
throw new IllegalArgumentException("index must be smaller than the queue size ("
+ queue.size() + ")");
}
if (queue.isEmpty()) {
reset();
} else {
boolean nowPlayingDiff = !queue.get(index).equals(getNowPlaying());
boolean queueDiff = !queue.equals(mQueue);
mQueue = Collections.unmodifiableList(new ArrayList<>(queue));
mQueueIndex = index;
if (nowPlayingDiff) {
prepare(isPlaying(), true);
} else if ((queueDiff && !isPlaying())) {
prepare(false, false);
} else {
mInvalid |= queueDiff;
}
}
}
@Override
public void setQueueIndex(int index) {
if (index == mQueueIndex) {
seekTo(0);
} else {
mQueueIndex = index;
if (mRepeatOne || mInvalid) {
prepare(true, true);
} else {
mExoPlayer.seekTo(index, 0);
}
}
}
@Override
public int getQueueIndex() {
return mQueueIndex;
}
private void prepare(boolean playWhenReady, boolean resetPosition) {
mInvalid = false;
if (mQueue == null) {
return;
}
DataSource.Factory srcFactory = new DefaultDataSourceFactory(mContext, USER_AGENT);
ExtractorsFactory extFactory = new DefaultExtractorsFactory();
int startingPosition = resetPosition ? 0 : getCurrentPosition();
if (mRepeatOne) {
mExoPlayer.prepare(buildRepeatOneMediaSource(srcFactory, extFactory));
} else if (mRepeatAll) {
mExoPlayer.prepare(buildRepeatAllMediaSource(srcFactory, extFactory));
} else {
mExoPlayer.prepare(buildNoRepeatMediaSource(srcFactory, extFactory));
}
mExoPlayer.seekTo(mQueueIndex, startingPosition);
mExoPlayer.setPlayWhenReady(playWhenReady);
}
private MediaSource buildRepeatOneMediaSource(DataSource.Factory srcFactory,
ExtractorsFactory extFactory) {
if (mQueue.isEmpty()) {
// We need to return an empty MediaSource (can't be null), so return a
// ConcatenatingMediaSource with nothing to concatenate
return new ConcatenatingMediaSource();
}
Uri uri = mQueue.get(mQueueIndex).getLocation();
MediaSource source = new ExtractorMediaSource(uri, srcFactory, extFactory, null, null);
return new LoopingMediaSource(source);
}
private MediaSource buildNoRepeatMediaSource(DataSource.Factory srcFactory,
ExtractorsFactory extFactory) {
MediaSource[] queue = new MediaSource[mQueue.size()];
for (int i = 0; i < queue.length; i++) {
Uri uri = mQueue.get(i).getLocation();
queue[i] = new ExtractorMediaSource(uri, srcFactory, extFactory, null, null);
}
return new ConcatenatingMediaSource(queue);
}
private MediaSource buildRepeatAllMediaSource(DataSource.Factory sourceFactory,
ExtractorsFactory extractorsFactory) {
MediaSource queue = buildNoRepeatMediaSource(sourceFactory, extractorsFactory);
return new LoopingMediaSource(queue);
}
@Override
public void skip() {
mQueueIndex++;
mQueueIndex %= mQueue.size();
if (mRepeatOne || mInvalid) {
prepare(true, true);
} else {
mExoPlayer.seekTo(mQueueIndex, 0);
mExoPlayer.setPlayWhenReady(true);
}
}
@Override
public void skipPrevious() {
mQueueIndex--;
mQueueIndex %= mQueue.size();
if (mQueueIndex < 0) {
mQueueIndex += mQueue.size();
}
if (mRepeatOne || mInvalid) {
prepare(true, true);
} else {
mExoPlayer.seekTo(mQueueIndex, 0);
mExoPlayer.setPlayWhenReady(true);
}
}
@Override
public void seekTo(int mSec) {
if (mInvalid) {
prepare(isPlaying(), false);
}
mExoPlayer.seekTo(mQueueIndex, mSec);
}
@Override
public void stop() {
mExoPlayer.stop();
mExoPlayer.seekToDefaultPosition();
}
@Override
public void play() {
mExoPlayer.setPlayWhenReady(true);
}
@Override
public void pause() {
mExoPlayer.setPlayWhenReady(false);
if (mInvalid) {
prepare(false, false);
}
}
@Override
public int getCurrentPosition() {
return (int) mExoPlayer.getCurrentPosition();
}
@Override
public int getDuration() {
if (mExoPlayer.getDuration() > 0) {
mPrevDuration = (int) mExoPlayer.getDuration();
} else if (mPrevDuration <= 0 && getNowPlaying() != null) {
return (int) getNowPlaying().getSongDuration();
}
return mPrevDuration;
}
@Override
public boolean isComplete() {
return mState == ExoPlayerState.ENDED;
}
@Override
public boolean isPaused() {
return !mExoPlayer.getPlayWhenReady();
}
@Override
public boolean isStopped() {
return mState == ExoPlayerState.IDLE;
}
@Override
public boolean hasError() {
return mHasError;
}
@Override
public void setVolume(float volume) {
mExoPlayer.setVolume(volume);
}
@Override
public void setEqualizer(boolean enabled, Equalizer.Settings settings) {
mExoPlayer.setEqualizerSettings(enabled, settings);
}
@Override
public void enableRepeatAll() {
if (!mRepeatAll) {
mRepeatAll = true;
mRepeatOne = false;
mInvalid = true;
}
}
@Override
public void enableRepeatOne() {
if (!mRepeatOne) {
mRepeatOne = true;
mRepeatAll = false;
mInvalid = true;
}
}
@Override
public void enableRepeatNone() {
if (mRepeatAll || mRepeatOne) {
mRepeatOne = false;
mRepeatAll = false;
mInvalid = true;
}
}
@Override
public boolean isPlaying() {
return mExoPlayer.getPlayWhenReady();
}
@Override
public void reset() {
mQueue = Collections.emptyList();
mQueueIndex = 0;
prepare(false, true);
}
@Override
public void release() {
mExoPlayer.release();
mExoPlayer = null;
mContext = null;
}
}