Automatic conversion of LocalMediaPlayer to Kotlin
This commit is contained in:
parent
f361f584b9
commit
2260cc311f
File diff suppressed because it is too large
Load Diff
|
@ -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<DownloadFile?>? = null
|
||||
@JvmField
|
||||
var onSongCompleted: Consumer<DownloadFile?>? = null
|
||||
@JvmField
|
||||
var onPlayerStateChanged: BiConsumer<PlayerState, DownloadFile?>? = 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<DownloadFile?>() {
|
||||
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()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue