Skip to content

Instantly share code, notes, and snippets.

@jemshit
Last active March 16, 2022 15:48
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jemshit/3fd38d3ba04556c99bd7d953f5c056f7 to your computer and use it in GitHub Desktop.
Save jemshit/3fd38d3ba04556c99bd7d953f5c056f7 to your computer and use it in GitHub Desktop.
Android MediaPlayer has internal state of its own, but there is no get method for it. State diagram is here: https://developer.android.com/images/mediaplayer_state_diagram.gif. Invocation of method that is not allowed at current state results in exception. To avoid such exception and track states of MediaPlayer, MediaPlayerStateMachine is writte…
import android.util.Log
fun emptyMPStateMachineLogger(tag: String, message: String) {
// NoOp
}
fun defaultMPStateMachineLogger(tag: String, message: String) {
Log.d(tag, message)
}
const val MP_STATE_MACHINE_LOG_TAG = "MediaPlayerStateMachine"
sealed class MediaPlayerState {
object Idle : MediaPlayerState()
object End : MediaPlayerState()
object Error : MediaPlayerState()
object Initialized : MediaPlayerState()
object Preparing : MediaPlayerState()
object Prepared : MediaPlayerState()
object Started : MediaPlayerState()
object Stopped : MediaPlayerState()
object Paused : MediaPlayerState()
object Completed : MediaPlayerState()
}
sealed class MediaPlayerEvent {
object OnRelease : MediaPlayerEvent()
object OnReset : MediaPlayerEvent()
object OnSetDataSource : MediaPlayerEvent()
object OnError : MediaPlayerEvent()
object OnPrepare : MediaPlayerEvent()
object OnPrepareAsync : MediaPlayerEvent()
object OnPrepared : MediaPlayerEvent()
object OnSeekTo : MediaPlayerEvent()
object OnStop : MediaPlayerEvent()
object OnStart : MediaPlayerEvent()
object OnPause : MediaPlayerEvent()
data class OnComplete(val looping: Boolean) : MediaPlayerEvent()
}
sealed class MediaPlayerAction {
object SetLooping : MediaPlayerAction()
object SetVolume : MediaPlayerAction()
object SetAudioAttributes : MediaPlayerAction()
}
class MediaPlayerStateMachine(private val logger: (String, String) -> Unit = ::defaultMPStateMachineLogger,
private val logTag: String = MP_STATE_MACHINE_LOG_TAG) {
var state: MediaPlayerState = MediaPlayerState.Idle
fun transition(event: MediaPlayerEvent,
afterTransition: (() -> Unit)? = null
): Boolean = synchronized(this) {
logger(logTag, "transitionRequest -> event:${event.javaClass.simpleName}, currentState:${state.javaClass.simpleName}")
var transitionSuccess = false
if (event is MediaPlayerEvent.OnRelease) {
// if old sate is end or error, it can not transition to anything
transitionSuccess = !isFinalState()
if (transitionSuccess)
state = MediaPlayerState.End
} else if (event is MediaPlayerEvent.OnError) {
// if old sate is end or error, it can not transition to anything
transitionSuccess = !isFinalState()
if (transitionSuccess)
state = MediaPlayerState.Error
} else if (event is MediaPlayerEvent.OnReset) {
transitionSuccess = !isFinalState()
if (transitionSuccess)
state = MediaPlayerState.Idle
} else {
when (state) {
is MediaPlayerState.Idle -> {
when (event) {
is MediaPlayerEvent.OnSetDataSource -> {
state = MediaPlayerState.Initialized
transitionSuccess = true
}
}
}
is MediaPlayerState.End -> {
// NoOp after End
}
is MediaPlayerState.Initialized -> {
when (event) {
is MediaPlayerEvent.OnPrepare -> {
state = MediaPlayerState.Prepared
transitionSuccess = true
}
is MediaPlayerEvent.OnPrepareAsync -> {
state = MediaPlayerState.Preparing
transitionSuccess = true
}
}
}
is MediaPlayerState.Preparing -> {
when (event) {
is MediaPlayerEvent.OnPrepared -> {
state = MediaPlayerState.Prepared
transitionSuccess = true
}
}
}
is MediaPlayerState.Prepared -> {
when (event) {
is MediaPlayerEvent.OnSeekTo -> {
state = MediaPlayerState.Prepared
transitionSuccess = true
}
is MediaPlayerEvent.OnStop -> {
state = MediaPlayerState.Stopped
transitionSuccess = true
}
is MediaPlayerEvent.OnStart -> {
state = MediaPlayerState.Started
transitionSuccess = true
}
}
}
is MediaPlayerState.Started -> {
when (event) {
is MediaPlayerEvent.OnStop -> {
state = MediaPlayerState.Stopped
transitionSuccess = true
}
is MediaPlayerEvent.OnSeekTo -> {
state = MediaPlayerState.Started
transitionSuccess = true
}
is MediaPlayerEvent.OnStart -> {
state = MediaPlayerState.Started
transitionSuccess = true
}
is MediaPlayerEvent.OnComplete -> {
if (event.looping)
state = MediaPlayerState.Started
else
state = MediaPlayerState.Completed
transitionSuccess = true
}
is MediaPlayerEvent.OnPause -> {
state = MediaPlayerState.Paused
transitionSuccess = true
}
}
}
is MediaPlayerState.Stopped -> {
when (event) {
is MediaPlayerEvent.OnPrepare -> {
state = MediaPlayerState.Prepared
transitionSuccess = true
}
is MediaPlayerEvent.OnPrepareAsync -> {
state = MediaPlayerState.Preparing
transitionSuccess = true
}
is MediaPlayerEvent.OnStop -> {
state = MediaPlayerState.Stopped
transitionSuccess = true
}
}
}
is MediaPlayerState.Paused -> {
when (event) {
is MediaPlayerEvent.OnSeekTo -> {
state = MediaPlayerState.Paused
transitionSuccess = true
}
is MediaPlayerEvent.OnPause -> {
state = MediaPlayerState.Paused
transitionSuccess = true
}
is MediaPlayerEvent.OnStart -> {
state = MediaPlayerState.Started
transitionSuccess = true
}
is MediaPlayerEvent.OnStop -> {
state = MediaPlayerState.Stopped
transitionSuccess = true
}
}
}
is MediaPlayerState.Completed -> {
when (event) {
is MediaPlayerEvent.OnSeekTo -> {
state = MediaPlayerState.Completed
transitionSuccess = true
}
is MediaPlayerEvent.OnStart -> {
state = MediaPlayerState.Started
transitionSuccess = true
}
is MediaPlayerEvent.OnStop -> {
state = MediaPlayerState.Stopped
transitionSuccess = true
}
}
}
}
}
if (transitionSuccess) {
afterTransition?.invoke()
logger(logTag, "transitionSuccess <- toState:${state.javaClass.simpleName}")
} else {
logger(logTag, "transitionFailed <-")
}
return transitionSuccess
}
fun action(action: MediaPlayerAction,
afterAction: () -> Unit
) = synchronized(this) {
val actionAllowed = when (action) {
MediaPlayerAction.SetLooping,
MediaPlayerAction.SetAudioAttributes,
MediaPlayerAction.SetVolume -> {
!isFinalState()
&& state != MediaPlayerState.Idle
&& state != MediaPlayerState.Error
}
}
logger(logTag, "action: $actionAllowed; state:${state.javaClass.simpleName}")
if (actionAllowed)
afterAction.invoke()
}
private fun isFinalState(): Boolean {
return state is MediaPlayerState.End
}
}
import android.annotation.SuppressLint
import android.content.Context
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.media.MediaPlayer.SEEK_CLOSEST_SYNC
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import java.net.HttpCookie
fun emptyMPWrapperLogger(tag: String, message: String) {
// NoOp
}
fun defaultMPWrapperLogger(tag: String, message: String) {
Log.d(tag, message)
}
const val MP_WRAPPER_LOG_TAG = "MediaPlayerWrapper"
class MediaPlayerWrapper(private val useLifecycleObserver: Boolean,
private val playAfterPrepared: Boolean,
private val logger: (String, String) -> Unit = ::defaultMPWrapperLogger,
private val logTag: String = MP_WRAPPER_LOG_TAG
) : LifecycleObserver,
KoinComponent,
MediaPlayer.OnPreparedListener,
MediaPlayer.OnErrorListener,
MediaPlayer.OnCompletionListener {
private val mediaPlayer: MediaPlayer = MediaPlayer()
private val stateMachine = MediaPlayerStateMachine(logger = ::emptyMPStateMachineLogger, logTag = "AUTOPILOT MPStateMachine")
fun getState(): MediaPlayerState = stateMachine.state
private var lifecycleStopped = false
init {
mediaPlayer.setOnPreparedListener(this)
mediaPlayer.setOnErrorListener(this)
mediaPlayer.setOnCompletionListener(this)
}
// region Lifecycle callbacks
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart() {
logger(logTag, "onStart -> useLifecycleObserver:$useLifecycleObserver")
lifecycleStopped = false
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() {
logger(logTag, "onStop -> useLifecycleObserver:$useLifecycleObserver")
lifecycleStopped = true
if (useLifecycleObserver)
pause()
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
logger(logTag, "onDestroy -> useLifecycleObserver:$useLifecycleObserver")
lifecycleStopped = true
if (useLifecycleObserver)
release()
}
//endregion
//region MediaPlayer Callbacks
private val onPreparedListeners = mutableListOf<MediaPlayer.OnPreparedListener>()
private val onErrorListeners = mutableListOf<MediaPlayer.OnErrorListener>()
private val onCompleteListeners = mutableListOf<MediaPlayer.OnCompletionListener>()
fun setOnPreparedListener(listener: MediaPlayer.OnPreparedListener) =
onPreparedListeners.add(listener)
fun removeOnPreparedListener(listener: MediaPlayer.OnPreparedListener) =
onPreparedListeners.remove(listener)
fun setOnErrorListener(listener: MediaPlayer.OnErrorListener) =
onErrorListeners.add(listener)
fun removeOnErrorListener(listener: MediaPlayer.OnErrorListener) =
onErrorListeners.remove(listener)
fun setOnCompletionListener(listener: MediaPlayer.OnCompletionListener) =
onCompleteListeners.add(listener)
fun removeOnCompletionListener(listener: MediaPlayer.OnCompletionListener) =
onCompleteListeners.remove(listener)
override fun onPrepared(mp: MediaPlayer?) {
if (stateMachine.state != MediaPlayerState.Prepared) {
stateMachine.transition(MediaPlayerEvent.OnPrepared) {
logger(logTag, "onPrepared callback <- transition success, playAfterPrepared:$playAfterPrepared, lifecycleStopped:$lifecycleStopped")
onPreparedListeners.forEach { it.onPrepared(mp) }
if (playAfterPrepared && !lifecycleStopped)
start()
}
} else {
onPreparedListeners.forEach { it.onPrepared(mp) }
if (playAfterPrepared && !lifecycleStopped)
start()
}
}
override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean {
logger(logTag, "onError <- callback")
stateMachine.transition(MediaPlayerEvent.OnError)
onErrorListeners.forEach { it.onError(mp, what, extra) }
return true
}
override fun onCompletion(mp: MediaPlayer?) {
logger(logTag, "onCompletion <- callback, isLooping:${mediaPlayer.isLooping}")
stateMachine.transition(MediaPlayerEvent.OnComplete(mediaPlayer.isLooping))
onCompleteListeners.forEach { it.onCompletion(mp) }
}
//endregion
// region Method delegations after StateMachine check
fun reset() {
stateMachine.transition(MediaPlayerEvent.OnReset) {
mediaPlayer.reset()
logger(logTag, "reset <- success")
}
}
fun release() {
stateMachine.transition(MediaPlayerEvent.OnRelease) {
mediaPlayer.release()
logger(logTag, "release <- success")
}
}
fun setDataSource(path: String) {
stateMachine.transition(MediaPlayerEvent.OnSetDataSource) {
mediaPlayer.setDataSource(path)
logger(logTag, "setDataSource <- success")
}
}
fun setDataSource(context: Context, uri: Uri) {
stateMachine.transition(MediaPlayerEvent.OnSetDataSource) {
mediaPlayer.setDataSource(context, uri)
logger(logTag, "setDataSource <- success, [uri:$uri]")
}
}
@RequiresApi(Build.VERSION_CODES.O)
fun setDataSource(context: Context,
uri: Uri,
headers: Map<String, String>) {
stateMachine.transition(MediaPlayerEvent.OnSetDataSource) {
mediaPlayer.setDataSource(context, uri, headers)
logger(logTag, "setDataSource <- success, [uri:$uri]")
}
}
@RequiresApi(Build.VERSION_CODES.O)
fun setDataSource(context: Context,
uri: Uri,
headers: Map<String, String>,
cookies: List<HttpCookie>) {
stateMachine.transition(MediaPlayerEvent.OnSetDataSource) {
mediaPlayer.setDataSource(context, uri, headers, cookies)
logger(logTag, "setDataSource <- success, [uri:$uri]")
}
}
fun prepare() {
stateMachine.transition(MediaPlayerEvent.OnPrepare) {
mediaPlayer.prepare()
logger(logTag, "prepare <- success")
}
}
/*suspend fun prepareSuspended() = withContext(dispatcherProvider.computation()) {
stateMachine.transition(MediaPlayerEvent.OnPrepare) {
mediaPlayer.prepare()
logger(logTag, "prepareSuspended <- success")
}
}*/
fun prepareAsync() {
stateMachine.transition(MediaPlayerEvent.OnPrepareAsync) {
mediaPlayer.prepareAsync()
logger(logTag, "prepareAsync <- success")
}
}
fun seekTo(msec: Long, @SuppressLint("InlinedApi") mode: Int = SEEK_CLOSEST_SYNC) {
stateMachine.transition(MediaPlayerEvent.OnSeekTo) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mediaPlayer.seekTo(msec, mode)
} else {
mediaPlayer.seekTo(msec.toInt())
}
logger(logTag, "seekTo <- success, [msec:$msec, mode:$mode]")
}
}
fun start() {
stateMachine.transition(MediaPlayerEvent.OnStart) {
mediaPlayer.start()
logger(logTag, "start <- success")
}
}
fun stop() {
stateMachine.transition(MediaPlayerEvent.OnStop) {
mediaPlayer.stop()
logger(logTag, "stop <- success")
}
}
fun pause() {
stateMachine.transition(MediaPlayerEvent.OnPause) {
mediaPlayer.pause()
logger(logTag, "pause <- success")
}
}
fun setLooping(looping: Boolean) {
stateMachine.action(MediaPlayerAction.SetLooping) {
mediaPlayer.isLooping = looping
}
}
fun setVolume(left: Float, right: Float) {
stateMachine.action(MediaPlayerAction.SetVolume) {
mediaPlayer.setVolume(left, right)
}
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun setAudioAttributes(attributes: Any) {
stateMachine.action(MediaPlayerAction.SetAudioAttributes) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
mediaPlayer.setAudioAttributes(attributes as AudioAttributes)
}
}
//endregion
fun play() {
if (stateMachine.state == MediaPlayerState.Prepared
|| stateMachine.state == MediaPlayerState.Paused
|| stateMachine.state == MediaPlayerState.Completed) {
logger(logTag, "play -> state:${stateMachine.state.javaClass.simpleName}, start() called")
start()
} else if (stateMachine.state == MediaPlayerState.Initialized
|| stateMachine.state == MediaPlayerState.Stopped) {
logger(logTag, "play -> state:${stateMachine.state.javaClass.simpleName}, prepareAsync() called")
prepareAsync()
}
}
/*suspend fun seekToAndPlay(msec: Long,
@SuppressLint("InlinedApi") mode: Int = SEEK_CLOSEST_SYNC,
volume: Float
) = withContext(dispatcherProvider.computation()) {
fun _seekToAndPlay() {
seekTo(msec, mode)
setVolume(volume, volume)
start()
}
if (stateMachine.state == MediaPlayerState.Prepared
|| stateMachine.state == MediaPlayerState.Paused
|| stateMachine.state == MediaPlayerState.Completed) {
logger(logTag, "seekToAndPlay -> state:${stateMachine.state.javaClass.simpleName}, seekTo(), start() called. [msec:$msec, mode:$mode]")
_seekToAndPlay()
} else if (stateMachine.state == MediaPlayerState.Initialized
|| stateMachine.state == MediaPlayerState.Stopped) {
logger(logTag, "seekToAndPlay -> state:${stateMachine.state.javaClass.simpleName}, prepare() called")
prepare()
if (!lifecycleStopped && isActive) {
logger(logTag, "seekToAndPlay -> state:${stateMachine.state.javaClass.simpleName}, seekTo(), start() called. [msec:$msec, mode:$mode]")
_seekToAndPlay()
}
} else {
if (stateMachine.state == MediaPlayerState.Preparing) {
logger(logTag, "seekToAndPlay -> state:${stateMachine.state.javaClass.simpleName}, DELAYED")
delay(50L)
if (stateMachine.state == MediaPlayerState.Prepared) {
logger(logTag, "seekToAndPlay -> state:${stateMachine.state.javaClass.simpleName}, after delay and prepared, seekTo(), start() called. [msec:$msec, mode:$mode]")
_seekToAndPlay()
} else {
logger(logTag, "seekToAndPlay -> state:${stateMachine.state.javaClass.simpleName}, after delay, still NOT PREPARED!")
}
} else {
logger(logTag, "seekToAndPlay -> state:${stateMachine.state.javaClass.simpleName}, INVALID STATE!")
}
}
}*/
}
@jemshit
Copy link
Author

jemshit commented Jul 24, 2019

You can replace your MediaPlayer with MediaPlayerWrapper and most methods are delegate methods with same name. If you need to access MediaPlayer itself, use MediaPlayerWrapper.mediaPlayer.

It is suggested to create some MyAudioManager class and use MediaPlayerWrapper in it. Other classes should call your MyAudioManager and be abstracted away from MediaPlayer implementation.

MediaPlayerWrapper is LifecycleObserver, don't forget to bind it with lifecycle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment