2021-04-16 11:45:37 +02:00
|
|
|
/*
|
|
|
|
* LocalMediaPlayer.kt
|
|
|
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
|
|
|
*
|
|
|
|
* Distributed under terms of the GNU GPLv3 license.
|
|
|
|
*/
|
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
package org.moire.ultrasonic.service
|
|
|
|
|
|
|
|
import android.content.Context
|
2021-03-24 15:04:25 +01:00
|
|
|
import android.content.Context.POWER_SERVICE
|
2021-03-24 12:47:31 +01:00
|
|
|
import android.content.Intent
|
|
|
|
import android.media.AudioManager
|
|
|
|
import android.media.MediaPlayer
|
|
|
|
import android.media.MediaPlayer.OnCompletionListener
|
|
|
|
import android.media.audiofx.AudioEffect
|
|
|
|
import android.os.Build
|
|
|
|
import android.os.Handler
|
|
|
|
import android.os.Looper
|
|
|
|
import android.os.PowerManager
|
2021-03-24 15:04:25 +01:00
|
|
|
import android.os.PowerManager.PARTIAL_WAKE_LOCK
|
2021-03-24 12:47:31 +01:00
|
|
|
import android.os.PowerManager.WakeLock
|
2021-03-24 15:04:25 +01:00
|
|
|
import java.io.File
|
|
|
|
import java.net.URLEncoder
|
|
|
|
import java.util.Locale
|
|
|
|
import kotlin.math.abs
|
|
|
|
import kotlin.math.max
|
2021-03-24 12:47:31 +01:00
|
|
|
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
|
2021-03-24 15:04:25 +01:00
|
|
|
import org.moire.ultrasonic.util.CancellableTask
|
|
|
|
import org.moire.ultrasonic.util.Constants
|
|
|
|
import org.moire.ultrasonic.util.StreamProxy
|
|
|
|
import org.moire.ultrasonic.util.Util
|
2021-03-24 12:47:31 +01:00
|
|
|
import timber.log.Timber
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Represents a Media Player which uses the mobile's resources for playback
|
|
|
|
*/
|
2021-03-24 15:04:25 +01:00
|
|
|
class LocalMediaPlayer(
|
|
|
|
private val audioFocusHandler: AudioFocusHandler,
|
|
|
|
private val context: Context
|
|
|
|
) {
|
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
@JvmField
|
2021-04-22 11:47:15 +02:00
|
|
|
var onCurrentPlayingChanged: ((DownloadFile?) -> Unit?)? = null
|
2021-03-24 15:04:25 +01:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
@JvmField
|
2021-04-22 11:47:15 +02:00
|
|
|
var onSongCompleted: ((DownloadFile?) -> Unit?)? = null
|
2021-03-24 15:04:25 +01:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
@JvmField
|
2021-04-22 11:47:15 +02:00
|
|
|
var onPlayerStateChanged: ((PlayerState, DownloadFile?) -> Unit?)? = null
|
2021-03-24 15:04:25 +01:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
@JvmField
|
2021-04-22 11:47:15 +02:00
|
|
|
var onPrepared: (() -> Any?)? = null
|
2021-03-24 15:04:25 +01:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
@JvmField
|
|
|
|
var onNextSongRequested: Runnable? = null
|
2021-03-24 15:04:25 +01:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
@JvmField
|
|
|
|
var playerState = PlayerState.IDLE
|
2021-03-24 15:04:25 +01:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
@JvmField
|
|
|
|
var currentPlaying: DownloadFile? = null
|
2021-03-24 15:04:25 +01:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
@JvmField
|
|
|
|
var nextPlaying: DownloadFile? = null
|
2021-03-24 15:04:25 +01:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
private var nextPlayerState = PlayerState.IDLE
|
|
|
|
private var nextSetup = false
|
|
|
|
private var nextPlayingTask: CancellableTask? = null
|
2021-03-24 13:28:54 +01:00
|
|
|
private var mediaPlayer: MediaPlayer = MediaPlayer()
|
2021-03-24 12:47:31 +01:00
|
|
|
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 bufferTask: CancellableTask? = null
|
|
|
|
private var positionCache: PositionCache? = null
|
|
|
|
private var secondaryProgress = -1
|
2021-03-24 15:04:25 +01:00
|
|
|
private val pm = context.getSystemService(POWER_SERVICE) as PowerManager
|
|
|
|
private val wakeLock: WakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, this.javaClass.name)
|
2021-03-24 12:59:07 +01:00
|
|
|
|
2021-03-30 14:38:59 +02:00
|
|
|
fun init() {
|
2021-03-24 12:47:31 +01:00
|
|
|
Thread {
|
|
|
|
Thread.currentThread().name = "MediaPlayerThread"
|
|
|
|
Looper.prepare()
|
2021-04-22 11:00:20 +02:00
|
|
|
mediaPlayer.setWakeMode(context, PARTIAL_WAKE_LOCK)
|
2021-03-24 22:27:15 +01:00
|
|
|
mediaPlayer.setOnErrorListener { _, what, more ->
|
2021-03-24 15:04:25 +01:00
|
|
|
handleError(
|
|
|
|
Exception(
|
|
|
|
String.format(
|
|
|
|
Locale.getDefault(),
|
|
|
|
"MediaPlayer error: %d (%d)", what, more
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
2021-03-24 12:47:31 +01:00
|
|
|
false
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
val i = Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
|
2021-03-24 13:28:54 +01:00
|
|
|
i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.audioSessionId)
|
2021-03-24 12:47:31 +01:00
|
|
|
i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
|
|
|
|
context.sendBroadcast(i)
|
|
|
|
} catch (e: Throwable) {
|
|
|
|
// Froyo or lower
|
|
|
|
}
|
|
|
|
mediaPlayerLooper = Looper.myLooper()
|
2021-03-24 14:48:17 +01:00
|
|
|
mediaPlayerHandler = Handler(mediaPlayerLooper!!)
|
2021-03-24 12:47:31 +01:00
|
|
|
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()
|
2021-03-24 12:59:07 +01:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
wakeLock.setReferenceCounted(false)
|
|
|
|
Util.registerMediaButtonEventReceiver(context, true)
|
|
|
|
Timber.i("LocalMediaPlayer created")
|
|
|
|
}
|
|
|
|
|
2021-03-30 14:38:59 +02:00
|
|
|
fun release() {
|
2021-03-24 12:47:31 +01:00
|
|
|
reset()
|
|
|
|
try {
|
|
|
|
val i = Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
|
2021-03-24 13:28:54 +01:00
|
|
|
i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.audioSessionId)
|
2021-03-24 12:47:31 +01:00
|
|
|
i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
|
|
|
|
context.sendBroadcast(i)
|
|
|
|
EqualizerController.release()
|
|
|
|
VisualizerController.release()
|
2021-03-24 13:28:54 +01:00
|
|
|
mediaPlayer.release()
|
2021-03-30 14:38:59 +02:00
|
|
|
|
|
|
|
mediaPlayer = MediaPlayer()
|
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
if (nextMediaPlayer != null) {
|
|
|
|
nextMediaPlayer!!.release()
|
|
|
|
}
|
|
|
|
mediaPlayerLooper!!.quit()
|
|
|
|
if (bufferTask != null) {
|
|
|
|
bufferTask!!.cancel()
|
|
|
|
}
|
|
|
|
if (nextPlayingTask != null) {
|
|
|
|
nextPlayingTask!!.cancel()
|
|
|
|
}
|
|
|
|
Util.unregisterMediaButtonEventReceiver(context, true)
|
2021-03-24 12:59:07 +01:00
|
|
|
wakeLock.release()
|
2021-03-24 12:47:31 +01:00
|
|
|
} 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()
|
|
|
|
}
|
2021-04-17 18:39:17 +02:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
if (onPlayerStateChanged != null) {
|
|
|
|
val mainHandler = Handler(context.mainLooper)
|
2021-04-22 11:47:15 +02:00
|
|
|
|
2021-03-24 15:04:25 +01:00
|
|
|
val myRunnable = Runnable {
|
2021-04-22 11:47:15 +02:00
|
|
|
onPlayerStateChanged!!(playerState, currentPlaying)
|
2021-03-24 15:04:25 +01:00
|
|
|
}
|
2021-03-24 12:47:31 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-24 13:28:54 +01:00
|
|
|
/*
|
|
|
|
* Set the current playing file. It's called with null to reset the player.
|
|
|
|
*/
|
2021-03-24 12:47:31 +01:00
|
|
|
@Synchronized
|
|
|
|
fun setCurrentPlaying(currentPlaying: DownloadFile?) {
|
|
|
|
Timber.v("setCurrentPlaying %s", currentPlaying)
|
|
|
|
this.currentPlaying = currentPlaying
|
2021-03-24 13:28:54 +01:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
if (onCurrentPlayingChanged != null) {
|
|
|
|
val mainHandler = Handler(context.mainLooper)
|
2021-04-22 11:47:15 +02:00
|
|
|
val myRunnable = Runnable { onCurrentPlayingChanged!!(currentPlaying) }
|
2021-03-24 12:47:31 +01:00
|
|
|
mainHandler.post(myRunnable)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-24 13:28:54 +01:00
|
|
|
/*
|
2021-03-24 13:45:46 +01:00
|
|
|
* Set the next playing file. nextToPlay cannot be null
|
2021-03-24 13:28:54 +01:00
|
|
|
*/
|
2021-03-24 12:47:31 +01:00
|
|
|
@Synchronized
|
2021-03-24 13:28:54 +01:00
|
|
|
fun setNextPlaying(nextToPlay: DownloadFile) {
|
2021-03-24 12:47:31 +01:00
|
|
|
nextPlaying = nextToPlay
|
|
|
|
nextPlayingTask = CheckCompletionTask(nextPlaying)
|
2021-03-24 12:59:07 +01:00
|
|
|
nextPlayingTask?.start()
|
2021-03-24 12:47:31 +01:00
|
|
|
}
|
|
|
|
|
2021-03-24 13:45:46 +01:00
|
|
|
/*
|
|
|
|
* Clear the next playing file. setIdle controls whether the playerState is affected as well
|
|
|
|
*/
|
2021-03-24 12:47:31 +01:00
|
|
|
@Synchronized
|
2021-03-24 13:28:54 +01:00
|
|
|
fun clearNextPlaying(setIdle: Boolean) {
|
2021-03-24 12:47:31 +01:00
|
|
|
nextSetup = false
|
|
|
|
nextPlaying = null
|
|
|
|
if (nextPlayingTask != null) {
|
|
|
|
nextPlayingTask!!.cancel()
|
|
|
|
nextPlayingTask = null
|
|
|
|
}
|
2021-03-24 13:28:54 +01:00
|
|
|
|
|
|
|
if (setIdle) {
|
|
|
|
setNextPlayerState(PlayerState.IDLE)
|
|
|
|
}
|
2021-03-24 12:47:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@Synchronized
|
|
|
|
fun setNextPlayerState(playerState: PlayerState) {
|
|
|
|
Timber.i("Next: %s -> %s (%s)", nextPlayerState.name, playerState.name, nextPlaying)
|
|
|
|
nextPlayerState = playerState
|
|
|
|
}
|
|
|
|
|
2021-03-24 13:45:46 +01:00
|
|
|
/*
|
|
|
|
* Public method to play a given file.
|
|
|
|
* Optionally specify a position to start at.
|
|
|
|
*/
|
2021-03-24 12:47:31 +01:00
|
|
|
@Synchronized
|
2021-03-24 13:45:46 +01:00
|
|
|
@JvmOverloads
|
|
|
|
fun play(fileToPlay: DownloadFile?, position: Int = 0, autoStart: Boolean = true) {
|
2021-03-24 12:47:31 +01:00
|
|
|
if (nextPlayingTask != null) {
|
|
|
|
nextPlayingTask!!.cancel()
|
|
|
|
nextPlayingTask = null
|
|
|
|
}
|
|
|
|
setCurrentPlaying(fileToPlay)
|
2021-03-24 13:45:46 +01:00
|
|
|
|
|
|
|
if (fileToPlay != null) {
|
|
|
|
bufferAndPlay(fileToPlay, position, autoStart)
|
|
|
|
}
|
2021-03-24 12:47:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@Synchronized
|
|
|
|
fun playNext() {
|
2021-03-25 17:20:30 +01:00
|
|
|
if (nextMediaPlayer == null || nextPlaying == null) return
|
2021-03-24 13:28:54 +01:00
|
|
|
|
|
|
|
mediaPlayer = nextMediaPlayer!!
|
2021-03-24 14:03:01 +01:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
setCurrentPlaying(nextPlaying)
|
|
|
|
setPlayerState(PlayerState.STARTED)
|
2021-03-24 14:03:01 +01:00
|
|
|
|
2021-03-25 17:20:30 +01:00
|
|
|
attachHandlersToPlayer(mediaPlayer, nextPlaying!!, false)
|
2021-03-24 14:03:01 +01:00
|
|
|
|
2021-03-24 14:10:27 +01:00
|
|
|
postRunnable(onNextSongRequested)
|
2021-03-24 12:47:31 +01:00
|
|
|
|
|
|
|
// Proxy should not be being used here since the next player was already setup to play
|
2021-03-24 13:28:54 +01:00
|
|
|
proxy?.stop()
|
|
|
|
proxy = null
|
2021-03-24 12:47:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@Synchronized
|
|
|
|
fun pause() {
|
|
|
|
try {
|
2021-03-24 13:28:54 +01:00
|
|
|
mediaPlayer.pause()
|
2021-03-24 12:47:31 +01:00
|
|
|
} catch (x: Exception) {
|
|
|
|
handleError(x)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Synchronized
|
|
|
|
fun start() {
|
|
|
|
try {
|
2021-03-24 13:28:54 +01:00
|
|
|
mediaPlayer.start()
|
2021-03-24 12:47:31 +01:00
|
|
|
} catch (x: Exception) {
|
|
|
|
handleError(x)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Synchronized
|
|
|
|
fun seekTo(position: Int) {
|
|
|
|
try {
|
2021-03-24 13:28:54 +01:00
|
|
|
mediaPlayer.seekTo(position)
|
2021-03-24 12:47:31 +01:00
|
|
|
cachedPosition = position
|
|
|
|
} catch (x: Exception) {
|
|
|
|
handleError(x)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@get:Synchronized
|
|
|
|
val playerPosition: Int
|
|
|
|
get() = try {
|
2021-03-24 15:04:25 +01:00
|
|
|
when (playerState) {
|
|
|
|
PlayerState.IDLE -> 0
|
|
|
|
PlayerState.DOWNLOADING -> 0
|
|
|
|
PlayerState.PREPARING -> 0
|
|
|
|
else -> cachedPosition
|
|
|
|
}
|
2021-03-24 12:47:31 +01:00
|
|
|
} 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
|
|
|
|
}
|
|
|
|
}
|
2021-03-24 15:04:25 +01:00
|
|
|
if (playerState !== PlayerState.IDLE &&
|
|
|
|
playerState !== PlayerState.DOWNLOADING &&
|
|
|
|
playerState !== PlayerState.PREPARING
|
|
|
|
) {
|
2021-03-24 12:47:31 +01:00
|
|
|
try {
|
2021-03-24 13:28:54 +01:00
|
|
|
return mediaPlayer.duration
|
2021-03-24 12:47:31 +01:00
|
|
|
} catch (x: Exception) {
|
|
|
|
handleError(x)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
fun setVolume(volume: Float) {
|
2021-03-24 22:27:15 +01:00
|
|
|
mediaPlayer.setVolume(volume, volume)
|
2021-03-24 12:47:31 +01:00
|
|
|
}
|
|
|
|
|
2021-03-24 14:03:01 +01:00
|
|
|
@Synchronized
|
|
|
|
private fun bufferAndPlay(fileToPlay: DownloadFile, position: Int, autoStart: Boolean) {
|
|
|
|
if (playerState !== PlayerState.PREPARED) {
|
|
|
|
reset()
|
2021-03-25 20:33:19 +01:00
|
|
|
bufferTask = BufferTask(fileToPlay, position, autoStart)
|
2021-03-24 14:03:01 +01:00
|
|
|
bufferTask!!.start()
|
|
|
|
} else {
|
|
|
|
doPlay(fileToPlay, position, autoStart)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
@Synchronized
|
2021-03-24 13:45:46 +01:00
|
|
|
private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean) {
|
2021-03-25 17:20:30 +01:00
|
|
|
|
|
|
|
// In many cases we will be resetting the mediaPlayer a second time here.
|
|
|
|
// figure out if we can remove this call...
|
|
|
|
resetMediaPlayer()
|
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
try {
|
2021-03-24 13:45:46 +01:00
|
|
|
downloadFile.setPlaying(false)
|
|
|
|
|
2021-03-24 15:04:25 +01:00
|
|
|
val file = downloadFile.completeOrPartialFile
|
|
|
|
val partial = !downloadFile.isCompleteFileAvailable
|
2021-03-24 13:45:46 +01:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
downloadFile.updateModificationDate()
|
2021-03-24 13:28:54 +01:00
|
|
|
mediaPlayer.setOnCompletionListener(null)
|
2021-03-24 12:47:31 +01:00
|
|
|
secondaryProgress = -1 // Ensure seeking in non StreamProxy playback works
|
2021-03-25 17:20:30 +01:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
setPlayerState(PlayerState.IDLE)
|
2021-04-22 10:59:05 +02:00
|
|
|
setAudioAttributes(mediaPlayer)
|
2021-03-24 13:45:46 +01:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
var dataSource = file.path
|
|
|
|
if (partial) {
|
|
|
|
if (proxy == null) {
|
|
|
|
proxy = StreamProxy(object : Supplier<DownloadFile?>() {
|
|
|
|
override fun get(): DownloadFile {
|
|
|
|
return currentPlaying!!
|
|
|
|
}
|
|
|
|
})
|
|
|
|
proxy!!.start()
|
|
|
|
}
|
2021-03-24 15:04:25 +01:00
|
|
|
dataSource = String.format(
|
|
|
|
Locale.getDefault(), "http://127.0.0.1:%d/%s",
|
|
|
|
proxy!!.port, URLEncoder.encode(dataSource, Constants.UTF_8)
|
|
|
|
)
|
2021-03-24 12:47:31 +01:00
|
|
|
Timber.i("Data Source: %s", dataSource)
|
|
|
|
} else if (proxy != null) {
|
2021-03-24 14:03:01 +01:00
|
|
|
proxy?.stop()
|
2021-03-24 12:47:31 +01:00
|
|
|
proxy = null
|
|
|
|
}
|
2021-03-24 13:45:46 +01:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
Timber.i("Preparing media player")
|
2021-03-24 13:45:46 +01:00
|
|
|
|
2021-03-24 13:28:54 +01:00
|
|
|
mediaPlayer.setDataSource(dataSource)
|
2021-03-24 12:47:31 +01:00
|
|
|
setPlayerState(PlayerState.PREPARING)
|
2021-03-24 13:45:46 +01:00
|
|
|
|
2021-03-24 13:28:54 +01:00
|
|
|
mediaPlayer.setOnBufferingUpdateListener { mp, percent ->
|
2021-03-24 12:47:31 +01:00
|
|
|
val progressBar = PlayerFragment.getProgressBar()
|
|
|
|
val song = downloadFile.song
|
2021-03-24 15:04:25 +01:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
if (percent == 100) {
|
|
|
|
mp.setOnBufferingUpdateListener(null)
|
2021-03-24 15:04:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
secondaryProgress = (percent.toDouble() / 100.toDouble() * progressBar.max).toInt()
|
|
|
|
|
|
|
|
if (song.transcodedContentType == null && Util.getMaxBitRate(context) == 0) {
|
|
|
|
progressBar?.secondaryProgress = secondaryProgress
|
2021-03-24 12:47:31 +01:00
|
|
|
}
|
|
|
|
}
|
2021-03-24 13:45:46 +01:00
|
|
|
|
2021-03-24 13:28:54 +01:00
|
|
|
mediaPlayer.setOnPreparedListener {
|
2021-03-24 12:47:31 +01:00
|
|
|
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) {
|
2021-03-24 13:28:54 +01:00
|
|
|
mediaPlayer.start()
|
2021-03-24 12:47:31 +01:00
|
|
|
setPlayerState(PlayerState.STARTED)
|
|
|
|
} else {
|
|
|
|
setPlayerState(PlayerState.PAUSED)
|
|
|
|
}
|
|
|
|
}
|
2021-03-24 14:10:27 +01:00
|
|
|
|
2021-04-22 11:47:15 +02:00
|
|
|
postRunnable {
|
|
|
|
onPrepared
|
|
|
|
}
|
2021-03-24 12:47:31 +01:00
|
|
|
}
|
2021-03-24 13:28:54 +01:00
|
|
|
attachHandlersToPlayer(mediaPlayer, downloadFile, partial)
|
|
|
|
mediaPlayer.prepareAsync()
|
2021-03-24 12:47:31 +01:00
|
|
|
} catch (x: Exception) {
|
|
|
|
handleError(x)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-22 10:59:05 +02:00
|
|
|
private fun setAudioAttributes(player: MediaPlayer) {
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
|
|
player.setAudioAttributes(AudioFocusHandler.getAudioAttributes())
|
|
|
|
} else {
|
|
|
|
@Suppress("DEPRECATION")
|
|
|
|
player.setAudioStreamType(AudioManager.STREAM_MUSIC)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
@Synchronized
|
|
|
|
private fun setupNext(downloadFile: DownloadFile) {
|
|
|
|
try {
|
2021-03-24 15:04:25 +01:00
|
|
|
val file = downloadFile.completeOrPartialFile
|
|
|
|
|
2021-04-17 19:56:48 +02:00
|
|
|
// Release the media player if it is not our active player
|
|
|
|
if (nextMediaPlayer != null && nextMediaPlayer != mediaPlayer) {
|
2021-03-24 12:47:31 +01:00
|
|
|
nextMediaPlayer!!.setOnCompletionListener(null)
|
|
|
|
nextMediaPlayer!!.release()
|
|
|
|
nextMediaPlayer = null
|
|
|
|
}
|
|
|
|
nextMediaPlayer = MediaPlayer()
|
2021-04-22 11:00:20 +02:00
|
|
|
nextMediaPlayer!!.setWakeMode(context, PARTIAL_WAKE_LOCK)
|
2021-04-22 10:59:05 +02:00
|
|
|
|
|
|
|
setAudioAttributes(nextMediaPlayer!!)
|
|
|
|
|
|
|
|
// This has nothing to do with the MediaSession, it is used to associate
|
|
|
|
// the equalizer or visualizer with the player
|
2021-03-24 12:47:31 +01:00
|
|
|
try {
|
2021-03-24 13:28:54 +01:00
|
|
|
nextMediaPlayer!!.audioSessionId = mediaPlayer.audioSessionId
|
2021-03-24 12:47:31 +01:00
|
|
|
} catch (e: Throwable) {
|
|
|
|
}
|
2021-04-22 10:59:05 +02:00
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
nextMediaPlayer!!.setDataSource(file.path)
|
|
|
|
setNextPlayerState(PlayerState.PREPARING)
|
|
|
|
nextMediaPlayer!!.setOnPreparedListener {
|
|
|
|
try {
|
|
|
|
setNextPlayerState(PlayerState.PREPARED)
|
2021-03-24 15:04:25 +01:00
|
|
|
if (Util.getGaplessPlaybackPreference(context) &&
|
|
|
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
|
|
|
|
(
|
|
|
|
playerState === PlayerState.STARTED ||
|
|
|
|
playerState === PlayerState.PAUSED
|
|
|
|
)
|
|
|
|
) {
|
2021-03-24 13:28:54 +01:00
|
|
|
mediaPlayer.setNextMediaPlayer(nextMediaPlayer)
|
2021-03-24 12:47:31 +01:00
|
|
|
nextSetup = true
|
|
|
|
}
|
|
|
|
} catch (x: Exception) {
|
|
|
|
handleErrorNext(x)
|
|
|
|
}
|
|
|
|
}
|
2021-03-24 22:27:15 +01:00
|
|
|
nextMediaPlayer!!.setOnErrorListener { _, what, extra ->
|
2021-03-24 12:47:31 +01:00
|
|
|
Timber.w("Error on playing next (%d, %d): %s", what, extra, downloadFile)
|
|
|
|
true
|
|
|
|
}
|
|
|
|
nextMediaPlayer!!.prepareAsync()
|
|
|
|
} catch (x: Exception) {
|
|
|
|
handleErrorNext(x)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-24 15:04:25 +01:00
|
|
|
private fun attachHandlersToPlayer(
|
|
|
|
mediaPlayer: MediaPlayer,
|
|
|
|
downloadFile: DownloadFile,
|
|
|
|
isPartial: Boolean
|
|
|
|
) {
|
2021-03-24 13:28:54 +01:00
|
|
|
mediaPlayer.setOnErrorListener { _, what, extra ->
|
2021-03-24 12:47:31 +01:00
|
|
|
Timber.w("Error on playing file (%d, %d): %s", what, extra, downloadFile)
|
|
|
|
val pos = cachedPosition
|
|
|
|
reset()
|
2021-03-24 13:28:54 +01:00
|
|
|
downloadFile.setPlaying(false)
|
2021-03-24 12:47:31 +01:00
|
|
|
doPlay(downloadFile, pos, true)
|
|
|
|
downloadFile.setPlaying(true)
|
|
|
|
true
|
|
|
|
}
|
2021-03-24 13:28:54 +01:00
|
|
|
|
2021-03-24 15:04:25 +01:00
|
|
|
var duration = 0
|
|
|
|
if (downloadFile.song.duration != null) {
|
|
|
|
duration = downloadFile.song.duration!! * 1000
|
|
|
|
}
|
2021-03-24 13:28:54 +01:00
|
|
|
|
|
|
|
mediaPlayer.setOnCompletionListener(object : OnCompletionListener {
|
2021-03-24 12:47:31 +01:00
|
|
|
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.
|
2021-03-24 12:59:07 +01:00
|
|
|
wakeLock.acquire(60000)
|
2021-03-24 12:47:31 +01:00
|
|
|
val pos = cachedPosition
|
|
|
|
Timber.i("Ending position %d of %d", pos, duration)
|
2021-03-24 12:59:07 +01:00
|
|
|
if (!isPartial || downloadFile.isWorkDone && abs(duration - pos) < 1000) {
|
2021-03-24 12:47:31 +01:00
|
|
|
setPlayerState(PlayerState.COMPLETED)
|
2021-03-24 15:04:25 +01:00
|
|
|
if (Util.getGaplessPlaybackPreference(context) &&
|
|
|
|
nextPlaying != null &&
|
|
|
|
nextPlayerState === PlayerState.PREPARED
|
|
|
|
) {
|
2021-03-24 12:47:31 +01:00
|
|
|
if (nextSetup) {
|
|
|
|
nextSetup = false
|
|
|
|
}
|
|
|
|
playNext()
|
|
|
|
} else {
|
|
|
|
if (onSongCompleted != null) {
|
|
|
|
val mainHandler = Handler(context.mainLooper)
|
2021-04-22 11:47:15 +02:00
|
|
|
val myRunnable = Runnable { onSongCompleted!!(currentPlaying) }
|
2021-03-24 12:47:31 +01:00
|
|
|
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)
|
2021-03-24 12:59:07 +01:00
|
|
|
bufferTask!!.start()
|
2021-03-24 12:47:31 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
@Synchronized
|
|
|
|
fun reset() {
|
|
|
|
if (bufferTask != null) {
|
|
|
|
bufferTask!!.cancel()
|
|
|
|
}
|
2021-03-25 17:20:30 +01:00
|
|
|
|
|
|
|
resetMediaPlayer()
|
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
try {
|
|
|
|
setPlayerState(PlayerState.IDLE)
|
2021-03-24 13:28:54 +01:00
|
|
|
mediaPlayer.setOnErrorListener(null)
|
|
|
|
mediaPlayer.setOnCompletionListener(null)
|
2021-03-24 12:47:31 +01:00
|
|
|
} catch (x: Exception) {
|
|
|
|
handleError(x)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-25 17:20:30 +01:00
|
|
|
@Synchronized
|
|
|
|
fun resetMediaPlayer() {
|
|
|
|
try {
|
|
|
|
mediaPlayer.reset()
|
|
|
|
} catch (x: Exception) {
|
|
|
|
Timber.w(x, "MediaPlayer was released but LocalMediaPlayer was not destroyed")
|
|
|
|
|
|
|
|
// Recreate MediaPlayer
|
|
|
|
mediaPlayer = MediaPlayer()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-24 15:04:25 +01:00
|
|
|
private inner class BufferTask(
|
|
|
|
private val downloadFile: DownloadFile,
|
2021-03-25 20:33:19 +01:00
|
|
|
private val position: Int,
|
|
|
|
private val autoStart: Boolean = true
|
2021-03-24 15:04:25 +01:00
|
|
|
) : CancellableTask() {
|
2021-03-24 12:47:31 +01:00
|
|
|
private val expectedFileSize: Long
|
2021-03-24 16:39:34 +01:00
|
|
|
private val partialFile: File = downloadFile.partialFile
|
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
override fun execute() {
|
|
|
|
setPlayerState(PlayerState.DOWNLOADING)
|
|
|
|
while (!bufferComplete() && !isOffline(context)) {
|
|
|
|
Util.sleepQuietly(1000L)
|
|
|
|
if (isCancelled) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2021-03-24 16:39:34 +01:00
|
|
|
|
2021-03-25 20:33:19 +01:00
|
|
|
doPlay(downloadFile, position, autoStart)
|
2021-03-24 12:47:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun bufferComplete(): Boolean {
|
2021-03-24 16:39:34 +01:00
|
|
|
val completeFileAvailable = downloadFile.isWorkDone
|
2021-03-24 12:47:31 +01:00
|
|
|
val size = partialFile.length()
|
2021-03-24 15:04:25 +01:00
|
|
|
|
|
|
|
Timber.i(
|
|
|
|
"Buffering %s (%d/%d, %s)",
|
|
|
|
partialFile, size, expectedFileSize, completeFileAvailable
|
|
|
|
)
|
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
return completeFileAvailable || size >= expectedFileSize
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun toString(): String {
|
|
|
|
return String.format("BufferTask (%s)", downloadFile)
|
|
|
|
}
|
|
|
|
|
|
|
|
init {
|
|
|
|
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.
|
2021-04-10 15:59:33 +02:00
|
|
|
val bitRate = downloadFile.getBitRate()
|
2021-03-24 12:59:07 +01:00
|
|
|
val byteCount = max(100000, bitRate * 1024L / 8L * bufferLength)
|
2021-03-24 12:47:31 +01:00
|
|
|
|
|
|
|
// 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
|
2021-03-24 15:04:25 +01:00
|
|
|
val state = (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED)
|
|
|
|
|
2021-03-24 12:47:31 +01:00
|
|
|
Timber.i("Buffering next %s (%d)", partialFile, partialFile!!.length())
|
2021-03-24 15:04:25 +01:00
|
|
|
|
|
|
|
return completeFileAvailable && state
|
2021-03-24 12:47:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2021-03-24 22:27:15 +01:00
|
|
|
if (playerState === PlayerState.STARTED) {
|
2021-03-24 13:28:54 +01:00
|
|
|
cachedPosition = mediaPlayer.currentPosition
|
2021-03-24 12:47:31 +01:00
|
|
|
}
|
|
|
|
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 {
|
2021-03-24 13:28:54 +01:00
|
|
|
mediaPlayer.reset()
|
2021-03-24 12:47:31 +01:00
|
|
|
} 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()
|
|
|
|
}
|
2021-03-24 14:10:27 +01:00
|
|
|
|
|
|
|
private fun postRunnable(runnable: Runnable?) {
|
|
|
|
if (runnable != null) {
|
|
|
|
val mainHandler = Handler(context.mainLooper)
|
|
|
|
val myRunnable = Runnable { runnable.run() }
|
|
|
|
mainHandler.post(myRunnable)
|
|
|
|
}
|
|
|
|
}
|
2021-03-24 15:04:25 +01:00
|
|
|
}
|