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)
|
implementation(libs.vorbisspi)
|
||||||
}
|
}
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
|
// Coroutines
|
||||||
|
implementation(libs.kotlinx.coroutinesCore)
|
||||||
|
|
||||||
// DI
|
// DI
|
||||||
implementation(project.dependencies.platform(libs.koin.bom))
|
implementation(project.dependencies.platform(libs.koin.bom))
|
||||||
implementation(libs.koin.core)
|
implementation(libs.koin.core)
|
||||||
|
|||||||
+27
@@ -2,13 +2,40 @@ package fr.ajaury.gwenedeg.player
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
class AndroidAudioPlayer(
|
class AndroidAudioPlayer(
|
||||||
context: Context,
|
context: Context,
|
||||||
) : AudioPlayer {
|
) : AudioPlayer {
|
||||||
private val player = ExoPlayer.Builder(context).build()
|
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) {
|
override fun load(uri: String) {
|
||||||
val mediaItem = MediaItem.fromUri(uri)
|
val mediaItem = MediaItem.fromUri(uri)
|
||||||
player.setMediaItem(mediaItem)
|
player.setMediaItem(mediaItem)
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package fr.ajaury.gwenedeg.player
|
package fr.ajaury.gwenedeg.player
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface AudioPlayer {
|
interface AudioPlayer {
|
||||||
|
val playbackState: StateFlow<PlaybackState>
|
||||||
|
|
||||||
fun play()
|
fun play()
|
||||||
|
|
||||||
fun pause()
|
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
|
package fr.ajaury.gwenedeg.player
|
||||||
|
|
||||||
import kotlinx.cinterop.ExperimentalForeignApi
|
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.AVPlayer
|
||||||
|
import platform.AVFoundation.AVPlayerItem
|
||||||
|
import platform.AVFoundation.AVPlayerItemDidPlayToEndTimeNotification
|
||||||
import platform.AVFoundation.pause
|
import platform.AVFoundation.pause
|
||||||
import platform.AVFoundation.play
|
import platform.AVFoundation.play
|
||||||
import platform.AVFoundation.replaceCurrentItemWithPlayerItem
|
import platform.AVFoundation.replaceCurrentItemWithPlayerItem
|
||||||
|
import platform.Foundation.NSNotificationCenter
|
||||||
|
import platform.Foundation.NSOperationQueue
|
||||||
import platform.Foundation.NSURL
|
import platform.Foundation.NSURL
|
||||||
|
import platform.darwin.NSObjectProtocol
|
||||||
|
|
||||||
class IosAudioPlayer : AudioPlayer {
|
class IosAudioPlayer : AudioPlayer {
|
||||||
private var player: AVPlayer? = null
|
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)
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
override fun load(uri: String) {
|
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() {
|
override fun play() {
|
||||||
player?.play()
|
player?.play()
|
||||||
|
_playbackState.value = PlaybackState.PLAYING
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pause() {
|
override fun pause() {
|
||||||
player?.pause()
|
player?.pause()
|
||||||
|
_playbackState.value = PlaybackState.PAUSED
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
@@ -31,6 +55,8 @@ class IosAudioPlayer : AudioPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun release() {
|
override fun release() {
|
||||||
|
endObserver?.let { NSNotificationCenter.defaultCenter.removeObserver(it) }
|
||||||
|
endObserver = null
|
||||||
player?.pause()
|
player?.pause()
|
||||||
player = null
|
player = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
package fr.ajaury.gwenedeg.player
|
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.io.BufferedInputStream
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import javax.sound.sampled.AudioFormat
|
import javax.sound.sampled.AudioFormat
|
||||||
import javax.sound.sampled.AudioSystem
|
import javax.sound.sampled.AudioSystem
|
||||||
import javax.sound.sampled.Clip
|
import javax.sound.sampled.Clip
|
||||||
|
import javax.sound.sampled.LineEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Desktop [AudioPlayer] backed by [javax.sound.sampled].
|
* Desktop [AudioPlayer] backed by [javax.sound.sampled].
|
||||||
@@ -16,6 +20,11 @@ import javax.sound.sampled.Clip
|
|||||||
class JvmAudioPlayer : AudioPlayer {
|
class JvmAudioPlayer : AudioPlayer {
|
||||||
private var clip: Clip? = null
|
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) {
|
override fun load(uri: String) {
|
||||||
release()
|
release()
|
||||||
|
|
||||||
@@ -33,18 +42,41 @@ class JvmAudioPlayer : AudioPlayer {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val decodedInput = AudioSystem.getAudioInputStream(pcmFormat, encodedInput)
|
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() {
|
override fun play() {
|
||||||
|
stoppedByUser = false
|
||||||
clip?.start()
|
clip?.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pause() {
|
override fun pause() {
|
||||||
|
stoppedByUser = true
|
||||||
clip?.stop()
|
clip?.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
|
stoppedByUser = true
|
||||||
clip?.apply {
|
clip?.apply {
|
||||||
stop()
|
stop()
|
||||||
framePosition = 0
|
framePosition = 0
|
||||||
@@ -52,6 +84,7 @@ class JvmAudioPlayer : AudioPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun release() {
|
override fun release() {
|
||||||
|
stoppedByUser = true
|
||||||
clip?.apply {
|
clip?.apply {
|
||||||
stop()
|
stop()
|
||||||
close()
|
close()
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
package fr.ajaury.gwenedeg.player
|
package fr.ajaury.gwenedeg.player
|
||||||
|
|
||||||
import kotlinx.browser.document
|
import kotlinx.browser.document
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import org.w3c.dom.HTMLAudioElement
|
import org.w3c.dom.HTMLAudioElement
|
||||||
|
|
||||||
class WebAudioPlayer : AudioPlayer {
|
class WebAudioPlayer : AudioPlayer {
|
||||||
private var audio: HTMLAudioElement? = null
|
private var audio: HTMLAudioElement? = null
|
||||||
|
|
||||||
|
private val _playbackState = MutableStateFlow(PlaybackState.IDLE)
|
||||||
|
override val playbackState: StateFlow<PlaybackState> = _playbackState.asStateFlow()
|
||||||
|
|
||||||
override fun load(uri: String) {
|
override fun load(uri: String) {
|
||||||
|
release()
|
||||||
|
|
||||||
audio = (document.createElement("audio") as HTMLAudioElement).apply {
|
audio = (document.createElement("audio") as HTMLAudioElement).apply {
|
||||||
src = uri
|
src = uri
|
||||||
|
addEventListener("play") { _playbackState.value = PlaybackState.PLAYING }
|
||||||
|
addEventListener("ended") { _playbackState.value = PlaybackState.ENDED }
|
||||||
|
addEventListener("pause") { _playbackState.value = PlaybackState.PAUSED }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ koin-core = { module = "io.insert-koin:koin-core" }
|
|||||||
koin-core-viewmodel = { module = "io.insert-koin:koin-core-viewmodel" }
|
koin-core-viewmodel = { module = "io.insert-koin:koin-core-viewmodel" }
|
||||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||||
kotlinx-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version.ref = "kotlinxBrowser" }
|
kotlinx-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version.ref = "kotlinxBrowser" }
|
||||||
|
kotlinx-coroutinesCore = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
|
||||||
kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutines" }
|
kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutines" }
|
||||||
lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
|
lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
|
||||||
lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
|
lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
|
||||||
|
|||||||
@@ -1,26 +1,35 @@
|
|||||||
package fr.ajaury.gwenedeg.player.ui
|
package fr.ajaury.gwenedeg.player.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import fr.ajaury.gwenedeg.player.PlaybackState
|
||||||
|
import fr.ajaury.gwenedeg.player.ui.viewmodel.PlayerUiState
|
||||||
import fr.ajaury.gwenedeg.player.ui.viewmodel.PlayerViewModel
|
import fr.ajaury.gwenedeg.player.ui.viewmodel.PlayerViewModel
|
||||||
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
import fr.ajaury.gwenedeg.theme.GwenedegTheme
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalResourceApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PlayerScreen(
|
fun PlayerScreen(
|
||||||
recordTitle: String,
|
recordTitle: String,
|
||||||
viewModel: PlayerViewModel = koinViewModel(),
|
viewModel: PlayerViewModel = koinViewModel(),
|
||||||
) {
|
) {
|
||||||
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
viewModel.play(filePath = "files/demat.ogg")
|
viewModel.play(filePath = "files/demat.ogg")
|
||||||
onDispose {
|
onDispose {
|
||||||
@@ -28,6 +37,19 @@ fun PlayerScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PlayerScreen(
|
||||||
|
recordTitle = recordTitle,
|
||||||
|
uiState = uiState,
|
||||||
|
onMainPlayActionButtonClicked = viewModel::performMainPlayAction,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PlayerScreen(
|
||||||
|
recordTitle: String,
|
||||||
|
uiState: PlayerUiState,
|
||||||
|
onMainPlayActionButtonClicked: () -> Unit,
|
||||||
|
) {
|
||||||
Scaffold { innerPadding ->
|
Scaffold { innerPadding ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -35,12 +57,62 @@ fun PlayerScreen(
|
|||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Playing: $recordTitle",
|
text = "Playing: $recordTitle",
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Button(onClick = onMainPlayActionButtonClicked) {
|
||||||
|
Text(text = uiState.playbackState.toActionLabel())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PlaybackState.toActionLabel(): String =
|
||||||
|
when (this) {
|
||||||
|
PlaybackState.PLAYING -> "Pause"
|
||||||
|
PlaybackState.ENDED -> "Replay"
|
||||||
|
PlaybackState.PAUSED, PlaybackState.IDLE -> "Play"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun PlayerScreenPlayingPreview() {
|
||||||
|
GwenedegTheme {
|
||||||
|
PlayerScreen(
|
||||||
|
recordTitle = "Demat",
|
||||||
|
uiState = PlayerUiState(playbackState = PlaybackState.PLAYING),
|
||||||
|
onMainPlayActionButtonClicked = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun PlayerScreenPausedPreview() {
|
||||||
|
GwenedegTheme {
|
||||||
|
PlayerScreen(
|
||||||
|
recordTitle = "Demat",
|
||||||
|
uiState = PlayerUiState(playbackState = PlaybackState.PAUSED),
|
||||||
|
onMainPlayActionButtonClicked = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun PlayerScreenEndedPreview() {
|
||||||
|
GwenedegTheme {
|
||||||
|
PlayerScreen(
|
||||||
|
recordTitle = "Demat",
|
||||||
|
uiState = PlayerUiState(playbackState = PlaybackState.ENDED),
|
||||||
|
onMainPlayActionButtonClicked = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package fr.ajaury.gwenedeg.player.ui.viewmodel
|
||||||
|
|
||||||
|
import fr.ajaury.gwenedeg.player.PlaybackState
|
||||||
|
|
||||||
|
data class PlayerUiState(
|
||||||
|
val playbackState: PlaybackState = PlaybackState.IDLE,
|
||||||
|
)
|
||||||
+25
-2
@@ -1,25 +1,48 @@
|
|||||||
package fr.ajaury.gwenedeg.player.ui.viewmodel
|
package fr.ajaury.gwenedeg.player.ui.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import fr.ajaury.gwenedeg.player.AudioPlayer
|
import fr.ajaury.gwenedeg.player.AudioPlayer
|
||||||
import fr.ajaury.gwenedeg.player.AudioSessionManager
|
import fr.ajaury.gwenedeg.player.AudioSessionManager
|
||||||
|
import fr.ajaury.gwenedeg.player.PlaybackState
|
||||||
import gwenedeg.shared.generated.resources.Res
|
import gwenedeg.shared.generated.resources.Res
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.WhileSubscribed
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class PlayerViewModel(
|
class PlayerViewModel(
|
||||||
private val audioPlayer: AudioPlayer,
|
private val audioPlayer: AudioPlayer,
|
||||||
private val audioSessionManager: AudioSessionManager,
|
private val audioSessionManager: AudioSessionManager,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
private var filePath: String? = null
|
||||||
|
|
||||||
|
val uiState: StateFlow<PlayerUiState> = audioPlayer.playbackState
|
||||||
|
.map { PlayerUiState(playbackState = it) }
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5.seconds),
|
||||||
|
initialValue = PlayerUiState(),
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
audioSessionManager.activate()
|
audioSessionManager.activate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun play(filePath: String) {
|
fun play(filePath: String) {
|
||||||
|
this.filePath = filePath
|
||||||
audioPlayer.load(uri = Res.getUri(filePath))
|
audioPlayer.load(uri = Res.getUri(filePath))
|
||||||
audioPlayer.play()
|
audioPlayer.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pause() {
|
fun performMainPlayAction() {
|
||||||
audioPlayer.pause()
|
when (uiState.value.playbackState) {
|
||||||
|
PlaybackState.PLAYING -> audioPlayer.pause()
|
||||||
|
PlaybackState.ENDED -> filePath?.let { play(it) }
|
||||||
|
PlaybackState.PAUSED, PlaybackState.IDLE -> audioPlayer.play()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
|
|||||||
Reference in New Issue
Block a user