diff --git a/composeApp/src/commonMain/kotlin/org/overengineer/talelistener/platform/AudioPlayer.kt b/composeApp/src/commonMain/kotlin/org/overengineer/talelistener/platform/AudioPlayer.kt index 1735c7d..ccedf91 100644 --- a/composeApp/src/commonMain/kotlin/org/overengineer/talelistener/platform/AudioPlayer.kt +++ b/composeApp/src/commonMain/kotlin/org/overengineer/talelistener/platform/AudioPlayer.kt @@ -7,6 +7,7 @@ * - Updated package statement and adjusted imports. * - Migrated to kotlin multiplatform * - Split the code into android specific (AudioPlayerAndroid) and common (AudioPlayer) + * - Added audioPlayerInitState */ package org.overengineer.talelistener.platform @@ -23,6 +24,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext +import org.overengineer.talelistener.common.AudioPlayerInitState import org.overengineer.talelistener.content.TLMediaProvider import org.overengineer.talelistener.domain.CurrentEpisodeTimerOption import org.overengineer.talelistener.domain.DetailedItem @@ -37,6 +39,9 @@ abstract class AudioPlayer( ) : IAudioPlayer { val playerScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + protected val _audioPlayerInitState = MutableStateFlow(AudioPlayerInitState.WAITING) + override val audioPlayerInitState: StateFlow get() = _audioPlayerInitState.asStateFlow() + protected val _isPlaying = MutableStateFlow(false) override val isPlaying: StateFlow get() = _isPlaying.asStateFlow() diff --git a/composeApp/src/commonMain/kotlin/org/overengineer/talelistener/platform/IAudioPlayer.kt b/composeApp/src/commonMain/kotlin/org/overengineer/talelistener/platform/IAudioPlayer.kt index 908d3b5..fa8355c 100644 --- a/composeApp/src/commonMain/kotlin/org/overengineer/talelistener/platform/IAudioPlayer.kt +++ b/composeApp/src/commonMain/kotlin/org/overengineer/talelistener/platform/IAudioPlayer.kt @@ -11,6 +11,7 @@ import org.overengineer.talelistener.domain.TimerOption * This is somewhat the equivalent to the MediaRepository in Lissen */ interface IAudioPlayer { + val audioPlayerInitState: Flow val isPlaying: Flow val timerOption: Flow val isPlaybackReady: Flow @@ -22,7 +23,6 @@ interface IAudioPlayer { val currentChapterPosition: Flow val currentChapterDuration: Flow - fun getInitState(): AudioPlayerInitState fun updateTimer(timerOption: TimerOption?, position: Double? = null) fun rewind() fun forward() diff --git a/composeApp/src/commonMain/kotlin/org/overengineer/talelistener/ui/viewmodel/PlayerViewModel.kt b/composeApp/src/commonMain/kotlin/org/overengineer/talelistener/ui/viewmodel/PlayerViewModel.kt index 467a5f7..ec94473 100644 --- a/composeApp/src/commonMain/kotlin/org/overengineer/talelistener/ui/viewmodel/PlayerViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/overengineer/talelistener/ui/viewmodel/PlayerViewModel.kt @@ -17,6 +17,8 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.overengineer.talelistener.common.AudioPlayerInitState import org.overengineer.talelistener.domain.BookChapter @@ -29,8 +31,7 @@ class PlayerViewModel( ) { private val viewModelScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - private val _audioPlayerInitState = MutableStateFlow(null) - val audioPlayerInitState: StateFlow = _audioPlayerInitState.asStateFlow() + val audioPlayerInitState: StateFlow = audioPlayer.audioPlayerInitState val book: StateFlow = audioPlayer.playingBook @@ -55,10 +56,6 @@ class PlayerViewModel( val isPlaying: StateFlow = audioPlayer.isPlaying - init { - _audioPlayerInitState.value = audioPlayer.getInitState() - } - fun expandPlayingQueue() { _playingQueueExpanded.value = true } @@ -115,4 +112,5 @@ class PlayerViewModel( fun previousTrack() = audioPlayer.previousTrack() - fun togglePlayPause() = audioPlayer.togglePlayPause()} \ No newline at end of file + fun togglePlayPause() = audioPlayer.togglePlayPause() +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/org/overengineer/talelistener/platform/AudioPlayerDesktop.kt b/composeApp/src/desktopMain/kotlin/org/overengineer/talelistener/platform/AudioPlayerDesktop.kt index 307c6f1..4ec1d42 100644 --- a/composeApp/src/desktopMain/kotlin/org/overengineer/talelistener/platform/AudioPlayerDesktop.kt +++ b/composeApp/src/desktopMain/kotlin/org/overengineer/talelistener/platform/AudioPlayerDesktop.kt @@ -7,6 +7,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -29,7 +32,6 @@ class AudioPlayerDesktop( private var playbackSynchronizationService: PlaybackSynchronizationServiceDesktop? = null private var mediaPlayerFactory: MediaPlayerFactory? = null private var mediaPlayer: MediaPlayer? = null - private var isVlcFound = false private var timerJob: Job? = null @@ -40,89 +42,94 @@ class AudioPlayerDesktop( private var isPausedEventFromSeekTo = false init { - // todo launch async because it is very slow on windows if not found - Napier.d("NativeDiscovery starting") - isVlcFound = NativeDiscovery().discover() - Napier.d("NativeDiscovery finished, found: $isVlcFound") + CoroutineScope(Dispatchers.IO).launch { + Napier.d("NativeDiscovery starting") + val vlcFound = NativeDiscovery().discover() + Napier.d("NativeDiscovery finished, found: $vlcFound") + + if (!vlcFound) { + _audioPlayerInitState.value = AudioPlayerInitState.DESKTOP_VLC_NOT_FOUND + return@launch + } - if (isVlcFound) { mediaPlayerFactory = MediaPlayerFactory() mediaPlayer = mediaPlayerFactory!!.mediaPlayers().newMediaPlayer() playbackSynchronizationService = PlaybackSynchronizationServiceDesktop( mediaChannel = mediaChannel, sharedPreferences = preferences, mediaPlayer = mediaPlayer!!, - audioPlayerDesktop = this + audioPlayerDesktop = this@AudioPlayerDesktop ) - } - // todo check this out: https://github.com/JetBrains/compose-multiplatform/blob/master/experimental/components/VideoPlayer/library/src/desktopMain/kotlin/org/jetbrains/compose/videoplayer/DesktopVideoPlayer.kt - // -> they work with DisposableEffect, etc. - mediaPlayer?.events()?.addMediaPlayerEventListener(object : MediaPlayerEventAdapter() { - override fun playing(mediaPlayer: MediaPlayer) { - if (isPlayingEventFromSeekTo) { - isPlayingEventFromSeekTo = false - return - } - Napier.d("playing event") - _isPlaying.value = true - playbackSynchronizationService?.playing() + if (mediaPlayerFactory == null || mediaPlayer == null) { + Napier.d("MediaPlayerFactory or MediaPlayer null") + return@launch } - override fun paused(mediaPlayer: MediaPlayer) { - if (isPausedEventFromSeekTo) { - isPausedEventFromSeekTo = false - return - } - Napier.d("paused event") - _isPlaying.value = false - playbackSynchronizationService?.paused() - } - - override fun finished(mediaPlayer: MediaPlayer?) { - val book = _playingBook.value ?: return + _audioPlayerInitState.value = AudioPlayerInitState.SUCCESS + + CoroutineScope(Dispatchers.Main).launch { + // todo check this out: https://github.com/JetBrains/compose-multiplatform/blob/master/experimental/components/VideoPlayer/library/src/desktopMain/kotlin/org/jetbrains/compose/videoplayer/DesktopVideoPlayer.kt + // -> they work with DisposableEffect, etc. + mediaPlayer?.events()?.addMediaPlayerEventListener(object : MediaPlayerEventAdapter() { + override fun playing(mediaPlayer: MediaPlayer) { + if (isPlayingEventFromSeekTo) { + isPlayingEventFromSeekTo = false + return + } + Napier.d("playing event") + _isPlaying.value = true + playbackSynchronizationService?.playing() + } - if (currentPlayingIndex >= book.files.size) { - Napier.d("finished event - end of book reached") - _isPlaying.value = false - playbackSynchronizationService?.finished() + override fun paused(mediaPlayer: MediaPlayer) { + if (isPausedEventFromSeekTo) { + isPausedEventFromSeekTo = false + return + } + Napier.d("paused event") + _isPlaying.value = false + playbackSynchronizationService?.paused() + } - } else { - currentPlayingIndex++ - mediaPlayer?.submit { - val playing = mediaPlayer.media().play(getMrlForIndex(currentPlayingIndex, book)) - Napier.d("finished event - next playing: $playing") + override fun finished(mediaPlayer: MediaPlayer?) { + val book = _playingBook.value ?: return + + if (currentPlayingIndex >= book.files.size) { + Napier.d("finished event - end of book reached") + _isPlaying.value = false + playbackSynchronizationService?.finished() + + } else { + currentPlayingIndex++ + mediaPlayer?.submit { + val playing = mediaPlayer.media().play(getMrlForIndex(currentPlayingIndex, book)) + Napier.d("finished event - next playing: $playing") + } + } } - } - } - override fun timeChanged(mediaPlayer: MediaPlayer?, newTime: Long) { - CoroutineScope(Dispatchers.Main).launch { - val files = playingBook.value?.files - if (files == null) { - Napier.w("files is null, skipping update") - return@launch + override fun timeChanged(mediaPlayer: MediaPlayer?, newTime: Long) { + CoroutineScope(Dispatchers.Main).launch { + val files = playingBook.value?.files + if (files == null) { + Napier.w("files is null, skipping update") + return@launch + } + val accumulated = files.take(currentPlayingIndex).sumOf { it.duration } + val currentFilePosition = newTime / 1000.0 + + _totalPosition.value = (accumulated + currentFilePosition) + } } - val accumulated = files.take(currentPlayingIndex).sumOf { it.duration } - val currentFilePosition = newTime / 1000.0 + }) - _totalPosition.value = (accumulated + currentFilePosition) - } + // Restore default playback speed from preferences + val lastSpeed = preferences.getPlaybackSpeed() + _playbackSpeed.value = lastSpeed + mediaPlayer?.controls()?.setRate(lastSpeed) } - }) - - // Restore default playback speed from preferences - val lastSpeed = preferences.getPlaybackSpeed() - _playbackSpeed.value = lastSpeed - mediaPlayer?.controls()?.setRate(lastSpeed) - } - - override fun getInitState(): AudioPlayerInitState { - Napier.d("getInitState()") - if (isVlcFound) { - return AudioPlayerInitState.SUCCESS } - return AudioPlayerInitState.DESKTOP_VLC_NOT_FOUND } override fun setPlaybackSpeed(factor: Float) {