Merge branch 'advplyr:master' into master

This commit is contained in:
Rasmus Krämer
2022-04-11 11:29:46 +02:00
committed by GitHub
123 changed files with 12478 additions and 31498 deletions
+6 -3
View File
@@ -7,16 +7,18 @@
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 mr-2 cursor-pointer">
<span class="material-icons text-3xl text-white">arrow_back</span>
</a>
<div v-if="socketConnected">
<div v-if="user">
<div class="px-4 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</svg>
<p class="text-lg font-book leading-4 ml-2">{{ currentLibraryName }}</p>
<p class="text-base font-book leading-4 ml-2">{{ currentLibraryName }}</p>
</div>
</div>
<div class="flex-grow" />
<widgets-download-progress-indicator />
<nuxt-link class="h-7 mx-2" to="/search">
<span class="material-icons" style="font-size: 1.75rem">search</span>
</nuxt-link>
@@ -44,6 +46,7 @@ export default {
return this.currentLibrary ? this.currentLibrary.name : 'Main'
},
showBack() {
if (!this.$route.name) return true
return this.$route.name !== 'index' && !this.$route.name.startsWith('bookshelf')
},
user() {
+110 -237
View File
@@ -1,5 +1,5 @@
<template>
<div class="fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="showFullscreen ? 'fullscreen' : ''">
<div v-if="playbackSession" id="streamContainer" class="fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="showFullscreen ? 'fullscreen' : ''">
<div v-if="showFullscreen" class="w-full h-full z-10 bg-bg absolute top-0 left-0 pointer-events-auto">
<div class="top-2 left-4 absolute cursor-pointer">
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
@@ -31,13 +31,13 @@
<div class="cover-wrapper absolute z-30 pointer-events-auto" :class="bookCoverAspectRatio === 1 ? 'square-cover' : ''" @click="clickContainer">
<div class="cover-container bookCoverWrapper bg-black bg-opacity-75 w-full h-full">
<covers-book-cover :audiobook="audiobook" :download-cover="downloadedCover" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
</div>
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden">
<p class="title-text font-book truncate">{{ title }}</p>
<p class="author-text text-white text-opacity-75 truncate">by {{ authorFL }}</p>
<p class="author-text text-white text-opacity-75 truncate">by {{ authorName }}</p>
</div>
<div id="streamContainer" class="w-full z-20 bg-primary absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" @click="clickContainer">
@@ -52,25 +52,25 @@
<p class="text-xl font-mono text-success">{{ sleepTimeRemainingPretty }}</p>
</div>
<span class="material-icons text-3xl text-white cursor-pointer" :class="chapters.length ? 'text-opacity-75' : 'text-opacity-10'" @click="$emit('selectChapter')">format_list_bulleted</span>
<span class="material-icons text-3xl text-white cursor-pointer" :class="chapters.length ? 'text-opacity-75' : 'text-opacity-10'" @click="showChapterModal = true">format_list_bulleted</span>
</div>
</div>
<div id="playerControls" class="absolute right-0 bottom-0 py-2">
<div class="flex items-center justify-center">
<span v-show="showFullscreen" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" :class="loading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
<span class="material-icons jump-icon text-white cursor-pointer" :class="loading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="backward10">replay_10</span>
<span v-show="showFullscreen" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
<span class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="backward10">replay_10</span>
<div class="play-btn cursor-pointer shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
<span v-if="!loading" class="material-icons">{{ seekLoading ? 'autorenew' : isPaused ? 'play_arrow' : 'pause' }}</span>
<span v-if="!isLoading" class="material-icons">{{ seekLoading ? 'autorenew' : isPaused ? 'play_arrow' : 'pause' }}</span>
<widgets-spinner-icon v-else class="h-8 w-8" />
</div>
<span class="material-icons jump-icon text-white cursor-pointer" :class="loading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="forward10">forward_10</span>
<span v-show="showFullscreen" class="material-icons next-icon text-white cursor-pointer" :class="nextChapter && !loading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
<span class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="forward10">forward_10</span>
<span v-show="showFullscreen" class="material-icons next-icon text-white cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
</div>
</div>
<div id="playerTrack" class="absolute bottom-0 left-0 w-full px-3">
<div ref="track" class="h-2 w-full bg-gray-500 bg-opacity-50 relative" :class="loading ? 'animate-pulse' : ''" @click="clickTrack">
<div ref="track" class="h-2 w-full bg-gray-500 bg-opacity-50 relative" :class="isLoading ? 'animate-pulse' : ''" @click="clickTrack">
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
<div ref="bufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
@@ -84,33 +84,29 @@
</div>
</div>
</div>
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" />
</div>
</template>
<script>
import MyNativeAudio from '@/plugins/my-native-audio'
import { Capacitor } from '@capacitor/core'
import { AbsAudioPlayer } from '@/plugins/capacitor'
export default {
props: {
playing: Boolean,
audiobook: {
type: Object,
default: () => {}
},
download: {
type: Object,
default: () => {}
},
bookmarks: {
type: Array,
default: () => []
},
loading: Boolean,
sleepTimerRunning: Boolean,
sleepTimeRemaining: Number
},
data() {
return {
playbackSession: null,
showChapterModal: false,
showCastBtn: false,
showFullscreen: false,
totalDuration: 0,
@@ -118,9 +114,6 @@ export default {
currentTime: 0,
bufferedTime: 0,
isResetting: false,
initObject: null,
streamId: null,
audiobookId: null,
stateName: 'idle',
playInterval: null,
trackWidth: 0,
@@ -131,16 +124,15 @@ export default {
playedTrackWidth: 0,
seekedTime: 0,
seekLoading: false,
onPlaybackSessionListener: null,
onPlaybackClosedListener: null,
onPlayingUpdateListener: null,
onMetadataListener: null,
// noSyncUpdateTime: false,
touchStartY: 0,
touchStartTime: 0,
touchEndY: 0,
listenTimeInterval: null,
listeningTimeSinceLastUpdate: 0,
totalListeningTimeInSession: 0,
useChapterTrack: false
useChapterTrack: false,
isLoading: true
}
},
computed: {
@@ -175,25 +167,42 @@ export default {
}
return this.showFullscreen ? 200 : 60
},
book() {
return this.audiobook.book || {}
mediaMetadata() {
return this.playbackSession ? this.playbackSession.mediaMetadata : null
},
libraryItem() {
return this.playbackSession ? this.playbackSession.libraryItem || null : null
},
localLibraryItem() {
return this.playbackSession ? this.playbackSession.localLibraryItem || null : null
},
localLibraryItemCoverSrc() {
var localItemCover = this.localLibraryItem ? this.localLibraryItem.coverContentUrl : null
if (localItemCover) return Capacitor.convertFileSrc(localItemCover)
return null
},
playMethod() {
return this.playbackSession ? this.playbackSession.playMethod : null
},
isLocalPlayMethod() {
return this.playMethod == this.$constants.PlayMethod.LOCAL
},
title() {
return this.book.title
if (this.playbackSession) return this.playbackSession.displayTitle
return this.mediaMetadata ? this.mediaMetadata.title : 'Title'
},
authorFL() {
return this.book.authorFL
authorName() {
if (this.playbackSession) return this.playbackSession.displayAuthor
return this.mediaMetadata ? this.mediaMetadata.authorName : 'Author'
},
chapters() {
return (this.audiobook ? this.audiobook.chapters || [] : []).map((chapter) => {
var chap = { ...chapter }
chap.start = Number(chap.start)
chap.end = Number(chap.end)
return chap
})
if (this.playbackSession && this.playbackSession.chapters) {
return this.playbackSession.chapters
}
return []
},
currentChapter() {
if (!this.audiobook || !this.chapters.length) return null
if (!this.chapters.length) return null
return this.chapters.find((ch) => Number(Number(ch.start).toFixed(2)) <= this.currentTime && Number(Number(ch.end).toFixed(2)) > this.currentTime)
},
nextChapter() {
@@ -206,9 +215,6 @@ export default {
currentChapterDuration() {
return this.currentChapter ? this.currentChapter.end - this.currentChapter.start : this.totalDuration
},
downloadedCover() {
return this.download ? this.download.cover : null
},
totalDurationPretty() {
return this.$secondsToTimestamp(this.totalDuration)
},
@@ -241,10 +247,6 @@ export default {
if (!this.currentChapter) return 0
return this.currentChapter.end - this.currentTime
},
// sleepTimeRemaining() {
// if (!this.sleepTimerEndTime) return 0
// return Math.max(0, this.sleepTimerEndTime / 1000 - this.currentTime)
// },
sleepTimeRemainingPretty() {
if (!this.sleepTimeRemaining) return '0s'
var secondsRemaining = Math.round(this.sleepTimeRemaining)
@@ -256,65 +258,13 @@ export default {
}
},
methods: {
selectChapter(chapter) {
this.seek(chapter.start)
this.showChapterModal = false
},
castClick() {
console.log('Cast Btn Click')
MyNativeAudio.requestSession()
},
sendStreamSync(timeListened = 0) {
var syncData = {
timeListened,
currentTime: this.currentTime,
streamId: this.streamId,
audiobookId: this.audiobookId,
totalDuration: this.totalDuration
}
this.$emit('sync', syncData)
},
sendAddListeningTime() {
var listeningTimeToAdd = Math.floor(this.listeningTimeSinceLastUpdate)
this.listeningTimeSinceLastUpdate = Math.max(0, this.listeningTimeSinceLastUpdate - listeningTimeToAdd)
this.sendStreamSync(listeningTimeToAdd)
},
cancelListenTimeInterval() {
this.sendAddListeningTime()
clearInterval(this.listenTimeInterval)
this.listenTimeInterval = null
},
startListenTimeInterval() {
clearInterval(this.listenTimeInterval)
var lastTime = this.currentTime
var lastTick = Date.now()
var noProgressCount = 0
this.listenTimeInterval = setInterval(() => {
var timeSinceLastTick = Date.now() - lastTick
lastTick = Date.now()
var expectedAudioTime = lastTime + timeSinceLastTick / 1000
var currentTime = this.currentTime
var differenceFromExpected = expectedAudioTime - currentTime
if (currentTime === lastTime) {
noProgressCount++
if (noProgressCount > 3) {
console.error('Audio current time has not increased - cancel interval and pause player')
this.pause()
}
} else if (Math.abs(differenceFromExpected) > 0.1) {
noProgressCount = 0
console.warn('Invalid time between interval - resync last', differenceFromExpected)
lastTime = currentTime
} else {
noProgressCount = 0
var exactPlayTimeDifference = currentTime - lastTime
// console.log('Difference from expected', differenceFromExpected, 'Exact play time diff', exactPlayTimeDifference)
lastTime = currentTime
this.listeningTimeSinceLastUpdate += exactPlayTimeDifference
this.totalListeningTimeInSession += exactPlayTimeDifference
// console.log('Time since last update:', this.listeningTimeSinceLastUpdate, 'Session listening time:', this.totalListeningTimeInSession)
if (this.listeningTimeSinceLastUpdate > 5) {
this.sendAddListeningTime()
}
}
}, 1000)
AbsAudioPlayer.requestSession()
},
clickContainer() {
this.showFullscreen = true
@@ -329,18 +279,18 @@ export default {
this.forceCloseDropdownMenu()
},
jumpNextChapter() {
if (this.loading) return
if (this.isLoading) return
if (!this.nextChapter) return
this.seek(this.nextChapter.start)
},
jumpChapterStart() {
if (this.loading) return
if (this.isLoading) return
if (!this.currentChapter) {
return this.restart()
}
// If 1 second or less into current chapter, then go to previous
if (this.currentTime - this.currentChapter.start <= 1) {
// If 4 seconds or less into current chapter, then go to previous
if (this.currentTime - this.currentChapter.start <= 4) {
var currChapterIndex = this.chapters.findIndex((ch) => Number(ch.start) <= this.currentTime && Number(ch.end) >= this.currentTime)
if (currChapterIndex > 0) {
var prevChapter = this.chapters[currChapterIndex - 1]
@@ -356,18 +306,18 @@ export default {
setPlaybackSpeed(speed) {
console.log(`[AudioPlayer] Set Playback Rate: ${speed}`)
this.currentPlaybackRate = speed
MyNativeAudio.setPlaybackSpeed({ speed: speed })
AbsAudioPlayer.setPlaybackSpeed({ speed: speed })
},
restart() {
this.seek(0)
},
backward10() {
if (this.loading) return
MyNativeAudio.seekBackward({ amount: '10000' })
if (this.isLoading) return
AbsAudioPlayer.seekBackward({ amount: '10000' })
},
forward10() {
if (this.loading) return
MyNativeAudio.seekForward({ amount: '10000' })
if (this.isLoading) return
AbsAudioPlayer.seekForward({ amount: '10000' })
},
setStreamReady() {
this.readyTrackWidth = this.trackWidth
@@ -461,7 +411,7 @@ export default {
}
},
seek(time) {
if (this.loading) return
if (this.isLoading) return
if (this.seekLoading) {
console.error('Already seek loading', this.seekedTime)
return
@@ -469,7 +419,7 @@ export default {
this.seekedTime = time
this.seekLoading = true
MyNativeAudio.seekPlayer({ timeMs: String(Math.floor(time * 1000)) })
AbsAudioPlayer.seekPlayer({ timeMs: String(Math.floor(time * 1000)) })
if (this.$refs.playedTrack) {
var perc = time / this.totalDuration
@@ -482,7 +432,7 @@ export default {
}
},
clickTrack(e) {
if (this.loading) return
if (this.isLoading) return
if (!this.showFullscreen) {
// Track not clickable on mini-player
return
@@ -503,120 +453,24 @@ export default {
}
this.seek(time)
},
playPauseClick() {
if (this.loading) return
if (this.isPaused) {
console.log('playPause PLAY')
this.play()
} else {
console.log('playPause PAUSE')
this.pause()
}
},
calcSeekBackTime(lastUpdate) {
var time = Date.now() - lastUpdate
var seekback = 0
if (time < 60000) seekback = 0
else if (time < 120000) seekback = 10000
else if (time < 300000) seekback = 15000
else if (time < 1800000) seekback = 20000
else if (time < 3600000) seekback = 25000
else seekback = 29500
return seekback
},
async set(audiobookStreamData, stream, fromAppDestroy) {
this.isResetting = false
this.bufferedTime = 0
this.streamId = stream ? stream.id : null
this.audiobookId = audiobookStreamData.audiobookId
this.initObject = { ...audiobookStreamData }
console.log('[AudioPlayer] Set Audio Player', !!stream)
var init = true
if (!!stream) {
//console.log(JSON.stringify(stream))
var data = await MyNativeAudio.getStreamSyncData()
console.log('getStreamSyncData', JSON.stringify(data))
console.log('lastUpdate', stream.lastUpdate || 0)
//Same audiobook
if (data.id == stream.id && (data.isPlaying || data.lastPauseTime >= (stream.lastUpdate || 0))) {
console.log('Same audiobook')
this.isPaused = !data.isPlaying
this.currentTime = Number((data.currentTime / 1000).toFixed(2))
this.totalDuration = Number((data.duration / 1000).toFixed(2))
this.$emit('setTotalDuration', this.totalDuration)
this.timeupdate()
if (data.isPlaying) {
console.log('playing - continue')
if (fromAppDestroy) this.startPlayInterval()
} else console.log('paused and newer')
if (!fromAppDestroy) return
init = false
this.initObject.startTime = String(Math.floor(this.currentTime * 1000))
}
//new audiobook stream or sync from other client
else if (stream.clientCurrentTime > 0) {
console.log('new audiobook stream or sync from other client')
if (!!stream.lastUpdate) {
var backTime = this.calcSeekBackTime(stream.lastUpdate)
var currentTime = Math.floor(stream.clientCurrentTime * 1000)
if (backTime >= currentTime) backTime = currentTime - 500
console.log('SeekBackTime', backTime)
this.initObject.startTime = String(Math.floor(currentTime - backTime))
}
}
}
this.currentPlaybackRate = this.initObject.playbackSpeed
console.log(`[AudioPlayer] Set Stream Playback Rate: ${this.currentPlaybackRate}`)
if (init)
MyNativeAudio.initPlayer(this.initObject).then((res) => {
if (res && res.success) {
console.log('Success init audio player')
} else {
console.error('Failed to init audio player')
}
})
if (audiobookStreamData.isLocal) {
this.setStreamReady()
}
},
setFromObj() {
if (!this.initObject) {
console.error('Cannot set from obj')
return
}
this.isResetting = false
MyNativeAudio.initPlayer(this.initObject).then((res) => {
if (res && res.success) {
console.log('Success init audio player')
} else {
console.error('Failed to init audio player')
}
})
if (audiobookStreamData.isLocal) {
this.setStreamReady()
}
async playPauseClick() {
if (this.isLoading) return
this.isPlaying = !!((await AbsAudioPlayer.playPause()) || {}).playing
},
play() {
MyNativeAudio.playPlayer()
AbsAudioPlayer.playPlayer()
this.startPlayInterval()
this.isPlaying = true
},
pause() {
MyNativeAudio.pausePlayer()
AbsAudioPlayer.pausePlayer()
this.stopPlayInterval()
this.isPlaying = false
},
startPlayInterval() {
this.startListenTimeInterval()
clearInterval(this.playInterval)
this.playInterval = setInterval(async () => {
var data = await MyNativeAudio.getCurrentTime()
var data = await AbsAudioPlayer.getCurrentTime()
this.currentTime = Number((data.value / 1000).toFixed(2))
this.bufferedTime = Number((data.bufferedTime / 1000).toFixed(2))
console.log('[AudioPlayer] Got Current Time', this.currentTime)
@@ -624,24 +478,20 @@ export default {
}, 1000)
},
stopPlayInterval() {
this.cancelListenTimeInterval()
clearInterval(this.playInterval)
},
resetStream(startTime) {
var _time = String(Math.floor(startTime * 1000))
if (!this.initObject) {
console.error('Terminate stream when no init object is set...')
return
}
this.isResetting = true
this.initObject.currentTime = _time
this.terminateStream()
},
terminateStream() {
MyNativeAudio.terminateStream()
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 {
@@ -649,8 +499,11 @@ export default {
}
},
onMetadata(data) {
this.totalDuration = Number((data.duration / 1000).toFixed(2))
this.$emit('setTotalDuration', this.totalDuration)
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
@@ -664,17 +517,35 @@ export default {
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.onPlayingUpdateListener = MyNativeAudio.addListener('onPlayingUpdate', this.onPlayingUpdate)
this.onMetadataListener = MyNativeAudio.addListener('onMetadata', this.onMetadata)
if (this.$refs.track) {
this.trackWidth = this.$refs.track.clientWidth
} else {
console.error('Track not loaded', this.$refs)
}
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)
},
handleGesture() {
var touchDistance = this.touchEndY - this.touchStartY
@@ -714,7 +585,7 @@ export default {
})
this.$localStore.setUseChapterTrack(this.useChapterTrack)
} else if (action === 'close') {
this.$emit('close')
this.terminateStream()
}
},
forceCloseDropdownMenu() {
@@ -736,6 +607,8 @@ export default {
if (this.onPlayingUpdateListener) this.onPlayingUpdateListener.remove()
if (this.onMetadataListener) this.onMetadataListener.remove()
if (this.onPlaybackSessionListener) this.onPlaybackSessionListener.remove()
if (this.onPlaybackClosedListener) this.onPlaybackClosedListener.remove()
clearInterval(this.playInterval)
}
}
+68 -359
View File
@@ -1,37 +1,15 @@
<template>
<div>
<div v-if="audiobook" id="streamContainer">
<app-audio-player
ref="audioPlayer"
:playing.sync="isPlaying"
:audiobook="audiobook"
:download="download"
:loading="isLoading"
:bookmarks="bookmarks"
:sleep-timer-running="isSleepTimerRunning"
:sleep-time-remaining="sleepTimeRemaining"
@close="cancelStream"
@sync="sync"
@setTotalDuration="setTotalDuration"
@selectPlaybackSpeed="showPlaybackSpeedModal = true"
@selectChapter="clickChapterBtn"
@updateTime="(t) => (currentTime = t)"
@showSleepTimer="showSleepTimer"
@showBookmarks="showBookmarks"
@hook:mounted="audioPlayerMounted"
/>
</div>
<app-audio-player ref="audioPlayer" :playing.sync="isPlaying" :bookmarks="bookmarks" :sleep-timer-running="isSleepTimerRunning" :sleep-time-remaining="sleepTimeRemaining" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @updateTime="(t) => (currentTime = t)" @showSleepTimer="showSleepTimer" @showBookmarks="showBookmarks" />
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-rate.sync="playbackSpeed" @update:playbackRate="updatePlaybackSpeed" @change="changePlaybackSpeed" />
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" />
<modals-sleep-timer-modal v-model="showSleepTimerModal" :current-time="sleepTimeRemaining" :sleep-timer-running="isSleepTimerRunning" :current-end-of-chapter-time="currentEndOfChapterTime" @change="selectSleepTimeout" @cancel="cancelSleepTimer" @increase="increaseSleepTimer" @decrease="decreaseSleepTimer" />
<modals-bookmarks-modal v-model="showBookmarksModal" :audiobook-id="audiobookId" :bookmarks="bookmarks" :current-time="currentTime" @select="selectBookmark" />
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="currentTime" @select="selectBookmark" />
</div>
</template>
<script>
import { Dialog } from '@capacitor/dialog'
import MyNativeAudio from '@/plugins/my-native-audio'
import { AbsAudioPlayer } from '@/plugins/capacitor'
export default {
data() {
@@ -40,21 +18,19 @@ export default {
audioPlayerReady: false,
stream: null,
download: null,
lastProgressTimeUpdate: 0,
showPlaybackSpeedModal: false,
showBookmarksModal: false,
showSleepTimerModal: false,
playbackSpeed: 1,
showChapterModal: false,
currentTime: 0,
isSleepTimerRunning: false,
sleepTimerEndTime: 0,
sleepTimerRemaining: 0,
sleepTimeRemaining: 0,
onLocalMediaProgressUpdateListener: null,
onSleepTimerEndedListener: null,
onSleepTimerSetListener: null,
sleepInterval: null,
currentEndOfChapterTime: 0,
totalDuration: 0
currentEndOfChapterTime: 0
}
},
watch: {
@@ -66,95 +42,13 @@ export default {
}
},
computed: {
userToken() {
return this.$store.getters['user/getToken']
},
userAudiobook() {
if (!this.audiobookId) return
return this.$store.getters['user/getUserAudiobookData'](this.audiobookId)
},
bookmarks() {
if (!this.userAudiobook) return []
return this.userAudiobook.bookmarks || []
},
currentChapter() {
if (!this.audiobook || !this.chapters.length) return null
return this.chapters.find((ch) => Number(ch.start) <= this.currentTime && Number(ch.end) > this.currentTime)
// return this.$store.getters['user/getUserBookmarksForItem'](this.)
return []
},
socketConnected() {
return this.$store.state.socketConnected
},
isLoading() {
if (this.playingDownload) return false
if (!this.streamAudiobook) return false
return !this.stream || this.streamAudiobook.id !== this.stream.audiobook.id
},
playingDownload() {
return this.$store.state.playingDownload
},
audiobook() {
if (this.playingDownload) return this.playingDownload.audiobook
return this.streamAudiobook
},
audiobookId() {
return this.audiobook ? this.audiobook.id : null
},
streamAudiobook() {
return this.$store.state.streamAudiobook
},
book() {
return this.audiobook ? this.audiobook.book || {} : {}
},
title() {
return this.book ? this.book.title : ''
},
author() {
return this.book ? this.book.author : ''
},
cover() {
return this.book ? this.book.cover : ''
},
series() {
return this.book ? this.book.series : ''
},
chapters() {
return this.audiobook ? this.audiobook.chapters || [] : []
},
volumeNumber() {
return this.book ? this.book.volumeNumber : ''
},
seriesTxt() {
if (!this.series) return ''
if (!this.volumeNumber) return this.series
return `${this.series} #${this.volumeNumber}`
},
duration() {
return this.audiobook ? this.audiobook.duration || 0 : 0
},
coverForNative() {
if (!this.cover) {
return `${this.$store.state.serverUrl}/Logo.png`
}
if (this.cover.startsWith('http')) return this.cover
var coverSrc = this.$store.getters['audiobooks/getBookCoverSrc'](this.audiobook)
return coverSrc
},
tracksForCast() {
if (!this.audiobook || !this.audiobook.tracks) {
return []
}
var abpath = this.audiobook.path
var tracks = this.audiobook.tracks.map((t) => {
var trelpath = t.path.replace(abpath, '')
if (trelpath.startsWith('/')) trelpath = trelpath.substr(1)
return `${this.$store.state.serverUrl}/s/book/${this.audiobook.id}/${trelpath}?token=${this.userToken}`
})
return tracks
}
// sleepTimeRemaining() {
// if (!this.sleepTimerEndTime) return 0
// return Math.max(0, this.sleepTimerEndTime / 1000 - this.currentTime)
// }
},
methods: {
showBookmarks() {
@@ -173,7 +67,7 @@ export default {
if (currentPosition) {
console.log('Sleep Timer Ended Current Position: ' + currentPosition)
var currentTime = Math.floor(currentPosition / 1000)
this.updateTime(currentTime)
// TODO: Was syncing to the server here before
}
},
onSleepTimerSet({ value: sleepTimeRemaining }) {
@@ -188,8 +82,8 @@ export default {
this.sleepTimeRemaining = sleepTimeRemaining
},
showSleepTimer() {
if (this.currentChapter) {
this.currentEndOfChapterTime = Math.floor(this.currentChapter.end)
if (this.$refs.audioPlayer && this.$refs.audioPlayer.currentChapter) {
this.currentEndOfChapterTime = Math.floor(this.$refs.audioPlayer.currentChapter.end)
} else {
this.currentEndOfChapterTime = 0
}
@@ -197,101 +91,24 @@ export default {
},
async selectSleepTimeout({ time, isChapterTime }) {
console.log('Setting sleep timer', time, isChapterTime)
var res = await MyNativeAudio.setSleepTimer({ time: String(time), isChapterTime })
var res = await AbsAudioPlayer.setSleepTimer({ time: String(time), isChapterTime })
if (!res.success) {
return this.$toast.error('Sleep timer did not set, invalid time')
}
},
increaseSleepTimer() {
// Default time to increase = 5 min
MyNativeAudio.increaseSleepTime({ time: '300000' })
AbsAudioPlayer.increaseSleepTime({ time: '300000' })
},
decreaseSleepTimer() {
MyNativeAudio.decreaseSleepTime({ time: '300000' })
AbsAudioPlayer.decreaseSleepTime({ time: '300000' })
},
async cancelSleepTimer() {
console.log('Canceling sleep timer')
await MyNativeAudio.cancelSleepTimer()
await AbsAudioPlayer.cancelSleepTimer()
},
clickChapterBtn() {
if (!this.chapters.length) return
this.showChapterModal = true
},
selectChapter(chapter) {
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.seek(chapter.start)
}
this.showChapterModal = false
},
async cancelStream() {
this.currentTime = 0
if (this.download) {
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.terminateStream()
}
this.download = null
this.$store.commit('setPlayingDownload', null)
this.$localStore.setCurrent(null)
} else {
const { value } = await Dialog.confirm({
title: 'Confirm',
message: 'Cancel this stream?'
})
if (value) {
this.$server.socket.emit('close_stream')
this.$store.commit('setStreamAudiobook', null)
this.$server.stream = null
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.terminateStream()
}
}
}
},
sync(syncData) {
var diff = syncData.currentTime - this.lastServerUpdateSentSeconds
if (Math.abs(diff) < 1 && !syncData.timeListened) {
// No need to sync
return
}
if (this.stream) {
this.$server.socket.emit('stream_sync', syncData)
} else {
var progressUpdate = {
audiobookId: syncData.audiobookId,
currentTime: syncData.currentTime,
totalDuration: syncData.totalDuration,
progress: syncData.totalDuration ? Number((syncData.currentTime / syncData.totalDuration).toFixed(3)) : 0,
lastUpdate: Date.now(),
isRead: false
}
if (this.$server.connected) {
this.$server.socket.emit('progress_update', progressUpdate)
} else {
this.$store.dispatch('user/updateUserAudiobookData', progressUpdate)
}
}
},
updateTime(currentTime) {
this.sync({
currentTime,
audiobookId: this.audiobookId,
streamId: this.stream ? this.stream.id : null,
timeListened: 0,
totalDuration: this.totalDuration || 0
})
},
setTotalDuration(duration) {
this.totalDuration = duration
},
streamClosed(audiobookId) {
streamClosed() {
console.log('Stream Closed')
if (this.stream.audiobook.id === audiobookId || audiobookId === 'n/a') {
this.$store.commit('setStreamAudiobook', null)
}
},
streamProgress(data) {
if (!data.numSegments) return
@@ -307,131 +124,13 @@ export default {
}
},
streamReset({ streamId, startTime }) {
console.log('received stream reset', streamId, startTime)
if (this.$refs.audioPlayer) {
if (this.stream && this.stream.id === streamId) {
this.$refs.audioPlayer.resetStream(startTime)
}
}
},
async getDownloadStartTime() {
var userAudiobook = this.$store.getters['user/getUserAudiobookData'](this.audiobookId)
if (!userAudiobook) {
console.log('[StreamContainer] getDownloadStartTime no user audiobook record found')
return 0
}
return userAudiobook.currentTime
},
async playDownload() {
if (this.stream) {
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.terminateStream()
}
this.stream = null
}
this.lastProgressTimeUpdate = 0
console.log('[StreamContainer] Playing local', this.playingDownload)
if (!this.$refs.audioPlayer) {
console.error('No Audio Player Mini')
return
}
var playOnLoad = this.$store.state.playOnLoad
if (playOnLoad) this.$store.commit('setPlayOnLoad', false)
var currentTime = await this.getDownloadStartTime()
if (isNaN(currentTime) || currentTime === null) currentTime = 0
this.currentTime = currentTime
// Update local current time
this.$localStore.setCurrent({
audiobookId: this.download.id,
lastUpdate: Date.now()
})
var audiobookStreamData = {
id: 'download',
title: this.title,
author: this.author,
playWhenReady: !!playOnLoad,
startTime: String(Math.floor(currentTime * 1000)),
playbackSpeed: this.playbackSpeed || 1,
cover: this.download.coverUrl || null,
duration: String(Math.floor(this.duration * 1000)),
series: this.seriesTxt,
token: this.userToken,
contentUrl: this.playingDownload.contentUrl,
isLocal: true,
audiobookId: this.download.id
}
this.$refs.audioPlayer.set(audiobookStreamData, null, false)
},
streamOpen(stream) {
if (this.download) {
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.terminateStream()
}
this.download = null
}
this.lastProgressTimeUpdate = 0
console.log('[StreamContainer] Stream Open: ' + this.title)
if (!this.$refs.audioPlayer) {
console.error('[StreamContainer] No Audio Player Mini')
return
}
// Update local remove current
this.$localStore.setCurrent(null)
var playlistUrl = stream.clientPlaylistUri
var currentTime = stream.clientCurrentTime || 0
this.currentTime = currentTime
var playOnLoad = this.$store.state.playOnLoad
if (playOnLoad) this.$store.commit('setPlayOnLoad', false)
var audiobookStreamData = {
id: stream.id,
title: this.title,
author: this.author,
playWhenReady: !!playOnLoad,
startTime: String(Math.floor(currentTime * 1000)),
playbackSpeed: this.playbackSpeed || 1,
cover: this.coverForNative,
duration: String(Math.floor(this.duration * 1000)),
series: this.seriesTxt,
playlistUrl: this.$server.url + playlistUrl,
token: this.userToken,
audiobookId: this.audiobookId,
tracks: this.tracksForCast
}
console.log('[StreamContainer] Set Audio Player', JSON.stringify(audiobookStreamData))
if (!this.$refs.audioPlayer) {
console.error('[StreamContainer] Invalid no audio player')
} else {
console.log('[StreamContainer] Has Audio Player Ref')
}
this.$refs.audioPlayer.set(audiobookStreamData, stream, !this.stream)
this.stream = stream
},
audioPlayerMounted() {
console.log('Audio Player Mounted', this.$server.stream)
this.audioPlayerReady = true
if (this.playingDownload) {
console.log('[StreamContainer] Play download on audio mount')
if (!this.download) {
this.download = { ...this.playingDownload }
}
this.playDownload()
} else if (this.$server.stream) {
console.log('[StreamContainer] Open stream on audio mount')
this.streamOpen(this.$server.stream)
}
},
updatePlaybackSpeed(speed) {
if (this.$refs.audioPlayer) {
console.log(`[AudioPlayerContainer] Update Playback Speed: ${speed}`)
@@ -450,66 +149,76 @@ export default {
this.$refs.audioPlayer.setPlaybackSpeed(this.playbackSpeed)
}
},
streamUpdated(type, data) {
if (type === 'download') {
if (data) {
this.download = { ...data }
if (this.audioPlayerReady) {
this.playDownload()
}
} else if (this.download) {
this.cancelStream()
}
}
},
setListeners() {
if (!this.$server.socket) {
console.error('Invalid server socket not set')
return
}
this.$server.socket.on('stream_open', this.streamOpen)
this.$server.socket.on('stream_closed', this.streamClosed)
this.$server.socket.on('stream_progress', this.streamProgress)
this.$server.socket.on('stream_ready', this.streamReady)
this.$server.socket.on('stream_reset', this.streamReset)
// if (!this.$server.socket) {
// console.error('Invalid server socket not set')
// return
// }
// this.$server.socket.on('stream_open', this.streamOpen)
// this.$server.socket.on('stream_closed', this.streamClosed)
// this.$server.socket.on('stream_progress', this.streamProgress)
// this.$server.socket.on('stream_ready', this.streamReady)
// this.$server.socket.on('stream_reset', this.streamReset)
},
closeStreamOnly() {
// If user logs out or disconnects from server, close audio if streaming
if (!this.download) {
this.$store.commit('setStreamAudiobook', null)
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.terminateStream()
}
// If user logs out or disconnects from server and not playing local
if (this.$refs.audioPlayer && !this.$refs.audioPlayer.isLocalPlayMethod) {
this.$refs.audioPlayer.terminateStream()
}
},
async playLibraryItem(payload) {
var libraryItemId = payload.libraryItemId
var episodeId = payload.episodeId
console.log('Called playLibraryItem', libraryItemId)
AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId, playWhenReady: true })
.then((data) => {
console.log('Library item play response', JSON.stringify(data))
})
.catch((error) => {
console.error('Failed', error)
})
},
pauseItem() {
if (this.$refs.audioPlayer && !this.$refs.audioPlayer.isPaused) {
this.$refs.audioPlayer.pause()
}
},
onLocalMediaProgressUpdate(localMediaProgress) {
console.log('Got local media progress update', localMediaProgress.progress, JSON.stringify(localMediaProgress))
this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress)
}
},
mounted() {
this.onSleepTimerEndedListener = MyNativeAudio.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
this.onSleepTimerSetListener = MyNativeAudio.addListener('onSleepTimerSet', this.onSleepTimerSet)
this.onLocalMediaProgressUpdateListener = AbsAudioPlayer.addListener('onLocalMediaProgressUpdate', this.onLocalMediaProgressUpdate)
this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet)
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
this.setListeners()
this.$eventBus.$on('close_stream', this.closeStreamOnly)
this.$eventBus.$on('play-item', this.playLibraryItem)
this.$eventBus.$on('pause-item', this.pauseItem)
this.$eventBus.$on('close-stream', this.closeStreamOnly)
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
this.$store.commit('setStreamListener', this.streamUpdated)
},
beforeDestroy() {
if (this.onLocalMediaProgressUpdateListener) this.onLocalMediaProgressUpdateListener.remove()
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove()
if (this.$server.socket) {
this.$server.socket.off('stream_open', this.streamOpen)
this.$server.socket.off('stream_closed', this.streamClosed)
this.$server.socket.off('stream_progress', this.streamProgress)
this.$server.socket.off('stream_ready', this.streamReady)
this.$server.socket.off('stream_reset', this.streamReset)
}
this.$eventBus.$off('close_stream', this.closeStreamOnly)
// if (this.$server.socket) {
// this.$server.socket.off('stream_open', this.streamOpen)
// this.$server.socket.off('stream_closed', this.streamClosed)
// this.$server.socket.off('stream_progress', this.streamProgress)
// this.$server.socket.off('stream_ready', this.streamReady)
// this.$server.socket.off('stream_reset', this.streamReset)
// }
this.$eventBus.$off('play-item', this.playLibraryItem)
this.$eventBus.$off('pause-item', this.pauseItem)
this.$eventBus.$off('close-stream', this.closeStreamOnly)
this.$store.commit('user/removeSettingsListener', 'streamContainer')
this.$store.commit('removeStreamListener')
}
}
</script>
+37 -26
View File
@@ -3,25 +3,31 @@
<div class="absolute top-0 left-0 w-full h-full bg-black transition-opacity duration-200" :class="show ? 'bg-opacity-60 pointer-events-auto' : 'bg-opacity-0'" @click="clickBackground" />
<div class="absolute top-0 right-0 w-64 h-full bg-primary transform transition-transform py-6 pointer-events-auto" :class="show ? '' : 'translate-x-64'" @click.stop>
<div class="px-6 mb-4">
<p v-if="socketConnected" class="text-base">
<p v-if="user" class="text-base">
Welcome,
<strong>{{ username }}</strong>
</p>
</div>
<div class="w-full overflow-y-auto">
<template v-for="item in navItems">
<nuxt-link :to="item.to" :key="item.text" class="w-full hover:bg-bg hover:bg-opacity-60 flex items-center py-3 px-6 text-gray-300">
<nuxt-link :to="item.to" :key="item.text" class="w-full hover:bg-bg hover:bg-opacity-60 flex items-center py-3 px-6 text-gray-300" :class="currentRoutePath.startsWith(item.to) ? 'bg-bg bg-opacity-60' : ''">
<span class="text-lg" :class="item.iconOutlined ? 'material-icons-outlined' : 'material-icons'">{{ item.icon }}</span>
<p class="pl-4">{{ item.text }}</p>
</nuxt-link>
</template>
</div>
<div class="absolute bottom-0 left-0 w-full flex items-center py-6 px-6 text-gray-300">
<p class="text-xs">{{ $config.version }}</p>
<div class="flex-grow" />
<div v-if="socketConnected" class="flex items-center" @click="logout">
<p class="text-xs pr-2">Logout</p>
<span class="material-icons text-sm">logout</span>
<div class="absolute bottom-0 left-0 w-full py-6 px-6 text-gray-300">
<div v-if="serverConnectionConfig" class="mb-4 flex justify-center">
<p class="text-xs">{{ serverConnectionConfig.address }}</p>
</div>
<div class="flex items-center">
<p class="text-xs">{{ $config.version }}</p>
<div class="flex-grow" />
<div v-if="user" class="flex items-center" @click="logout">
<p class="text-xs pr-2">Logout</p>
<span class="material-icons text-sm">logout</span>
</div>
</div>
</div>
</div>
@@ -62,6 +68,9 @@ export default {
user() {
return this.$store.state.user.user
},
serverConnectionConfig() {
return this.$store.state.user.serverConnectionConfig
},
username() {
return this.user ? this.user.username : ''
},
@@ -74,25 +83,9 @@ export default {
icon: 'home',
text: 'Home',
to: '/bookshelf'
},
{
icon: 'person',
text: 'Account',
to: '/account'
},
{
icon: 'folder',
iconOutlined: true,
text: 'Downloads',
to: '/downloads'
}
// {
// icon: 'settings',
// text: 'Settings',
// to: '/config'
// }
]
if (!this.socketConnected) {
if (!this.serverConnectionConfig) {
items = [
{
icon: 'cloud_off',
@@ -100,8 +93,24 @@ export default {
to: '/connect'
}
].concat(items)
} else {
items.push({
icon: 'person',
text: 'Account',
to: '/account'
})
}
items.push({
icon: 'folder',
iconOutlined: true,
text: 'Local Media',
to: '/localMedia/folders'
})
return items
},
currentRoutePath() {
return this.$route.path
}
},
methods: {
@@ -112,7 +121,9 @@ export default {
await this.$axios.$post('/logout').catch((error) => {
console.error(error)
})
this.$server.logout()
this.$socket.logout()
await this.$db.logout()
this.$store.commit('user/logout')
this.$router.push('/connect')
},
touchstart(e) {