From 2260cc311fea0484957118c14f0c094f65d06101 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 24 Mar 2021 12:47:31 +0100 Subject: [PATCH 01/14] Automatic conversion of LocalMediaPlayer to Kotlin --- .../ultrasonic/service/LocalMediaPlayer.java | 1037 ----------------- .../ultrasonic/service/LocalMediaPlayer.kt | 694 +++++++++++ 2 files changed, 694 insertions(+), 1037 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/LocalMediaPlayer.java create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/LocalMediaPlayer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/LocalMediaPlayer.java deleted file mode 100644 index e12fed1a..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/LocalMediaPlayer.java +++ /dev/null @@ -1,1037 +0,0 @@ -package org.moire.ultrasonic.service; - -import android.annotation.SuppressLint; -import android.app.PendingIntent; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.media.AudioManager; -import android.media.MediaMetadataRetriever; -import android.media.MediaPlayer; -import android.media.RemoteControlClient; -import android.media.audiofx.AudioEffect; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.os.PowerManager; -import timber.log.Timber; -import android.widget.SeekBar; - -import org.jetbrains.annotations.NotNull; -import org.moire.ultrasonic.audiofx.EqualizerController; -import org.moire.ultrasonic.audiofx.VisualizerController; -import org.moire.ultrasonic.data.ActiveServerProvider; -import org.moire.ultrasonic.domain.MusicDirectory; -import org.moire.ultrasonic.domain.PlayerState; -import org.moire.ultrasonic.fragment.PlayerFragment; -import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver; -import org.moire.ultrasonic.util.CancellableTask; -import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.FileUtil; -import org.moire.ultrasonic.util.StreamProxy; -import org.moire.ultrasonic.util.Util; - -import java.io.File; -import java.net.URLEncoder; -import java.util.Locale; - -import static org.moire.ultrasonic.domain.PlayerState.COMPLETED; -import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING; -import static org.moire.ultrasonic.domain.PlayerState.IDLE; -import static org.moire.ultrasonic.domain.PlayerState.PAUSED; -import static org.moire.ultrasonic.domain.PlayerState.PREPARED; -import static org.moire.ultrasonic.domain.PlayerState.PREPARING; -import static org.moire.ultrasonic.domain.PlayerState.STARTED; - -/** - * Represents a Media Player which uses the mobile's resources for playback - */ -public class LocalMediaPlayer -{ - public Consumer onCurrentPlayingChanged; - public Consumer onSongCompleted; - public BiConsumer onPlayerStateChanged; - public Runnable onPrepared; - public Runnable onNextSongRequested; - - public PlayerState playerState = IDLE; - public DownloadFile currentPlaying; - public DownloadFile nextPlaying; - - private PlayerState nextPlayerState = IDLE; - private boolean nextSetup; - private CancellableTask nextPlayingTask; - private PowerManager.WakeLock wakeLock; - - private MediaPlayer mediaPlayer; - private MediaPlayer nextMediaPlayer; - private Looper mediaPlayerLooper; - private Handler mediaPlayerHandler; - private int cachedPosition; - private StreamProxy proxy; - - private AudioManager audioManager; - private RemoteControlClient remoteControlClient; - - private CancellableTask bufferTask; - private PositionCache positionCache; - private int secondaryProgress = -1; - - private final AudioFocusHandler audioFocusHandler; - private final Context context; - - public LocalMediaPlayer(AudioFocusHandler audioFocusHandler, Context context) - { - this.audioFocusHandler = audioFocusHandler; - this.context = context; - } - - public void onCreate() - { - if (mediaPlayer != null) - { - mediaPlayer.release(); - } - - mediaPlayer = new MediaPlayer(); - - new Thread(new Runnable() - { - @Override - public void run() - { - Thread.currentThread().setName("MediaPlayerThread"); - Looper.prepare(); - mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); - - mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() - { - @Override - public boolean onError(MediaPlayer mediaPlayer, int what, int more) - { - handleError(new Exception(String.format(Locale.getDefault(), "MediaPlayer error: %d (%d)", what, more))); - return false; - } - }); - - try - { - Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); - i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId()); - i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName()); - context.sendBroadcast(i); - } - catch (Throwable e) - { - // Froyo or lower - } - - mediaPlayerLooper = Looper.myLooper(); - mediaPlayerHandler = new Handler(mediaPlayerLooper); - Looper.loop(); - } - }).start(); - - // Create Equalizer and Visualizer on a new thread as this can potentially take some time - new Thread(new Runnable() { - @Override - public void run() { - EqualizerController.create(context, mediaPlayer); - VisualizerController.create(mediaPlayer); - } - }).start(); - - PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName()); - wakeLock.setReferenceCounted(false); - - audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - Util.registerMediaButtonEventReceiver(context, true); - setUpRemoteControlClient(); - - Timber.i("LocalMediaPlayer created"); - } - - public void onDestroy() - { - reset(); - - try - { - Intent i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); - i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId()); - i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName()); - context.sendBroadcast(i); - - EqualizerController.release(); - VisualizerController.release(); - - mediaPlayer.release(); - if (nextMediaPlayer != null) - { - nextMediaPlayer.release(); - } - - mediaPlayerLooper.quit(); - - if (bufferTask != null) - { - bufferTask.cancel(); - } - - if (nextPlayingTask != null) - { - nextPlayingTask.cancel(); - } - - audioManager.unregisterRemoteControlClient(remoteControlClient); - clearRemoteControl(); - Util.unregisterMediaButtonEventReceiver(context, true); - wakeLock.release(); - } - catch (Throwable exception) - { - Timber.w(exception, "LocalMediaPlayer onDestroy exception: "); - } - - Timber.i("LocalMediaPlayer destroyed"); - } - - public synchronized void setPlayerState(final PlayerState playerState) - { - Timber.i("%s -> %s (%s)", this.playerState.name(), playerState.name(), currentPlaying); - - this.playerState = playerState; - - if (playerState == PlayerState.STARTED) - { - audioFocusHandler.requestAudioFocus(); - } - - if (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED) - { - updateRemoteControl(); - } - - if (onPlayerStateChanged != null) - { - Handler mainHandler = new Handler(context.getMainLooper()); - Runnable myRunnable = new Runnable() { - @Override - public void run() { - onPlayerStateChanged.accept(playerState, currentPlaying); - } - }; - mainHandler.post(myRunnable); - } - - if (playerState == STARTED && positionCache == null) - { - positionCache = new PositionCache(); - Thread thread = new Thread(positionCache); - thread.start(); - } - else if (playerState != STARTED && positionCache != null) - { - positionCache.stop(); - positionCache = null; - } - } - - public synchronized void setCurrentPlaying(final DownloadFile currentPlaying) - { - Timber.v("setCurrentPlaying %s", currentPlaying); - this.currentPlaying = currentPlaying; - updateRemoteControl(); - - if (onCurrentPlayingChanged != null) - { - Handler mainHandler = new Handler(context.getMainLooper()); - Runnable myRunnable = new Runnable() { - @Override - public void run() { - onCurrentPlayingChanged.accept(currentPlaying); - } - }; - mainHandler.post(myRunnable); - } - } - - public synchronized void setNextPlaying(DownloadFile nextToPlay) - { - if (nextToPlay == null) - { - nextPlaying = null; - setNextPlayerState(IDLE); - return; - } - - nextPlaying = nextToPlay; - nextPlayingTask = new CheckCompletionTask(nextPlaying); - nextPlayingTask.start(); - } - - public synchronized void clearNextPlaying() - { - nextSetup = false; - nextPlaying = null; - if (nextPlayingTask != null) - { - nextPlayingTask.cancel(); - nextPlayingTask = null; - } - } - - public synchronized void setNextPlayerState(PlayerState playerState) - { - Timber.i("Next: %s -> %s (%s)", nextPlayerState.name(), playerState.name(), nextPlaying); - nextPlayerState = playerState; - } - - public synchronized void bufferAndPlay() - { - if (playerState != PREPARED) - { - reset(); - - bufferTask = new BufferTask(currentPlaying, 0); - bufferTask.start(); - } - else - { - doPlay(currentPlaying, 0, true); - } - } - - public synchronized void play(DownloadFile fileToPlay) - { - if (nextPlayingTask != null) - { - nextPlayingTask.cancel(); - nextPlayingTask = null; - } - - setCurrentPlaying(fileToPlay); - bufferAndPlay(); - } - - public synchronized void playNext() - { - MediaPlayer tmp = mediaPlayer; - mediaPlayer = nextMediaPlayer; - nextMediaPlayer = tmp; - setCurrentPlaying(nextPlaying); - setPlayerState(PlayerState.STARTED); - setupHandlers(currentPlaying, false); - - if (onNextSongRequested != null) { - Handler mainHandler = new Handler(context.getMainLooper()); - Runnable myRunnable = new Runnable() { - @Override - public void run() { - onNextSongRequested.run(); - } - }; - mainHandler.post(myRunnable); - } - - // Proxy should not be being used here since the next player was already setup to play - if (proxy != null) - { - proxy.stop(); - proxy = null; - } - } - - public synchronized void pause() - { - try - { - mediaPlayer.pause(); - } - catch (Exception x) - { - handleError(x); - } - } - - public synchronized void start() - { - try - { - mediaPlayer.start(); - } - catch (Exception x) - { - handleError(x); - } - } - - private void updateRemoteControl() - { - if (!Util.isLockScreenEnabled(context)) - { - clearRemoteControl(); - return; - } - - if (remoteControlClient != null) - { - audioManager.unregisterRemoteControlClient(remoteControlClient); - audioManager.registerRemoteControlClient(remoteControlClient); - } - else - { - setUpRemoteControlClient(); - } - - Timber.i("In updateRemoteControl, playerState: %s [%d]", playerState, getPlayerPosition()); - - if (playerState == STARTED) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { - remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); - } else { - remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING, getPlayerPosition(), 1.0f); - } - } else { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { - remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); - } else { - remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED, getPlayerPosition(), 1.0f); - } - } - - if (currentPlaying != null) - { - MusicDirectory.Entry currentSong = currentPlaying.getSong(); - - Bitmap lockScreenBitmap = FileUtil.getAlbumArtBitmap(context, currentSong, Util.getMinDisplayMetric(context), true); - - String artist = currentSong.getArtist(); - String album = currentSong.getAlbum(); - String title = currentSong.getTitle(); - Integer currentSongDuration = currentSong.getDuration(); - long duration = 0L; - - if (currentSongDuration != null) duration = (long) currentSongDuration * 1000; - - remoteControlClient.editMetadata(true) - .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, artist) - .putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, artist) - .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, album) - .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, title) - .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration) - .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, lockScreenBitmap) - .apply(); - } - } - - public void clearRemoteControl() - { - if (remoteControlClient != null) - { - remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); - audioManager.unregisterRemoteControlClient(remoteControlClient); - remoteControlClient = null; - } - } - - private void setUpRemoteControlClient() - { - if (!Util.isLockScreenEnabled(context)) return; - - ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName()); - - if (remoteControlClient == null) - { - final Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); - mediaButtonIntent.setComponent(componentName); - PendingIntent broadcast = PendingIntent.getBroadcast(context, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT); - remoteControlClient = new RemoteControlClient(broadcast); - audioManager.registerRemoteControlClient(remoteControlClient); - - // Flags for the media transport control that this client supports. - int flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS | - RemoteControlClient.FLAG_KEY_MEDIA_NEXT | - RemoteControlClient.FLAG_KEY_MEDIA_PLAY | - RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | - RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE | - RemoteControlClient.FLAG_KEY_MEDIA_STOP; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) - { - flags |= RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE; - - remoteControlClient.setOnGetPlaybackPositionListener(new RemoteControlClient.OnGetPlaybackPositionListener() - { - @Override - public long onGetPlaybackPosition() - { - return mediaPlayer.getCurrentPosition(); - } - }); - - remoteControlClient.setPlaybackPositionUpdateListener(new RemoteControlClient.OnPlaybackPositionUpdateListener() - { - @Override - public void onPlaybackPositionUpdate(long newPositionMs) - { - seekTo((int) newPositionMs); - } - }); - } - - remoteControlClient.setTransportControlFlags(flags); - } - } - - public synchronized void seekTo(int position) - { - try - { - mediaPlayer.seekTo(position); - cachedPosition = position; - - updateRemoteControl(); - } - catch (Exception x) - { - handleError(x); - } - } - - public synchronized int getPlayerPosition() - { - try - { - if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) - { - return 0; - } - - return cachedPosition; - } - catch (Exception x) - { - handleError(x); - return 0; - } - } - - public synchronized int getPlayerDuration() - { - if (currentPlaying != null) - { - Integer duration = currentPlaying.getSong().getDuration(); - if (duration != null) - { - return duration * 1000; - } - } - if (playerState != IDLE && playerState != DOWNLOADING && playerState != PlayerState.PREPARING) - { - try - { - return mediaPlayer.getDuration(); - } - catch (Exception x) - { - handleError(x); - } - } - return 0; - } - - public void setVolume(float volume) - { - if (mediaPlayer != null) - { - mediaPlayer.setVolume(volume, volume); - } - } - - public synchronized void doPlay(final DownloadFile downloadFile, final int position, final boolean start) - { - try - { - downloadFile.setPlaying(false); - //downloadFile.setPlaying(true); - final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); - boolean partial = file.equals(downloadFile.getPartialFile()); - downloadFile.updateModificationDate(); - - mediaPlayer.setOnCompletionListener(null); - secondaryProgress = -1; // Ensure seeking in non StreamProxy playback works - mediaPlayer.reset(); - setPlayerState(IDLE); - mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - String dataSource = file.getPath(); - - if (partial) - { - if (proxy == null) - { - proxy = new StreamProxy(new Supplier() { - @Override - public DownloadFile get() { return currentPlaying; } - }); - proxy.start(); - } - - dataSource = String.format(Locale.getDefault(), "http://127.0.0.1:%d/%s", - proxy.getPort(), URLEncoder.encode(dataSource, Constants.UTF_8)); - Timber.i("Data Source: %s", dataSource); - } - else if (proxy != null) - { - proxy.stop(); - proxy = null; - } - - Timber.i("Preparing media player"); - mediaPlayer.setDataSource(dataSource); - setPlayerState(PREPARING); - - mediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() - { - @Override - public void onBufferingUpdate(MediaPlayer mp, int percent) - { - SeekBar progressBar = PlayerFragment.getProgressBar(); - MusicDirectory.Entry song = downloadFile.getSong(); - - if (percent == 100) - { - if (progressBar != null) - { - progressBar.setSecondaryProgress(100 * progressBar.getMax()); - } - - mp.setOnBufferingUpdateListener(null); - } - else if (progressBar != null && song.getTranscodedContentType() == null && Util.getMaxBitRate(context) == 0) - { - secondaryProgress = (int) (((double) percent / (double) 100) * progressBar.getMax()); - progressBar.setSecondaryProgress(secondaryProgress); - } - } - }); - - mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() - { - @Override - public void onPrepared(MediaPlayer mp) - { - Timber.i("Media player prepared"); - - setPlayerState(PREPARED); - - SeekBar progressBar = PlayerFragment.getProgressBar(); - - if (progressBar != null && downloadFile.isWorkDone()) - { - // Populate seek bar secondary progress if we have a complete file for consistency - PlayerFragment.getProgressBar().setSecondaryProgress(100 * progressBar.getMax()); - } - - synchronized (LocalMediaPlayer.this) - { - if (position != 0) - { - Timber.i("Restarting player from position %d", position); - seekTo(position); - } - cachedPosition = position; - - if (start) - { - mediaPlayer.start(); - setPlayerState(STARTED); - } - else - { - setPlayerState(PAUSED); - } - } - - if (onPrepared != null) { - Handler mainHandler = new Handler(context.getMainLooper()); - Runnable myRunnable = new Runnable() { - @Override - public void run() { - onPrepared.run(); - } - }; - mainHandler.post(myRunnable); - } - } - }); - - setupHandlers(downloadFile, partial); - - mediaPlayer.prepareAsync(); - } - catch (Exception x) - { - handleError(x); - } - } - - private synchronized void setupNext(final DownloadFile downloadFile) - { - try - { - final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); - - if (nextMediaPlayer != null) - { - nextMediaPlayer.setOnCompletionListener(null); - nextMediaPlayer.release(); - nextMediaPlayer = null; - } - - nextMediaPlayer = new MediaPlayer(); - nextMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); - - try - { - nextMediaPlayer.setAudioSessionId(mediaPlayer.getAudioSessionId()); - } - catch (Throwable e) - { - nextMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - } - - nextMediaPlayer.setDataSource(file.getPath()); - setNextPlayerState(PREPARING); - - nextMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() - { - @Override - @SuppressLint("NewApi") - public void onPrepared(MediaPlayer mp) - { - try - { - setNextPlayerState(PREPARED); - - if (Util.getGaplessPlaybackPreference(context) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED)) - { - mediaPlayer.setNextMediaPlayer(nextMediaPlayer); - nextSetup = true; - } - } - catch (Exception x) - { - handleErrorNext(x); - } - } - }); - - nextMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() - { - @Override - public boolean onError(MediaPlayer mediaPlayer, int what, int extra) - { - Timber.w("Error on playing next (%d, %d): %s", what, extra, downloadFile); - return true; - } - }); - - nextMediaPlayer.prepareAsync(); - } - catch (Exception x) - { - handleErrorNext(x); - } - } - - private void setupHandlers(final DownloadFile downloadFile, final boolean isPartial) - { - mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() - { - @Override - public boolean onError(MediaPlayer mediaPlayer, int what, int extra) - { - Timber.w("Error on playing file (%d, %d): %s", what, extra, downloadFile); - int pos = cachedPosition; - reset(); - downloadFile.setPlaying(false); - doPlay(downloadFile, pos, true); - downloadFile.setPlaying(true); - return true; - } - }); - - final int duration = downloadFile.getSong().getDuration() == null ? 0 : downloadFile.getSong().getDuration() * 1000; - - mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() - { - @Override - public void onCompletion(MediaPlayer mediaPlayer) - { - // Acquire a temporary wakelock, since when we return from - // this callback the MediaPlayer will release its wakelock - // and allow the device to go to sleep. - wakeLock.acquire(60000); - - int pos = cachedPosition; - Timber.i("Ending position %d of %d", pos, duration); - - if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 1000))) - { - setPlayerState(COMPLETED); - - if (Util.getGaplessPlaybackPreference(context) && nextPlaying != null && nextPlayerState == PlayerState.PREPARED) - { - if (nextSetup) - { - nextSetup = false; - } - playNext(); - } - else - { - if (onSongCompleted != null) - { - Handler mainHandler = new Handler(context.getMainLooper()); - Runnable myRunnable = new Runnable() { - @Override - public void run() { - onSongCompleted.accept(currentPlaying); - } - }; - mainHandler.post(myRunnable); - } - } - - return; - } - - synchronized (this) - { - if (downloadFile.isWorkDone()) - { - // Complete was called early even though file is fully buffered - Timber.i("Requesting restart from %d of %d", pos, duration); - reset(); - downloadFile.setPlaying(false); - doPlay(downloadFile, pos, true); - downloadFile.setPlaying(true); - } - else - { - Timber.i("Requesting restart from %d of %d", pos, duration); - reset(); - bufferTask = new BufferTask(downloadFile, pos); - bufferTask.start(); - } - } - } - }); - } - - public synchronized void reset() - { - if (bufferTask != null) - { - bufferTask.cancel(); - } - try - { - setPlayerState(IDLE); - mediaPlayer.setOnErrorListener(null); - mediaPlayer.setOnCompletionListener(null); - mediaPlayer.reset(); - } - catch (Exception x) - { - handleError(x); - } - } - - private class BufferTask extends CancellableTask - { - private final DownloadFile downloadFile; - private final int position; - private final long expectedFileSize; - private final File partialFile; - - public BufferTask(DownloadFile downloadFile, int position) - { - this.downloadFile = downloadFile; - this.position = position; - partialFile = downloadFile.getPartialFile(); - - long bufferLength = Util.getBufferLength(context); - - if (bufferLength == 0) - { - // Set to seconds in a day, basically infinity - bufferLength = 86400L; - } - - // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to. - int bitRate = downloadFile.getBitRate(); - long byteCount = Math.max(100000, bitRate * 1024L / 8L * bufferLength); - - // Find out how large the file should grow before resuming playback. - Timber.i("Buffering from position %d and bitrate %d", position, bitRate); - expectedFileSize = (position * bitRate / 8) + byteCount; - } - - @Override - public void execute() - { - setPlayerState(DOWNLOADING); - - while (!bufferComplete() && !ActiveServerProvider.Companion.isOffline(context)) - { - Util.sleepQuietly(1000L); - if (isCancelled()) - { - return; - } - } - doPlay(downloadFile, position, true); - } - - private boolean bufferComplete() - { - boolean completeFileAvailable = downloadFile.isWorkDone(); - long size = partialFile.length(); - - Timber.i("Buffering %s (%d/%d, %s)", partialFile, size, expectedFileSize, completeFileAvailable); - return completeFileAvailable || size >= expectedFileSize; - } - - @NotNull - @Override - public String toString() - { - return String.format("BufferTask (%s)", downloadFile); - } - } - - private class CheckCompletionTask extends CancellableTask - { - private final DownloadFile downloadFile; - private final File partialFile; - - public CheckCompletionTask(DownloadFile downloadFile) - { - super(); - setNextPlayerState(PlayerState.IDLE); - - this.downloadFile = downloadFile; - - partialFile = downloadFile != null ? downloadFile.getPartialFile() : null; - } - - @Override - public void execute() - { - Thread.currentThread().setName("CheckCompletionTask"); - - if (downloadFile == null) - { - return; - } - - // Do an initial sleep so this prepare can't compete with main prepare - Util.sleepQuietly(5000L); - - while (!bufferComplete()) - { - Util.sleepQuietly(5000L); - - if (isCancelled()) - { - return; - } - } - - // Start the setup of the next media player - mediaPlayerHandler.post(new Runnable() - { - @Override - public void run() - { - setupNext(downloadFile); - } - }); - } - - private boolean bufferComplete() - { - boolean completeFileAvailable = downloadFile.isWorkDone(); - Timber.i("Buffering next %s (%d)", partialFile, partialFile.length()); - return completeFileAvailable && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED); - } - - @NotNull - @Override - public String toString() - { - return String.format("CheckCompletionTask (%s)", downloadFile); - } - - } - - private class PositionCache implements Runnable - { - boolean isRunning = true; - - public void stop() - { - isRunning = false; - } - - @Override - public void run() - { - Thread.currentThread().setName("PositionCache"); - - // Stop checking position before the song reaches completion - while (isRunning) - { - try - { - if (mediaPlayer != null && playerState == STARTED) - { - cachedPosition = mediaPlayer.getCurrentPosition(); - } - - Util.sleepQuietly(50L); - } - catch (Exception e) - { - Timber.w(e, "Crashed getting current position"); - isRunning = false; - positionCache = null; - } - } - } - } - - private void handleError(Exception x) - { - Timber.w(x,"Media player error"); - - try - { - mediaPlayer.reset(); - } - catch (Exception ex) - { - Timber.w(ex, "Exception encountered when resetting media player"); - } - } - - private void handleErrorNext(Exception x) - { - Timber.w(x, "Next Media player error"); - nextMediaPlayer.reset(); - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt new file mode 100644 index 00000000..80309435 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -0,0 +1,694 @@ +package org.moire.ultrasonic.service + +import android.app.PendingIntent +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.media.AudioManager +import android.media.MediaMetadataRetriever +import android.media.MediaPlayer +import android.media.MediaPlayer.OnCompletionListener +import android.media.RemoteControlClient +import android.media.audiofx.AudioEffect +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.os.PowerManager +import android.os.PowerManager.WakeLock +import org.moire.ultrasonic.audiofx.EqualizerController +import org.moire.ultrasonic.audiofx.VisualizerController +import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline +import org.moire.ultrasonic.domain.PlayerState +import org.moire.ultrasonic.fragment.PlayerFragment +import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver +import org.moire.ultrasonic.util.* +import timber.log.Timber +import java.io.File +import java.net.URLEncoder +import java.util.* + +/** + * Represents a Media Player which uses the mobile's resources for playback + */ +class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private val context: Context) { + @JvmField + var onCurrentPlayingChanged: Consumer? = null + @JvmField + var onSongCompleted: Consumer? = null + @JvmField + var onPlayerStateChanged: BiConsumer? = null + @JvmField + var onPrepared: Runnable? = null + @JvmField + var onNextSongRequested: Runnable? = null + @JvmField + var playerState = PlayerState.IDLE + @JvmField + var currentPlaying: DownloadFile? = null + @JvmField + var nextPlaying: DownloadFile? = null + private var nextPlayerState = PlayerState.IDLE + private var nextSetup = false + private var nextPlayingTask: CancellableTask? = null + private var wakeLock: WakeLock? = null + private var mediaPlayer: MediaPlayer? = null + private var nextMediaPlayer: MediaPlayer? = null + private var mediaPlayerLooper: Looper? = null + private var mediaPlayerHandler: Handler? = null + private var cachedPosition = 0 + private var proxy: StreamProxy? = null + private var audioManager: AudioManager? = null + private var remoteControlClient: RemoteControlClient? = null + private var bufferTask: CancellableTask? = null + private var positionCache: PositionCache? = null + private var secondaryProgress = -1 + fun onCreate() { + if (mediaPlayer != null) { + mediaPlayer!!.release() + } + mediaPlayer = MediaPlayer() + Thread { + Thread.currentThread().name = "MediaPlayerThread" + Looper.prepare() + mediaPlayer!!.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK) + mediaPlayer!!.setOnErrorListener { mediaPlayer, what, more -> + handleError(Exception(String.format(Locale.getDefault(), "MediaPlayer error: %d (%d)", what, more))) + false + } + try { + val i = Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION) + i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer!!.audioSessionId) + i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName) + context.sendBroadcast(i) + } catch (e: Throwable) { + // Froyo or lower + } + mediaPlayerLooper = Looper.myLooper() + mediaPlayerHandler = Handler(mediaPlayerLooper) + Looper.loop() + }.start() + + // Create Equalizer and Visualizer on a new thread as this can potentially take some time + Thread { + EqualizerController.create(context, mediaPlayer) + VisualizerController.create(mediaPlayer) + }.start() + val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.javaClass.name) + wakeLock.setReferenceCounted(false) + audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + Util.registerMediaButtonEventReceiver(context, true) + setUpRemoteControlClient() + Timber.i("LocalMediaPlayer created") + } + + fun onDestroy() { + reset() + try { + val i = Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION) + i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer!!.audioSessionId) + i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName) + context.sendBroadcast(i) + EqualizerController.release() + VisualizerController.release() + mediaPlayer!!.release() + if (nextMediaPlayer != null) { + nextMediaPlayer!!.release() + } + mediaPlayerLooper!!.quit() + if (bufferTask != null) { + bufferTask!!.cancel() + } + if (nextPlayingTask != null) { + nextPlayingTask!!.cancel() + } + audioManager!!.unregisterRemoteControlClient(remoteControlClient) + clearRemoteControl() + Util.unregisterMediaButtonEventReceiver(context, true) + wakeLock!!.release() + } catch (exception: Throwable) { + Timber.w(exception, "LocalMediaPlayer onDestroy exception: ") + } + Timber.i("LocalMediaPlayer destroyed") + } + + @Synchronized + fun setPlayerState(playerState: PlayerState) { + Timber.i("%s -> %s (%s)", this.playerState.name, playerState.name, currentPlaying) + this.playerState = playerState + if (playerState === PlayerState.STARTED) { + audioFocusHandler.requestAudioFocus() + } + if (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED) { + updateRemoteControl() + } + if (onPlayerStateChanged != null) { + val mainHandler = Handler(context.mainLooper) + val myRunnable = Runnable { onPlayerStateChanged!!.accept(playerState, currentPlaying) } + mainHandler.post(myRunnable) + } + if (playerState === PlayerState.STARTED && positionCache == null) { + positionCache = PositionCache() + val thread = Thread(positionCache) + thread.start() + } else if (playerState !== PlayerState.STARTED && positionCache != null) { + positionCache!!.stop() + positionCache = null + } + } + + @Synchronized + fun setCurrentPlaying(currentPlaying: DownloadFile?) { + Timber.v("setCurrentPlaying %s", currentPlaying) + this.currentPlaying = currentPlaying + updateRemoteControl() + if (onCurrentPlayingChanged != null) { + val mainHandler = Handler(context.mainLooper) + val myRunnable = Runnable { onCurrentPlayingChanged!!.accept(currentPlaying) } + mainHandler.post(myRunnable) + } + } + + @Synchronized + fun setNextPlaying(nextToPlay: DownloadFile?) { + if (nextToPlay == null) { + nextPlaying = null + setNextPlayerState(PlayerState.IDLE) + return + } + nextPlaying = nextToPlay + nextPlayingTask = CheckCompletionTask(nextPlaying) + nextPlayingTask.start() + } + + @Synchronized + fun clearNextPlaying() { + nextSetup = false + nextPlaying = null + if (nextPlayingTask != null) { + nextPlayingTask!!.cancel() + nextPlayingTask = null + } + } + + @Synchronized + fun setNextPlayerState(playerState: PlayerState) { + Timber.i("Next: %s -> %s (%s)", nextPlayerState.name, playerState.name, nextPlaying) + nextPlayerState = playerState + } + + @Synchronized + fun bufferAndPlay() { + if (playerState !== PlayerState.PREPARED) { + reset() + bufferTask = BufferTask(currentPlaying, 0) + bufferTask.start() + } else { + doPlay(currentPlaying, 0, true) + } + } + + @Synchronized + fun play(fileToPlay: DownloadFile?) { + if (nextPlayingTask != null) { + nextPlayingTask!!.cancel() + nextPlayingTask = null + } + setCurrentPlaying(fileToPlay) + bufferAndPlay() + } + + @Synchronized + fun playNext() { + val tmp = mediaPlayer + mediaPlayer = nextMediaPlayer + nextMediaPlayer = tmp + setCurrentPlaying(nextPlaying) + setPlayerState(PlayerState.STARTED) + setupHandlers(currentPlaying, false) + if (onNextSongRequested != null) { + val mainHandler = Handler(context.mainLooper) + val myRunnable = Runnable { onNextSongRequested!!.run() } + mainHandler.post(myRunnable) + } + + // Proxy should not be being used here since the next player was already setup to play + if (proxy != null) { + proxy!!.stop() + proxy = null + } + } + + @Synchronized + fun pause() { + try { + mediaPlayer!!.pause() + } catch (x: Exception) { + handleError(x) + } + } + + @Synchronized + fun start() { + try { + mediaPlayer!!.start() + } catch (x: Exception) { + handleError(x) + } + } + + private fun updateRemoteControl() { + if (!Util.isLockScreenEnabled(context)) { + clearRemoteControl() + return + } + if (remoteControlClient != null) { + audioManager!!.unregisterRemoteControlClient(remoteControlClient) + audioManager!!.registerRemoteControlClient(remoteControlClient) + } else { + setUpRemoteControlClient() + } + Timber.i("In updateRemoteControl, playerState: %s [%d]", playerState, playerPosition) + if (playerState === PlayerState.STARTED) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + remoteControlClient!!.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING) + } else { + remoteControlClient!!.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING, playerPosition.toLong(), 1.0f) + } + } else { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + remoteControlClient!!.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED) + } else { + remoteControlClient!!.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED, playerPosition.toLong(), 1.0f) + } + } + if (currentPlaying != null) { + val currentSong = currentPlaying!!.song + val lockScreenBitmap = FileUtil.getAlbumArtBitmap(context, currentSong, Util.getMinDisplayMetric(context), true) + val artist = currentSong.artist + val album = currentSong.album + val title = currentSong.title + val currentSongDuration = currentSong.duration + var duration = 0L + if (currentSongDuration != null) duration = currentSongDuration as Long * 1000 + remoteControlClient!!.editMetadata(true) + .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, artist) + .putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, artist) + .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, album) + .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, title) + .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration) + .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, lockScreenBitmap) + .apply() + } + } + + fun clearRemoteControl() { + if (remoteControlClient != null) { + remoteControlClient!!.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED) + audioManager!!.unregisterRemoteControlClient(remoteControlClient) + remoteControlClient = null + } + } + + private fun setUpRemoteControlClient() { + if (!Util.isLockScreenEnabled(context)) return + val componentName = ComponentName(context.packageName, MediaButtonIntentReceiver::class.java.name) + if (remoteControlClient == null) { + val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON) + mediaButtonIntent.component = componentName + val broadcast = PendingIntent.getBroadcast(context, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT) + remoteControlClient = RemoteControlClient(broadcast) + audioManager!!.registerRemoteControlClient(remoteControlClient) + + // Flags for the media transport control that this client supports. + var flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS or + RemoteControlClient.FLAG_KEY_MEDIA_NEXT or + RemoteControlClient.FLAG_KEY_MEDIA_PLAY or + RemoteControlClient.FLAG_KEY_MEDIA_PAUSE or + RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE or + RemoteControlClient.FLAG_KEY_MEDIA_STOP + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + flags = flags or RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE + remoteControlClient!!.setOnGetPlaybackPositionListener { mediaPlayer!!.currentPosition.toLong() } + remoteControlClient!!.setPlaybackPositionUpdateListener { newPositionMs -> seekTo(newPositionMs.toInt()) } + } + remoteControlClient!!.setTransportControlFlags(flags) + } + } + + @Synchronized + fun seekTo(position: Int) { + try { + mediaPlayer!!.seekTo(position) + cachedPosition = position + updateRemoteControl() + } catch (x: Exception) { + handleError(x) + } + } + + @get:Synchronized + val playerPosition: Int + get() = try { + if (playerState === PlayerState.IDLE || playerState === PlayerState.DOWNLOADING || playerState === PlayerState.PREPARING) { + 0 + } else cachedPosition + } catch (x: Exception) { + handleError(x) + 0 + } + + @get:Synchronized + val playerDuration: Int + get() { + if (currentPlaying != null) { + val duration = currentPlaying!!.song.duration + if (duration != null) { + return duration * 1000 + } + } + if (playerState !== PlayerState.IDLE && playerState !== PlayerState.DOWNLOADING && playerState !== PlayerState.PREPARING) { + try { + return mediaPlayer!!.duration + } catch (x: Exception) { + handleError(x) + } + } + return 0 + } + + fun setVolume(volume: Float) { + if (mediaPlayer != null) { + mediaPlayer!!.setVolume(volume, volume) + } + } + + @Synchronized + fun doPlay(downloadFile: DownloadFile?, position: Int, start: Boolean) { + try { + downloadFile!!.setPlaying(false) + //downloadFile.setPlaying(true); + val file = if (downloadFile.isCompleteFileAvailable) downloadFile.completeFile else downloadFile.partialFile + val partial = file == downloadFile.partialFile + downloadFile.updateModificationDate() + mediaPlayer!!.setOnCompletionListener(null) + secondaryProgress = -1 // Ensure seeking in non StreamProxy playback works + mediaPlayer!!.reset() + setPlayerState(PlayerState.IDLE) + mediaPlayer!!.setAudioStreamType(AudioManager.STREAM_MUSIC) + var dataSource = file.path + if (partial) { + if (proxy == null) { + proxy = StreamProxy(object : Supplier() { + override fun get(): DownloadFile { + return currentPlaying!! + } + }) + proxy!!.start() + } + dataSource = String.format(Locale.getDefault(), "http://127.0.0.1:%d/%s", + proxy!!.port, URLEncoder.encode(dataSource, Constants.UTF_8)) + Timber.i("Data Source: %s", dataSource) + } else if (proxy != null) { + proxy!!.stop() + proxy = null + } + Timber.i("Preparing media player") + mediaPlayer!!.setDataSource(dataSource) + setPlayerState(PlayerState.PREPARING) + mediaPlayer!!.setOnBufferingUpdateListener { mp, percent -> + val progressBar = PlayerFragment.getProgressBar() + val song = downloadFile.song + if (percent == 100) { + if (progressBar != null) { + progressBar.secondaryProgress = 100 * progressBar.max + } + mp.setOnBufferingUpdateListener(null) + } else if (progressBar != null && song.transcodedContentType == null && Util.getMaxBitRate(context) == 0) { + secondaryProgress = (percent.toDouble() / 100.toDouble() * progressBar.max).toInt() + progressBar.secondaryProgress = secondaryProgress + } + } + mediaPlayer!!.setOnPreparedListener { + Timber.i("Media player prepared") + setPlayerState(PlayerState.PREPARED) + val progressBar = PlayerFragment.getProgressBar() + if (progressBar != null && downloadFile.isWorkDone) { + // Populate seek bar secondary progress if we have a complete file for consistency + PlayerFragment.getProgressBar().secondaryProgress = 100 * progressBar.max + } + synchronized(this@LocalMediaPlayer) { + if (position != 0) { + Timber.i("Restarting player from position %d", position) + seekTo(position) + } + cachedPosition = position + if (start) { + mediaPlayer!!.start() + setPlayerState(PlayerState.STARTED) + } else { + setPlayerState(PlayerState.PAUSED) + } + } + if (onPrepared != null) { + val mainHandler = Handler(context.mainLooper) + val myRunnable = Runnable { onPrepared!!.run() } + mainHandler.post(myRunnable) + } + } + setupHandlers(downloadFile, partial) + mediaPlayer!!.prepareAsync() + } catch (x: Exception) { + handleError(x) + } + } + + @Synchronized + private fun setupNext(downloadFile: DownloadFile) { + try { + val file = if (downloadFile.isCompleteFileAvailable) downloadFile.completeFile else downloadFile.partialFile + if (nextMediaPlayer != null) { + nextMediaPlayer!!.setOnCompletionListener(null) + nextMediaPlayer!!.release() + nextMediaPlayer = null + } + nextMediaPlayer = MediaPlayer() + nextMediaPlayer!!.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK) + try { + nextMediaPlayer!!.audioSessionId = mediaPlayer!!.audioSessionId + } catch (e: Throwable) { + nextMediaPlayer!!.setAudioStreamType(AudioManager.STREAM_MUSIC) + } + nextMediaPlayer!!.setDataSource(file.path) + setNextPlayerState(PlayerState.PREPARING) + nextMediaPlayer!!.setOnPreparedListener { + try { + setNextPlayerState(PlayerState.PREPARED) + if (Util.getGaplessPlaybackPreference(context) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED)) { + mediaPlayer!!.setNextMediaPlayer(nextMediaPlayer) + nextSetup = true + } + } catch (x: Exception) { + handleErrorNext(x) + } + } + nextMediaPlayer!!.setOnErrorListener { mediaPlayer, what, extra -> + Timber.w("Error on playing next (%d, %d): %s", what, extra, downloadFile) + true + } + nextMediaPlayer!!.prepareAsync() + } catch (x: Exception) { + handleErrorNext(x) + } + } + + private fun setupHandlers(downloadFile: DownloadFile?, isPartial: Boolean) { + mediaPlayer!!.setOnErrorListener { mediaPlayer, what, extra -> + Timber.w("Error on playing file (%d, %d): %s", what, extra, downloadFile) + val pos = cachedPosition + reset() + downloadFile!!.setPlaying(false) + doPlay(downloadFile, pos, true) + downloadFile.setPlaying(true) + true + } + val duration = if (downloadFile!!.song.duration == null) 0 else downloadFile.song.duration!! * 1000 + mediaPlayer!!.setOnCompletionListener(object : OnCompletionListener { + override fun onCompletion(mediaPlayer: MediaPlayer) { + // Acquire a temporary wakelock, since when we return from + // this callback the MediaPlayer will release its wakelock + // and allow the device to go to sleep. + wakeLock!!.acquire(60000) + val pos = cachedPosition + Timber.i("Ending position %d of %d", pos, duration) + if (!isPartial || downloadFile.isWorkDone && Math.abs(duration - pos) < 1000) { + setPlayerState(PlayerState.COMPLETED) + if (Util.getGaplessPlaybackPreference(context) && nextPlaying != null && nextPlayerState === PlayerState.PREPARED) { + if (nextSetup) { + nextSetup = false + } + playNext() + } else { + if (onSongCompleted != null) { + val mainHandler = Handler(context.mainLooper) + val myRunnable = Runnable { onSongCompleted!!.accept(currentPlaying) } + mainHandler.post(myRunnable) + } + } + return + } + synchronized(this) { + if (downloadFile.isWorkDone) { + // Complete was called early even though file is fully buffered + Timber.i("Requesting restart from %d of %d", pos, duration) + reset() + downloadFile.setPlaying(false) + doPlay(downloadFile, pos, true) + downloadFile.setPlaying(true) + } else { + Timber.i("Requesting restart from %d of %d", pos, duration) + reset() + bufferTask = BufferTask(downloadFile, pos) + bufferTask.start() + } + } + } + }) + } + + @Synchronized + fun reset() { + if (bufferTask != null) { + bufferTask!!.cancel() + } + try { + setPlayerState(PlayerState.IDLE) + mediaPlayer!!.setOnErrorListener(null) + mediaPlayer!!.setOnCompletionListener(null) + mediaPlayer!!.reset() + } catch (x: Exception) { + handleError(x) + } + } + + private inner class BufferTask(private val downloadFile: DownloadFile?, private val position: Int) : CancellableTask() { + private val expectedFileSize: Long + private val partialFile: File + override fun execute() { + setPlayerState(PlayerState.DOWNLOADING) + while (!bufferComplete() && !isOffline(context)) { + Util.sleepQuietly(1000L) + if (isCancelled) { + return + } + } + doPlay(downloadFile, position, true) + } + + private fun bufferComplete(): Boolean { + val completeFileAvailable = downloadFile!!.isWorkDone + val size = partialFile.length() + Timber.i("Buffering %s (%d/%d, %s)", partialFile, size, expectedFileSize, completeFileAvailable) + return completeFileAvailable || size >= expectedFileSize + } + + override fun toString(): String { + return String.format("BufferTask (%s)", downloadFile) + } + + init { + partialFile = downloadFile!!.partialFile + var bufferLength = Util.getBufferLength(context).toLong() + if (bufferLength == 0L) { + // Set to seconds in a day, basically infinity + bufferLength = 86400L + } + + // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to. + val bitRate = downloadFile.bitRate + val byteCount = Math.max(100000, bitRate * 1024L / 8L * bufferLength) + + // Find out how large the file should grow before resuming playback. + Timber.i("Buffering from position %d and bitrate %d", position, bitRate) + expectedFileSize = position * bitRate / 8 + byteCount + } + } + + private inner class CheckCompletionTask(downloadFile: DownloadFile?) : CancellableTask() { + private val downloadFile: DownloadFile? + private val partialFile: File? + override fun execute() { + Thread.currentThread().name = "CheckCompletionTask" + if (downloadFile == null) { + return + } + + // Do an initial sleep so this prepare can't compete with main prepare + Util.sleepQuietly(5000L) + while (!bufferComplete()) { + Util.sleepQuietly(5000L) + if (isCancelled) { + return + } + } + + // Start the setup of the next media player + mediaPlayerHandler!!.post { setupNext(downloadFile) } + } + + private fun bufferComplete(): Boolean { + val completeFileAvailable = downloadFile!!.isWorkDone + Timber.i("Buffering next %s (%d)", partialFile, partialFile!!.length()) + return completeFileAvailable && (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED) + } + + override fun toString(): String { + return String.format("CheckCompletionTask (%s)", downloadFile) + } + + init { + setNextPlayerState(PlayerState.IDLE) + this.downloadFile = downloadFile + partialFile = downloadFile?.partialFile + } + } + + private inner class PositionCache : Runnable { + var isRunning = true + fun stop() { + isRunning = false + } + + override fun run() { + Thread.currentThread().name = "PositionCache" + + // Stop checking position before the song reaches completion + while (isRunning) { + try { + if (mediaPlayer != null && playerState === PlayerState.STARTED) { + cachedPosition = mediaPlayer!!.currentPosition + } + Util.sleepQuietly(50L) + } catch (e: Exception) { + Timber.w(e, "Crashed getting current position") + isRunning = false + positionCache = null + } + } + } + } + + private fun handleError(x: Exception) { + Timber.w(x, "Media player error") + try { + mediaPlayer!!.reset() + } catch (ex: Exception) { + Timber.w(ex, "Exception encountered when resetting media player") + } + } + + private fun handleErrorNext(x: Exception) { + Timber.w(x, "Next Media player error") + nextMediaPlayer!!.reset() + } +} \ No newline at end of file From a467abf10ba217df599fb413ded0dd5f95551499 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 24 Mar 2021 12:59:07 +0100 Subject: [PATCH 02/14] Fix errors and warnings --- .../ultrasonic/service/LocalMediaPlayer.kt | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 80309435..8d9f1ceb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -26,6 +26,8 @@ import timber.log.Timber import java.io.File import java.net.URLEncoder import java.util.* +import kotlin.math.abs +import kotlin.math.max /** * Represents a Media Player which uses the mobile's resources for playback @@ -50,7 +52,8 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private private var nextPlayerState = PlayerState.IDLE private var nextSetup = false private var nextPlayingTask: CancellableTask? = null - private var wakeLock: WakeLock? = null + private val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager + private val wakeLock: WakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.javaClass.name) private var mediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null private var mediaPlayerLooper: Looper? = null @@ -62,6 +65,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private private var bufferTask: CancellableTask? = null private var positionCache: PositionCache? = null private var secondaryProgress = -1 + fun onCreate() { if (mediaPlayer != null) { mediaPlayer!!.release() @@ -93,8 +97,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private EqualizerController.create(context, mediaPlayer) VisualizerController.create(mediaPlayer) }.start() - val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.javaClass.name) + wakeLock.setReferenceCounted(false) audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager Util.registerMediaButtonEventReceiver(context, true) @@ -125,7 +128,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private audioManager!!.unregisterRemoteControlClient(remoteControlClient) clearRemoteControl() Util.unregisterMediaButtonEventReceiver(context, true) - wakeLock!!.release() + wakeLock.release() } catch (exception: Throwable) { Timber.w(exception, "LocalMediaPlayer onDestroy exception: ") } @@ -178,7 +181,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } nextPlaying = nextToPlay nextPlayingTask = CheckCompletionTask(nextPlaying) - nextPlayingTask.start() + nextPlayingTask?.start() } @Synchronized @@ -202,7 +205,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private if (playerState !== PlayerState.PREPARED) { reset() bufferTask = BufferTask(currentPlaying, 0) - bufferTask.start() + bufferTask!!.start() } else { doPlay(currentPlaying, 0, true) } @@ -518,10 +521,10 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private // Acquire a temporary wakelock, since when we return from // this callback the MediaPlayer will release its wakelock // and allow the device to go to sleep. - wakeLock!!.acquire(60000) + wakeLock.acquire(60000) val pos = cachedPosition Timber.i("Ending position %d of %d", pos, duration) - if (!isPartial || downloadFile.isWorkDone && Math.abs(duration - pos) < 1000) { + if (!isPartial || downloadFile.isWorkDone && abs(duration - pos) < 1000) { setPlayerState(PlayerState.COMPLETED) if (Util.getGaplessPlaybackPreference(context) && nextPlaying != null && nextPlayerState === PlayerState.PREPARED) { if (nextSetup) { @@ -549,7 +552,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private Timber.i("Requesting restart from %d of %d", pos, duration) reset() bufferTask = BufferTask(downloadFile, pos) - bufferTask.start() + bufferTask!!.start() } } } @@ -606,7 +609,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to. val bitRate = downloadFile.bitRate - val byteCount = Math.max(100000, bitRate * 1024L / 8L * bufferLength) + val byteCount = max(100000, bitRate * 1024L / 8L * bufferLength) // Find out how large the file should grow before resuming playback. Timber.i("Buffering from position %d and bitrate %d", position, bitRate) From d017ca9fb2561060eff34082304664e0d55a18c4 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 24 Mar 2021 13:28:54 +0100 Subject: [PATCH 03/14] Initialize some vals onCreate, thus making them explicitely non-null Also modify setNextPlaying to accept only non-null files. --- .../service/MediaPlayerService.java | 6 +- .../ultrasonic/service/LocalMediaPlayer.kt | 121 +++++++++--------- 2 files changed, 65 insertions(+), 62 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java index 76608c5d..f808959c 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java @@ -305,7 +305,7 @@ public class MediaPlayerService extends Service if (!gaplessPlayback) { - localMediaPlayer.setNextPlaying(null); + localMediaPlayer.clearNextPlaying(true); return; } @@ -327,7 +327,7 @@ public class MediaPlayerService extends Service } } - localMediaPlayer.clearNextPlaying(); + localMediaPlayer.clearNextPlaying(false); if (index < downloader.downloadList.size() && index != -1) { @@ -335,7 +335,7 @@ public class MediaPlayerService extends Service } else { - localMediaPlayer.setNextPlaying(null); + localMediaPlayer.clearNextPlaying(true); } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 8d9f1ceb..138824e5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -52,36 +52,32 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private private var nextPlayerState = PlayerState.IDLE private var nextSetup = false private var nextPlayingTask: CancellableTask? = null - private val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager - private val wakeLock: WakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.javaClass.name) - private var mediaPlayer: MediaPlayer? = null + private var mediaPlayer: MediaPlayer = MediaPlayer() private var nextMediaPlayer: MediaPlayer? = null private var mediaPlayerLooper: Looper? = null private var mediaPlayerHandler: Handler? = null private var cachedPosition = 0 private var proxy: StreamProxy? = null - private var audioManager: AudioManager? = null + private var audioManager: AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager private var remoteControlClient: RemoteControlClient? = null private var bufferTask: CancellableTask? = null private var positionCache: PositionCache? = null private var secondaryProgress = -1 + private val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager + private val wakeLock: WakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.javaClass.name) fun onCreate() { - if (mediaPlayer != null) { - mediaPlayer!!.release() - } - mediaPlayer = MediaPlayer() Thread { Thread.currentThread().name = "MediaPlayerThread" Looper.prepare() - mediaPlayer!!.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK) - mediaPlayer!!.setOnErrorListener { mediaPlayer, what, more -> + mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK) + mediaPlayer.setOnErrorListener { mediaPlayer, what, more -> handleError(Exception(String.format(Locale.getDefault(), "MediaPlayer error: %d (%d)", what, more))) false } try { val i = Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION) - i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer!!.audioSessionId) + i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.audioSessionId) i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName) context.sendBroadcast(i) } catch (e: Throwable) { @@ -99,7 +95,6 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private }.start() wakeLock.setReferenceCounted(false) - audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager Util.registerMediaButtonEventReceiver(context, true) setUpRemoteControlClient() Timber.i("LocalMediaPlayer created") @@ -109,12 +104,12 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private reset() try { val i = Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION) - i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer!!.audioSessionId) + i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.audioSessionId) i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName) context.sendBroadcast(i) EqualizerController.release() VisualizerController.release() - mediaPlayer!!.release() + mediaPlayer.release() if (nextMediaPlayer != null) { nextMediaPlayer!!.release() } @@ -125,7 +120,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private if (nextPlayingTask != null) { nextPlayingTask!!.cancel() } - audioManager!!.unregisterRemoteControlClient(remoteControlClient) + audioManager.unregisterRemoteControlClient(remoteControlClient) clearRemoteControl() Util.unregisterMediaButtonEventReceiver(context, true) wakeLock.release() @@ -160,11 +155,15 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } } + /* + * Set the current playing file. It's called with null to reset the player. + */ @Synchronized fun setCurrentPlaying(currentPlaying: DownloadFile?) { Timber.v("setCurrentPlaying %s", currentPlaying) this.currentPlaying = currentPlaying updateRemoteControl() + if (onCurrentPlayingChanged != null) { val mainHandler = Handler(context.mainLooper) val myRunnable = Runnable { onCurrentPlayingChanged!!.accept(currentPlaying) } @@ -172,26 +171,28 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } } + /* + * Set the next playing file. + */ @Synchronized - fun setNextPlaying(nextToPlay: DownloadFile?) { - if (nextToPlay == null) { - nextPlaying = null - setNextPlayerState(PlayerState.IDLE) - return - } + fun setNextPlaying(nextToPlay: DownloadFile) { nextPlaying = nextToPlay nextPlayingTask = CheckCompletionTask(nextPlaying) nextPlayingTask?.start() } @Synchronized - fun clearNextPlaying() { + fun clearNextPlaying(setIdle: Boolean) { nextSetup = false nextPlaying = null if (nextPlayingTask != null) { nextPlayingTask!!.cancel() nextPlayingTask = null } + + if (setIdle) { + setNextPlayerState(PlayerState.IDLE) + } } @Synchronized @@ -223,12 +224,14 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private @Synchronized fun playNext() { + if (nextMediaPlayer == null || currentPlaying == null) return + val tmp = mediaPlayer - mediaPlayer = nextMediaPlayer + mediaPlayer = nextMediaPlayer!! nextMediaPlayer = tmp setCurrentPlaying(nextPlaying) setPlayerState(PlayerState.STARTED) - setupHandlers(currentPlaying, false) + attachHandlersToPlayer(mediaPlayer, currentPlaying!!, false) if (onNextSongRequested != null) { val mainHandler = Handler(context.mainLooper) val myRunnable = Runnable { onNextSongRequested!!.run() } @@ -236,16 +239,14 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } // Proxy should not be being used here since the next player was already setup to play - if (proxy != null) { - proxy!!.stop() - proxy = null - } + proxy?.stop() + proxy = null } @Synchronized fun pause() { try { - mediaPlayer!!.pause() + mediaPlayer.pause() } catch (x: Exception) { handleError(x) } @@ -254,7 +255,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private @Synchronized fun start() { try { - mediaPlayer!!.start() + mediaPlayer.start() } catch (x: Exception) { handleError(x) } @@ -266,8 +267,8 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private return } if (remoteControlClient != null) { - audioManager!!.unregisterRemoteControlClient(remoteControlClient) - audioManager!!.registerRemoteControlClient(remoteControlClient) + audioManager.unregisterRemoteControlClient(remoteControlClient) + audioManager.registerRemoteControlClient(remoteControlClient) } else { setUpRemoteControlClient() } @@ -308,7 +309,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private fun clearRemoteControl() { if (remoteControlClient != null) { remoteControlClient!!.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED) - audioManager!!.unregisterRemoteControlClient(remoteControlClient) + audioManager.unregisterRemoteControlClient(remoteControlClient) remoteControlClient = null } } @@ -321,7 +322,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private mediaButtonIntent.component = componentName val broadcast = PendingIntent.getBroadcast(context, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT) remoteControlClient = RemoteControlClient(broadcast) - audioManager!!.registerRemoteControlClient(remoteControlClient) + audioManager.registerRemoteControlClient(remoteControlClient) // Flags for the media transport control that this client supports. var flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS or @@ -332,7 +333,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private RemoteControlClient.FLAG_KEY_MEDIA_STOP if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { flags = flags or RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE - remoteControlClient!!.setOnGetPlaybackPositionListener { mediaPlayer!!.currentPosition.toLong() } + remoteControlClient!!.setOnGetPlaybackPositionListener { mediaPlayer.currentPosition.toLong() } remoteControlClient!!.setPlaybackPositionUpdateListener { newPositionMs -> seekTo(newPositionMs.toInt()) } } remoteControlClient!!.setTransportControlFlags(flags) @@ -342,7 +343,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private @Synchronized fun seekTo(position: Int) { try { - mediaPlayer!!.seekTo(position) + mediaPlayer.seekTo(position) cachedPosition = position updateRemoteControl() } catch (x: Exception) { @@ -372,7 +373,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } if (playerState !== PlayerState.IDLE && playerState !== PlayerState.DOWNLOADING && playerState !== PlayerState.PREPARING) { try { - return mediaPlayer!!.duration + return mediaPlayer.duration } catch (x: Exception) { handleError(x) } @@ -382,7 +383,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private fun setVolume(volume: Float) { if (mediaPlayer != null) { - mediaPlayer!!.setVolume(volume, volume) + mediaPlayer.setVolume(volume, volume) } } @@ -394,11 +395,11 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private val file = if (downloadFile.isCompleteFileAvailable) downloadFile.completeFile else downloadFile.partialFile val partial = file == downloadFile.partialFile downloadFile.updateModificationDate() - mediaPlayer!!.setOnCompletionListener(null) + mediaPlayer.setOnCompletionListener(null) secondaryProgress = -1 // Ensure seeking in non StreamProxy playback works - mediaPlayer!!.reset() + mediaPlayer.reset() setPlayerState(PlayerState.IDLE) - mediaPlayer!!.setAudioStreamType(AudioManager.STREAM_MUSIC) + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC) var dataSource = file.path if (partial) { if (proxy == null) { @@ -417,9 +418,9 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private proxy = null } Timber.i("Preparing media player") - mediaPlayer!!.setDataSource(dataSource) + mediaPlayer.setDataSource(dataSource) setPlayerState(PlayerState.PREPARING) - mediaPlayer!!.setOnBufferingUpdateListener { mp, percent -> + mediaPlayer.setOnBufferingUpdateListener { mp, percent -> val progressBar = PlayerFragment.getProgressBar() val song = downloadFile.song if (percent == 100) { @@ -432,7 +433,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private progressBar.secondaryProgress = secondaryProgress } } - mediaPlayer!!.setOnPreparedListener { + mediaPlayer.setOnPreparedListener { Timber.i("Media player prepared") setPlayerState(PlayerState.PREPARED) val progressBar = PlayerFragment.getProgressBar() @@ -447,7 +448,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } cachedPosition = position if (start) { - mediaPlayer!!.start() + mediaPlayer.start() setPlayerState(PlayerState.STARTED) } else { setPlayerState(PlayerState.PAUSED) @@ -459,8 +460,8 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private mainHandler.post(myRunnable) } } - setupHandlers(downloadFile, partial) - mediaPlayer!!.prepareAsync() + attachHandlersToPlayer(mediaPlayer, downloadFile, partial) + mediaPlayer.prepareAsync() } catch (x: Exception) { handleError(x) } @@ -478,7 +479,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private nextMediaPlayer = MediaPlayer() nextMediaPlayer!!.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK) try { - nextMediaPlayer!!.audioSessionId = mediaPlayer!!.audioSessionId + nextMediaPlayer!!.audioSessionId = mediaPlayer.audioSessionId } catch (e: Throwable) { nextMediaPlayer!!.setAudioStreamType(AudioManager.STREAM_MUSIC) } @@ -488,7 +489,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private try { setNextPlayerState(PlayerState.PREPARED) if (Util.getGaplessPlaybackPreference(context) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED)) { - mediaPlayer!!.setNextMediaPlayer(nextMediaPlayer) + mediaPlayer.setNextMediaPlayer(nextMediaPlayer) nextSetup = true } } catch (x: Exception) { @@ -505,18 +506,20 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } } - private fun setupHandlers(downloadFile: DownloadFile?, isPartial: Boolean) { - mediaPlayer!!.setOnErrorListener { mediaPlayer, what, extra -> + private fun attachHandlersToPlayer(mediaPlayer: MediaPlayer, downloadFile: DownloadFile, isPartial: Boolean) { + mediaPlayer.setOnErrorListener { _, what, extra -> Timber.w("Error on playing file (%d, %d): %s", what, extra, downloadFile) val pos = cachedPosition reset() - downloadFile!!.setPlaying(false) + downloadFile.setPlaying(false) doPlay(downloadFile, pos, true) downloadFile.setPlaying(true) true } - val duration = if (downloadFile!!.song.duration == null) 0 else downloadFile.song.duration!! * 1000 - mediaPlayer!!.setOnCompletionListener(object : OnCompletionListener { + + val duration = if (downloadFile.song.duration == null) 0 else downloadFile.song.duration!! * 1000 + + mediaPlayer.setOnCompletionListener(object : OnCompletionListener { override fun onCompletion(mediaPlayer: MediaPlayer) { // Acquire a temporary wakelock, since when we return from // this callback the MediaPlayer will release its wakelock @@ -566,9 +569,9 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } try { setPlayerState(PlayerState.IDLE) - mediaPlayer!!.setOnErrorListener(null) - mediaPlayer!!.setOnCompletionListener(null) - mediaPlayer!!.reset() + mediaPlayer.setOnErrorListener(null) + mediaPlayer.setOnCompletionListener(null) + mediaPlayer.reset() } catch (x: Exception) { handleError(x) } @@ -669,7 +672,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private while (isRunning) { try { if (mediaPlayer != null && playerState === PlayerState.STARTED) { - cachedPosition = mediaPlayer!!.currentPosition + cachedPosition = mediaPlayer.currentPosition } Util.sleepQuietly(50L) } catch (e: Exception) { @@ -684,7 +687,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private private fun handleError(x: Exception) { Timber.w(x, "Media player error") try { - mediaPlayer!!.reset() + mediaPlayer.reset() } catch (ex: Exception) { Timber.w(ex, "Exception encountered when resetting media player") } From 09fb6aa4871848e26f8124458de328a937c80c4d Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 24 Mar 2021 13:45:46 +0100 Subject: [PATCH 04/14] Make doPlay method a private method, and play the only public entry point. --- .../service/MediaPlayerControllerImpl.java | 2 +- .../ultrasonic/service/LocalMediaPlayer.kt | 40 ++++++++++++++----- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerControllerImpl.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerControllerImpl.java index 0c173960..9fb95b13 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerControllerImpl.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerControllerImpl.java @@ -131,7 +131,7 @@ public class MediaPlayerControllerImpl implements MediaPlayerController { if (localMediaPlayer.currentPlaying.isCompleteFileAvailable()) { - localMediaPlayer.doPlay(localMediaPlayer.currentPlaying, currentPlayingPosition, autoPlay); + localMediaPlayer.play(localMediaPlayer.currentPlaying, currentPlayingPosition, autoPlay); } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 138824e5..6dc1ed63 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -172,7 +172,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } /* - * Set the next playing file. + * Set the next playing file. nextToPlay cannot be null */ @Synchronized fun setNextPlaying(nextToPlay: DownloadFile) { @@ -181,6 +181,9 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private nextPlayingTask?.start() } + /* + * Clear the next playing file. setIdle controls whether the playerState is affected as well + */ @Synchronized fun clearNextPlaying(setIdle: Boolean) { nextSetup = false @@ -202,26 +205,35 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } @Synchronized - fun bufferAndPlay() { + private fun bufferAndPlay(fileToPlay: DownloadFile, position: Int, autoStart: Boolean) { if (playerState !== PlayerState.PREPARED) { reset() - bufferTask = BufferTask(currentPlaying, 0) + bufferTask = BufferTask(fileToPlay, position) bufferTask!!.start() } else { - doPlay(currentPlaying, 0, true) + doPlay(fileToPlay, position, autoStart) } } + /* + * Public method to play a given file. + * Optionally specify a position to start at. + */ @Synchronized - fun play(fileToPlay: DownloadFile?) { + @JvmOverloads + fun play(fileToPlay: DownloadFile?, position: Int = 0, autoStart: Boolean = true) { if (nextPlayingTask != null) { nextPlayingTask!!.cancel() nextPlayingTask = null } setCurrentPlaying(fileToPlay) - bufferAndPlay() + + if (fileToPlay != null) { + bufferAndPlay(fileToPlay, position, autoStart) + } } + @Synchronized fun playNext() { if (nextMediaPlayer == null || currentPlaying == null) return @@ -388,18 +400,20 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } @Synchronized - fun doPlay(downloadFile: DownloadFile?, position: Int, start: Boolean) { + private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean) { try { - downloadFile!!.setPlaying(false) - //downloadFile.setPlaying(true); + downloadFile.setPlaying(false) + val file = if (downloadFile.isCompleteFileAvailable) downloadFile.completeFile else downloadFile.partialFile val partial = file == downloadFile.partialFile + downloadFile.updateModificationDate() mediaPlayer.setOnCompletionListener(null) secondaryProgress = -1 // Ensure seeking in non StreamProxy playback works mediaPlayer.reset() setPlayerState(PlayerState.IDLE) mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC) + var dataSource = file.path if (partial) { if (proxy == null) { @@ -417,9 +431,12 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private proxy!!.stop() proxy = null } + Timber.i("Preparing media player") + mediaPlayer.setDataSource(dataSource) setPlayerState(PlayerState.PREPARING) + mediaPlayer.setOnBufferingUpdateListener { mp, percent -> val progressBar = PlayerFragment.getProgressBar() val song = downloadFile.song @@ -433,6 +450,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private progressBar.secondaryProgress = secondaryProgress } } + mediaPlayer.setOnPreparedListener { Timber.i("Media player prepared") setPlayerState(PlayerState.PREPARED) @@ -588,7 +606,9 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private return } } - doPlay(downloadFile, position, true) + if (downloadFile != null) { + doPlay(downloadFile, position, true) + } } private fun bufferComplete(): Boolean { From 93eced9516ab408c25b4bb3d93416c38c76e7adb Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 24 Mar 2021 14:03:01 +0100 Subject: [PATCH 05/14] Move bufferAndPlay to another position in the file; annotate some possible bugs. --- .../ultrasonic/service/LocalMediaPlayer.kt | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 6dc1ed63..a5c236aa 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -84,6 +84,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private // Froyo or lower } mediaPlayerLooper = Looper.myLooper() + // FIXME: Looper null?? mediaPlayerHandler = Handler(mediaPlayerLooper) Looper.loop() }.start() @@ -204,17 +205,6 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private nextPlayerState = playerState } - @Synchronized - private fun bufferAndPlay(fileToPlay: DownloadFile, position: Int, autoStart: Boolean) { - if (playerState !== PlayerState.PREPARED) { - reset() - bufferTask = BufferTask(fileToPlay, position) - bufferTask!!.start() - } else { - doPlay(fileToPlay, position, autoStart) - } - } - /* * Public method to play a given file. * Optionally specify a position to start at. @@ -238,12 +228,18 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private fun playNext() { if (nextMediaPlayer == null || currentPlaying == null) return - val tmp = mediaPlayer + val oldPlayer = mediaPlayer mediaPlayer = nextMediaPlayer!! - nextMediaPlayer = tmp + + // FIXME: Why is this being done? + nextMediaPlayer = oldPlayer + setCurrentPlaying(nextPlaying) setPlayerState(PlayerState.STARTED) + + // FIXME: Why is currentPlaying passed here and not nextPlaying?! attachHandlersToPlayer(mediaPlayer, currentPlaying!!, false) + if (onNextSongRequested != null) { val mainHandler = Handler(context.mainLooper) val myRunnable = Runnable { onNextSongRequested!!.run() } @@ -306,7 +302,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private val title = currentSong.title val currentSongDuration = currentSong.duration var duration = 0L - if (currentSongDuration != null) duration = currentSongDuration as Long * 1000 + if (currentSongDuration != null) duration = currentSongDuration!! as Long * 1000 remoteControlClient!!.editMetadata(true) .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, artist) .putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, artist) @@ -399,6 +395,17 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } } + @Synchronized + private fun bufferAndPlay(fileToPlay: DownloadFile, position: Int, autoStart: Boolean) { + if (playerState !== PlayerState.PREPARED) { + reset() + bufferTask = BufferTask(fileToPlay, position) + bufferTask!!.start() + } else { + doPlay(fileToPlay, position, autoStart) + } + } + @Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean) { try { @@ -428,7 +435,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private proxy!!.port, URLEncoder.encode(dataSource, Constants.UTF_8)) Timber.i("Data Source: %s", dataSource) } else if (proxy != null) { - proxy!!.stop() + proxy?.stop() proxy = null } From 493a587b373ba2dcdee386974b8ef65f004933a1 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 24 Mar 2021 14:10:27 +0100 Subject: [PATCH 06/14] Introduce postRunnable helper function --- .../ultrasonic/service/LocalMediaPlayer.kt | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index a5c236aa..61fcf574 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -240,17 +240,15 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private // FIXME: Why is currentPlaying passed here and not nextPlaying?! attachHandlersToPlayer(mediaPlayer, currentPlaying!!, false) - if (onNextSongRequested != null) { - val mainHandler = Handler(context.mainLooper) - val myRunnable = Runnable { onNextSongRequested!!.run() } - mainHandler.post(myRunnable) - } + postRunnable(onNextSongRequested) // Proxy should not be being used here since the next player was already setup to play proxy?.stop() proxy = null } + + @Synchronized fun pause() { try { @@ -479,11 +477,9 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private setPlayerState(PlayerState.PAUSED) } } - if (onPrepared != null) { - val mainHandler = Handler(context.mainLooper) - val myRunnable = Runnable { onPrepared!!.run() } - mainHandler.post(myRunnable) - } + + postRunnable(onPrepared) + } attachHandlersToPlayer(mediaPlayer, downloadFile, partial) mediaPlayer.prepareAsync() @@ -724,4 +720,12 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private Timber.w(x, "Next Media player error") nextMediaPlayer!!.reset() } + + private fun postRunnable(runnable: Runnable?) { + if (runnable != null) { + val mainHandler = Handler(context.mainLooper) + val myRunnable = Runnable { runnable.run() } + mainHandler.post(myRunnable) + } + } } \ No newline at end of file From 60a0fe17dd8d2b6c4cad91c82c05d0a64cb1037e Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 24 Mar 2021 14:44:22 +0100 Subject: [PATCH 07/14] Fix a bad cast --- .../kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 61fcf574..1e49832c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -300,7 +300,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private val title = currentSong.title val currentSongDuration = currentSong.duration var duration = 0L - if (currentSongDuration != null) duration = currentSongDuration!! as Long * 1000 + if (currentSongDuration != null) duration = (currentSongDuration * 1000).toLong() remoteControlClient!!.editMetadata(true) .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, artist) .putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, artist) From 2171c971a3172cdc94920fe7a85d26e4936e7452 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 24 Mar 2021 14:48:17 +0100 Subject: [PATCH 08/14] Make remoteControl code more functional Fixes #390 --- .../ultrasonic/service/LocalMediaPlayer.kt | 69 ++++++++++++------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 1e49832c..bf0923f8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -84,8 +84,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private // Froyo or lower } mediaPlayerLooper = Looper.myLooper() - // FIXME: Looper null?? - mediaPlayerHandler = Handler(mediaPlayerLooper) + mediaPlayerHandler = Handler(mediaPlayerLooper!!) Looper.loop() }.start() @@ -267,18 +266,26 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } } + + /* + * The remote control API is deprecated in API 21 + */ private fun updateRemoteControl() { if (!Util.isLockScreenEnabled(context)) { clearRemoteControl() return } - if (remoteControlClient != null) { + + if (remoteControlClient == null) { + remoteControlClient = createRemoteControlClient() + } else { + // FIXME: This looks like a hack. Why is it needed? audioManager.unregisterRemoteControlClient(remoteControlClient) audioManager.registerRemoteControlClient(remoteControlClient) - } else { - setUpRemoteControlClient() } + Timber.i("In updateRemoteControl, playerState: %s [%d]", playerState, playerPosition) + if (playerState === PlayerState.STARTED) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { remoteControlClient!!.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING) @@ -292,6 +299,7 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private remoteControlClient!!.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED, playerPosition.toLong(), 1.0f) } } + if (currentPlaying != null) { val currentSong = currentPlaying!!.song val lockScreenBitmap = FileUtil.getAlbumArtBitmap(context, currentSong, Util.getMinDisplayMetric(context), true) @@ -322,30 +330,40 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private private fun setUpRemoteControlClient() { if (!Util.isLockScreenEnabled(context)) return - val componentName = ComponentName(context.packageName, MediaButtonIntentReceiver::class.java.name) - if (remoteControlClient == null) { - val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON) - mediaButtonIntent.component = componentName - val broadcast = PendingIntent.getBroadcast(context, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT) - remoteControlClient = RemoteControlClient(broadcast) - audioManager.registerRemoteControlClient(remoteControlClient) - // Flags for the media transport control that this client supports. - var flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS or - RemoteControlClient.FLAG_KEY_MEDIA_NEXT or - RemoteControlClient.FLAG_KEY_MEDIA_PLAY or - RemoteControlClient.FLAG_KEY_MEDIA_PAUSE or - RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE or - RemoteControlClient.FLAG_KEY_MEDIA_STOP - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - flags = flags or RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE - remoteControlClient!!.setOnGetPlaybackPositionListener { mediaPlayer.currentPosition.toLong() } - remoteControlClient!!.setPlaybackPositionUpdateListener { newPositionMs -> seekTo(newPositionMs.toInt()) } - } - remoteControlClient!!.setTransportControlFlags(flags) + if (remoteControlClient == null) { + remoteControlClient = createRemoteControlClient() } } + private fun createRemoteControlClient(): RemoteControlClient { + val componentName = ComponentName(context.packageName, MediaButtonIntentReceiver::class.java.name) + val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON) + mediaButtonIntent.component = componentName + + val broadcast = PendingIntent.getBroadcast(context, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT) + val remoteControlClient = RemoteControlClient(broadcast) + audioManager.registerRemoteControlClient(remoteControlClient) + + // Flags for the media transport control that this client supports. + var flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS or + RemoteControlClient.FLAG_KEY_MEDIA_NEXT or + RemoteControlClient.FLAG_KEY_MEDIA_PLAY or + RemoteControlClient.FLAG_KEY_MEDIA_PAUSE or + RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE or + RemoteControlClient.FLAG_KEY_MEDIA_STOP + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + flags = flags or RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE + remoteControlClient.setOnGetPlaybackPositionListener { mediaPlayer.currentPosition.toLong() } + remoteControlClient.setPlaybackPositionUpdateListener { newPositionMs -> seekTo(newPositionMs.toInt()) } + } + + remoteControlClient.setTransportControlFlags(flags) + + return remoteControlClient + } + @Synchronized fun seekTo(position: Int) { try { @@ -728,4 +746,5 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private mainHandler.post(myRunnable) } } + } \ No newline at end of file From 8d65b1d25fa74f32f26d3676f34307c7ba700e37 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 24 Mar 2021 16:39:34 +0100 Subject: [PATCH 09/14] Private BufferTask() accepts only non-null now --- .../moire/ultrasonic/service/LocalMediaPlayer.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index bf0923f8..39518922 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -616,9 +616,10 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } } - private inner class BufferTask(private val downloadFile: DownloadFile?, private val position: Int) : CancellableTask() { + private inner class BufferTask(private val downloadFile: DownloadFile, private val position: Int) : CancellableTask() { private val expectedFileSize: Long - private val partialFile: File + private val partialFile: File = downloadFile.partialFile + override fun execute() { setPlayerState(PlayerState.DOWNLOADING) while (!bufferComplete() && !isOffline(context)) { @@ -627,13 +628,13 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private return } } - if (downloadFile != null) { - doPlay(downloadFile, position, true) - } + + doPlay(downloadFile, position, true) + } private fun bufferComplete(): Boolean { - val completeFileAvailable = downloadFile!!.isWorkDone + val completeFileAvailable = downloadFile.isWorkDone val size = partialFile.length() Timber.i("Buffering %s (%d/%d, %s)", partialFile, size, expectedFileSize, completeFileAvailable) return completeFileAvailable || size >= expectedFileSize @@ -644,7 +645,6 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } init { - partialFile = downloadFile!!.partialFile var bufferLength = Util.getBufferLength(context).toLong() if (bufferLength == 0L) { // Set to seconds in a day, basically infinity From 8e7cf487fd7c81ff02e426f898b37ac70e555949 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 24 Mar 2021 15:04:25 +0100 Subject: [PATCH 10/14] Checkstyle fixes --- .../ultrasonic/service/DownloadFile.java | 8 + .../ultrasonic/service/LocalMediaPlayer.kt | 206 ++++++++++++------ 2 files changed, 152 insertions(+), 62 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java index d1ae5500..fac7939f 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java @@ -143,6 +143,14 @@ public class DownloadFile return saveFile; } + public File getCompleteOrPartialFile() { + if (isCompleteFileAvailable()) { + return getCompleteFile(); + } else { + return getPartialFile(); + } + } + public File getPartialFile() { return partialFile; diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 39518922..7c804800 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -3,6 +3,8 @@ package org.moire.ultrasonic.service import android.app.PendingIntent import android.content.ComponentName import android.content.Context +import android.content.Context.AUDIO_SERVICE +import android.content.Context.POWER_SERVICE import android.content.Intent import android.media.AudioManager import android.media.MediaMetadataRetriever @@ -14,41 +16,57 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.os.PowerManager +import android.os.PowerManager.PARTIAL_WAKE_LOCK import android.os.PowerManager.WakeLock +import java.io.File +import java.net.URLEncoder +import java.util.Locale +import kotlin.math.abs +import kotlin.math.max import org.moire.ultrasonic.audiofx.EqualizerController import org.moire.ultrasonic.audiofx.VisualizerController import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.fragment.PlayerFragment import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver -import org.moire.ultrasonic.util.* +import org.moire.ultrasonic.util.CancellableTask +import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.StreamProxy +import org.moire.ultrasonic.util.Util import timber.log.Timber -import java.io.File -import java.net.URLEncoder -import java.util.* -import kotlin.math.abs -import kotlin.math.max /** * Represents a Media Player which uses the mobile's resources for playback */ -class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private val context: Context) { +class LocalMediaPlayer( + private val audioFocusHandler: AudioFocusHandler, + private val context: Context +) { + @JvmField var onCurrentPlayingChanged: Consumer? = null + @JvmField var onSongCompleted: Consumer? = null + @JvmField var onPlayerStateChanged: BiConsumer? = null + @JvmField var onPrepared: Runnable? = null + @JvmField var onNextSongRequested: Runnable? = null + @JvmField var playerState = PlayerState.IDLE + @JvmField var currentPlaying: DownloadFile? = null + @JvmField var nextPlaying: DownloadFile? = null + private var nextPlayerState = PlayerState.IDLE private var nextSetup = false private var nextPlayingTask: CancellableTask? = null @@ -58,13 +76,13 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private private var mediaPlayerHandler: Handler? = null private var cachedPosition = 0 private var proxy: StreamProxy? = null - private var audioManager: AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + private var audioManager: AudioManager = context.getSystemService(AUDIO_SERVICE) as AudioManager private var remoteControlClient: RemoteControlClient? = null private var bufferTask: CancellableTask? = null private var positionCache: PositionCache? = null private var secondaryProgress = -1 - private val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager - private val wakeLock: WakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.javaClass.name) + private val pm = context.getSystemService(POWER_SERVICE) as PowerManager + private val wakeLock: WakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, this.javaClass.name) fun onCreate() { Thread { @@ -72,7 +90,14 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private Looper.prepare() mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK) mediaPlayer.setOnErrorListener { mediaPlayer, what, more -> - handleError(Exception(String.format(Locale.getDefault(), "MediaPlayer error: %d (%d)", what, more))) + handleError( + Exception( + String.format( + Locale.getDefault(), + "MediaPlayer error: %d (%d)", what, more + ) + ) + ) false } try { @@ -142,7 +167,9 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } if (onPlayerStateChanged != null) { val mainHandler = Handler(context.mainLooper) - val myRunnable = Runnable { onPlayerStateChanged!!.accept(playerState, currentPlaying) } + val myRunnable = Runnable { + onPlayerStateChanged!!.accept(playerState, currentPlaying) + } mainHandler.post(myRunnable) } if (playerState === PlayerState.STARTED && positionCache == null) { @@ -222,7 +249,6 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } } - @Synchronized fun playNext() { if (nextMediaPlayer == null || currentPlaying == null) return @@ -246,8 +272,6 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private proxy = null } - - @Synchronized fun pause() { try { @@ -266,7 +290,6 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } } - /* * The remote control API is deprecated in API 21 */ @@ -284,25 +307,37 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private audioManager.registerRemoteControlClient(remoteControlClient) } - Timber.i("In updateRemoteControl, playerState: %s [%d]", playerState, playerPosition) + Timber.i( + "In updateRemoteControl, playerState: %s [%d]", + playerState, playerPosition + ) if (playerState === PlayerState.STARTED) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { remoteControlClient!!.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING) } else { - remoteControlClient!!.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING, playerPosition.toLong(), 1.0f) + remoteControlClient!!.setPlaybackState( + RemoteControlClient.PLAYSTATE_PLAYING, + playerPosition.toLong(), 1.0f + ) } } else { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { remoteControlClient!!.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED) } else { - remoteControlClient!!.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED, playerPosition.toLong(), 1.0f) + remoteControlClient!!.setPlaybackState( + RemoteControlClient.PLAYSTATE_PAUSED, + playerPosition.toLong(), 1.0f + ) } } if (currentPlaying != null) { val currentSong = currentPlaying!!.song - val lockScreenBitmap = FileUtil.getAlbumArtBitmap(context, currentSong, Util.getMinDisplayMetric(context), true) + val lockScreenBitmap = FileUtil.getAlbumArtBitmap( + context, currentSong, + Util.getMinDisplayMetric(context), true + ) val artist = currentSong.artist val album = currentSong.album val title = currentSong.title @@ -310,13 +345,13 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private var duration = 0L if (currentSongDuration != null) duration = (currentSongDuration * 1000).toLong() remoteControlClient!!.editMetadata(true) - .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, artist) - .putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, artist) - .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, album) - .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, title) - .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration) - .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, lockScreenBitmap) - .apply() + .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, artist) + .putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, artist) + .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, album) + .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, title) + .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration) + .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, lockScreenBitmap) + .apply() } } @@ -337,26 +372,39 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } private fun createRemoteControlClient(): RemoteControlClient { - val componentName = ComponentName(context.packageName, MediaButtonIntentReceiver::class.java.name) + val componentName = ComponentName( + context.packageName, + MediaButtonIntentReceiver::class.java.name + ) + val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON) mediaButtonIntent.component = componentName - val broadcast = PendingIntent.getBroadcast(context, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT) + val broadcast = PendingIntent.getBroadcast( + context, 0, + mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT + ) + val remoteControlClient = RemoteControlClient(broadcast) audioManager.registerRemoteControlClient(remoteControlClient) // Flags for the media transport control that this client supports. var flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS or - RemoteControlClient.FLAG_KEY_MEDIA_NEXT or - RemoteControlClient.FLAG_KEY_MEDIA_PLAY or - RemoteControlClient.FLAG_KEY_MEDIA_PAUSE or - RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE or - RemoteControlClient.FLAG_KEY_MEDIA_STOP + RemoteControlClient.FLAG_KEY_MEDIA_NEXT or + RemoteControlClient.FLAG_KEY_MEDIA_PLAY or + RemoteControlClient.FLAG_KEY_MEDIA_PAUSE or + RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE or + RemoteControlClient.FLAG_KEY_MEDIA_STOP if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { flags = flags or RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE - remoteControlClient.setOnGetPlaybackPositionListener { mediaPlayer.currentPosition.toLong() } - remoteControlClient.setPlaybackPositionUpdateListener { newPositionMs -> seekTo(newPositionMs.toInt()) } + remoteControlClient.setOnGetPlaybackPositionListener { + mediaPlayer.currentPosition.toLong() + } + remoteControlClient.setPlaybackPositionUpdateListener { + newPositionMs -> + seekTo(newPositionMs.toInt()) + } } remoteControlClient.setTransportControlFlags(flags) @@ -378,9 +426,12 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private @get:Synchronized val playerPosition: Int get() = try { - if (playerState === PlayerState.IDLE || playerState === PlayerState.DOWNLOADING || playerState === PlayerState.PREPARING) { - 0 - } else cachedPosition + when (playerState) { + PlayerState.IDLE -> 0 + PlayerState.DOWNLOADING -> 0 + PlayerState.PREPARING -> 0 + else -> cachedPosition + } } catch (x: Exception) { handleError(x) 0 @@ -395,7 +446,10 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private return duration * 1000 } } - if (playerState !== PlayerState.IDLE && playerState !== PlayerState.DOWNLOADING && playerState !== PlayerState.PREPARING) { + if (playerState !== PlayerState.IDLE && + playerState !== PlayerState.DOWNLOADING && + playerState !== PlayerState.PREPARING + ) { try { return mediaPlayer.duration } catch (x: Exception) { @@ -427,8 +481,8 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private try { downloadFile.setPlaying(false) - val file = if (downloadFile.isCompleteFileAvailable) downloadFile.completeFile else downloadFile.partialFile - val partial = file == downloadFile.partialFile + val file = downloadFile.completeOrPartialFile + val partial = !downloadFile.isCompleteFileAvailable downloadFile.updateModificationDate() mediaPlayer.setOnCompletionListener(null) @@ -447,8 +501,10 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private }) proxy!!.start() } - dataSource = String.format(Locale.getDefault(), "http://127.0.0.1:%d/%s", - proxy!!.port, URLEncoder.encode(dataSource, Constants.UTF_8)) + dataSource = String.format( + Locale.getDefault(), "http://127.0.0.1:%d/%s", + proxy!!.port, URLEncoder.encode(dataSource, Constants.UTF_8) + ) Timber.i("Data Source: %s", dataSource) } else if (proxy != null) { proxy?.stop() @@ -463,14 +519,15 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private mediaPlayer.setOnBufferingUpdateListener { mp, percent -> val progressBar = PlayerFragment.getProgressBar() val song = downloadFile.song + if (percent == 100) { - if (progressBar != null) { - progressBar.secondaryProgress = 100 * progressBar.max - } mp.setOnBufferingUpdateListener(null) - } else if (progressBar != null && song.transcodedContentType == null && Util.getMaxBitRate(context) == 0) { - secondaryProgress = (percent.toDouble() / 100.toDouble() * progressBar.max).toInt() - progressBar.secondaryProgress = secondaryProgress + } + + secondaryProgress = (percent.toDouble() / 100.toDouble() * progressBar.max).toInt() + + if (song.transcodedContentType == null && Util.getMaxBitRate(context) == 0) { + progressBar?.secondaryProgress = secondaryProgress } } @@ -497,7 +554,6 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } postRunnable(onPrepared) - } attachHandlersToPlayer(mediaPlayer, downloadFile, partial) mediaPlayer.prepareAsync() @@ -509,7 +565,8 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private @Synchronized private fun setupNext(downloadFile: DownloadFile) { try { - val file = if (downloadFile.isCompleteFileAvailable) downloadFile.completeFile else downloadFile.partialFile + val file = downloadFile.completeOrPartialFile + if (nextMediaPlayer != null) { nextMediaPlayer!!.setOnCompletionListener(null) nextMediaPlayer!!.release() @@ -527,7 +584,13 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private nextMediaPlayer!!.setOnPreparedListener { try { setNextPlayerState(PlayerState.PREPARED) - if (Util.getGaplessPlaybackPreference(context) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED)) { + if (Util.getGaplessPlaybackPreference(context) && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && + ( + playerState === PlayerState.STARTED || + playerState === PlayerState.PAUSED + ) + ) { mediaPlayer.setNextMediaPlayer(nextMediaPlayer) nextSetup = true } @@ -545,7 +608,11 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } } - private fun attachHandlersToPlayer(mediaPlayer: MediaPlayer, downloadFile: DownloadFile, isPartial: Boolean) { + private fun attachHandlersToPlayer( + mediaPlayer: MediaPlayer, + downloadFile: DownloadFile, + isPartial: Boolean + ) { mediaPlayer.setOnErrorListener { _, what, extra -> Timber.w("Error on playing file (%d, %d): %s", what, extra, downloadFile) val pos = cachedPosition @@ -556,7 +623,10 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private true } - val duration = if (downloadFile.song.duration == null) 0 else downloadFile.song.duration!! * 1000 + var duration = 0 + if (downloadFile.song.duration != null) { + duration = downloadFile.song.duration!! * 1000 + } mediaPlayer.setOnCompletionListener(object : OnCompletionListener { override fun onCompletion(mediaPlayer: MediaPlayer) { @@ -568,7 +638,10 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private Timber.i("Ending position %d of %d", pos, duration) if (!isPartial || downloadFile.isWorkDone && abs(duration - pos) < 1000) { setPlayerState(PlayerState.COMPLETED) - if (Util.getGaplessPlaybackPreference(context) && nextPlaying != null && nextPlayerState === PlayerState.PREPARED) { + if (Util.getGaplessPlaybackPreference(context) && + nextPlaying != null && + nextPlayerState === PlayerState.PREPARED + ) { if (nextSetup) { nextSetup = false } @@ -616,7 +689,10 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } } - private inner class BufferTask(private val downloadFile: DownloadFile, private val position: Int) : CancellableTask() { + private inner class BufferTask( + private val downloadFile: DownloadFile, + private val position: Int + ) : CancellableTask() { private val expectedFileSize: Long private val partialFile: File = downloadFile.partialFile @@ -630,13 +706,17 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private } doPlay(downloadFile, position, true) - } private fun bufferComplete(): Boolean { val completeFileAvailable = downloadFile.isWorkDone val size = partialFile.length() - Timber.i("Buffering %s (%d/%d, %s)", partialFile, size, expectedFileSize, completeFileAvailable) + + Timber.i( + "Buffering %s (%d/%d, %s)", + partialFile, size, expectedFileSize, completeFileAvailable + ) + return completeFileAvailable || size >= expectedFileSize } @@ -685,8 +765,11 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private private fun bufferComplete(): Boolean { val completeFileAvailable = downloadFile!!.isWorkDone + val state = (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED) + Timber.i("Buffering next %s (%d)", partialFile, partialFile!!.length()) - return completeFileAvailable && (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED) + + return completeFileAvailable && state } override fun toString(): String { @@ -746,5 +829,4 @@ class LocalMediaPlayer(private val audioFocusHandler: AudioFocusHandler, private mainHandler.post(myRunnable) } } - -} \ No newline at end of file +} From 51dafd542a731139c86e89b7ab3c7ebaea937e80 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 24 Mar 2021 22:27:15 +0100 Subject: [PATCH 11/14] Static analysis fixes --- .../org/moire/ultrasonic/service/LocalMediaPlayer.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 7c804800..5875664b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -31,6 +31,7 @@ import org.moire.ultrasonic.fragment.PlayerFragment import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver import org.moire.ultrasonic.util.CancellableTask import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.StreamProxy import org.moire.ultrasonic.util.Util import timber.log.Timber @@ -89,7 +90,7 @@ class LocalMediaPlayer( Thread.currentThread().name = "MediaPlayerThread" Looper.prepare() mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK) - mediaPlayer.setOnErrorListener { mediaPlayer, what, more -> + mediaPlayer.setOnErrorListener { _, what, more -> handleError( Exception( String.format( @@ -460,9 +461,7 @@ class LocalMediaPlayer( } fun setVolume(volume: Float) { - if (mediaPlayer != null) { - mediaPlayer.setVolume(volume, volume) - } + mediaPlayer.setVolume(volume, volume) } @Synchronized @@ -598,7 +597,7 @@ class LocalMediaPlayer( handleErrorNext(x) } } - nextMediaPlayer!!.setOnErrorListener { mediaPlayer, what, extra -> + nextMediaPlayer!!.setOnErrorListener { _, what, extra -> Timber.w("Error on playing next (%d, %d): %s", what, extra, downloadFile) true } @@ -795,7 +794,7 @@ class LocalMediaPlayer( // Stop checking position before the song reaches completion while (isRunning) { try { - if (mediaPlayer != null && playerState === PlayerState.STARTED) { + if (playerState === PlayerState.STARTED) { cachedPosition = mediaPlayer.currentPosition } Util.sleepQuietly(50L) From bf106b0384b2e63bc47f3d705f26914217ce8abb Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 25 Mar 2021 17:20:30 +0100 Subject: [PATCH 12/14] Handle a case when reset() is called after release(). --- .../ultrasonic/service/LocalMediaPlayer.kt | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 5875664b..873d8b4e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -252,19 +252,14 @@ class LocalMediaPlayer( @Synchronized fun playNext() { - if (nextMediaPlayer == null || currentPlaying == null) return + if (nextMediaPlayer == null || nextPlaying == null) return - val oldPlayer = mediaPlayer mediaPlayer = nextMediaPlayer!! - // FIXME: Why is this being done? - nextMediaPlayer = oldPlayer - setCurrentPlaying(nextPlaying) setPlayerState(PlayerState.STARTED) - // FIXME: Why is currentPlaying passed here and not nextPlaying?! - attachHandlersToPlayer(mediaPlayer, currentPlaying!!, false) + attachHandlersToPlayer(mediaPlayer, nextPlaying!!, false) postRunnable(onNextSongRequested) @@ -303,7 +298,13 @@ class LocalMediaPlayer( if (remoteControlClient == null) { remoteControlClient = createRemoteControlClient() } else { - // FIXME: This looks like a hack. Why is it needed? + // This is probably needed only in API <=17 + // "You must register your RemoteControlDisplay every time when the View which + // displays metadata is shown to the user. This is because 4.2.2 and lower + // versions support only one RemoteControlDisplay, and if system will + // decide to register it's own RCD, your RCD will be + // unregistered automatically. + // https://forum.xda-developers.com/t/guide-implement-your-own-lockscreen-like-music-controls.2401597/ audioManager.unregisterRemoteControlClient(remoteControlClient) audioManager.registerRemoteControlClient(remoteControlClient) } @@ -477,6 +478,11 @@ class LocalMediaPlayer( @Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean) { + + // In many cases we will be resetting the mediaPlayer a second time here. + // figure out if we can remove this call... + resetMediaPlayer() + try { downloadFile.setPlaying(false) @@ -486,7 +492,7 @@ class LocalMediaPlayer( downloadFile.updateModificationDate() mediaPlayer.setOnCompletionListener(null) secondaryProgress = -1 // Ensure seeking in non StreamProxy playback works - mediaPlayer.reset() + setPlayerState(PlayerState.IDLE) mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC) @@ -678,16 +684,30 @@ class LocalMediaPlayer( if (bufferTask != null) { bufferTask!!.cancel() } + + resetMediaPlayer() + try { setPlayerState(PlayerState.IDLE) mediaPlayer.setOnErrorListener(null) mediaPlayer.setOnCompletionListener(null) - mediaPlayer.reset() } catch (x: Exception) { handleError(x) } } + @Synchronized + fun resetMediaPlayer() { + try { + mediaPlayer.reset() + } catch (x: Exception) { + Timber.w(x, "MediaPlayer was released but LocalMediaPlayer was not destroyed") + + // Recreate MediaPlayer + mediaPlayer = MediaPlayer() + } + } + private inner class BufferTask( private val downloadFile: DownloadFile, private val position: Int From 970d93bd910bdaee88f909d51c0dc89026eef255 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 25 Mar 2021 20:33:19 +0100 Subject: [PATCH 13/14] Pass down the autoPlay value to BufferTask() --- .../org/moire/ultrasonic/service/LocalMediaPlayer.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 873d8b4e..8d411a84 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -469,7 +469,7 @@ class LocalMediaPlayer( private fun bufferAndPlay(fileToPlay: DownloadFile, position: Int, autoStart: Boolean) { if (playerState !== PlayerState.PREPARED) { reset() - bufferTask = BufferTask(fileToPlay, position) + bufferTask = BufferTask(fileToPlay, position, autoStart) bufferTask!!.start() } else { doPlay(fileToPlay, position, autoStart) @@ -710,7 +710,8 @@ class LocalMediaPlayer( private inner class BufferTask( private val downloadFile: DownloadFile, - private val position: Int + private val position: Int, + private val autoStart: Boolean = true ) : CancellableTask() { private val expectedFileSize: Long private val partialFile: File = downloadFile.partialFile @@ -724,7 +725,7 @@ class LocalMediaPlayer( } } - doPlay(downloadFile, position, true) + doPlay(downloadFile, position, autoStart) } private fun bufferComplete(): Boolean { From 05067aaf3c564c4b2276176ac5c42c380c7d1fb1 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 30 Mar 2021 14:38:59 +0200 Subject: [PATCH 14/14] Rename onCreate/onDestroy functions; create a new MediaPlayer instance on release() --- .../org/moire/ultrasonic/service/MediaPlayerService.java | 4 ++-- .../org/moire/ultrasonic/service/LocalMediaPlayer.kt | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java index f808959c..c7ae0cf7 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java @@ -146,7 +146,7 @@ public class MediaPlayerService extends Service downloader.onCreate(); shufflePlayBuffer.onCreate(); - localMediaPlayer.onCreate(); + localMediaPlayer.init(); setupOnCurrentPlayingChangedHandler(); setupOnPlayerStateChangedHandler(); setupOnSongCompletedHandler(); @@ -198,7 +198,7 @@ public class MediaPlayerService extends Service instance = null; try { - localMediaPlayer.onDestroy(); + localMediaPlayer.release(); downloader.stop(); shufflePlayBuffer.onDestroy(); } catch (Throwable ignored) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 8d411a84..87df94f7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -85,7 +85,7 @@ class LocalMediaPlayer( private val pm = context.getSystemService(POWER_SERVICE) as PowerManager private val wakeLock: WakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, this.javaClass.name) - fun onCreate() { + fun init() { Thread { Thread.currentThread().name = "MediaPlayerThread" Looper.prepare() @@ -126,7 +126,7 @@ class LocalMediaPlayer( Timber.i("LocalMediaPlayer created") } - fun onDestroy() { + fun release() { reset() try { val i = Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION) @@ -136,6 +136,9 @@ class LocalMediaPlayer( EqualizerController.release() VisualizerController.release() mediaPlayer.release() + + mediaPlayer = MediaPlayer() + if (nextMediaPlayer != null) { nextMediaPlayer!!.release() }