Skip to content

Commit

Permalink
[#257] Added a callback for media playback states
Browse files Browse the repository at this point in the history
  • Loading branch information
brianwernick committed Aug 3, 2022
1 parent ac5b5c6 commit 5cf138c
Show file tree
Hide file tree
Showing 13 changed files with 354 additions and 157 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import androidx.appcompat.widget.AppCompatImageButton
import android.view.MenuItem
import androidx.appcompat.widget.AppCompatImageButton
import androidx.media3.exoplayer.util.EventLogger
import com.devbrackets.android.exomedia.core.renderer.RendererType
import com.devbrackets.android.exomedia.listener.OnVideoSizeChangedListener
import com.devbrackets.android.exomedia.ui.listener.VideoControlsSeekListener
Expand All @@ -17,15 +18,14 @@ import com.devbrackets.android.exomediademo.R
import com.devbrackets.android.exomediademo.data.MediaItem
import com.devbrackets.android.exomediademo.data.Samples
import com.devbrackets.android.exomediademo.databinding.VideoPlayerActivityBinding
import com.devbrackets.android.exomediademo.playlist.manager.PlaylistManager
import com.devbrackets.android.exomediademo.playlist.VideoApi
import com.devbrackets.android.exomediademo.playlist.manager.PlaylistManager
import com.devbrackets.android.exomediademo.ui.support.BindingActivity
import com.devbrackets.android.exomediademo.ui.support.CaptionPopupManager
import com.devbrackets.android.exomediademo.ui.support.CaptionPopupManager.Companion.CC_DEFAULT
import com.devbrackets.android.exomediademo.ui.support.CaptionPopupManager.Companion.CC_DISABLED
import com.devbrackets.android.exomediademo.ui.support.CaptionPopupManager.Companion.CC_GROUP_INDEX_MOD
import com.devbrackets.android.exomediademo.ui.support.FullscreenManager
import androidx.media3.exoplayer.util.EventLogger

open class VideoPlayerActivity : BindingActivity<VideoPlayerActivityBinding>(), VideoControlsSeekListener {
companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.compose.material.icons.rounded.Videocam
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
Expand Down Expand Up @@ -143,6 +144,7 @@ fun MediaCategoryCard(
Card(
modifier = Modifier
.size(136.dp, 136.dp)
.clip(MaterialTheme.shapes.medium)
.clickable(onClick = onClick)
.align(Alignment.Center),
elevation = 2.dp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,17 @@ import com.devbrackets.android.exomedia.core.ListenerMux
import com.devbrackets.android.exomedia.core.audio.AudioPlayerApi
import com.devbrackets.android.exomedia.core.audio.ExoAudioPlayer
import com.devbrackets.android.exomedia.core.listener.MetadataListener
import com.devbrackets.android.exomedia.core.state.PlaybackState
import com.devbrackets.android.exomedia.core.state.PlaybackStateListener
import com.devbrackets.android.exomedia.listener.*
import com.devbrackets.android.exomedia.nmp.ExoMediaPlayer
import com.devbrackets.android.exomedia.nmp.config.PlayerConfig
import com.devbrackets.android.exomedia.nmp.config.PlayerConfigBuilder

/**
* An AudioPlayer that uses the ExoPlayer as the backing architecture. If the current device
* does *NOT* pass the Android Compatibility Test Suite (CTS) then the backing architecture
* will fall back to using the default Android MediaPlayer.
*
*
* To help with quick conversions from the Android MediaPlayer this class follows the APIs
* the Android MediaPlayer provides.
* An AudioPlayer that uses the ExoPlayer as the backing implementation. If the
* current device does *NOT* support the ExoPlayer then the AudioPlayer will
* fallback to using the OS MediaPlayer.
*/
open class AudioPlayer(protected val audioPlayerImpl: AudioPlayerApi) : AudioPlayerApi by audioPlayerImpl {
companion object {
Expand All @@ -45,7 +43,7 @@ open class AudioPlayer(protected val audioPlayerImpl: AudioPlayerApi) : AudioPla
}

/**
* Retrieves the duration of the current audio item. This should only be called after
* Retrieves the duration of the current audio item. This should only be called after
* the item is prepared (see [.setOnPreparedListener]).
* If [.overrideDuration] is set then that value will be returned.
*
Expand All @@ -57,7 +55,7 @@ open class AudioPlayer(protected val audioPlayerImpl: AudioPlayerApi) : AudioPla
} else audioPlayerImpl.duration

/**
* Retrieves the current buffer percent of the audio item. If an audio item is not currently
* Retrieves the current buffer percent of the audio item. If an audio item is not currently
* prepared or buffering the value will be 0. This should only be called after the audio item is
* prepared (see [.setOnPreparedListener])
*
Expand All @@ -83,7 +81,7 @@ open class AudioPlayer(protected val audioPlayerImpl: AudioPlayerApi) : AudioPla
}

/**
* Setting this will override the duration that the item may actually be. This method should
* Setting this will override the duration that the item may actually be. This method should
* only be used when the item doesn't return the correct duration such as with audio streams.
* This only overrides the current audio item.
*
Expand Down Expand Up @@ -156,6 +154,26 @@ open class AudioPlayer(protected val audioPlayerImpl: AudioPlayerApi) : AudioPla
listenerMux.setAnalyticsListener(listener)
}

/**
* Sets the listener to inform of playback state changes. If only the current value
* is needed then [getPlaybackState] can be used.
*
* @param listener The listener to inform of [PlaybackState] changes
*/
fun setPlaybackStateListener(listener: PlaybackStateListener?) {
listenerMux.setPlaybackStateListener(listener)
}

/**
* Retrieves the current [PlaybackState] of this [AudioPlayer]. Changes to this value
* can also be listened to via the [setPlaybackStateListener].
*
* @return The current [PlaybackState] of this [AudioPlayer]
*/
fun getPlaybackState(): PlaybackState {
return listenerMux.playbackState
}

/**
* Performs the functionality to stop the progress polling, and stop any other
* procedures from running that we no longer need.
Expand All @@ -165,10 +183,6 @@ open class AudioPlayer(protected val audioPlayerImpl: AudioPlayerApi) : AudioPla
}

private inner class MuxNotifier : ListenerMux.Notifier() {
override fun shouldNotifyCompletion(endLeeway: Long): Boolean {
return currentPosition > 0 && duration > 0 && currentPosition + endLeeway >= duration
}

override fun onExoPlayerError(exoMediaPlayer: ExoMediaPlayer, e: Exception?) {
stop()
exoMediaPlayer.forcePrepare()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import android.os.Looper
import androidx.annotation.IntRange
import androidx.media3.common.Metadata
import androidx.media3.common.Player
import androidx.media3.common.Player.State
import androidx.media3.exoplayer.analytics.AnalyticsListener
import com.devbrackets.android.exomedia.core.listener.ExoPlayerListener
import com.devbrackets.android.exomedia.core.listener.MetadataListener
import com.devbrackets.android.exomedia.core.state.PlaybackState
import com.devbrackets.android.exomedia.core.state.PlaybackStateListener
import com.devbrackets.android.exomedia.core.video.surface.SurfaceEnvelope
import com.devbrackets.android.exomedia.fallback.FallbackMediaPlayer
import com.devbrackets.android.exomedia.fallback.exception.NativeMediaPlaybackException
Expand All @@ -31,14 +34,9 @@ class ListenerMux(
MetadataListener,
AnalyticsListener by analyticsDelegate
{

companion object {
//The amount of time the current position can be off the duration to call the onCompletion listener
private const val COMPLETED_DURATION_LEEWAY: Long = 1_000
}

private val delayedHandler = Handler(Looper.getMainLooper())

private var playbackStateListener: PlaybackStateListener? = null
private var preparedListener: OnPreparedListener? = null
private var completionListener: OnCompletionListener? = null
private var bufferUpdateListener: OnBufferUpdateListener? = null
Expand All @@ -58,17 +56,8 @@ class ListenerMux(
private var notifiedCompleted = false
private var clearRequested = false

override fun onStateChange(state: FallbackMediaPlayer.State) {
when (state) {
FallbackMediaPlayer.State.COMPLETED -> completionListener?.onCompletion()
FallbackMediaPlayer.State.READY -> {
if (!isPrepared) {
notifyPreparedListener()
}
}
else -> {}
}
}
var playbackState = PlaybackState.IDLE
private set

override fun onBufferUpdate(mediaPlayer: FallbackMediaPlayer, percent: Int) {
onBufferingUpdate(percent)
Expand All @@ -86,61 +75,76 @@ class ListenerMux(
muxNotifier.onVideoSizeChanged(width, height, 0, 1f)
}

override fun onError(player: ExoMediaPlayer, e: Exception?) {
muxNotifier.onMediaPlaybackEnded()
muxNotifier.onExoPlayerError(player, e)
notifyErrorListener(e)
}
override fun onPlaybackStateChange(state: PlaybackState) {
playbackState = state
playbackStateListener?.onPlaybackStateChange(state)

override fun onStateChanged(playWhenReady: Boolean, playbackState: Int) {
when (playbackState) {
Player.STATE_READY -> {
if (!isPrepared) {
notifyPreparedListener()
}
if (playWhenReady) {
//Updates the previewImage
muxNotifier.onPreviewImageStateChanged(false)
}
}
Player.STATE_IDLE -> {
when (state) {
PlaybackState.IDLE -> {
if (clearRequested) {
//Clears the textureView when requested
clearRequested = false
surfaceEnvelopeRef.get()?.let {
it.clearSurface()
surfaceEnvelopeRef.clear()
}
}
}
Player.STATE_ENDED -> {
PlaybackState.READY -> {
if (!isPrepared) {
notifyPreparedListener()
}
}
PlaybackState.COMPLETED -> {
notifyCompletionListener()
}
PlaybackState.STOPPED, PlaybackState.RELEASED -> {
muxNotifier.onMediaPlaybackEnded()
}
else -> {}
}
}

if (!notifiedCompleted) {
notifyCompletionListener()
/**
* TODO: migrate functionality
* This method has been temporarily retained until the VideoView has been updated to monitor the
* state change itself to dismiss the preview image.
*/
@Deprecated("Use onPlaybackStateChange")
override fun onStateChanged(playWhenReady: Boolean, @State playbackState: Int) {
when (playbackState) {
Player.STATE_READY -> {
if (playWhenReady) {
// Updates the previewImage
muxNotifier.onPreviewImageStateChanged(false)
}
}
else -> {}
}
}

override fun onError(player: ExoMediaPlayer, e: Exception?) {
muxNotifier.onMediaPlaybackEnded()
muxNotifier.onExoPlayerError(player, e)
notifyErrorListener(e)
}

override fun onSeekComplete() {
muxNotifier.onSeekComplete()
seekCompletionListener?.onSeekComplete()
}

override fun onVideoSizeChanged(width: Int, height: Int, unAppliedRotationDegrees: Int, pixelWidthHeightRatio: Float) {
muxNotifier.onVideoSizeChanged(width, height, unAppliedRotationDegrees, pixelWidthHeightRatio)
}

override fun onBufferingUpdate(@IntRange(from = 0, to = 100) percent: Int) {
muxNotifier.onBufferUpdated(percent)
bufferUpdateListener?.onBufferingUpdate(percent)
}

override fun onMetadata(metadata: Metadata) {
metadataListener?.onMetadata(metadata)
}

override fun onVideoSizeChanged(width: Int, height: Int, unAppliedRotationDegrees: Int, pixelWidthHeightRatio: Float) {
muxNotifier.onVideoSizeChanged(width, height, unAppliedRotationDegrees, pixelWidthHeightRatio)
}

/**
* Specifies the surface to clear when the playback reaches an appropriate state.
* Once the [SurfaceEnvelope] is cleared, the reference will be removed
Expand Down Expand Up @@ -215,6 +219,15 @@ class ListenerMux(
analyticsDelegate.listener = listener
}

/**
* Sets the listener to inform of playback state changes
*
* @param listener The listener to inform
*/
fun setPlaybackStateListener(listener: PlaybackStateListener?) {
playbackStateListener = listener
}

/**
* Sets weather the listener was notified when we became prepared.
*
Expand All @@ -236,47 +249,36 @@ class ListenerMux(
}

private fun notifyErrorListener(e: Exception?): Boolean {
return errorListener?.onError(e) == true
return (errorListener?.onError(e) == true).also {
muxNotifier.onMediaPlaybackEnded()
}
}

private fun notifyPreparedListener() {
isPrepared = true

delayedHandler.post {
performPreparedHandlerNotification()
muxNotifier.onPrepared()
preparedListener?.onPrepared()
}
}

private fun performPreparedHandlerNotification() {
muxNotifier.onPrepared()
preparedListener?.onPrepared()
}

private fun notifyCompletionListener() {
if (!muxNotifier.shouldNotifyCompletion(COMPLETED_DURATION_LEEWAY)) {
return
}

notifiedCompleted = true

delayedHandler.post {
completionListener?.onCompletion()
}
completionListener?.onCompletion()
muxNotifier.onMediaPlaybackEnded()
}

abstract class Notifier {
@Deprecated("Implementations should observe the PlaybackState instead")
open fun onSeekComplete() {
//Purposefully left blank
}

fun onBufferUpdated(percent: Int) {
//Purposefully left blank
}

open fun onVideoSizeChanged(width: Int, height: Int, unAppliedRotationDegrees: Int, pixelWidthHeightRatio: Float) {
//Purposefully left blank
}

@Deprecated("Implementations should observe the PlaybackState instead")
open fun onPrepared() {
//Purposefully left blank
}
Expand All @@ -285,8 +287,6 @@ class ListenerMux(
//Purposefully left blank
}

abstract fun shouldNotifyCompletion(endLeeway: Long): Boolean

abstract fun onExoPlayerError(exoMediaPlayer: ExoMediaPlayer, e: Exception?)

abstract fun onMediaPlaybackEnded()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.devbrackets.android.exomedia.core.listener

import androidx.media3.common.Player.State
import com.devbrackets.android.exomedia.core.state.PlaybackStateListener
import com.devbrackets.android.exomedia.listener.OnSeekCompletionListener
import com.devbrackets.android.exomedia.nmp.ExoMediaPlayer

/**
* A listener for core [ExoMediaPlayer] events
*/
interface ExoPlayerListener : OnSeekCompletionListener {
fun onStateChanged(playWhenReady: Boolean, playbackState: Int)
interface ExoPlayerListener : OnSeekCompletionListener, PlaybackStateListener {
@Deprecated("Use onPlaybackStateChange")
fun onStateChanged(playWhenReady: Boolean, @State playbackState: Int)

fun onError(player: ExoMediaPlayer, e: Exception?)

Expand Down
Loading

0 comments on commit 5cf138c

Please sign in to comment.