new: add playback state handling across platforms and integrate main play action in PlayerScreen
This commit is contained in:
@@ -48,6 +48,9 @@ kotlin {
|
||||
implementation(libs.vorbisspi)
|
||||
}
|
||||
commonMain.dependencies {
|
||||
// Coroutines
|
||||
implementation(libs.kotlinx.coroutinesCore)
|
||||
|
||||
// DI
|
||||
implementation(project.dependencies.platform(libs.koin.bom))
|
||||
implementation(libs.koin.core)
|
||||
|
||||
+27
@@ -2,13 +2,40 @@ package fr.ajaury.gwenedeg.player
|
||||
|
||||
import android.content.Context
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class AndroidAudioPlayer(
|
||||
context: Context,
|
||||
) : AudioPlayer {
|
||||
private val player = ExoPlayer.Builder(context).build()
|
||||
|
||||
private val _playbackState = MutableStateFlow(PlaybackState.IDLE)
|
||||
override val playbackState: StateFlow<PlaybackState> = _playbackState.asStateFlow()
|
||||
|
||||
init {
|
||||
player.addListener(
|
||||
object : Player.Listener {
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
if (playbackState == Player.STATE_ENDED) {
|
||||
_playbackState.value = PlaybackState.ENDED
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
_playbackState.value = when {
|
||||
isPlaying -> PlaybackState.PLAYING
|
||||
player.playbackState == Player.STATE_ENDED -> PlaybackState.ENDED
|
||||
else -> PlaybackState.PAUSED
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun load(uri: String) {
|
||||
val mediaItem = MediaItem.fromUri(uri)
|
||||
player.setMediaItem(mediaItem)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package fr.ajaury.gwenedeg.player
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface AudioPlayer {
|
||||
val playbackState: StateFlow<PlaybackState>
|
||||
|
||||
fun play()
|
||||
|
||||
fun pause()
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package fr.ajaury.gwenedeg.player
|
||||
|
||||
enum class PlaybackState {
|
||||
IDLE,
|
||||
PLAYING,
|
||||
PAUSED,
|
||||
ENDED,
|
||||
}
|
||||
@@ -1,28 +1,52 @@
|
||||
package fr.ajaury.gwenedeg.player
|
||||
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import platform.AVFoundation.AVPlayer
|
||||
import platform.AVFoundation.AVPlayerItem
|
||||
import platform.AVFoundation.AVPlayerItemDidPlayToEndTimeNotification
|
||||
import platform.AVFoundation.pause
|
||||
import platform.AVFoundation.play
|
||||
import platform.AVFoundation.replaceCurrentItemWithPlayerItem
|
||||
import platform.Foundation.NSNotificationCenter
|
||||
import platform.Foundation.NSOperationQueue
|
||||
import platform.Foundation.NSURL
|
||||
import platform.darwin.NSObjectProtocol
|
||||
|
||||
class IosAudioPlayer : AudioPlayer {
|
||||
private var player: AVPlayer? = null
|
||||
private var endObserver: NSObjectProtocol? = null
|
||||
|
||||
private val _playbackState = MutableStateFlow(PlaybackState.IDLE)
|
||||
override val playbackState: StateFlow<PlaybackState> = _playbackState.asStateFlow()
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
override fun load(uri: String) {
|
||||
val nsUrl = NSURL.URLWithString(uri) ?: error("Invalid audio URI: $uri")
|
||||
release()
|
||||
|
||||
player = AVPlayer(nsUrl)
|
||||
val nsUrl = NSURL.URLWithString(uri) ?: error("Invalid audio URI: $uri")
|
||||
val item = AVPlayerItem(uRL = nsUrl)
|
||||
player = AVPlayer(playerItem = item)
|
||||
|
||||
endObserver = NSNotificationCenter.defaultCenter.addObserverForName(
|
||||
name = AVPlayerItemDidPlayToEndTimeNotification,
|
||||
`object` = item,
|
||||
queue = NSOperationQueue.mainQueue,
|
||||
) {
|
||||
_playbackState.value = PlaybackState.ENDED
|
||||
}
|
||||
}
|
||||
|
||||
override fun play() {
|
||||
player?.play()
|
||||
_playbackState.value = PlaybackState.PLAYING
|
||||
}
|
||||
|
||||
override fun pause() {
|
||||
player?.pause()
|
||||
_playbackState.value = PlaybackState.PAUSED
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
@@ -31,6 +55,8 @@ class IosAudioPlayer : AudioPlayer {
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
endObserver?.let { NSNotificationCenter.defaultCenter.removeObserver(it) }
|
||||
endObserver = null
|
||||
player?.pause()
|
||||
player = null
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package fr.ajaury.gwenedeg.player
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import java.io.BufferedInputStream
|
||||
import java.net.URI
|
||||
import javax.sound.sampled.AudioFormat
|
||||
import javax.sound.sampled.AudioSystem
|
||||
import javax.sound.sampled.Clip
|
||||
import javax.sound.sampled.LineEvent
|
||||
|
||||
/**
|
||||
* Desktop [AudioPlayer] backed by [javax.sound.sampled].
|
||||
@@ -16,6 +20,11 @@ import javax.sound.sampled.Clip
|
||||
class JvmAudioPlayer : AudioPlayer {
|
||||
private var clip: Clip? = null
|
||||
|
||||
private var stoppedByUser = false
|
||||
|
||||
private val _playbackState = MutableStateFlow(PlaybackState.IDLE)
|
||||
override val playbackState: StateFlow<PlaybackState> = _playbackState.asStateFlow()
|
||||
|
||||
override fun load(uri: String) {
|
||||
release()
|
||||
|
||||
@@ -33,18 +42,41 @@ class JvmAudioPlayer : AudioPlayer {
|
||||
)
|
||||
|
||||
val decodedInput = AudioSystem.getAudioInputStream(pcmFormat, encodedInput)
|
||||
clip = AudioSystem.getClip().apply { open(decodedInput) }
|
||||
clip = AudioSystem.getClip().apply {
|
||||
addLineListener { event ->
|
||||
// Listener events arrive asynchronously; ignore those from a clip we replaced.
|
||||
if (event.line !== clip) return@addLineListener
|
||||
|
||||
// A clip emits the same `STOP` event whether it was paused/stopped by us or it reached the
|
||||
// end on its own, and the decoded frame length is unreliable for Vorbis. We therefore track
|
||||
// whether the last stop was user-initiated to tell a pause apart from a natural end.
|
||||
when (event.type) {
|
||||
LineEvent.Type.START -> {
|
||||
_playbackState.value = PlaybackState.PLAYING
|
||||
}
|
||||
|
||||
LineEvent.Type.STOP -> {
|
||||
_playbackState.value =
|
||||
if (stoppedByUser) PlaybackState.PAUSED else PlaybackState.ENDED
|
||||
}
|
||||
}
|
||||
}
|
||||
open(decodedInput)
|
||||
}
|
||||
}
|
||||
|
||||
override fun play() {
|
||||
stoppedByUser = false
|
||||
clip?.start()
|
||||
}
|
||||
|
||||
override fun pause() {
|
||||
stoppedByUser = true
|
||||
clip?.stop()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
stoppedByUser = true
|
||||
clip?.apply {
|
||||
stop()
|
||||
framePosition = 0
|
||||
@@ -52,6 +84,7 @@ class JvmAudioPlayer : AudioPlayer {
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
stoppedByUser = true
|
||||
clip?.apply {
|
||||
stop()
|
||||
close()
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
package fr.ajaury.gwenedeg.player
|
||||
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.w3c.dom.HTMLAudioElement
|
||||
|
||||
class WebAudioPlayer : AudioPlayer {
|
||||
private var audio: HTMLAudioElement? = null
|
||||
|
||||
private val _playbackState = MutableStateFlow(PlaybackState.IDLE)
|
||||
override val playbackState: StateFlow<PlaybackState> = _playbackState.asStateFlow()
|
||||
|
||||
override fun load(uri: String) {
|
||||
release()
|
||||
|
||||
audio = (document.createElement("audio") as HTMLAudioElement).apply {
|
||||
src = uri
|
||||
addEventListener("play") { _playbackState.value = PlaybackState.PLAYING }
|
||||
addEventListener("ended") { _playbackState.value = PlaybackState.ENDED }
|
||||
addEventListener("pause") { _playbackState.value = PlaybackState.PAUSED }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user