Skip to content

Instantly share code, notes, and snippets.

@jemshit
Last active March 16, 2022 15:48
Show Gist options
  • Select an option

  • Save jemshit/3fd38d3ba04556c99bd7d953f5c056f7 to your computer and use it in GitHub Desktop.

Select an option

Save jemshit/3fd38d3ba04556c99bd7d953f5c056f7 to your computer and use it in GitHub Desktop.

Revisions

  1. jemshit revised this gist Aug 15, 2019. 2 changed files with 52 additions and 4 deletions.
    26 changes: 25 additions & 1 deletion MediaPlayerStateMachine.kt
    Original file line number Diff line number Diff line change
    @@ -38,6 +38,12 @@ sealed class 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) {

    @@ -206,7 +212,25 @@ class MediaPlayerStateMachine(private val logger: (String, String) -> Unit = ::d

    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
    }
    30 changes: 27 additions & 3 deletions MediaPlayerWrapper.kt
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,6 @@
    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
    @@ -29,12 +30,13 @@ class MediaPlayerWrapper(private val useLifecycleObserver: Boolean,
    private val logger: (String, String) -> Unit = ::defaultMPWrapperLogger,
    private val logTag: String = MP_WRAPPER_LOG_TAG
    ) : LifecycleObserver,
    KoinComponent,
    MediaPlayer.OnPreparedListener,
    MediaPlayer.OnErrorListener,
    MediaPlayer.OnCompletionListener {

    val mediaPlayer: MediaPlayer = MediaPlayer()
    private val stateMachine = MediaPlayerStateMachine()
    private val mediaPlayer: MediaPlayer = MediaPlayer()
    private val stateMachine = MediaPlayerStateMachine(logger = ::emptyMPStateMachineLogger, logTag = "AUTOPILOT MPStateMachine")
    fun getState(): MediaPlayerState = stateMachine.state
    private var lifecycleStopped = false

    @@ -228,6 +230,26 @@ class MediaPlayerWrapper(private val useLifecycleObserver: Boolean,
    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() {
    @@ -245,11 +267,13 @@ class MediaPlayerWrapper(private val useLifecycleObserver: Boolean,
    }

    /*suspend fun seekToAndPlay(msec: Long,
    @SuppressLint("InlinedApi") mode: Int = SEEK_CLOSEST_SYNC
    @SuppressLint("InlinedApi") mode: Int = SEEK_CLOSEST_SYNC,
    volume: Float
    ) = withContext(dispatcherProvider.computation()) {
    fun _seekToAndPlay() {
    seekTo(msec, mode)
    setVolume(volume, volume)
    start()
    }
  2. jemshit revised this gist Aug 8, 2019. 1 changed file with 1 addition and 4 deletions.
    5 changes: 1 addition & 4 deletions MediaPlayerStateMachine.kt
    Original file line number Diff line number Diff line change
    @@ -79,9 +79,6 @@ class MediaPlayerStateMachine(private val logger: (String, String) -> Unit = ::d
    is MediaPlayerState.End -> {
    // NoOp after End
    }
    is MediaPlayerState.Error -> {
    // NoOp after Error
    }
    is MediaPlayerState.Initialized -> {
    when (event) {
    is MediaPlayerEvent.OnPrepare -> {
    @@ -211,6 +208,6 @@ class MediaPlayerStateMachine(private val logger: (String, String) -> Unit = ::d
    }

    private fun isFinalState(): Boolean {
    return state is MediaPlayerState.End || state is MediaPlayerState.Error
    return state is MediaPlayerState.End
    }
    }
  3. jemshit revised this gist Aug 8, 2019. 1 changed file with 7 additions and 1 deletion.
    8 changes: 7 additions & 1 deletion MediaPlayerWrapper.kt
    Original file line number Diff line number Diff line change
    @@ -45,6 +45,12 @@ class MediaPlayerWrapper(private val useLifecycleObserver: Boolean,
    }

    // 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")
    @@ -56,7 +62,7 @@ class MediaPlayerWrapper(private val useLifecycleObserver: Boolean,
    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestroy() {
    logger(logTag, "onDestroy -> useLifecycleObserver:$useLifecycleObserver")
    lifecycleStopped = false
    lifecycleStopped = true
    if (useLifecycleObserver)
    release()
    }
  4. jemshit revised this gist Aug 8, 2019. 1 changed file with 13 additions and 7 deletions.
    20 changes: 13 additions & 7 deletions MediaPlayerStateMachine.kt
    Original file line number Diff line number Diff line change
    @@ -52,18 +52,20 @@ class MediaPlayerStateMachine(private val logger: (String, String) -> Unit = ::d

    if (event is MediaPlayerEvent.OnRelease) {
    // if old sate is end or error, it can not transition to anything
    transitionSuccess = state !is MediaPlayerState.End && state !is MediaPlayerState.Error
    state = MediaPlayerState.End
    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 = state !is MediaPlayerState.End && state !is MediaPlayerState.Error
    state = MediaPlayerState.Error
    transitionSuccess = !isFinalState()
    if (transitionSuccess)
    state = MediaPlayerState.Error

    } else if (event is MediaPlayerEvent.OnReset) {
    state = MediaPlayerState.Idle
    transitionSuccess = true

    transitionSuccess = !isFinalState()
    if (transitionSuccess)
    state = MediaPlayerState.Idle
    } else {
    when (state) {
    is MediaPlayerState.Idle -> {
    @@ -207,4 +209,8 @@ class MediaPlayerStateMachine(private val logger: (String, String) -> Unit = ::d

    return transitionSuccess
    }

    private fun isFinalState(): Boolean {
    return state is MediaPlayerState.End || state is MediaPlayerState.Error
    }
    }
  5. jemshit revised this gist Jul 25, 2019. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion MediaPlayerWrapper.kt
    Original file line number Diff line number Diff line change
    @@ -29,7 +29,6 @@ class MediaPlayerWrapper(private val useLifecycleObserver: Boolean,
    private val logger: (String, String) -> Unit = ::defaultMPWrapperLogger,
    private val logTag: String = MP_WRAPPER_LOG_TAG
    ) : LifecycleObserver,
    KoinComponent,
    MediaPlayer.OnPreparedListener,
    MediaPlayer.OnErrorListener,
    MediaPlayer.OnCompletionListener {
  6. jemshit revised this gist Jul 25, 2019. 2 changed files with 146 additions and 32 deletions.
    78 changes: 49 additions & 29 deletions MediaPlayerStateMachine.kt
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,15 @@
    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()
    @@ -26,35 +38,39 @@ sealed class MediaPlayerEvent {
    data class OnComplete(val looping: Boolean) : MediaPlayerEvent()
    }

    class MediaPlayerStateMachine {
    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) {

    var transitionDone = false
    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 = state !is MediaPlayerState.End && state !is MediaPlayerState.Error
    state = MediaPlayerState.End
    transitionDone = state !is MediaPlayerState.End && state !is MediaPlayerState.Error

    } else if (event is MediaPlayerEvent.OnError) {
    // if old sate is end or error, it can not transition to anything
    transitionSuccess = state !is MediaPlayerState.End && state !is MediaPlayerState.Error
    state = MediaPlayerState.Error
    transitionDone = state !is MediaPlayerState.End && state !is MediaPlayerState.Error

    } else if (event is MediaPlayerEvent.OnReset) {
    state = MediaPlayerState.Idle
    transitionDone = true
    transitionSuccess = true

    } else {
    when (state) {
    is MediaPlayerState.Idle -> {
    when (event) {
    is MediaPlayerEvent.OnSetDataSource -> {
    state = MediaPlayerState.Initialized
    transitionDone = true
    transitionSuccess = true
    }
    }
    }
    @@ -68,123 +84,127 @@ class MediaPlayerStateMachine {
    when (event) {
    is MediaPlayerEvent.OnPrepare -> {
    state = MediaPlayerState.Prepared
    transitionDone = true
    transitionSuccess = true
    }
    is MediaPlayerEvent.OnPrepareAsync -> {
    state = MediaPlayerState.Preparing
    transitionDone = true
    transitionSuccess = true
    }
    }
    }
    is MediaPlayerState.Preparing -> {
    when (event) {
    is MediaPlayerEvent.OnPrepared -> {
    state = MediaPlayerState.Prepared
    transitionDone = true
    transitionSuccess = true
    }
    }
    }
    is MediaPlayerState.Prepared -> {
    when (event) {
    is MediaPlayerEvent.OnSeekTo -> {
    state = MediaPlayerState.Prepared
    transitionDone = true
    transitionSuccess = true
    }
    is MediaPlayerEvent.OnStop -> {
    state = MediaPlayerState.Stopped
    transitionDone = true
    transitionSuccess = true
    }
    is MediaPlayerEvent.OnStart -> {
    state = MediaPlayerState.Started
    transitionDone = true
    transitionSuccess = true
    }
    }
    }
    is MediaPlayerState.Started -> {
    when (event) {
    is MediaPlayerEvent.OnStop -> {
    state = MediaPlayerState.Stopped
    transitionDone = true
    transitionSuccess = true
    }
    is MediaPlayerEvent.OnSeekTo -> {
    state = MediaPlayerState.Started
    transitionDone = true
    transitionSuccess = true
    }
    is MediaPlayerEvent.OnStart -> {
    state = MediaPlayerState.Started
    transitionDone = true
    transitionSuccess = true
    }
    is MediaPlayerEvent.OnComplete -> {
    if (event.looping)
    state = MediaPlayerState.Started
    else
    state = MediaPlayerState.Completed
    transitionDone = true
    transitionSuccess = true
    }
    is MediaPlayerEvent.OnPause -> {
    state = MediaPlayerState.Paused
    transitionDone = true
    transitionSuccess = true
    }
    }
    }
    is MediaPlayerState.Stopped -> {
    when (event) {
    is MediaPlayerEvent.OnPrepare -> {
    state = MediaPlayerState.Prepared
    transitionDone = true
    transitionSuccess = true
    }
    is MediaPlayerEvent.OnPrepareAsync -> {
    state = MediaPlayerState.Preparing
    transitionDone = true
    transitionSuccess = true
    }
    is MediaPlayerEvent.OnStop -> {
    state = MediaPlayerState.Stopped
    transitionDone = true
    transitionSuccess = true
    }
    }
    }
    is MediaPlayerState.Paused -> {
    when (event) {
    is MediaPlayerEvent.OnSeekTo -> {
    state = MediaPlayerState.Paused
    transitionDone = true
    transitionSuccess = true
    }
    is MediaPlayerEvent.OnPause -> {
    state = MediaPlayerState.Paused
    transitionDone = true
    transitionSuccess = true
    }
    is MediaPlayerEvent.OnStart -> {
    state = MediaPlayerState.Started
    transitionDone = true
    transitionSuccess = true
    }
    is MediaPlayerEvent.OnStop -> {
    state = MediaPlayerState.Stopped
    transitionDone = true
    transitionSuccess = true
    }
    }
    }
    is MediaPlayerState.Completed -> {
    when (event) {
    is MediaPlayerEvent.OnSeekTo -> {
    state = MediaPlayerState.Completed
    transitionDone = true
    transitionSuccess = true
    }
    is MediaPlayerEvent.OnStart -> {
    state = MediaPlayerState.Started
    transitionDone = true
    transitionSuccess = true
    }
    is MediaPlayerEvent.OnStop -> {
    state = MediaPlayerState.Stopped
    transitionDone = true
    transitionSuccess = true
    }
    }
    }
    }
    }

    if (transitionDone)
    if (transitionSuccess) {
    afterTransition?.invoke()
    logger(logTag, "transitionSuccess <- toState:${state.javaClass.simpleName}")
    } else {
    logger(logTag, "transitionFailed <-")
    }

    return transitionDone
    return transitionSuccess
    }
    }
    100 changes: 97 additions & 3 deletions MediaPlayerWrapper.kt
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,35 @@
    import android.annotation.SuppressLint
    import android.content.Context
    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 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 {
    @@ -19,13 +48,15 @@ class MediaPlayerWrapper(private val useLifecycleObserver: Boolean,
    // region Lifecycle callbacks
    @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 = false
    if (useLifecycleObserver)
    release()
    @@ -56,7 +87,16 @@ class MediaPlayerWrapper(private val useLifecycleObserver: Boolean,
    onCompleteListeners.remove(listener)

    override fun onPrepared(mp: MediaPlayer?) {
    stateMachine.transition(MediaPlayerEvent.OnPrepared) {
    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)
    @@ -65,13 +105,15 @@ class MediaPlayerWrapper(private val useLifecycleObserver: Boolean,
    }

    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) }
    @@ -82,24 +124,28 @@ class MediaPlayerWrapper(private val useLifecycleObserver: Boolean,
    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]")
    }
    }

    @@ -109,6 +155,7 @@ class MediaPlayerWrapper(private val useLifecycleObserver: Boolean,
    headers: Map<String, String>) {
    stateMachine.transition(MediaPlayerEvent.OnSetDataSource) {
    mediaPlayer.setDataSource(context, uri, headers)
    logger(logTag, "setDataSource <- success, [uri:$uri]")
    }
    }

    @@ -120,24 +167,28 @@ class MediaPlayerWrapper(private val useLifecycleObserver: Boolean,
    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")
    }
    }

    @@ -148,24 +199,28 @@ class MediaPlayerWrapper(private val useLifecycleObserver: Boolean,
    } 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")
    }
    }
    //endregion
    @@ -174,14 +229,53 @@ class MediaPlayerWrapper(private val useLifecycleObserver: Boolean,
    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()
    }
    }

    // Your custom methods depending on your usecases
    /*suspend fun seekToAndPlay(msec: Long,
    @SuppressLint("InlinedApi") mode: Int = SEEK_CLOSEST_SYNC
    ) = withContext(dispatcherProvider.computation()) {
    fun _seekToAndPlay() {
    seekTo(msec, mode)
    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!")
    }
    }
    }*/
    }
  7. jemshit revised this gist Jul 24, 2019. 1 changed file with 187 additions and 0 deletions.
    187 changes: 187 additions & 0 deletions MediaPlayerWrapper.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,187 @@
    class MediaPlayerWrapper(private val useLifecycleObserver: Boolean,
    private val playAfterPrepared: Boolean
    ) : LifecycleObserver,
    MediaPlayer.OnPreparedListener,
    MediaPlayer.OnErrorListener,
    MediaPlayer.OnCompletionListener {

    val mediaPlayer: MediaPlayer = MediaPlayer()
    private val stateMachine = MediaPlayerStateMachine()
    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_STOP)
    fun onStop() {
    lifecycleStopped = true
    if (useLifecycleObserver)
    pause()
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestroy() {
    lifecycleStopped = false
    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?) {
    stateMachine.transition(MediaPlayerEvent.OnPrepared) {
    onPreparedListeners.forEach { it.onPrepared(mp) }

    if (playAfterPrepared && !lifecycleStopped)
    start()
    }
    }

    override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean {
    stateMachine.transition(MediaPlayerEvent.OnError)

    onErrorListeners.forEach { it.onError(mp, what, extra) }
    return true
    }

    override fun onCompletion(mp: MediaPlayer?) {
    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()
    }
    }

    fun release() {
    stateMachine.transition(MediaPlayerEvent.OnRelease) {
    mediaPlayer.release()
    }
    }

    fun setDataSource(path: String) {
    stateMachine.transition(MediaPlayerEvent.OnSetDataSource) {
    mediaPlayer.setDataSource(path)
    }
    }

    fun setDataSource(context: Context, uri: Uri) {
    stateMachine.transition(MediaPlayerEvent.OnSetDataSource) {
    mediaPlayer.setDataSource(context, 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)
    }
    }


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

    fun prepare() {
    stateMachine.transition(MediaPlayerEvent.OnPrepare) {
    mediaPlayer.prepare()
    }
    }

    /*suspend fun prepareSuspended() = withContext(dispatcherProvider.computation()) {
    stateMachine.transition(MediaPlayerEvent.OnPrepare) {
    mediaPlayer.prepare()
    }
    }*/

    fun prepareAsync() {
    stateMachine.transition(MediaPlayerEvent.OnPrepareAsync) {
    mediaPlayer.prepareAsync()
    }
    }

    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())
    }
    }
    }

    fun start() {
    stateMachine.transition(MediaPlayerEvent.OnStart) {
    mediaPlayer.start()
    }
    }

    fun stop() {
    stateMachine.transition(MediaPlayerEvent.OnStop) {
    mediaPlayer.stop()
    }
    }

    fun pause() {
    stateMachine.transition(MediaPlayerEvent.OnPause) {
    mediaPlayer.pause()
    }
    }
    //endregion

    fun play() {
    if (stateMachine.state == MediaPlayerState.Prepared
    || stateMachine.state == MediaPlayerState.Paused
    || stateMachine.state == MediaPlayerState.Completed) {
    start()

    } else if (stateMachine.state == MediaPlayerState.Initialized
    || stateMachine.state == MediaPlayerState.Stopped) {
    prepareAsync()
    }
    }

    // Your custom methods depending on your usecases

    }
  8. jemshit created this gist Jul 24, 2019.
    190 changes: 190 additions & 0 deletions MediaPlayerStateMachine.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,190 @@
    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()
    }

    class MediaPlayerStateMachine {

    var state: MediaPlayerState = MediaPlayerState.Idle

    fun transition(event: MediaPlayerEvent,
    afterTransition: (() -> Unit)? = null
    ): Boolean = synchronized(this) {

    var transitionDone = false

    if (event is MediaPlayerEvent.OnRelease) {
    state = MediaPlayerState.End
    transitionDone = state !is MediaPlayerState.End && state !is MediaPlayerState.Error

    } else if (event is MediaPlayerEvent.OnError) {
    state = MediaPlayerState.Error
    transitionDone = state !is MediaPlayerState.End && state !is MediaPlayerState.Error

    } else if (event is MediaPlayerEvent.OnReset) {
    state = MediaPlayerState.Idle
    transitionDone = true

    } else {
    when (state) {
    is MediaPlayerState.Idle -> {
    when (event) {
    is MediaPlayerEvent.OnSetDataSource -> {
    state = MediaPlayerState.Initialized
    transitionDone = true
    }
    }
    }
    is MediaPlayerState.End -> {
    // NoOp after End
    }
    is MediaPlayerState.Error -> {
    // NoOp after Error
    }
    is MediaPlayerState.Initialized -> {
    when (event) {
    is MediaPlayerEvent.OnPrepare -> {
    state = MediaPlayerState.Prepared
    transitionDone = true
    }
    is MediaPlayerEvent.OnPrepareAsync -> {
    state = MediaPlayerState.Preparing
    transitionDone = true
    }
    }
    }
    is MediaPlayerState.Preparing -> {
    when (event) {
    is MediaPlayerEvent.OnPrepared -> {
    state = MediaPlayerState.Prepared
    transitionDone = true
    }
    }
    }
    is MediaPlayerState.Prepared -> {
    when (event) {
    is MediaPlayerEvent.OnSeekTo -> {
    state = MediaPlayerState.Prepared
    transitionDone = true
    }
    is MediaPlayerEvent.OnStop -> {
    state = MediaPlayerState.Stopped
    transitionDone = true
    }
    is MediaPlayerEvent.OnStart -> {
    state = MediaPlayerState.Started
    transitionDone = true
    }
    }
    }
    is MediaPlayerState.Started -> {
    when (event) {
    is MediaPlayerEvent.OnStop -> {
    state = MediaPlayerState.Stopped
    transitionDone = true
    }
    is MediaPlayerEvent.OnSeekTo -> {
    state = MediaPlayerState.Started
    transitionDone = true
    }
    is MediaPlayerEvent.OnStart -> {
    state = MediaPlayerState.Started
    transitionDone = true
    }
    is MediaPlayerEvent.OnComplete -> {
    if (event.looping)
    state = MediaPlayerState.Started
    else
    state = MediaPlayerState.Completed
    transitionDone = true
    }
    is MediaPlayerEvent.OnPause -> {
    state = MediaPlayerState.Paused
    transitionDone = true
    }
    }
    }
    is MediaPlayerState.Stopped -> {
    when (event) {
    is MediaPlayerEvent.OnPrepare -> {
    state = MediaPlayerState.Prepared
    transitionDone = true
    }
    is MediaPlayerEvent.OnPrepareAsync -> {
    state = MediaPlayerState.Preparing
    transitionDone = true
    }
    is MediaPlayerEvent.OnStop -> {
    state = MediaPlayerState.Stopped
    transitionDone = true
    }
    }
    }
    is MediaPlayerState.Paused -> {
    when (event) {
    is MediaPlayerEvent.OnSeekTo -> {
    state = MediaPlayerState.Paused
    transitionDone = true
    }
    is MediaPlayerEvent.OnPause -> {
    state = MediaPlayerState.Paused
    transitionDone = true
    }
    is MediaPlayerEvent.OnStart -> {
    state = MediaPlayerState.Started
    transitionDone = true
    }
    is MediaPlayerEvent.OnStop -> {
    state = MediaPlayerState.Stopped
    transitionDone = true
    }
    }
    }
    is MediaPlayerState.Completed -> {
    when (event) {
    is MediaPlayerEvent.OnSeekTo -> {
    state = MediaPlayerState.Completed
    transitionDone = true
    }
    is MediaPlayerEvent.OnStart -> {
    state = MediaPlayerState.Started
    transitionDone = true
    }
    is MediaPlayerEvent.OnStop -> {
    state = MediaPlayerState.Stopped
    transitionDone = true
    }
    }
    }
    }
    }

    if (transitionDone)
    afterTransition?.invoke()

    return transitionDone
    }
    }