use exoplayer as music backend
This commit is contained in:
parent
f3eedc0a0f
commit
5c7be50dfa
5 changed files with 204 additions and 157 deletions
|
|
@ -1,44 +1,163 @@
|
|||
package com.dkanada.gramophone.service;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.MediaPlayer;
|
||||
import android.os.PowerManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.net.Uri;
|
||||
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.dkanada.gramophone.R;
|
||||
import com.dkanada.gramophone.service.playback.Playback;
|
||||
|
||||
public class MultiPlayer implements Playback, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener {
|
||||
public static final String TAG = MultiPlayer.class.getSimpleName();
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
private MediaPlayer mCurrentMediaPlayer = new MediaPlayer();
|
||||
private MediaPlayer mNextMediaPlayer;
|
||||
import java.io.IOException;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class MultiPlayer implements Playback {
|
||||
public static final String TAG = MultiPlayer.class.getSimpleName();
|
||||
|
||||
private Context context;
|
||||
|
||||
@Nullable
|
||||
private OkHttpClient httpClient;
|
||||
private SimpleExoPlayer exoPlayer;
|
||||
|
||||
private ConcatenatingMediaSource mediaSource;
|
||||
private Playback.PlaybackCallbacks callbacks;
|
||||
|
||||
private boolean mIsInitialized = false;
|
||||
private boolean isReady = false;
|
||||
private boolean isPlaying = false;
|
||||
private boolean isNew = false;
|
||||
private boolean isFirst = true;
|
||||
|
||||
private ExoPlayer.EventListener eventListener = new ExoPlayer.EventListener() {
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
Log.i(TAG,"onTracksChanged");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingChanged(boolean isLoading) {
|
||||
Log.i(TAG,"onLoadingChanged: isLoading = " + isLoading);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
Log.i(TAG,"onPlayerStateChanged: playWhenReady = " + playWhenReady);
|
||||
Log.i(TAG,"onPlayerStateChanged: playbackState = " + playbackState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(int reason) {
|
||||
Log.i(TAG,"onPositionDiscontinuity: reason = " + reason);
|
||||
int windowIndex = exoPlayer.getCurrentWindowIndex();
|
||||
|
||||
if (windowIndex == 1) {
|
||||
mediaSource.removeMediaSource(0);
|
||||
if (mediaSource.getSize() != 0) {
|
||||
// there are still songs left in the queue
|
||||
callbacks.onTrackWentToNext();
|
||||
} else {
|
||||
callbacks.onTrackEnded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
Log.i(TAG,"onPlaybackError: " + error.getMessage());
|
||||
if (context != null) {
|
||||
Toast.makeText(context, context.getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
stop();
|
||||
}
|
||||
};
|
||||
|
||||
public MultiPlayer(final Context context) {
|
||||
this.context = context;
|
||||
mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
|
||||
|
||||
httpClient = new OkHttpClient();
|
||||
exoPlayer = new SimpleExoPlayer.Builder(context).build();
|
||||
mediaSource = new ConcatenatingMediaSource();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setDataSource(@NonNull final String path) {
|
||||
return true;
|
||||
public void setDataSource(@NonNull final String path) {
|
||||
isReady = false;
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
isNew = true;
|
||||
mediaSource = new ConcatenatingMediaSource();
|
||||
|
||||
exoPlayer.addListener(eventListener);
|
||||
exoPlayer.prepare(mediaSource);
|
||||
|
||||
appendDataSource(path, true);
|
||||
isReady = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNextDataSource(@Nullable final String path) {
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mediaSource.getSize() >= 2) {
|
||||
mediaSource.removeMediaSource(1);
|
||||
}
|
||||
|
||||
appendDataSource(path, false);
|
||||
}
|
||||
|
||||
private boolean appendDataSource(@NonNull final String path) {
|
||||
return true;
|
||||
private void appendDataSource(String path, boolean next) {
|
||||
Uri uri = Uri.parse(path);
|
||||
|
||||
DataSource.Factory dataSource = new DefaultHttpDataSourceFactory(Util.getUserAgent(context, this.getClass().getName()));
|
||||
httpClient.newCall(new Request.Builder().url(path).head().build()).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Toast.makeText(context, context.getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) {
|
||||
MediaSource source;
|
||||
if (response.header("Content-Type").equals("application/x-mpegURL")) {
|
||||
source = new HlsMediaSource.Factory(dataSource).createMediaSource(uri);
|
||||
} else {
|
||||
source = new ProgressiveMediaSource.Factory(dataSource).createMediaSource(uri);
|
||||
}
|
||||
|
||||
mediaSource.addMediaSource(source);
|
||||
if (!isFirst && next) {
|
||||
start();
|
||||
}
|
||||
|
||||
isFirst = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -48,135 +167,61 @@ public class MultiPlayer implements Playback, MediaPlayer.OnErrorListener, Media
|
|||
|
||||
@Override
|
||||
public boolean isInitialized() {
|
||||
return mIsInitialized;
|
||||
return isReady;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean start() {
|
||||
try {
|
||||
mCurrentMediaPlayer.start();
|
||||
return true;
|
||||
} catch (IllegalStateException e) {
|
||||
return false;
|
||||
isPlaying = true;
|
||||
exoPlayer.setPlayWhenReady(true);
|
||||
|
||||
if (isNew) {
|
||||
callbacks.onTrackStarted();
|
||||
isNew = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
mCurrentMediaPlayer.reset();
|
||||
mIsInitialized = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
stop();
|
||||
|
||||
mCurrentMediaPlayer.release();
|
||||
if (mNextMediaPlayer != null) {
|
||||
mNextMediaPlayer.release();
|
||||
}
|
||||
exoPlayer.release();
|
||||
isReady = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean pause() {
|
||||
try {
|
||||
mCurrentMediaPlayer.pause();
|
||||
return true;
|
||||
} catch (IllegalStateException e) {
|
||||
return false;
|
||||
}
|
||||
isPlaying = false;
|
||||
exoPlayer.setPlayWhenReady(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
return mIsInitialized && mCurrentMediaPlayer.isPlaying();
|
||||
return isReady && isPlaying;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int duration() {
|
||||
if (!mIsInitialized) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
try {
|
||||
return mCurrentMediaPlayer.getDuration();
|
||||
} catch (IllegalStateException e) {
|
||||
return -1;
|
||||
}
|
||||
if (!isReady) return -1;
|
||||
return (int) exoPlayer.getDuration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int position() {
|
||||
if (!mIsInitialized) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
try {
|
||||
return mCurrentMediaPlayer.getCurrentPosition();
|
||||
} catch (IllegalStateException e) {
|
||||
return -1;
|
||||
}
|
||||
if (!isReady) return -1;
|
||||
return (int) exoPlayer.getCurrentPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int seek(final int whereto) {
|
||||
try {
|
||||
mCurrentMediaPlayer.seekTo(whereto);
|
||||
return whereto;
|
||||
} catch (IllegalStateException e) {
|
||||
return -1;
|
||||
}
|
||||
public int seek(int position) {
|
||||
exoPlayer.seekTo(position);
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setVolume(final float vol) {
|
||||
try {
|
||||
mCurrentMediaPlayer.setVolume(vol, vol);
|
||||
return true;
|
||||
} catch (IllegalStateException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setAudioSessionId(final int sessionId) {
|
||||
try {
|
||||
mCurrentMediaPlayer.setAudioSessionId(sessionId);
|
||||
return true;
|
||||
} catch (@NonNull IllegalArgumentException | IllegalStateException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAudioSessionId() {
|
||||
return mCurrentMediaPlayer.getAudioSessionId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onError(final MediaPlayer mp, final int what, final int extra) {
|
||||
mIsInitialized = false;
|
||||
mCurrentMediaPlayer.release();
|
||||
mCurrentMediaPlayer = new MediaPlayer();
|
||||
mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
|
||||
if (context != null) {
|
||||
Toast.makeText(context, context.getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompletion(final MediaPlayer mp) {
|
||||
if (mp == mCurrentMediaPlayer && mNextMediaPlayer != null) {
|
||||
mIsInitialized = false;
|
||||
mCurrentMediaPlayer.release();
|
||||
mCurrentMediaPlayer = mNextMediaPlayer;
|
||||
mIsInitialized = true;
|
||||
mNextMediaPlayer = null;
|
||||
if (callbacks != null) callbacks.onTrackWentToNext();
|
||||
} else {
|
||||
if (callbacks != null) callbacks.onTrackEnded();
|
||||
}
|
||||
public boolean setVolume(float volume) {
|
||||
exoPlayer.setVolume(volume);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -423,7 +423,6 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
|
|||
|
||||
position = restoredPosition;
|
||||
openCurrent();
|
||||
prepareNext();
|
||||
|
||||
if (restoredPositionInTrack > 0) seek(restoredPositionInTrack);
|
||||
|
||||
|
|
@ -451,7 +450,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
|
|||
queueSaveHandler.removeCallbacksAndMessages(null);
|
||||
queueSaveHandlerThread.quitSafely();
|
||||
|
||||
playback.release();
|
||||
playback.stop();
|
||||
playback = null;
|
||||
mediaSession.release();
|
||||
}
|
||||
|
|
@ -468,24 +467,20 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
|
|||
playSongAt(getNextPosition(force));
|
||||
}
|
||||
|
||||
private boolean openTrackAndPrepareNextAt(int position) {
|
||||
private void openTrackAndPrepareNextAt(int position) {
|
||||
synchronized (this) {
|
||||
this.position = position;
|
||||
boolean prepared = openCurrent();
|
||||
if (prepared) prepareNextImpl();
|
||||
|
||||
openCurrent();
|
||||
|
||||
notifyChange(META_CHANGED);
|
||||
notHandledMetaChangedForCurrentTrack = false;
|
||||
return prepared;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean openCurrent() {
|
||||
private void openCurrent() {
|
||||
synchronized (this) {
|
||||
try {
|
||||
return playback.setDataSource(getTrackUri(getCurrentSong()));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
playback.setDataSource(getTrackUri(getCurrentSong()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -494,16 +489,10 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
|
|||
playerHandler.obtainMessage(PREPARE_NEXT).sendToTarget();
|
||||
}
|
||||
|
||||
private boolean prepareNextImpl() {
|
||||
private void prepareNextImpl() {
|
||||
synchronized (this) {
|
||||
try {
|
||||
int nextPosition = getNextPosition(false);
|
||||
playback.setNextDataSource(getTrackUri(getSongAt(nextPosition)));
|
||||
this.nextPosition = nextPosition;
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
nextPosition = getNextPosition(false);
|
||||
playback.setNextDataSource(getTrackUri(getSongAt(nextPosition)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -805,11 +794,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
|
|||
}
|
||||
|
||||
private void playSongAtImpl(int position) {
|
||||
if (openTrackAndPrepareNextAt(position)) {
|
||||
play();
|
||||
} else {
|
||||
Toast.makeText(this, getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
openTrackAndPrepareNextAt(position);
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
|
|
@ -1072,10 +1057,6 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
|
|||
}
|
||||
}
|
||||
|
||||
public int getAudioSessionId() {
|
||||
return playback.getAudioSessionId();
|
||||
}
|
||||
|
||||
public MediaSessionCompat getMediaSession() {
|
||||
return mediaSession;
|
||||
}
|
||||
|
|
@ -1093,13 +1074,6 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
|
|||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
switch (key) {
|
||||
case PreferenceUtil.GAPLESS_PLAYBACK:
|
||||
if (sharedPreferences.getBoolean(key, false)) {
|
||||
prepareNext();
|
||||
} else {
|
||||
playback.setNextDataSource(null);
|
||||
}
|
||||
break;
|
||||
case PreferenceUtil.SHOW_ALBUM_COVER:
|
||||
case PreferenceUtil.BLUR_ALBUM_COVER:
|
||||
updateMediaSessionMetaData();
|
||||
|
|
@ -1114,6 +1088,12 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrackStarted() {
|
||||
handleAndSendChangeInternal(PLAY_STATE_CHANGED);
|
||||
prepareNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrackWentToNext() {
|
||||
playerHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.dkanada.gramophone.service.playback;
|
|||
import androidx.annotation.Nullable;
|
||||
|
||||
public interface Playback {
|
||||
boolean setDataSource(String path);
|
||||
void setDataSource(String path);
|
||||
|
||||
void setNextDataSource(@Nullable String path);
|
||||
|
||||
|
|
@ -15,8 +15,6 @@ public interface Playback {
|
|||
|
||||
void stop();
|
||||
|
||||
void release();
|
||||
|
||||
boolean pause();
|
||||
|
||||
boolean isPlaying();
|
||||
|
|
@ -29,11 +27,9 @@ public interface Playback {
|
|||
|
||||
boolean setVolume(float vol);
|
||||
|
||||
boolean setAudioSessionId(int sessionId);
|
||||
|
||||
int getAudioSessionId();
|
||||
|
||||
interface PlaybackCallbacks {
|
||||
void onTrackStarted();
|
||||
|
||||
void onTrackWentToNext();
|
||||
|
||||
void onTrackEnded();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import android.net.Uri;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.dkanada.gramophone.App;
|
||||
|
|
@ -16,6 +17,7 @@ import com.dkanada.gramophone.model.Artist;
|
|||
import com.dkanada.gramophone.model.Genre;
|
||||
import com.dkanada.gramophone.model.Song;
|
||||
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.interaction.Response;
|
||||
import org.jellyfin.apiclient.model.dto.UserItemDataDto;
|
||||
|
||||
|
|
@ -24,7 +26,30 @@ import java.util.Locale;
|
|||
|
||||
public class MusicUtil {
|
||||
public static Uri getSongFileUri(Song song) {
|
||||
return Uri.parse(App.getApiClient().getApiUrl() + "/Audio/" + song.id + "/stream?static=true");
|
||||
if (song.id == null) return null;
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
ApiClient apiClient = App.getApiClient();
|
||||
|
||||
builder.append(apiClient.getApiUrl());
|
||||
builder.append("/Audio/");
|
||||
builder.append(song.id);
|
||||
builder.append("/universal");
|
||||
builder.append("?UserId=" + apiClient.getCurrentUserId());
|
||||
builder.append("&DeviceId=" + apiClient.getDeviceId());
|
||||
|
||||
// web max is 12444445 and 320kbps is 320000
|
||||
builder.append("&MaxStreamingBitrate=10000000");
|
||||
builder.append("&Container=flac");
|
||||
builder.append("&TranscodingContainer=ts");
|
||||
builder.append("&TranscodingProtocol=hls");
|
||||
|
||||
// preferred codec when transcoding
|
||||
builder.append("&AudioCodec=aac");
|
||||
builder.append("&api_key=" + apiClient.getAccessToken());
|
||||
|
||||
Log.i(MusicUtil.class.getName(), "playing audio: " + builder);
|
||||
return Uri.parse(builder.toString());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue