Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -37,6 +39,9 @@ abstract class AudioPlayer(
) : IAudioPlayer {
val playerScope = CoroutineScope(Dispatchers.Main + SupervisorJob())

protected val _audioPlayerInitState = MutableStateFlow(AudioPlayerInitState.WAITING)
override val audioPlayerInitState: StateFlow<AudioPlayerInitState> get() = _audioPlayerInitState.asStateFlow()

protected val _isPlaying = MutableStateFlow(false)
override val isPlaying: StateFlow<Boolean> get() = _isPlaying.asStateFlow()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AudioPlayerInitState>
val isPlaying: Flow<Boolean>
val timerOption: Flow<TimerOption?>
val isPlaybackReady: Flow<Boolean>
Expand All @@ -22,7 +23,6 @@ interface IAudioPlayer {
val currentChapterPosition: Flow<Double>
val currentChapterDuration: Flow<Double>

fun getInitState(): AudioPlayerInitState
fun updateTimer(timerOption: TimerOption?, position: Double? = null)
fun rewind()
fun forward()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,8 +31,7 @@ class PlayerViewModel(
) {
private val viewModelScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

private val _audioPlayerInitState = MutableStateFlow<AudioPlayerInitState?>(null)
val audioPlayerInitState: StateFlow<AudioPlayerInitState?> = _audioPlayerInitState.asStateFlow()
val audioPlayerInitState: StateFlow<AudioPlayerInitState> = audioPlayer.audioPlayerInitState

val book: StateFlow<DetailedItem?> = audioPlayer.playingBook

Expand All @@ -55,10 +56,6 @@ class PlayerViewModel(

val isPlaying: StateFlow<Boolean> = audioPlayer.isPlaying

init {
_audioPlayerInitState.value = audioPlayer.getInitState()
}

fun expandPlayingQueue() {
_playingQueueExpanded.value = true
}
Expand Down Expand Up @@ -115,4 +112,5 @@ class PlayerViewModel(

fun previousTrack() = audioPlayer.previousTrack()

fun togglePlayPause() = audioPlayer.togglePlayPause()}
fun togglePlayPause() = audioPlayer.togglePlayPause()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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) {
Expand Down