diff --git a/kotlin-audio-example/src/main/java/com/example/kotlin_audio_example/MainActivity.kt b/kotlin-audio-example/src/main/java/com/example/kotlin_audio_example/MainActivity.kt index 7cf74eef..dd6ed2f8 100644 --- a/kotlin-audio-example/src/main/java/com/example/kotlin_audio_example/MainActivity.kt +++ b/kotlin-audio-example/src/main/java/com/example/kotlin_audio_example/MainActivity.kt @@ -114,6 +114,7 @@ class MainActivity : ComponentActivity() { player.play() } }, + onSwitchExoPlayer = { player.switchExoplayer() }, onSeek = { player.seek(it, TimeUnit.MILLISECONDS) } ) } @@ -242,6 +243,7 @@ fun MainScreen( onTopBarAction: () -> Unit = {}, onPlayPause: () -> Unit = {}, onSeek: (Long) -> Unit = {}, + onSwitchExoPlayer: () -> Unit = {}, ) { Surface( modifier = Modifier.fillMaxSize(), @@ -282,6 +284,7 @@ fun MainScreen( onNext = onNext, isPaused = isPaused, onPlayPause = onPlayPause, + onSwitchExoPlayer = onSwitchExoPlayer, modifier = Modifier .fillMaxWidth() .padding(bottom = 60.dp) diff --git a/kotlin-audio-example/src/main/java/com/example/kotlin_audio_example/ui/component/PlayerControls.kt b/kotlin-audio-example/src/main/java/com/example/kotlin_audio_example/ui/component/PlayerControls.kt index 2afb5a46..13d918fb 100644 --- a/kotlin-audio-example/src/main/java/com/example/kotlin_audio_example/ui/component/PlayerControls.kt +++ b/kotlin-audio-example/src/main/java/com/example/kotlin_audio_example/ui/component/PlayerControls.kt @@ -12,6 +12,7 @@ import androidx.compose.material.icons.rounded.FastForward import androidx.compose.material.icons.rounded.FastRewind import androidx.compose.material.icons.rounded.PauseCircle import androidx.compose.material.icons.rounded.PlayCircle +import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable @@ -29,6 +30,7 @@ fun PlayerControls( onNext: () -> Unit = {}, isPaused: Boolean, onPlayPause: () -> Unit = {}, + onSwitchExoPlayer: () -> Unit = {}, ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -85,6 +87,26 @@ fun PlayerControls( ) } } + Row ( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = modifier + ) { + IconButton( + onClick = onSwitchExoPlayer, + modifier = Modifier + .height(48.dp) + .width(48.dp) + ) { + Icon( + Icons.Rounded.Refresh, + contentDescription = "changeExoPlayer", + modifier = Modifier + .height(48.dp) + .width(48.dp) + ) + } + } } @Preview(showBackground = true) diff --git a/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.kt b/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.kt index 3bc4f1cc..c7d0f59e 100644 --- a/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.kt +++ b/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.kt @@ -74,6 +74,7 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource import com.google.android.exoplayer2.upstream.cache.SimpleCache import com.google.android.exoplayer2.util.Util import kotlinx.coroutines.MainScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber import java.util.Locale @@ -87,16 +88,31 @@ abstract class BaseAudioPlayer internal constructor( ) : AudioManager.OnAudioFocusChangeListener { protected val exoPlayer: ExoPlayer + private var cache: SimpleCache? = null private val scope = MainScope() private var playerConfig: PlayerConfig = playerConfig - val notificationManager: NotificationManager + private var currentPlayer = 0 + protected val exoPlayer2: ExoPlayer + val playerListener = PlayerListener() + + fun currentPlayer(): ExoPlayer { + return if(currentPlayer == 0) exoPlayer; + else exoPlayer2; + } + + private fun otherPlayer(): ExoPlayer { + return if(currentPlayer == 1) exoPlayer; + else exoPlayer2; + } + + var notificationManager: NotificationManager open val playerOptions: PlayerOptions = DefaultPlayerOptions() open val currentItem: AudioItem? - get() = exoPlayer.currentMediaItem?.getAudioItemHolder()?.audioItem + get() = currentPlayer().currentMediaItem?.getAudioItemHolder()?.audioItem var playbackError: PlaybackError? = null var playerState: AudioPlayerState = AudioPlayerState.IDLE @@ -116,44 +132,44 @@ abstract class BaseAudioPlayer internal constructor( } var playWhenReady: Boolean - get() = exoPlayer.playWhenReady + get() = currentPlayer().playWhenReady set(value) { - exoPlayer.playWhenReady = value + currentPlayer().playWhenReady = value } val duration: Long get() { - return if (exoPlayer.duration == C.TIME_UNSET) 0 - else exoPlayer.duration + return if (currentPlayer().duration == C.TIME_UNSET) 0 + else currentPlayer().duration } val isCurrentMediaItemLive: Boolean - get() = exoPlayer.isCurrentMediaItemLive + get() = currentPlayer().isCurrentMediaItemLive private var oldPosition = 0L val position: Long get() { - return if (exoPlayer.currentPosition == C.POSITION_UNSET.toLong()) 0 - else exoPlayer.currentPosition + return if (currentPlayer().currentPosition == C.POSITION_UNSET.toLong()) 0 + else currentPlayer().currentPosition } val bufferedPosition: Long get() { - return if (exoPlayer.bufferedPosition == C.POSITION_UNSET.toLong()) 0 - else exoPlayer.bufferedPosition + return if (currentPlayer().bufferedPosition == C.POSITION_UNSET.toLong()) 0 + else currentPlayer().bufferedPosition } var volume: Float - get() = exoPlayer.volume + get() = currentPlayer().volume set(value) { - exoPlayer.volume = value * volumeMultiplier + currentPlayer().volume = value * volumeMultiplier } var playbackSpeed: Float - get() = exoPlayer.playbackParameters.speed + get() = currentPlayer().playbackParameters.speed set(value) { - exoPlayer.setPlaybackSpeed(value) + currentPlayer().setPlaybackSpeed(value) } var automaticallyUpdateNotificationMetadata: Boolean = true @@ -165,7 +181,7 @@ abstract class BaseAudioPlayer internal constructor( } val isPlaying - get() = exoPlayer.isPlaying + get() = currentPlayer().isPlaying private val notificationEventHolder = NotificationEventHolder() private val playerEventHolder = PlayerEventHolder() @@ -233,10 +249,93 @@ abstract class BaseAudioPlayer internal constructor( } .build() + exoPlayer2 = ExoPlayer.Builder(context) + .setHandleAudioBecomingNoisy(playerConfig.handleAudioBecomingNoisy) + .apply { + if (bufferConfig != null) setLoadControl(setupBuffer(bufferConfig)) + } + .build() mediaSession.isActive = true val playerToUse = - if (playerConfig.interceptPlayerActionsTriggeredExternally) createForwardingPlayer() else exoPlayer + if (playerConfig.interceptPlayerActionsTriggeredExternally) createForwardingPlayer() else currentPlayer() + + notificationManager = NotificationManager( + context, + playerToUse, + mediaSession, + mediaSessionConnector, + notificationEventHolder, + playerEventHolder + ) + + currentPlayer().addListener(playerListener) + + scope.launch { + // Whether ExoPlayer should manage audio focus for us automatically + // see https://medium.com/google-exoplayer/easy-audio-focus-with-exoplayer-a2dcbbe4640e + val audioAttributes = AudioAttributes.Builder() + .setUsage(C.USAGE_MEDIA) + .setContentType( + when (playerConfig.audioContentType) { + AudioContentType.MUSIC -> C.AUDIO_CONTENT_TYPE_MUSIC + AudioContentType.SPEECH -> C.AUDIO_CONTENT_TYPE_SPEECH + AudioContentType.SONIFICATION -> C.AUDIO_CONTENT_TYPE_SONIFICATION + AudioContentType.MOVIE -> C.AUDIO_CONTENT_TYPE_MOVIE + AudioContentType.UNKNOWN -> C.AUDIO_CONTENT_TYPE_UNKNOWN + } + ) + .build(); + currentPlayer().setAudioAttributes(audioAttributes, playerConfig.handleAudioFocus); + mediaSessionConnector.setPlayer(playerToUse) + mediaSessionConnector.setMediaMetadataProvider { + notificationManager.getMediaMetadataCompat() + } + } + + playerEventHolder.updateAudioPlayerState(AudioPlayerState.IDLE) + } + + fun playNext(exoplayer: ExoPlayer) { + exoplayer.seekToNextMediaItem() + } + + fun playPrevious(exoplayer: ExoPlayer) { + exoplayer.seekToPreviousMediaItem() + } + + fun playAtIndex(index: Int, exoplayer: ExoPlayer) { + exoplayer.seekTo(index, C.TIME_UNSET) + } + + fun switchExoplayer( + playerOperation: (exoplayer: ExoPlayer) -> Unit = ::playNext, + fadeDuration: Long = 1500, + fadeInterval: Long = 20, + fadeToVolume: Float = 1f + ){ + currentPlayer += 1; + if (currentPlayer > 1) { + currentPlayer = 0; + } + scope.launch { + val fadeOutPlayer = otherPlayer(); + fadeOutPlayer.removeListener(playerListener) + var fadeOutDuration = fadeDuration + val volumeDiff = -fadeOutPlayer.volume * fadeInterval / fadeOutDuration; + while (fadeOutDuration > 0) { + fadeOutDuration -= fadeInterval + fadeOutPlayer.volume += volumeDiff + delay(fadeInterval) + } + fadeOutPlayer.volume = 0f + fadeOutPlayer.playWhenReady = false + playerOperation(fadeOutPlayer) + fadeOutPlayer.pause() + } + val playerObject = currentPlayer() + val playerToUse = + if (playerConfig.interceptPlayerActionsTriggeredExternally) createForwardingPlayer() else playerObject notificationManager = NotificationManager( context, @@ -247,7 +346,7 @@ abstract class BaseAudioPlayer internal constructor( playerEventHolder ) - exoPlayer.addListener(PlayerListener()) + playerObject.addListener(playerListener) scope.launch { // Whether ExoPlayer should manage audio focus for us automatically @@ -264,18 +363,29 @@ abstract class BaseAudioPlayer internal constructor( } ) .build(); - exoPlayer.setAudioAttributes(audioAttributes, playerConfig.handleAudioFocus); + playerObject.setAudioAttributes(audioAttributes, false); mediaSessionConnector.setPlayer(playerToUse) mediaSessionConnector.setMediaMetadataProvider { notificationManager.getMediaMetadataCompat() } + playerObject.playWhenReady = true + playerObject.seekToNextMediaItem() + playerObject.prepare() + playerObject.volume = 0f + var fadeInDuration = fadeDuration + val volumeDiff = fadeToVolume * fadeInterval / fadeInDuration; + while (fadeInDuration > 0) { + fadeInDuration -= fadeInterval + playerObject.volume += volumeDiff + delay(fadeInterval) + } } playerEventHolder.updateAudioPlayerState(AudioPlayerState.IDLE) } private fun createForwardingPlayer(): ForwardingPlayer { - return object : ForwardingPlayer(exoPlayer) { + return object : ForwardingPlayer(currentPlayer()) { override fun play() { playerEventHolder.updateOnPlayerActionTriggeredExternally(MediaSessionCallback.PLAY) } @@ -354,7 +464,7 @@ abstract class BaseAudioPlayer internal constructor( * @param playWhenReady Whether playback starts automatically. */ open fun load(item: AudioItem, playWhenReady: Boolean = true) { - exoPlayer.playWhenReady = playWhenReady + currentPlayer().playWhenReady = playWhenReady load(item) } @@ -364,12 +474,12 @@ abstract class BaseAudioPlayer internal constructor( */ open fun load(item: AudioItem) { val mediaSource = getMediaSourceFromAudioItem(item) - exoPlayer.addMediaSource(mediaSource) - exoPlayer.prepare() + currentPlayer().addMediaSource(mediaSource) + currentPlayer().prepare() } fun togglePlaying() { - if (exoPlayer.isPlaying) { + if (currentPlayer().isPlaying) { pause() } else { play() @@ -377,26 +487,26 @@ abstract class BaseAudioPlayer internal constructor( } var skipSilence: Boolean - get() = exoPlayer.skipSilenceEnabled + get() = currentPlayer().skipSilenceEnabled set(value) { - exoPlayer.skipSilenceEnabled = value; + currentPlayer().skipSilenceEnabled = value; } fun play() { - exoPlayer.play() + currentPlayer().play() if (currentItem != null) { - exoPlayer.prepare() + currentPlayer().prepare() } } fun prepare() { if (currentItem != null) { - exoPlayer.prepare() + currentPlayer().prepare() } } fun pause() { - exoPlayer.pause() + currentPlayer().pause() } /** @@ -407,20 +517,21 @@ abstract class BaseAudioPlayer internal constructor( @CallSuper open fun stop() { playerState = AudioPlayerState.STOPPED - exoPlayer.playWhenReady = false - exoPlayer.stop() + currentPlayer().playWhenReady = false + currentPlayer().stop() } @CallSuper open fun clear() { exoPlayer.clearMediaItems() + exoPlayer2.clearMediaItems() } /** * Pause playback whenever an item plays to its end. */ fun setPauseAtEndOfItem(pause: Boolean) { - exoPlayer.pauseAtEndOfMediaItems = pause + currentPlayer().pauseAtEndOfMediaItems = pause } /** @@ -432,6 +543,7 @@ abstract class BaseAudioPlayer internal constructor( stop() notificationManager.destroy() exoPlayer.release() + exoPlayer2.release() cache?.release() cache = null mediaSession.isActive = false @@ -439,12 +551,12 @@ abstract class BaseAudioPlayer internal constructor( open fun seek(duration: Long, unit: TimeUnit) { val positionMs = TimeUnit.MILLISECONDS.convert(duration, unit) - exoPlayer.seekTo(positionMs) + currentPlayer().seekTo(positionMs) } open fun seekBy(offset: Long, unit: TimeUnit) { - val positionMs = exoPlayer.currentPosition + TimeUnit.MILLISECONDS.convert(offset, unit) - exoPlayer.seekTo(positionMs) + val positionMs = currentPlayer().currentPosition + TimeUnit.MILLISECONDS.convert(offset, unit) + currentPlayer().seekTo(positionMs) } protected fun getMediaSourceFromAudioItem(audioItem: AudioItem): MediaSource { diff --git a/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/players/QueuedAudioPlayer.kt b/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/players/QueuedAudioPlayer.kt index 78a5b621..c94e67e5 100644 --- a/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/players/QueuedAudioPlayer.kt +++ b/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/players/QueuedAudioPlayer.kt @@ -20,21 +20,21 @@ class QueuedAudioPlayer( override val playerOptions = DefaultQueuedPlayerOptions(exoPlayer) val currentIndex - get() = exoPlayer.currentMediaItemIndex + get() = currentPlayer().currentMediaItemIndex override val currentItem: AudioItem? get() = queue.getOrNull(currentIndex)?.mediaItem?.getAudioItemHolder()?.audioItem val nextIndex: Int? get() { - return if (exoPlayer.nextMediaItemIndex == C.INDEX_UNSET) null - else exoPlayer.nextMediaItemIndex + return if (currentPlayer().nextMediaItemIndex == C.INDEX_UNSET) null + else currentPlayer().nextMediaItemIndex } val previousIndex: Int? get() { - return if (exoPlayer.previousMediaItemIndex == C.INDEX_UNSET) null - else exoPlayer.previousMediaItemIndex + return if (currentPlayer().previousMediaItemIndex == C.INDEX_UNSET) null + else currentPlayer().previousMediaItemIndex } val items: List @@ -44,7 +44,7 @@ class QueuedAudioPlayer( get() { return if (queue.isEmpty()) emptyList() else queue - .subList(0, exoPlayer.currentMediaItemIndex) + .subList(0, currentPlayer().currentMediaItemIndex) .map { it.mediaItem.getAudioItemHolder().audioItem } } @@ -52,7 +52,7 @@ class QueuedAudioPlayer( get() { return if (queue.isEmpty()) emptyList() else queue - .subList(exoPlayer.currentMediaItemIndex, queue.lastIndex) + .subList(currentPlayer().currentMediaItemIndex, queue.lastIndex) .map { it.mediaItem.getAudioItemHolder().audioItem } } @@ -64,7 +64,7 @@ class QueuedAudioPlayer( override fun load(item: AudioItem, playWhenReady: Boolean) { load(item) - exoPlayer.playWhenReady = playWhenReady + currentPlayer().playWhenReady = playWhenReady } override fun load(item: AudioItem) { @@ -75,8 +75,10 @@ class QueuedAudioPlayer( queue[currentIndex] = mediaSource exoPlayer.addMediaSource(currentIndex + 1, mediaSource) exoPlayer.removeMediaItem(currentIndex) - exoPlayer.seekTo(currentIndex, C.TIME_UNSET); - exoPlayer.prepare() + exoPlayer2.addMediaSource(currentIndex + 1, getMediaSourceFromAudioItem(item)) + exoPlayer2.removeMediaItem(currentIndex) + currentPlayer().seekTo(currentIndex, C.TIME_UNSET) + currentPlayer().prepare() } } @@ -85,7 +87,7 @@ class QueuedAudioPlayer( * @param item The [AudioItem] to add. */ fun add(item: AudioItem, playWhenReady: Boolean) { - exoPlayer.playWhenReady = playWhenReady + currentPlayer().playWhenReady = playWhenReady add(item) } @@ -98,7 +100,8 @@ class QueuedAudioPlayer( val mediaSource = getMediaSourceFromAudioItem(item) queue.add(mediaSource) exoPlayer.addMediaSource(mediaSource) - exoPlayer.prepare() + exoPlayer2.addMediaSource(getMediaSourceFromAudioItem(item)) + currentPlayer().prepare() } /** @@ -107,7 +110,7 @@ class QueuedAudioPlayer( * @param playWhenReady Whether playback starts automatically. */ fun add(items: List, playWhenReady: Boolean) { - exoPlayer.playWhenReady = playWhenReady + currentPlayer().playWhenReady = playWhenReady add(items) } @@ -119,7 +122,8 @@ class QueuedAudioPlayer( val mediaSources = items.map { getMediaSourceFromAudioItem(it) } queue.addAll(mediaSources) exoPlayer.addMediaSources(mediaSources) - exoPlayer.prepare() + exoPlayer2.addMediaSources(items.map { getMediaSourceFromAudioItem(it) }) + currentPlayer().prepare() } @@ -132,7 +136,8 @@ class QueuedAudioPlayer( val mediaSources = items.map { getMediaSourceFromAudioItem(it) } queue.addAll(atIndex, mediaSources) exoPlayer.addMediaSources(atIndex, mediaSources) - exoPlayer.prepare() + exoPlayer2.addMediaSources(atIndex, mediaSources) + currentPlayer().prepare() } /** @@ -142,6 +147,7 @@ class QueuedAudioPlayer( fun remove(index: Int) { queue.removeAt(index) exoPlayer.removeMediaItem(index) + exoPlayer2.removeMediaItem(index) } /** @@ -164,7 +170,8 @@ class QueuedAudioPlayer( */ fun next() { exoPlayer.seekToNextMediaItem() - exoPlayer.prepare() + exoPlayer2.seekToNextMediaItem() + currentPlayer().prepare() } /** @@ -173,7 +180,8 @@ class QueuedAudioPlayer( */ fun previous() { exoPlayer.seekToPreviousMediaItem() - exoPlayer.prepare() + exoPlayer2.seekToPreviousMediaItem() + currentPlayer().prepare() } /** @@ -183,6 +191,7 @@ class QueuedAudioPlayer( */ fun move(fromIndex: Int, toIndex: Int) { exoPlayer.moveMediaItem(fromIndex, toIndex) + exoPlayer2.moveMediaItem(fromIndex, toIndex) val item = queue[fromIndex] queue.removeAt(fromIndex) queue.add(max(0, min(items.size, if (toIndex > fromIndex) toIndex else toIndex - 1)), item) @@ -194,7 +203,7 @@ class QueuedAudioPlayer( * @param playWhenReady Whether playback starts automatically. */ fun jumpToItem(index: Int, playWhenReady: Boolean) { - exoPlayer.playWhenReady = playWhenReady + currentPlayer().playWhenReady = playWhenReady jumpToItem(index) } @@ -205,7 +214,8 @@ class QueuedAudioPlayer( fun jumpToItem(index: Int) { try { exoPlayer.seekTo(index, C.TIME_UNSET) - exoPlayer.prepare() + exoPlayer2.seekTo(index, C.TIME_UNSET) + currentPlayer().prepare() } catch (e: IllegalSeekPositionException) { throw Error("This item index $index does not exist. The size of the queue is ${queue.size} items.") } @@ -232,6 +242,7 @@ class QueuedAudioPlayer( val fromIndex = currentIndex + 1 exoPlayer.removeMediaItems(fromIndex, lastIndex) + exoPlayer2.removeMediaItems(fromIndex, lastIndex) queue.subList(fromIndex, lastIndex).clear() } @@ -240,6 +251,7 @@ class QueuedAudioPlayer( */ fun removePreviousItems() { exoPlayer.removeMediaItems(0, currentIndex) + exoPlayer2.removeMediaItems(0, currentIndex) queue.subList(0, currentIndex).clear() }