From 6e8d84496b96b0bbafe9a9ef76d0ff07d445598f Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 11 Apr 2022 18:38:01 -0500 Subject: [PATCH 1/2] Updates to audio player plugin to method naming and using seconds for time values. Adding web audio player to run in browser. --- .../app/data/PlaybackMetadata.kt | 11 + .../app/player/MediaSessionCallback.kt | 2 +- .../app/player/PlayerListener.kt | 12 +- .../app/player/PlayerNotificationService.kt | 22 +- .../app/plugins/AbsAudioPlayer.kt | 29 +-- components/app/AudioPlayer.vue | 157 ++++++------ components/app/AudioPlayerContainer.vue | 5 +- nuxt.config.js | 1 + plugins/capacitor/AbsAudioPlayer.js | 233 +++++++++++++++++- plugins/constants.js | 14 +- 10 files changed, 367 insertions(+), 119 deletions(-) create mode 100644 android/app/src/main/java/com/audiobookshelf/app/data/PlaybackMetadata.kt diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackMetadata.kt b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackMetadata.kt new file mode 100644 index 00000000..33cfa9d3 --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackMetadata.kt @@ -0,0 +1,11 @@ +package com.audiobookshelf.app.data + +enum class PlayerState { + IDLE, BUFFERING, READY, ENDED +} + +data class PlaybackMetadata( + val duration:Double, + val currentTime:Double, + val playerState:PlayerState +) diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt index b30c5007..c8f4817d 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt @@ -141,7 +141,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi playerNotificationService.seekBackward(seekAmount) } KeyEvent.KEYCODE_MEDIA_STOP -> { - playerNotificationService.terminateStream() + playerNotificationService.closePlayback() } KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { if (playerNotificationService.mPlayer.isPlaying) { diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt index 2c5400ad..9251ba78 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt @@ -1,6 +1,7 @@ package com.audiobookshelf.app.player import android.util.Log +import com.audiobookshelf.app.data.PlayerState import com.google.android.exoplayer2.PlaybackException import com.google.android.exoplayer2.Player @@ -32,22 +33,21 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) : Log.d(tag, "STATE_READY : " + playerNotificationService.mPlayer.duration.toString()) if (lastPauseTime == 0L) { - playerNotificationService.sendClientMetadata("ready_no_sync") lastPauseTime = -1; - } else playerNotificationService.sendClientMetadata("ready") + } + playerNotificationService.sendClientMetadata(PlayerState.READY) } if (playerNotificationService.currentPlayer.playbackState == Player.STATE_BUFFERING) { Log.d(tag, "STATE_BUFFERING : " + playerNotificationService.mPlayer.currentPosition.toString()) - if (lastPauseTime == 0L) playerNotificationService.sendClientMetadata("buffering_no_sync") - else playerNotificationService.sendClientMetadata("buffering") + playerNotificationService.sendClientMetadata(PlayerState.BUFFERING) } if (playerNotificationService.currentPlayer.playbackState == Player.STATE_ENDED) { Log.d(tag, "STATE_ENDED") - playerNotificationService.sendClientMetadata("ended") + playerNotificationService.sendClientMetadata(PlayerState.ENDED) } if (playerNotificationService.currentPlayer.playbackState == Player.STATE_IDLE) { Log.d(tag, "STATE_IDLE") - playerNotificationService.sendClientMetadata("idle") + playerNotificationService.sendClientMetadata(PlayerState.IDLE) } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index 25ef78ca..e3fc7e5b 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -18,9 +18,7 @@ import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.media.MediaBrowserServiceCompat import androidx.media.utils.MediaConstants -import com.audiobookshelf.app.data.LibraryItem -import com.audiobookshelf.app.data.LocalMediaProgress -import com.audiobookshelf.app.data.PlaybackSession +import com.audiobookshelf.app.data.* import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.media.MediaManager import com.audiobookshelf.app.server.ApiHandler @@ -52,7 +50,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { fun onPlaybackSession(playbackSession:PlaybackSession) fun onPlaybackClosed() fun onPlayingUpdate(isPlaying: Boolean) - fun onMetadata(metadata: JSObject) + fun onMetadata(metadata: PlaybackMetadata) fun onPrepare(audiobookId: String, playWhenReady: Boolean) fun onSleepTimerEnded(currentPosition: Long) fun onSleepTimerSet(sleepTimeRemaining: Int) @@ -375,6 +373,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } } + fun getBufferedTimeSeconds() : Double { + return getBufferedTime() / 1000.0 + } + fun getDuration() : Long { return currentPlayer.duration } @@ -441,20 +443,16 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { currentPlayer.setPlaybackSpeed(speed) } - fun terminateStream() { + fun closePlayback() { currentPlayer.clearMediaItems() currentPlaybackSession = null clientEventEmitter?.onPlaybackClosed() PlayerListener.lastPauseTime = 0 } - fun sendClientMetadata(stateName: String) { - var metadata = JSObject() - var duration = currentPlaybackSession?.getTotalDuration() ?: 0 - metadata.put("duration", duration) - metadata.put("currentTime", getCurrentTime()) - metadata.put("stateName", stateName) - clientEventEmitter?.onMetadata(metadata) + fun sendClientMetadata(playerState: PlayerState) { + var duration = currentPlaybackSession?.getTotalDuration() ?: 0.0 + clientEventEmitter?.onMetadata(PlaybackMetadata(duration, getCurrentTimeSeconds(), playerState)) } // diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt index e27e3914..aa155b28 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt @@ -7,6 +7,7 @@ import android.util.Log import androidx.core.content.ContextCompat import com.audiobookshelf.app.MainActivity import com.audiobookshelf.app.data.LocalMediaProgress +import com.audiobookshelf.app.data.PlaybackMetadata import com.audiobookshelf.app.data.PlaybackSession import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.player.CastManager @@ -46,8 +47,8 @@ class AbsAudioPlayer : Plugin() { emit("onPlayingUpdate", isPlaying) } - override fun onMetadata(metadata: JSObject) { - notifyListeners("onMetadata", metadata) + override fun onMetadata(metadata: PlaybackMetadata) { + notifyListeners("onMetadata", JSObject(jacksonObjectMapper().writeValueAsString(metadata))) } override fun onPrepare(audiobookId: String, playWhenReady: Boolean) { @@ -123,8 +124,8 @@ class AbsAudioPlayer : Plugin() { @PluginMethod fun getCurrentTime(call: PluginCall) { Handler(Looper.getMainLooper()).post() { - var currentTime = playerNotificationService.getCurrentTime() - var bufferedTime = playerNotificationService.getBufferedTime() + var currentTime = playerNotificationService.getCurrentTimeSeconds() + var bufferedTime = playerNotificationService.getBufferedTimeSeconds() val ret = JSObject() ret.put("value", currentTime) ret.put("bufferedTime", bufferedTime) @@ -157,35 +158,35 @@ class AbsAudioPlayer : Plugin() { } @PluginMethod - fun seekPlayer(call: PluginCall) { - var time:Long = call.getString("timeMs", "0")!!.toLong() + fun seek(call: PluginCall) { + var time:Int = call.getInt("value", 0) ?: 0 // Value in seconds Handler(Looper.getMainLooper()).post() { - playerNotificationService.seekPlayer(time) + playerNotificationService.seekPlayer(time * 1000L) // convert to ms call.resolve() } } @PluginMethod fun seekForward(call: PluginCall) { - var amount:Long = call.getString("amount", "0")!!.toLong() + var amount:Int = call.getInt("value", 0) ?: 0 Handler(Looper.getMainLooper()).post() { - playerNotificationService.seekForward(amount) + playerNotificationService.seekForward(amount * 1000L) // convert to ms call.resolve() } } @PluginMethod fun seekBackward(call: PluginCall) { - var amount:Long = call.getString("amount", "0")!!.toLong() + var amount:Int = call.getInt("value", 0) ?: 0 // Value in seconds Handler(Looper.getMainLooper()).post() { - playerNotificationService.seekBackward(amount) + playerNotificationService.seekBackward(amount * 1000L) // convert to ms call.resolve() } } @PluginMethod fun setPlaybackSpeed(call: PluginCall) { - var playbackSpeed:Float = call.getFloat("speed", 1.0f)!! + var playbackSpeed:Float = call.getFloat("value", 1.0f) ?: 1.0f Handler(Looper.getMainLooper()).post() { playerNotificationService.setPlaybackSpeed(playbackSpeed) @@ -194,9 +195,9 @@ class AbsAudioPlayer : Plugin() { } @PluginMethod - fun terminateStream(call: PluginCall) { + fun closePlayback(call: PluginCall) { Handler(Looper.getMainLooper()).post() { - playerNotificationService.terminateStream() + playerNotificationService.closePlayback() call.resolve() } } diff --git a/components/app/AudioPlayer.vue b/components/app/AudioPlayer.vue index 13d001af..ca2ddda8 100644 --- a/components/app/AudioPlayer.vue +++ b/components/app/AudioPlayer.vue @@ -61,7 +61,7 @@ first_page replay_10
- {{ seekLoading ? 'autorenew' : isPaused ? 'play_arrow' : 'pause' }} + {{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}
forward_10 @@ -95,7 +95,6 @@ import { AbsAudioPlayer } from '@/plugins/capacitor' export default { props: { - playing: Boolean, bookmarks: { type: Array, default: () => [] @@ -113,12 +112,10 @@ export default { currentPlaybackRate: 1, currentTime: 0, bufferedTime: 0, - isResetting: false, - stateName: 'idle', playInterval: null, trackWidth: 0, - isPaused: true, - src: null, + isPlaying: false, + isEnded: false, volume: 0.5, readyTrackWidth: 0, playedTrackWidth: 0, @@ -136,14 +133,6 @@ export default { } }, computed: { - isPlaying: { - get() { - return this.playing - }, - set(val) { - this.$emit('update:playing', val) - } - }, menuItems() { var items = [] items.push({ @@ -306,18 +295,18 @@ export default { setPlaybackSpeed(speed) { console.log(`[AudioPlayer] Set Playback Rate: ${speed}`) this.currentPlaybackRate = speed - AbsAudioPlayer.setPlaybackSpeed({ speed: speed }) + AbsAudioPlayer.setPlaybackSpeed({ value: speed }) }, restart() { this.seek(0) }, backward10() { if (this.isLoading) return - AbsAudioPlayer.seekBackward({ amount: '10000' }) + AbsAudioPlayer.seekBackward({ value: 10 }) }, forward10() { if (this.isLoading) return - AbsAudioPlayer.seekForward({ amount: '10000' }) + AbsAudioPlayer.seekForward({ value: 10 }) }, setStreamReady() { this.readyTrackWidth = this.trackWidth @@ -387,6 +376,7 @@ export default { this.updateTrack() }, updateTrack() { + // Update progress track UI var percentDone = this.currentTime / this.totalDuration var totalPercentDone = percentDone var bufferedPercent = this.bufferedTime / this.totalDuration @@ -419,7 +409,8 @@ export default { this.seekedTime = time this.seekLoading = true - AbsAudioPlayer.seekPlayer({ timeMs: String(Math.floor(time * 1000)) }) + + AbsAudioPlayer.seek({ value: Math.floor(time) }) if (this.$refs.playedTrack) { var perc = time / this.totalDuration @@ -455,7 +446,9 @@ export default { }, async playPauseClick() { if (this.isLoading) return + this.isPlaying = !!((await AbsAudioPlayer.playPause()) || {}).playing + this.isEnded = false }, play() { AbsAudioPlayer.playPlayer() @@ -471,8 +464,8 @@ export default { clearInterval(this.playInterval) this.playInterval = setInterval(async () => { var data = await AbsAudioPlayer.getCurrentTime() - this.currentTime = Number((data.value / 1000).toFixed(2)) - this.bufferedTime = Number((data.bufferedTime / 1000).toFixed(2)) + this.currentTime = Number(data.value.toFixed(2)) + this.bufferedTime = Number(data.bufferedTime.toFixed(2)) console.log('[AudioPlayer] Got Current Time', this.currentTime) this.timeupdate() }, 1000) @@ -481,67 +474,11 @@ export default { clearInterval(this.playInterval) }, resetStream(startTime) { - this.isResetting = true - this.terminateStream() + this.closePlayback() }, - terminateStream() { + closePlayback() { if (!this.playbackSession) return - AbsAudioPlayer.terminateStream() - }, - onPlayingUpdate(data) { - console.log('onPlayingUpdate', JSON.stringify(data)) - this.isPaused = !data.value - this.$store.commit('setPlayerPlaying', !this.isPaused) - if (!this.isPaused) { - this.startPlayInterval() - } else { - this.stopPlayInterval() - } - }, - onMetadata(data) { - console.log('onMetadata', JSON.stringify(data)) - this.isLoading = false - - // this.totalDuration = Number((data.duration / 1000).toFixed(2)) - this.totalDuration = Number(data.duration.toFixed(2)) - this.currentTime = Number((data.currentTime / 1000).toFixed(2)) - this.stateName = data.stateName - - if (this.stateName === 'ended' && this.isResetting) { - this.setFromObj() - } - - this.timeupdate() - }, - // When a playback session is started the native android/ios will send the session - onPlaybackSession(playbackSession) { - console.log('onPlaybackSession received', JSON.stringify(playbackSession)) - this.playbackSession = playbackSession - - this.$store.commit('setPlayerItem', this.playbackSession) - - // Set track width - this.$nextTick(() => { - if (this.$refs.track) { - this.trackWidth = this.$refs.track.clientWidth - } else { - console.error('Track not loaded', this.$refs) - } - }) - }, - onPlaybackClosed() { - console.log('Received onPlaybackClosed evt') - this.$store.commit('setPlayerItem', null) - this.showFullscreen = false - this.playbackSession = null - }, - async init() { - this.useChapterTrack = await this.$localStore.getUseChapterTrack() - - this.onPlaybackSessionListener = AbsAudioPlayer.addListener('onPlaybackSession', this.onPlaybackSession) - this.onPlaybackClosedListener = AbsAudioPlayer.addListener('onPlaybackClosed', this.onPlaybackClosed) - this.onPlayingUpdateListener = AbsAudioPlayer.addListener('onPlayingUpdate', this.onPlayingUpdate) - this.onMetadataListener = AbsAudioPlayer.addListener('onMetadata', this.onMetadata) + AbsAudioPlayer.closePlayback() }, handleGesture() { var touchDistance = this.touchEndY - this.touchStartY @@ -581,13 +518,73 @@ export default { }) this.$localStore.setUseChapterTrack(this.useChapterTrack) } else if (action === 'close') { - this.terminateStream() + this.closePlayback() } }, forceCloseDropdownMenu() { if (this.$refs.dropdownMenu && this.$refs.dropdownMenu.closeMenu) { this.$refs.dropdownMenu.closeMenu() } + }, + // + // Listeners from audio AbsAudioPlayer + // + onPlayingUpdate(data) { + console.log('onPlayingUpdate', JSON.stringify(data)) + this.isPlaying = !!data.value + this.$store.commit('setPlayerPlaying', this.isPlaying) + if (this.isPlaying) { + this.startPlayInterval() + } else { + this.stopPlayInterval() + } + }, + onMetadata(data) { + console.log('onMetadata', JSON.stringify(data)) + this.isLoading = false + + this.totalDuration = Number(data.duration.toFixed(2)) + this.currentTime = Number(data.currentTime.toFixed(2)) + // Also includes player state data.playerState + + if (data.playerState == this.$constants.PlayerState.ENDED) { + console.log('[AudioPlayer] Playback ended') + } + this.isEnded = data.playerState == this.$constants.PlayerState.ENDED + + this.timeupdate() + }, + // When a playback session is started the native android/ios will send the session + onPlaybackSession(playbackSession) { + console.log('onPlaybackSession received', JSON.stringify(playbackSession)) + this.playbackSession = playbackSession + + this.isEnded = false + this.$store.commit('setPlayerItem', this.playbackSession) + + // Set track width + this.$nextTick(() => { + if (this.$refs.track) { + this.trackWidth = this.$refs.track.clientWidth + } else { + console.error('Track not loaded', this.$refs) + } + }) + }, + onPlaybackClosed() { + console.log('Received onPlaybackClosed evt') + this.$store.commit('setPlayerItem', null) + this.showFullscreen = false + this.isEnded = false + this.playbackSession = null + }, + async init() { + this.useChapterTrack = await this.$localStore.getUseChapterTrack() + + this.onPlaybackSessionListener = AbsAudioPlayer.addListener('onPlaybackSession', this.onPlaybackSession) + this.onPlaybackClosedListener = AbsAudioPlayer.addListener('onPlaybackClosed', this.onPlaybackClosed) + this.onPlayingUpdateListener = AbsAudioPlayer.addListener('onPlayingUpdate', this.onPlayingUpdate) + this.onMetadataListener = AbsAudioPlayer.addListener('onMetadata', this.onMetadata) } }, mounted() { diff --git a/components/app/AudioPlayerContainer.vue b/components/app/AudioPlayerContainer.vue index 14c96824..72444f9b 100644 --- a/components/app/AudioPlayerContainer.vue +++ b/components/app/AudioPlayerContainer.vue @@ -1,6 +1,6 @@