use exoplayer as music backend

This commit is contained in:
dkanada 2020-05-18 21:23:17 +09:00
commit 5c7be50dfa
5 changed files with 204 additions and 157 deletions

View file

@ -42,6 +42,7 @@ android {
dependencies {
implementation 'com.github.jellyfin.jellyfin-apiclient-java:android:0.6.1'
implementation 'com.google.android.exoplayer:exoplayer:2.11.4'
implementation 'androidx.core:core:1.2.0'
implementation 'androidx.media:media:1.1.0'

View file

@ -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;
public MultiPlayer(final Context context) {
this.context = context;
mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
private ExoPlayer.EventListener eventListener = new ExoPlayer.EventListener() {
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
Log.i(TAG,"onTracksChanged");
}
@Override
public boolean setDataSource(@NonNull final String path) {
return true;
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;
httpClient = new OkHttpClient();
exoPlayer = new SimpleExoPlayer.Builder(context).build();
mediaSource = new ConcatenatingMediaSource();
}
@Override
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;
}
private boolean appendDataSource(@NonNull final String path) {
return true;
if (mediaSource.getSize() >= 2) {
mediaSource.removeMediaSource(1);
}
appendDataSource(path, false);
}
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();
isPlaying = false;
exoPlayer.setPlayWhenReady(false);
return true;
} catch (IllegalStateException e) {
return false;
}
}
@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);
public boolean setVolume(float volume) {
exoPlayer.setVolume(volume);
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();
}
}
}

View file

@ -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);
nextPosition = getNextPosition(false);
playback.setNextDataSource(getTrackUri(getSongAt(nextPosition)));
this.nextPosition = nextPosition;
return true;
} catch (Exception e) {
return false;
}
}
}
@ -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);

View file

@ -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();

View file

@ -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