mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-29 07:04:31 +02:00
Rebuilding audio player and handling playback sessions in android. Moving all logic natively
This commit is contained in:
parent
314dc960f2
commit
f70f707100
19 changed files with 180 additions and 596 deletions
|
@ -5,6 +5,7 @@ import android.os.Handler
|
|||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.audiobookshelf.app.data.PlaybackSession
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
import com.capacitorjs.plugins.app.AppPlugin
|
||||
|
@ -30,7 +31,15 @@ class MyNativeAudio : Plugin() {
|
|||
|
||||
playerNotificationService.setBridge(bridge)
|
||||
|
||||
playerNotificationService.setCustomObjectListener(object : PlayerNotificationService.MyCustomObjectListener {
|
||||
playerNotificationService.clientEventEmitter = (object : PlayerNotificationService.ClientEventEmitter {
|
||||
override fun onPlaybackSession(playbackSession: PlaybackSession) {
|
||||
notifyListeners("onPlaybackSession", JSObject(jacksonObjectMapper().writeValueAsString(playbackSession)))
|
||||
}
|
||||
|
||||
override fun onPlaybackClosed() {
|
||||
emit("onPlaybackClosed", true)
|
||||
}
|
||||
|
||||
override fun onPlayingUpdate(isPlaying: Boolean) {
|
||||
emit("onPlayingUpdate", isPlaying)
|
||||
}
|
||||
|
@ -67,10 +76,9 @@ class MyNativeAudio : Plugin() {
|
|||
@PluginMethod
|
||||
fun prepareLibraryItem(call: PluginCall) {
|
||||
var libraryItemId = call.getString("libraryItemId", "").toString()
|
||||
var mediaEntityId = call.getString("mediaEntityId", "").toString()
|
||||
var playWhenReady = call.getBoolean("playWhenReady") == true
|
||||
|
||||
apiHandler.playLibraryItem(libraryItemId) {
|
||||
apiHandler.playLibraryItem(libraryItemId, false) {
|
||||
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
Log.d(tag, "Preparing Player TEST ${jacksonObjectMapper().writeValueAsString(it)}")
|
||||
|
|
|
@ -54,7 +54,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
var isStarted = false
|
||||
}
|
||||
|
||||
interface MyCustomObjectListener {
|
||||
interface ClientEventEmitter {
|
||||
fun onPlaybackSession(playbackSession:PlaybackSession)
|
||||
fun onPlaybackClosed()
|
||||
fun onPlayingUpdate(isPlaying: Boolean)
|
||||
fun onMetadata(metadata: JSObject)
|
||||
fun onPrepare(audiobookId: String, playWhenReady: Boolean)
|
||||
|
@ -65,7 +67,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
private val tag = "PlayerService"
|
||||
private val binder = LocalBinder()
|
||||
|
||||
var listener:MyCustomObjectListener? = null
|
||||
var clientEventEmitter:ClientEventEmitter? = null
|
||||
|
||||
private lateinit var ctx:Context
|
||||
private lateinit var mediaSessionConnector: MediaSessionConnector
|
||||
|
@ -106,9 +108,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
private var mShakeDetector: ShakeDetector? = null
|
||||
private var shakeSensorUnregisterTask:TimerTask? = null
|
||||
|
||||
fun setCustomObjectListener(mylistener: MyCustomObjectListener) {
|
||||
listener = mylistener
|
||||
}
|
||||
fun setBridge(bridge: Bridge) {
|
||||
webviewBridge = bridge
|
||||
}
|
||||
|
@ -380,8 +379,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
||||
var builder = MediaDescriptionCompat.Builder()
|
||||
.setMediaId(currentPlaybackSession!!.id)
|
||||
.setTitle(currentPlaybackSession!!.getTitle())
|
||||
.setSubtitle(currentPlaybackSession!!.getAuthor())
|
||||
.setTitle(currentPlaybackSession!!.displayTitle)
|
||||
.setSubtitle(currentPlaybackSession!!.displayAuthor)
|
||||
.setMediaUri(currentPlaybackSession!!.getContentUri())
|
||||
.setIconUri(currentPlaybackSession!!.getCoverUri())
|
||||
return builder.build()
|
||||
|
@ -656,7 +655,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
audiobookProgressSyncer.stop()
|
||||
}
|
||||
|
||||
listener?.onPlayingUpdate(player.isPlaying)
|
||||
clientEventEmitter?.onPlayingUpdate(player.isPlaying)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -668,6 +667,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
*/
|
||||
fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean) {
|
||||
currentPlaybackSession = playbackSession
|
||||
|
||||
clientEventEmitter?.onPlaybackSession(playbackSession)
|
||||
|
||||
var metadata = playbackSession.getMediaMetadataCompat()
|
||||
mediaSession.setMetadata(metadata)
|
||||
var mediaMetadata = playbackSession.getExoMediaMetadata()
|
||||
|
@ -679,7 +681,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
if (mPlayer == currentPlayer) {
|
||||
var mediaSource:MediaSource
|
||||
|
||||
if (currentPlaybackSession?.isLocal == true) {
|
||||
if (!playbackSession.isHLS) {
|
||||
Log.d(tag, "Playing Local File")
|
||||
var dataSourceFactory = DefaultDataSourceFactory(ctx, channelId)
|
||||
mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
|
||||
|
@ -881,11 +883,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
|
||||
fun terminateStream() {
|
||||
if (currentPlayer.playbackState == Player.STATE_READY) {
|
||||
// if (currentPlayer.playbackState == Player.STATE_READY) {
|
||||
// currentPlayer.clearMediaItems()
|
||||
// }
|
||||
currentPlayer.clearMediaItems()
|
||||
}
|
||||
currentAudiobookStreamData?.id = ""
|
||||
currentPlaybackSession = null
|
||||
lastPauseTime = 0
|
||||
clientEventEmitter?.onPlaybackClosed()
|
||||
}
|
||||
|
||||
fun sendClientMetadata(stateName: String) {
|
||||
|
@ -895,7 +899,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
metadata.put("duration", duration)
|
||||
metadata.put("currentTime", mPlayer.currentPosition)
|
||||
metadata.put("stateName", stateName)
|
||||
listener?.onMetadata(metadata)
|
||||
clientEventEmitter?.onMetadata(metadata)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
|||
}
|
||||
}
|
||||
|
||||
playerNotificationService.listener?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||
|
||||
sleepTimerRunning = true
|
||||
sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) {
|
||||
|
@ -99,14 +99,14 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
|||
Log.d(tag, "Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s")
|
||||
|
||||
if (sleepTimeSecondsRemaining > 0) {
|
||||
playerNotificationService.listener?.onSleepTimerSet(sleepTimeSecondsRemaining)
|
||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(sleepTimeSecondsRemaining)
|
||||
}
|
||||
|
||||
if (sleepTimeSecondsRemaining <= 0) {
|
||||
Log.d(tag, "Sleep Timer Pausing Player on Chapter")
|
||||
pause()
|
||||
|
||||
playerNotificationService.listener?.onSleepTimerEnded(getCurrentTime())
|
||||
playerNotificationService.clientEventEmitter?.onSleepTimerEnded(getCurrentTime())
|
||||
clearSleepTimer()
|
||||
sleepTimerFinishedAt = System.currentTimeMillis()
|
||||
} else if (sleepTimeSecondsRemaining <= 30) {
|
||||
|
@ -136,7 +136,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
|||
fun cancelSleepTimer() {
|
||||
Log.d(tag, "Canceling Sleep Timer")
|
||||
clearSleepTimer()
|
||||
playerNotificationService.listener?.onSleepTimerSet(0)
|
||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0)
|
||||
}
|
||||
|
||||
private fun extendSleepTime() {
|
||||
|
@ -150,7 +150,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
|||
if (sleepTimerEndTime > getDuration()) sleepTimerEndTime = getDuration()
|
||||
}
|
||||
|
||||
playerNotificationService.listener?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||
}
|
||||
|
||||
fun checkShouldExtendSleepTimer() {
|
||||
|
@ -197,7 +197,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
|||
}
|
||||
|
||||
setVolume(1F)
|
||||
playerNotificationService.listener?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||
}
|
||||
|
||||
fun decreaseSleepTime(time: Long) {
|
||||
|
@ -219,6 +219,6 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
|||
}
|
||||
|
||||
setVolume(1F)
|
||||
playerNotificationService.listener?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,8 @@ data class Book(
|
|||
var metadata:BookMetadata,
|
||||
var coverPath:String?,
|
||||
var tags:MutableList<String>,
|
||||
var audioFiles:MutableList<AudioFile>
|
||||
var audioFiles:MutableList<AudioFile>,
|
||||
var chapters:MutableList<BookChapter>
|
||||
) : MediaType()
|
||||
|
||||
// This auto-detects whether it is a Book or Podcast
|
||||
|
@ -154,3 +155,11 @@ data class AudioTrack(
|
|||
var localFileId:String?,
|
||||
var audioProbeResult:AudioProbeResult?
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class BookChapter(
|
||||
var id:Int,
|
||||
var start:Double,
|
||||
var end:Double,
|
||||
var title:String?
|
||||
)
|
||||
|
|
|
@ -52,12 +52,10 @@ class DbManager : Plugin() {
|
|||
}
|
||||
|
||||
fun saveLocalMediaItems(localMediaItems:List<LocalMediaItem>) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
localMediaItems.map {
|
||||
Paper.book("localMediaItems").write(it.id, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveLocalFolder(localFolder:LocalFolder) {
|
||||
Paper.book("localFolders").write(localFolder.id,localFolder)
|
||||
|
|
|
@ -29,7 +29,7 @@ data class LocalMediaItem(
|
|||
var absolutePath:String,
|
||||
var audioTracks:MutableList<AudioTrack>,
|
||||
var localFiles:MutableList<LocalFile>,
|
||||
var coverPath:String?
|
||||
var coverContentUrl:String?
|
||||
) {
|
||||
|
||||
@JsonIgnore
|
||||
|
@ -53,7 +53,7 @@ data class LocalMediaItem(
|
|||
var sessionId = "play-${UUID.randomUUID()}"
|
||||
|
||||
var mediaMetadata = getMediaMetadata()
|
||||
return PlaybackSession(sessionId,null,null,null,null,mediaType,mediaMetadata,null,getDuration(),PLAYMETHOD_LOCAL,audioTracks,0.0,null,this,null,null)
|
||||
return PlaybackSession(sessionId,null,null,null, mediaType, mediaMetadata, mutableListOf(), name, "author name here",null,getDuration(),PLAYMETHOD_LOCAL,audioTracks,0.0,null,this,null,null)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,9 +19,11 @@ class PlaybackSession(
|
|||
var userId:String?,
|
||||
var libraryItemId:String?,
|
||||
var episodeId:String?,
|
||||
var mediaEntityId:String?,
|
||||
var mediaType:String,
|
||||
var mediaMetadata:MediaTypeMetadata,
|
||||
var chapters:MutableList<BookChapter>,
|
||||
var displayTitle: String?,
|
||||
var displayAuthor: String?,
|
||||
var coverPath:String?,
|
||||
var duration:Double,
|
||||
var playMethod:Int,
|
||||
|
@ -37,23 +39,9 @@ class PlaybackSession(
|
|||
val isLocal get() = playMethod == PLAYMETHOD_LOCAL
|
||||
val currentTimeMs get() = (currentTime * 1000L).toLong()
|
||||
|
||||
@JsonIgnore
|
||||
fun getTitle():String {
|
||||
if (mediaMetadata == null) return "Unset"
|
||||
var metadata = mediaMetadata as BookMetadata
|
||||
return metadata.title
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getAuthor():String {
|
||||
if (mediaMetadata == null) return "Unset"
|
||||
var metadata = mediaMetadata as BookMetadata
|
||||
return metadata.authorName ?: "Unset"
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getCoverUri(): Uri {
|
||||
if (localMediaItem?.coverPath != null) return Uri.parse(localMediaItem?.coverPath) ?: Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
||||
if (localMediaItem?.coverContentUrl != null) return Uri.parse(localMediaItem?.coverContentUrl) ?: Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
||||
|
||||
if (coverPath == null) return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
||||
return Uri.parse("$serverUrl/api/items/$libraryItemId/cover?token=$token")
|
||||
|
@ -75,11 +63,11 @@ class PlaybackSession(
|
|||
@JsonIgnore
|
||||
fun getMediaMetadataCompat(): MediaMetadataCompat {
|
||||
var metadataBuilder = MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, this.getTitle())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, getTitle())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, this.getAuthor())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, this.getAuthor())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, this.getAuthor())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, displayTitle)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, displayAuthor)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, displayAuthor)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "series")
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||
return metadataBuilder.build()
|
||||
|
@ -87,13 +75,12 @@ class PlaybackSession(
|
|||
|
||||
@JsonIgnore
|
||||
fun getExoMediaMetadata(): MediaMetadata {
|
||||
var authorName = this.getAuthor()
|
||||
var metadataBuilder = MediaMetadata.Builder()
|
||||
.setTitle(this.getTitle())
|
||||
.setDisplayTitle(this.getTitle())
|
||||
.setArtist(authorName)
|
||||
.setAlbumArtist(authorName)
|
||||
.setSubtitle(authorName)
|
||||
.setTitle(displayTitle)
|
||||
.setDisplayTitle(displayTitle)
|
||||
.setArtist(displayAuthor)
|
||||
.setAlbumArtist(displayAuthor)
|
||||
.setSubtitle(displayAuthor)
|
||||
|
||||
var contentUri = this.getContentUri()
|
||||
metadataBuilder.setMediaUri(contentUri)
|
||||
|
|
|
@ -11,6 +11,9 @@ import com.arthenica.ffmpegkit.Level
|
|||
import com.audiobookshelf.app.data.*
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class FolderScanner(var ctx: Context) {
|
||||
private val tag = "FolderScanner"
|
||||
|
@ -69,7 +72,7 @@ class FolderScanner(var ctx: Context) {
|
|||
var localFiles = mutableListOf<LocalFile>()
|
||||
var index = 1
|
||||
var startOffset = 0.0
|
||||
var coverPath:String? = null
|
||||
var coverContentUrl:String? = null
|
||||
|
||||
var filesInFolder = it.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
||||
|
||||
|
@ -146,14 +149,14 @@ class FolderScanner(var ctx: Context) {
|
|||
if (existingLocalFile == null) {
|
||||
isNewOrUpdated = true
|
||||
}
|
||||
if (existingMediaItem != null && existingMediaItem.coverPath == null) {
|
||||
if (existingMediaItem != null && existingMediaItem.coverContentUrl == null) {
|
||||
// Existing media item did not have a cover - cover found on scan
|
||||
isNewOrUpdated = true
|
||||
}
|
||||
|
||||
// First image file use as cover path
|
||||
if (coverPath == null) {
|
||||
coverPath = localFile.contentUrl
|
||||
if (coverContentUrl == null) {
|
||||
coverContentUrl = localFile.contentUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -170,7 +173,7 @@ class FolderScanner(var ctx: Context) {
|
|||
else mediaItemsAdded++
|
||||
|
||||
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
|
||||
var localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, it.uri.toString(), it.getSimplePath(ctx), it.getAbsolutePath(ctx),audioTracks,localFiles,coverPath)
|
||||
var localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, it.uri.toString(), it.getSimplePath(ctx), it.getAbsolutePath(ctx),audioTracks,localFiles,coverContentUrl)
|
||||
mediaItems.add(localMediaItem)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,11 +109,14 @@ class ApiHandler {
|
|||
}
|
||||
}
|
||||
|
||||
fun playLibraryItem(libraryItemId:String, cb: (PlaybackSession) -> Unit) {
|
||||
fun playLibraryItem(libraryItemId:String, forceTranscode:Boolean, cb: (PlaybackSession) -> Unit) {
|
||||
val mapper = jacksonObjectMapper()
|
||||
var payload = JSObject()
|
||||
payload.put("mediaPlayer", "exo-player")
|
||||
payload.put("forceDirectPlay", true)
|
||||
|
||||
// Only if direct play fails do we force transcode
|
||||
if (!forceTranscode) payload.put("forceDirectPlay", true)
|
||||
else payload.put("forceTranscode", true)
|
||||
|
||||
postRequest("/api/items/$libraryItemId/play", payload) {
|
||||
it.put("serverUrl", serverUrl)
|
||||
|
|
|
@ -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" 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,7 +31,7 @@
|
|||
|
||||
<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 :library-item="libraryItem" :download-cover="downloadedCover" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<covers-book-cover v-if="libraryItem || localMediaItemCoverSrc" :library-item="libraryItem" :download-cover="localMediaItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -52,7 +52,7 @@
|
|||
<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>
|
||||
|
||||
|
@ -84,27 +84,18 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import MyNativeAudio from '@/plugins/my-native-audio'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
playing: Boolean,
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
mediaEntity: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
download: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
bookmarks: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
|
@ -114,6 +105,10 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
// Main
|
||||
playbackSession: null,
|
||||
// Others
|
||||
showChapterModal: false,
|
||||
showCastBtn: false,
|
||||
showFullscreen: false,
|
||||
totalDuration: 0,
|
||||
|
@ -121,9 +116,6 @@ export default {
|
|||
currentTime: 0,
|
||||
bufferedTime: 0,
|
||||
isResetting: false,
|
||||
initObject: null,
|
||||
streamId: null,
|
||||
audiobookId: null,
|
||||
stateName: 'idle',
|
||||
playInterval: null,
|
||||
trackWidth: 0,
|
||||
|
@ -134,15 +126,13 @@ 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,
|
||||
isLoading: true
|
||||
}
|
||||
|
@ -179,28 +169,42 @@ export default {
|
|||
}
|
||||
return this.showFullscreen ? 200 : 60
|
||||
},
|
||||
media() {
|
||||
return this.libraryItem.media || {}
|
||||
},
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
return this.playbackSession ? this.playbackSession.mediaMetadata : null
|
||||
},
|
||||
libraryItem() {
|
||||
return this.playbackSession ? this.playbackSession.libraryItem || null : null
|
||||
},
|
||||
localMediaItem() {
|
||||
return this.playbackSession ? this.playbackSession.localMediaItem || null : null
|
||||
},
|
||||
localMediaItemCoverSrc() {
|
||||
var localMediaItemCover = this.localMediaItem ? this.localMediaItem.coverContentUrl : null
|
||||
if (localMediaItemCover) return Capacitor.convertFileSrc(localMediaItemCover)
|
||||
return null
|
||||
},
|
||||
playMethod() {
|
||||
return this.playbackSession ? this.playbackSession.playMethod : null
|
||||
},
|
||||
isLocalPlayMethod() {
|
||||
return this.playMethod == this.$constants.PlayMethod.LOCAL
|
||||
},
|
||||
title() {
|
||||
return this.mediaMetadata.title
|
||||
if (this.playbackSession) return this.playbackSession.displayTitle
|
||||
return this.mediaMetadata ? this.mediaMetadata.title : 'Title'
|
||||
},
|
||||
authorName() {
|
||||
return this.mediaMetadata.authorName
|
||||
if (this.playbackSession) return this.playbackSession.displayAuthor
|
||||
return this.mediaMetadata ? this.mediaMetadata.authorName : 'Author'
|
||||
},
|
||||
chapters() {
|
||||
return (this.mediaEntity ? this.mediaEntity.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.mediaEntity || !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() {
|
||||
|
@ -213,9 +217,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)
|
||||
},
|
||||
|
@ -248,10 +249,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)
|
||||
|
@ -263,66 +260,14 @@ 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)
|
||||
},
|
||||
clickContainer() {
|
||||
this.showFullscreen = true
|
||||
|
||||
|
@ -520,94 +465,6 @@ export default {
|
|||
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()
|
||||
}
|
||||
},
|
||||
play() {
|
||||
MyNativeAudio.playPlayer()
|
||||
this.startPlayInterval()
|
||||
|
@ -619,8 +476,6 @@ export default {
|
|||
this.isPlaying = false
|
||||
},
|
||||
startPlayInterval() {
|
||||
this.startListenTimeInterval()
|
||||
|
||||
clearInterval(this.playInterval)
|
||||
this.playInterval = setInterval(async () => {
|
||||
var data = await MyNativeAudio.getCurrentTime()
|
||||
|
@ -631,20 +486,14 @@ 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() {
|
||||
if (!this.playbackSession) return
|
||||
MyNativeAudio.terminateStream()
|
||||
},
|
||||
onPlayingUpdate(data) {
|
||||
|
@ -671,17 +520,32 @@ export default {
|
|||
|
||||
this.timeupdate()
|
||||
},
|
||||
async init() {
|
||||
this.useChapterTrack = await this.$localStore.getUseChapterTrack()
|
||||
|
||||
this.onPlayingUpdateListener = MyNativeAudio.addListener('onPlayingUpdate', this.onPlayingUpdate)
|
||||
this.onMetadataListener = MyNativeAudio.addListener('onMetadata', this.onMetadata)
|
||||
// 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
|
||||
|
||||
// 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.showFullscreen = false
|
||||
this.playbackSession = null
|
||||
},
|
||||
async init() {
|
||||
this.useChapterTrack = await this.$localStore.getUseChapterTrack()
|
||||
|
||||
this.onPlaybackSessionListener = MyNativeAudio.addListener('onPlaybackSession', this.onPlaybackSession)
|
||||
this.onPlaybackClosedListener = MyNativeAudio.addListener('onPlaybackClosed', this.onPlaybackClosed)
|
||||
this.onPlayingUpdateListener = MyNativeAudio.addListener('onPlayingUpdate', this.onPlayingUpdate)
|
||||
this.onMetadataListener = MyNativeAudio.addListener('onMetadata', this.onMetadata)
|
||||
},
|
||||
handleGesture() {
|
||||
var touchDistance = this.touchEndY - this.touchStartY
|
||||
|
@ -721,7 +585,7 @@ export default {
|
|||
})
|
||||
this.$localStore.setUseChapterTrack(this.useChapterTrack)
|
||||
} else if (action === 'close') {
|
||||
this.$emit('close')
|
||||
this.terminateStream()
|
||||
}
|
||||
},
|
||||
forceCloseDropdownMenu() {
|
||||
|
@ -743,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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,14 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="libraryItemPlaying" id="streamContainer">
|
||||
<div id="streamContainer">
|
||||
<app-audio-player
|
||||
ref="audioPlayer"
|
||||
:playing.sync="isPlaying"
|
||||
:library-item="libraryItemPlaying"
|
||||
:media-entity="mediaEntityPlaying"
|
||||
:download="download"
|
||||
: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"
|
||||
|
@ -23,14 +17,12 @@
|
|||
</div>
|
||||
|
||||
<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" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
import MyNativeAudio from '@/plugins/my-native-audio'
|
||||
|
||||
export default {
|
||||
|
@ -40,12 +32,10 @@ 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,
|
||||
|
@ -66,96 +56,13 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
libraryItemPlaying() {
|
||||
return this.$store.state.globals.libraryItemPlaying
|
||||
},
|
||||
mediaEntityPlaying() {
|
||||
return this.$store.state.globals.mediaEntityPlaying
|
||||
},
|
||||
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
|
||||
},
|
||||
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() {
|
||||
|
@ -174,7 +81,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 }) {
|
||||
|
@ -189,8 +96,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
|
||||
}
|
||||
|
@ -214,85 +121,11 @@ export default {
|
|||
console.log('Canceling sleep timer')
|
||||
await MyNativeAudio.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
|
||||
|
@ -308,131 +141,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}`)
|
||||
|
@ -463,39 +178,24 @@ export default {
|
|||
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) {
|
||||
// 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(libraryItemId) {
|
||||
var libraryItem = await this.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => {
|
||||
console.error('Failed to fetch full item', error)
|
||||
return null
|
||||
})
|
||||
if (!libraryItem) return
|
||||
this.$store.commit('globals/setLibraryItemPlaying', libraryItem)
|
||||
|
||||
MyNativeAudio.prepareLibraryItem({ libraryItemId, playWhenReady: true })
|
||||
.then((data) => {
|
||||
console.log('TEST library item play response', JSON.stringify(data))
|
||||
var mediaEntity = data.mediaEntity
|
||||
this.$store.commit('globals/setMediaEntityPlaying', mediaEntity)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('TEST failed', error)
|
||||
})
|
||||
},
|
||||
async playLocalItem(localMediaItemId) {
|
||||
console.log('Called play local media item for lmi', localMediaItemId)
|
||||
MyNativeAudio.playLocalLibraryItem({ localMediaItemId, playWhenReady: true })
|
||||
.then((data) => {
|
||||
console.log('TEST library item play response', JSON.stringify(data))
|
||||
var mediaEntity = data.mediaEntity
|
||||
this.$store.commit('globals/setMediaEntityPlaying', mediaEntity)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('TEST failed', error)
|
||||
|
@ -512,7 +212,7 @@ export default {
|
|||
this.setListeners()
|
||||
this.$eventBus.$on('play-item', this.playLibraryItem)
|
||||
this.$eventBus.$on('play-local-item', this.playLocalItem)
|
||||
this.$eventBus.$on('close_stream', this.closeStreamOnly)
|
||||
this.$eventBus.$on('close-stream', this.closeStreamOnly)
|
||||
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
|
||||
},
|
||||
beforeDestroy() {
|
||||
|
@ -528,7 +228,7 @@ export default {
|
|||
}
|
||||
this.$eventBus.$off('play-item', this.playLibraryItem)
|
||||
this.$eventBus.$off('play-local-item', this.playLocalItem)
|
||||
this.$eventBus.$off('close_stream', this.closeStreamOnly)
|
||||
this.$eventBus.$off('close-stream', this.closeStreamOnly)
|
||||
this.$store.commit('user/removeSettingsListener', 'streamContainer')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="absolute cover-bg" ref="coverBg" />
|
||||
</div>
|
||||
|
||||
<img v-if="libraryItem" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
|
||||
<img v-if="fullCoverUrl" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
|
||||
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
||||
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||
<div class="absolute top-2 right-2">
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="relative w-full" v-click-outside="clickOutsideObj">
|
||||
<p class="text-sm font-semibold" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
||||
<button type="button" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left focus:outline-none sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||
<span class="flex items-center">
|
||||
<span class="flex items-center" :class="!selectedText ? 'text-gray-300' : 'text-white'">
|
||||
<span class="block truncate" :class="small ? 'text-sm' : ''">{{ selectedText || placeholder || '' }}</span>
|
||||
</span>
|
||||
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
|
|
|
@ -351,7 +351,7 @@ export default {
|
|||
// },
|
||||
userLoggedOut() {
|
||||
// Only cancels stream if streamining not playing downloaded
|
||||
this.$eventBus.$emit('close_stream')
|
||||
this.$eventBus.$emit('close-stream')
|
||||
},
|
||||
initSocketListeners() {
|
||||
if (this.$server.socket) {
|
||||
|
|
|
@ -102,8 +102,8 @@ export default {
|
|||
// When all items are up-to-date then local media items are not returned
|
||||
if (response.localMediaItems.length) {
|
||||
this.localMediaItems = response.localMediaItems.map((mi) => {
|
||||
if (mi.coverPath) {
|
||||
mi.coverPathSrc = Capacitor.convertFileSrc(mi.coverPath)
|
||||
if (mi.coverContentUrl) {
|
||||
mi.coverPathSrc = Capacitor.convertFileSrc(mi.coverContentUrl)
|
||||
}
|
||||
return mi
|
||||
})
|
||||
|
@ -123,7 +123,7 @@ export default {
|
|||
this.localMediaItems = items.map((lmi) => {
|
||||
return {
|
||||
...lmi,
|
||||
coverPathSrc: lmi.coverPath ? Capacitor.convertFileSrc(lmi.coverPath) : null
|
||||
coverPathSrc: lmi.coverContentUrl ? Capacitor.convertFileSrc(lmi.coverContentUrl) : null
|
||||
}
|
||||
})
|
||||
if (this.shouldScan) {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<div v-if="!localFolders.length" class="flex justify-center">
|
||||
<p class="text-center">No Media Folders</p>
|
||||
</div>
|
||||
<div class="flex p-2 border-t border-primary mt-2">
|
||||
<div class="flex border-t border-primary my-4">
|
||||
<div class="flex-grow pr-1">
|
||||
<ui-dropdown v-model="newFolderMediaType" placeholder="Select media type" :items="mediaTypeItems" />
|
||||
</div>
|
||||
|
@ -56,7 +56,7 @@ export default {
|
|||
methods: {
|
||||
async selectFolder() {
|
||||
if (!this.newFolderMediaType) {
|
||||
return this.$toast.warn('Must select a media type')
|
||||
return this.$toast.error('Must select a media type')
|
||||
}
|
||||
var folderObj = await StorageManager.selectFolder({ mediaType: this.newFolderMediaType })
|
||||
if (folderObj.error) {
|
||||
|
|
|
@ -15,10 +15,18 @@ const BookCoverAspectRatio = {
|
|||
SQUARE: 1
|
||||
}
|
||||
|
||||
const PlayMethod = {
|
||||
DIRECTPLAY: 0,
|
||||
DIRECTSTREAM: 1,
|
||||
TRANSCODE: 2,
|
||||
LOCAL: 3
|
||||
}
|
||||
|
||||
const Constants = {
|
||||
DownloadStatus,
|
||||
CoverDestination,
|
||||
BookCoverAspectRatio
|
||||
BookCoverAspectRatio,
|
||||
PlayMethod
|
||||
}
|
||||
|
||||
export default ({ app }, inject) => {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
export const state = () => ({
|
||||
libraryItemPlaying: null,
|
||||
mediaEntityPlaying: null
|
||||
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
|
@ -29,10 +28,5 @@ export const actions = {
|
|||
}
|
||||
|
||||
export const mutations = {
|
||||
setLibraryItemPlaying(state, libraryItem) {
|
||||
state.libraryItemPlaying = libraryItem
|
||||
},
|
||||
setMediaEntityPlaying(state, mediaEntity) {
|
||||
state.mediaEntityPlaying = mediaEntity
|
||||
}
|
||||
|
||||
}
|
|
@ -24,6 +24,10 @@ export const getters = {
|
|||
if (!state.user.libraryItemProgress) return null
|
||||
return state.user.libraryItemProgress.find(li => li.id == libraryItemId)
|
||||
},
|
||||
getUserBookmarksForItem: (state) => (libraryItemId) => {
|
||||
if (!state.user.bookmarks) return []
|
||||
return state.user.bookmarks.filter(bm => bm.libraryItemId === libraryItemId)
|
||||
},
|
||||
getUserAudiobookData: (state, getters) => (audiobookId) => {
|
||||
return getters.getUserAudiobook(audiobookId)
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue