mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-25 13:14:33 +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.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.audiobookshelf.app.data.PlaybackSession
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
import com.capacitorjs.plugins.app.AppPlugin
|
import com.capacitorjs.plugins.app.AppPlugin
|
||||||
|
@ -30,7 +31,15 @@ class MyNativeAudio : Plugin() {
|
||||||
|
|
||||||
playerNotificationService.setBridge(bridge)
|
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) {
|
override fun onPlayingUpdate(isPlaying: Boolean) {
|
||||||
emit("onPlayingUpdate", isPlaying)
|
emit("onPlayingUpdate", isPlaying)
|
||||||
}
|
}
|
||||||
|
@ -67,10 +76,9 @@ class MyNativeAudio : Plugin() {
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun prepareLibraryItem(call: PluginCall) {
|
fun prepareLibraryItem(call: PluginCall) {
|
||||||
var libraryItemId = call.getString("libraryItemId", "").toString()
|
var libraryItemId = call.getString("libraryItemId", "").toString()
|
||||||
var mediaEntityId = call.getString("mediaEntityId", "").toString()
|
|
||||||
var playWhenReady = call.getBoolean("playWhenReady") == true
|
var playWhenReady = call.getBoolean("playWhenReady") == true
|
||||||
|
|
||||||
apiHandler.playLibraryItem(libraryItemId) {
|
apiHandler.playLibraryItem(libraryItemId, false) {
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
Log.d(tag, "Preparing Player TEST ${jacksonObjectMapper().writeValueAsString(it)}")
|
Log.d(tag, "Preparing Player TEST ${jacksonObjectMapper().writeValueAsString(it)}")
|
||||||
|
|
|
@ -54,7 +54,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
var isStarted = false
|
var isStarted = false
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MyCustomObjectListener {
|
interface ClientEventEmitter {
|
||||||
|
fun onPlaybackSession(playbackSession:PlaybackSession)
|
||||||
|
fun onPlaybackClosed()
|
||||||
fun onPlayingUpdate(isPlaying: Boolean)
|
fun onPlayingUpdate(isPlaying: Boolean)
|
||||||
fun onMetadata(metadata: JSObject)
|
fun onMetadata(metadata: JSObject)
|
||||||
fun onPrepare(audiobookId: String, playWhenReady: Boolean)
|
fun onPrepare(audiobookId: String, playWhenReady: Boolean)
|
||||||
|
@ -65,7 +67,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
private val tag = "PlayerService"
|
private val tag = "PlayerService"
|
||||||
private val binder = LocalBinder()
|
private val binder = LocalBinder()
|
||||||
|
|
||||||
var listener:MyCustomObjectListener? = null
|
var clientEventEmitter:ClientEventEmitter? = null
|
||||||
|
|
||||||
private lateinit var ctx:Context
|
private lateinit var ctx:Context
|
||||||
private lateinit var mediaSessionConnector: MediaSessionConnector
|
private lateinit var mediaSessionConnector: MediaSessionConnector
|
||||||
|
@ -106,9 +108,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
private var mShakeDetector: ShakeDetector? = null
|
private var mShakeDetector: ShakeDetector? = null
|
||||||
private var shakeSensorUnregisterTask:TimerTask? = null
|
private var shakeSensorUnregisterTask:TimerTask? = null
|
||||||
|
|
||||||
fun setCustomObjectListener(mylistener: MyCustomObjectListener) {
|
|
||||||
listener = mylistener
|
|
||||||
}
|
|
||||||
fun setBridge(bridge: Bridge) {
|
fun setBridge(bridge: Bridge) {
|
||||||
webviewBridge = bridge
|
webviewBridge = bridge
|
||||||
}
|
}
|
||||||
|
@ -380,8 +379,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
||||||
var builder = MediaDescriptionCompat.Builder()
|
var builder = MediaDescriptionCompat.Builder()
|
||||||
.setMediaId(currentPlaybackSession!!.id)
|
.setMediaId(currentPlaybackSession!!.id)
|
||||||
.setTitle(currentPlaybackSession!!.getTitle())
|
.setTitle(currentPlaybackSession!!.displayTitle)
|
||||||
.setSubtitle(currentPlaybackSession!!.getAuthor())
|
.setSubtitle(currentPlaybackSession!!.displayAuthor)
|
||||||
.setMediaUri(currentPlaybackSession!!.getContentUri())
|
.setMediaUri(currentPlaybackSession!!.getContentUri())
|
||||||
.setIconUri(currentPlaybackSession!!.getCoverUri())
|
.setIconUri(currentPlaybackSession!!.getCoverUri())
|
||||||
return builder.build()
|
return builder.build()
|
||||||
|
@ -656,7 +655,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
audiobookProgressSyncer.stop()
|
audiobookProgressSyncer.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
listener?.onPlayingUpdate(player.isPlaying)
|
clientEventEmitter?.onPlayingUpdate(player.isPlaying)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -668,6 +667,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
*/
|
*/
|
||||||
fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean) {
|
fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean) {
|
||||||
currentPlaybackSession = playbackSession
|
currentPlaybackSession = playbackSession
|
||||||
|
|
||||||
|
clientEventEmitter?.onPlaybackSession(playbackSession)
|
||||||
|
|
||||||
var metadata = playbackSession.getMediaMetadataCompat()
|
var metadata = playbackSession.getMediaMetadataCompat()
|
||||||
mediaSession.setMetadata(metadata)
|
mediaSession.setMetadata(metadata)
|
||||||
var mediaMetadata = playbackSession.getExoMediaMetadata()
|
var mediaMetadata = playbackSession.getExoMediaMetadata()
|
||||||
|
@ -679,7 +681,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
if (mPlayer == currentPlayer) {
|
if (mPlayer == currentPlayer) {
|
||||||
var mediaSource:MediaSource
|
var mediaSource:MediaSource
|
||||||
|
|
||||||
if (currentPlaybackSession?.isLocal == true) {
|
if (!playbackSession.isHLS) {
|
||||||
Log.d(tag, "Playing Local File")
|
Log.d(tag, "Playing Local File")
|
||||||
var dataSourceFactory = DefaultDataSourceFactory(ctx, channelId)
|
var dataSourceFactory = DefaultDataSourceFactory(ctx, channelId)
|
||||||
mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
|
mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
|
||||||
|
@ -881,11 +883,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun terminateStream() {
|
fun terminateStream() {
|
||||||
if (currentPlayer.playbackState == Player.STATE_READY) {
|
// if (currentPlayer.playbackState == Player.STATE_READY) {
|
||||||
currentPlayer.clearMediaItems()
|
// currentPlayer.clearMediaItems()
|
||||||
}
|
// }
|
||||||
currentAudiobookStreamData?.id = ""
|
currentPlayer.clearMediaItems()
|
||||||
|
currentPlaybackSession = null
|
||||||
lastPauseTime = 0
|
lastPauseTime = 0
|
||||||
|
clientEventEmitter?.onPlaybackClosed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendClientMetadata(stateName: String) {
|
fun sendClientMetadata(stateName: String) {
|
||||||
|
@ -895,7 +899,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
metadata.put("duration", duration)
|
metadata.put("duration", duration)
|
||||||
metadata.put("currentTime", mPlayer.currentPosition)
|
metadata.put("currentTime", mPlayer.currentPosition)
|
||||||
metadata.put("stateName", stateName)
|
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
|
sleepTimerRunning = true
|
||||||
sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) {
|
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")
|
Log.d(tag, "Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s")
|
||||||
|
|
||||||
if (sleepTimeSecondsRemaining > 0) {
|
if (sleepTimeSecondsRemaining > 0) {
|
||||||
playerNotificationService.listener?.onSleepTimerSet(sleepTimeSecondsRemaining)
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(sleepTimeSecondsRemaining)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sleepTimeSecondsRemaining <= 0) {
|
if (sleepTimeSecondsRemaining <= 0) {
|
||||||
Log.d(tag, "Sleep Timer Pausing Player on Chapter")
|
Log.d(tag, "Sleep Timer Pausing Player on Chapter")
|
||||||
pause()
|
pause()
|
||||||
|
|
||||||
playerNotificationService.listener?.onSleepTimerEnded(getCurrentTime())
|
playerNotificationService.clientEventEmitter?.onSleepTimerEnded(getCurrentTime())
|
||||||
clearSleepTimer()
|
clearSleepTimer()
|
||||||
sleepTimerFinishedAt = System.currentTimeMillis()
|
sleepTimerFinishedAt = System.currentTimeMillis()
|
||||||
} else if (sleepTimeSecondsRemaining <= 30) {
|
} else if (sleepTimeSecondsRemaining <= 30) {
|
||||||
|
@ -136,7 +136,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
||||||
fun cancelSleepTimer() {
|
fun cancelSleepTimer() {
|
||||||
Log.d(tag, "Canceling Sleep Timer")
|
Log.d(tag, "Canceling Sleep Timer")
|
||||||
clearSleepTimer()
|
clearSleepTimer()
|
||||||
playerNotificationService.listener?.onSleepTimerSet(0)
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extendSleepTime() {
|
private fun extendSleepTime() {
|
||||||
|
@ -150,7 +150,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
||||||
if (sleepTimerEndTime > getDuration()) sleepTimerEndTime = getDuration()
|
if (sleepTimerEndTime > getDuration()) sleepTimerEndTime = getDuration()
|
||||||
}
|
}
|
||||||
|
|
||||||
playerNotificationService.listener?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkShouldExtendSleepTimer() {
|
fun checkShouldExtendSleepTimer() {
|
||||||
|
@ -197,7 +197,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
||||||
}
|
}
|
||||||
|
|
||||||
setVolume(1F)
|
setVolume(1F)
|
||||||
playerNotificationService.listener?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decreaseSleepTime(time: Long) {
|
fun decreaseSleepTime(time: Long) {
|
||||||
|
@ -219,6 +219,6 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
||||||
}
|
}
|
||||||
|
|
||||||
setVolume(1F)
|
setVolume(1F)
|
||||||
playerNotificationService.listener?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,8 @@ data class Book(
|
||||||
var metadata:BookMetadata,
|
var metadata:BookMetadata,
|
||||||
var coverPath:String?,
|
var coverPath:String?,
|
||||||
var tags:MutableList<String>,
|
var tags:MutableList<String>,
|
||||||
var audioFiles:MutableList<AudioFile>
|
var audioFiles:MutableList<AudioFile>,
|
||||||
|
var chapters:MutableList<BookChapter>
|
||||||
) : MediaType()
|
) : MediaType()
|
||||||
|
|
||||||
// This auto-detects whether it is a Book or Podcast
|
// This auto-detects whether it is a Book or Podcast
|
||||||
|
@ -154,3 +155,11 @@ data class AudioTrack(
|
||||||
var localFileId:String?,
|
var localFileId:String?,
|
||||||
var audioProbeResult:AudioProbeResult?
|
var audioProbeResult:AudioProbeResult?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
data class BookChapter(
|
||||||
|
var id:Int,
|
||||||
|
var start:Double,
|
||||||
|
var end:Double,
|
||||||
|
var title:String?
|
||||||
|
)
|
||||||
|
|
|
@ -52,11 +52,9 @@ class DbManager : Plugin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveLocalMediaItems(localMediaItems:List<LocalMediaItem>) {
|
fun saveLocalMediaItems(localMediaItems:List<LocalMediaItem>) {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
localMediaItems.map {
|
localMediaItems.map {
|
||||||
Paper.book("localMediaItems").write(it.id, it)
|
Paper.book("localMediaItems").write(it.id, it)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveLocalFolder(localFolder:LocalFolder) {
|
fun saveLocalFolder(localFolder:LocalFolder) {
|
||||||
|
|
|
@ -29,7 +29,7 @@ data class LocalMediaItem(
|
||||||
var absolutePath:String,
|
var absolutePath:String,
|
||||||
var audioTracks:MutableList<AudioTrack>,
|
var audioTracks:MutableList<AudioTrack>,
|
||||||
var localFiles:MutableList<LocalFile>,
|
var localFiles:MutableList<LocalFile>,
|
||||||
var coverPath:String?
|
var coverContentUrl:String?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
|
@ -53,7 +53,7 @@ data class LocalMediaItem(
|
||||||
var sessionId = "play-${UUID.randomUUID()}"
|
var sessionId = "play-${UUID.randomUUID()}"
|
||||||
|
|
||||||
var mediaMetadata = getMediaMetadata()
|
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 userId:String?,
|
||||||
var libraryItemId:String?,
|
var libraryItemId:String?,
|
||||||
var episodeId:String?,
|
var episodeId:String?,
|
||||||
var mediaEntityId:String?,
|
|
||||||
var mediaType:String,
|
var mediaType:String,
|
||||||
var mediaMetadata:MediaTypeMetadata,
|
var mediaMetadata:MediaTypeMetadata,
|
||||||
|
var chapters:MutableList<BookChapter>,
|
||||||
|
var displayTitle: String?,
|
||||||
|
var displayAuthor: String?,
|
||||||
var coverPath:String?,
|
var coverPath:String?,
|
||||||
var duration:Double,
|
var duration:Double,
|
||||||
var playMethod:Int,
|
var playMethod:Int,
|
||||||
|
@ -37,23 +39,9 @@ class PlaybackSession(
|
||||||
val isLocal get() = playMethod == PLAYMETHOD_LOCAL
|
val isLocal get() = playMethod == PLAYMETHOD_LOCAL
|
||||||
val currentTimeMs get() = (currentTime * 1000L).toLong()
|
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
|
@JsonIgnore
|
||||||
fun getCoverUri(): Uri {
|
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)
|
if (coverPath == null) return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
||||||
return Uri.parse("$serverUrl/api/items/$libraryItemId/cover?token=$token")
|
return Uri.parse("$serverUrl/api/items/$libraryItemId/cover?token=$token")
|
||||||
|
@ -75,11 +63,11 @@ class PlaybackSession(
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaMetadataCompat(): MediaMetadataCompat {
|
fun getMediaMetadataCompat(): MediaMetadataCompat {
|
||||||
var metadataBuilder = MediaMetadataCompat.Builder()
|
var metadataBuilder = MediaMetadataCompat.Builder()
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, this.getTitle())
|
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, displayTitle)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, getTitle())
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, this.getAuthor())
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, this.getAuthor())
|
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, displayAuthor)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, this.getAuthor())
|
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, displayAuthor)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "series")
|
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "series")
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||||
return metadataBuilder.build()
|
return metadataBuilder.build()
|
||||||
|
@ -87,13 +75,12 @@ class PlaybackSession(
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getExoMediaMetadata(): MediaMetadata {
|
fun getExoMediaMetadata(): MediaMetadata {
|
||||||
var authorName = this.getAuthor()
|
|
||||||
var metadataBuilder = MediaMetadata.Builder()
|
var metadataBuilder = MediaMetadata.Builder()
|
||||||
.setTitle(this.getTitle())
|
.setTitle(displayTitle)
|
||||||
.setDisplayTitle(this.getTitle())
|
.setDisplayTitle(displayTitle)
|
||||||
.setArtist(authorName)
|
.setArtist(displayAuthor)
|
||||||
.setAlbumArtist(authorName)
|
.setAlbumArtist(displayAuthor)
|
||||||
.setSubtitle(authorName)
|
.setSubtitle(displayAuthor)
|
||||||
|
|
||||||
var contentUri = this.getContentUri()
|
var contentUri = this.getContentUri()
|
||||||
metadataBuilder.setMediaUri(contentUri)
|
metadataBuilder.setMediaUri(contentUri)
|
||||||
|
|
|
@ -11,6 +11,9 @@ import com.arthenica.ffmpegkit.Level
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class FolderScanner(var ctx: Context) {
|
class FolderScanner(var ctx: Context) {
|
||||||
private val tag = "FolderScanner"
|
private val tag = "FolderScanner"
|
||||||
|
@ -69,7 +72,7 @@ class FolderScanner(var ctx: Context) {
|
||||||
var localFiles = mutableListOf<LocalFile>()
|
var localFiles = mutableListOf<LocalFile>()
|
||||||
var index = 1
|
var index = 1
|
||||||
var startOffset = 0.0
|
var startOffset = 0.0
|
||||||
var coverPath:String? = null
|
var coverContentUrl:String? = null
|
||||||
|
|
||||||
var filesInFolder = it.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
var filesInFolder = it.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
||||||
|
|
||||||
|
@ -146,14 +149,14 @@ class FolderScanner(var ctx: Context) {
|
||||||
if (existingLocalFile == null) {
|
if (existingLocalFile == null) {
|
||||||
isNewOrUpdated = true
|
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
|
// Existing media item did not have a cover - cover found on scan
|
||||||
isNewOrUpdated = true
|
isNewOrUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// First image file use as cover path
|
// First image file use as cover path
|
||||||
if (coverPath == null) {
|
if (coverContentUrl == null) {
|
||||||
coverPath = localFile.contentUrl
|
coverContentUrl = localFile.contentUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,7 +173,7 @@ class FolderScanner(var ctx: Context) {
|
||||||
else mediaItemsAdded++
|
else mediaItemsAdded++
|
||||||
|
|
||||||
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
|
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)
|
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()
|
val mapper = jacksonObjectMapper()
|
||||||
var payload = JSObject()
|
var payload = JSObject()
|
||||||
payload.put("mediaPlayer", "exo-player")
|
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) {
|
postRequest("/api/items/$libraryItemId/play", payload) {
|
||||||
it.put("serverUrl", serverUrl)
|
it.put("serverUrl", serverUrl)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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 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">
|
<div class="top-2 left-4 absolute cursor-pointer">
|
||||||
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
|
<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-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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
<p class="text-xl font-mono text-success">{{ sleepTimeRemainingPretty }}</p>
|
<p class="text-xl font-mono text-success">{{ sleepTimeRemainingPretty }}</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
|
@ -84,27 +84,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { Capacitor } from '@capacitor/core'
|
||||||
import MyNativeAudio from '@/plugins/my-native-audio'
|
import MyNativeAudio from '@/plugins/my-native-audio'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
playing: Boolean,
|
playing: Boolean,
|
||||||
libraryItem: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {}
|
|
||||||
},
|
|
||||||
mediaEntity: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {}
|
|
||||||
},
|
|
||||||
download: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {}
|
|
||||||
},
|
|
||||||
bookmarks: {
|
bookmarks: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
@ -114,6 +105,10 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
// Main
|
||||||
|
playbackSession: null,
|
||||||
|
// Others
|
||||||
|
showChapterModal: false,
|
||||||
showCastBtn: false,
|
showCastBtn: false,
|
||||||
showFullscreen: false,
|
showFullscreen: false,
|
||||||
totalDuration: 0,
|
totalDuration: 0,
|
||||||
|
@ -121,9 +116,6 @@ export default {
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
bufferedTime: 0,
|
bufferedTime: 0,
|
||||||
isResetting: false,
|
isResetting: false,
|
||||||
initObject: null,
|
|
||||||
streamId: null,
|
|
||||||
audiobookId: null,
|
|
||||||
stateName: 'idle',
|
stateName: 'idle',
|
||||||
playInterval: null,
|
playInterval: null,
|
||||||
trackWidth: 0,
|
trackWidth: 0,
|
||||||
|
@ -134,15 +126,13 @@ export default {
|
||||||
playedTrackWidth: 0,
|
playedTrackWidth: 0,
|
||||||
seekedTime: 0,
|
seekedTime: 0,
|
||||||
seekLoading: false,
|
seekLoading: false,
|
||||||
|
onPlaybackSessionListener: null,
|
||||||
|
onPlaybackClosedListener: null,
|
||||||
onPlayingUpdateListener: null,
|
onPlayingUpdateListener: null,
|
||||||
onMetadataListener: null,
|
onMetadataListener: null,
|
||||||
// noSyncUpdateTime: false,
|
|
||||||
touchStartY: 0,
|
touchStartY: 0,
|
||||||
touchStartTime: 0,
|
touchStartTime: 0,
|
||||||
touchEndY: 0,
|
touchEndY: 0,
|
||||||
listenTimeInterval: null,
|
|
||||||
listeningTimeSinceLastUpdate: 0,
|
|
||||||
totalListeningTimeInSession: 0,
|
|
||||||
useChapterTrack: false,
|
useChapterTrack: false,
|
||||||
isLoading: true
|
isLoading: true
|
||||||
}
|
}
|
||||||
|
@ -179,28 +169,42 @@ export default {
|
||||||
}
|
}
|
||||||
return this.showFullscreen ? 200 : 60
|
return this.showFullscreen ? 200 : 60
|
||||||
},
|
},
|
||||||
media() {
|
|
||||||
return this.libraryItem.media || {}
|
|
||||||
},
|
|
||||||
mediaMetadata() {
|
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() {
|
title() {
|
||||||
return this.mediaMetadata.title
|
if (this.playbackSession) return this.playbackSession.displayTitle
|
||||||
|
return this.mediaMetadata ? this.mediaMetadata.title : 'Title'
|
||||||
},
|
},
|
||||||
authorName() {
|
authorName() {
|
||||||
return this.mediaMetadata.authorName
|
if (this.playbackSession) return this.playbackSession.displayAuthor
|
||||||
|
return this.mediaMetadata ? this.mediaMetadata.authorName : 'Author'
|
||||||
},
|
},
|
||||||
chapters() {
|
chapters() {
|
||||||
return (this.mediaEntity ? this.mediaEntity.chapters || [] : []).map((chapter) => {
|
if (this.playbackSession && this.playbackSession.chapters) {
|
||||||
var chap = { ...chapter }
|
return this.playbackSession.chapters
|
||||||
chap.start = Number(chap.start)
|
}
|
||||||
chap.end = Number(chap.end)
|
return []
|
||||||
return chap
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
currentChapter() {
|
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)
|
return this.chapters.find((ch) => Number(Number(ch.start).toFixed(2)) <= this.currentTime && Number(Number(ch.end).toFixed(2)) > this.currentTime)
|
||||||
},
|
},
|
||||||
nextChapter() {
|
nextChapter() {
|
||||||
|
@ -213,9 +217,6 @@ export default {
|
||||||
currentChapterDuration() {
|
currentChapterDuration() {
|
||||||
return this.currentChapter ? this.currentChapter.end - this.currentChapter.start : this.totalDuration
|
return this.currentChapter ? this.currentChapter.end - this.currentChapter.start : this.totalDuration
|
||||||
},
|
},
|
||||||
downloadedCover() {
|
|
||||||
return this.download ? this.download.cover : null
|
|
||||||
},
|
|
||||||
totalDurationPretty() {
|
totalDurationPretty() {
|
||||||
return this.$secondsToTimestamp(this.totalDuration)
|
return this.$secondsToTimestamp(this.totalDuration)
|
||||||
},
|
},
|
||||||
|
@ -248,10 +249,6 @@ export default {
|
||||||
if (!this.currentChapter) return 0
|
if (!this.currentChapter) return 0
|
||||||
return this.currentChapter.end - this.currentTime
|
return this.currentChapter.end - this.currentTime
|
||||||
},
|
},
|
||||||
// sleepTimeRemaining() {
|
|
||||||
// if (!this.sleepTimerEndTime) return 0
|
|
||||||
// return Math.max(0, this.sleepTimerEndTime / 1000 - this.currentTime)
|
|
||||||
// },
|
|
||||||
sleepTimeRemainingPretty() {
|
sleepTimeRemainingPretty() {
|
||||||
if (!this.sleepTimeRemaining) return '0s'
|
if (!this.sleepTimeRemaining) return '0s'
|
||||||
var secondsRemaining = Math.round(this.sleepTimeRemaining)
|
var secondsRemaining = Math.round(this.sleepTimeRemaining)
|
||||||
|
@ -263,66 +260,14 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
selectChapter(chapter) {
|
||||||
|
this.seek(chapter.start)
|
||||||
|
this.showChapterModal = false
|
||||||
|
},
|
||||||
castClick() {
|
castClick() {
|
||||||
console.log('Cast Btn Click')
|
console.log('Cast Btn Click')
|
||||||
MyNativeAudio.requestSession()
|
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() {
|
clickContainer() {
|
||||||
this.showFullscreen = true
|
this.showFullscreen = true
|
||||||
|
|
||||||
|
@ -520,94 +465,6 @@ export default {
|
||||||
this.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()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
play() {
|
play() {
|
||||||
MyNativeAudio.playPlayer()
|
MyNativeAudio.playPlayer()
|
||||||
this.startPlayInterval()
|
this.startPlayInterval()
|
||||||
|
@ -619,8 +476,6 @@ export default {
|
||||||
this.isPlaying = false
|
this.isPlaying = false
|
||||||
},
|
},
|
||||||
startPlayInterval() {
|
startPlayInterval() {
|
||||||
this.startListenTimeInterval()
|
|
||||||
|
|
||||||
clearInterval(this.playInterval)
|
clearInterval(this.playInterval)
|
||||||
this.playInterval = setInterval(async () => {
|
this.playInterval = setInterval(async () => {
|
||||||
var data = await MyNativeAudio.getCurrentTime()
|
var data = await MyNativeAudio.getCurrentTime()
|
||||||
|
@ -631,20 +486,14 @@ export default {
|
||||||
}, 1000)
|
}, 1000)
|
||||||
},
|
},
|
||||||
stopPlayInterval() {
|
stopPlayInterval() {
|
||||||
this.cancelListenTimeInterval()
|
|
||||||
clearInterval(this.playInterval)
|
clearInterval(this.playInterval)
|
||||||
},
|
},
|
||||||
resetStream(startTime) {
|
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.isResetting = true
|
||||||
this.initObject.currentTime = _time
|
|
||||||
this.terminateStream()
|
this.terminateStream()
|
||||||
},
|
},
|
||||||
terminateStream() {
|
terminateStream() {
|
||||||
|
if (!this.playbackSession) return
|
||||||
MyNativeAudio.terminateStream()
|
MyNativeAudio.terminateStream()
|
||||||
},
|
},
|
||||||
onPlayingUpdate(data) {
|
onPlayingUpdate(data) {
|
||||||
|
@ -671,17 +520,32 @@ export default {
|
||||||
|
|
||||||
this.timeupdate()
|
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
|
||||||
|
|
||||||
|
// 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() {
|
async init() {
|
||||||
this.useChapterTrack = await this.$localStore.getUseChapterTrack()
|
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.onPlayingUpdateListener = MyNativeAudio.addListener('onPlayingUpdate', this.onPlayingUpdate)
|
||||||
this.onMetadataListener = MyNativeAudio.addListener('onMetadata', this.onMetadata)
|
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)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
handleGesture() {
|
handleGesture() {
|
||||||
var touchDistance = this.touchEndY - this.touchStartY
|
var touchDistance = this.touchEndY - this.touchStartY
|
||||||
|
@ -721,7 +585,7 @@ export default {
|
||||||
})
|
})
|
||||||
this.$localStore.setUseChapterTrack(this.useChapterTrack)
|
this.$localStore.setUseChapterTrack(this.useChapterTrack)
|
||||||
} else if (action === 'close') {
|
} else if (action === 'close') {
|
||||||
this.$emit('close')
|
this.terminateStream()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
forceCloseDropdownMenu() {
|
forceCloseDropdownMenu() {
|
||||||
|
@ -743,6 +607,8 @@ export default {
|
||||||
|
|
||||||
if (this.onPlayingUpdateListener) this.onPlayingUpdateListener.remove()
|
if (this.onPlayingUpdateListener) this.onPlayingUpdateListener.remove()
|
||||||
if (this.onMetadataListener) this.onMetadataListener.remove()
|
if (this.onMetadataListener) this.onMetadataListener.remove()
|
||||||
|
if (this.onPlaybackSessionListener) this.onPlaybackSessionListener.remove()
|
||||||
|
if (this.onPlaybackClosedListener) this.onPlaybackClosedListener.remove()
|
||||||
clearInterval(this.playInterval)
|
clearInterval(this.playInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="libraryItemPlaying" id="streamContainer">
|
<div id="streamContainer">
|
||||||
<app-audio-player
|
<app-audio-player
|
||||||
ref="audioPlayer"
|
ref="audioPlayer"
|
||||||
:playing.sync="isPlaying"
|
:playing.sync="isPlaying"
|
||||||
:library-item="libraryItemPlaying"
|
|
||||||
:media-entity="mediaEntityPlaying"
|
|
||||||
:download="download"
|
|
||||||
:bookmarks="bookmarks"
|
:bookmarks="bookmarks"
|
||||||
:sleep-timer-running="isSleepTimerRunning"
|
:sleep-timer-running="isSleepTimerRunning"
|
||||||
:sleep-time-remaining="sleepTimeRemaining"
|
:sleep-time-remaining="sleepTimeRemaining"
|
||||||
@close="cancelStream"
|
|
||||||
@sync="sync"
|
|
||||||
@setTotalDuration="setTotalDuration"
|
@setTotalDuration="setTotalDuration"
|
||||||
@selectPlaybackSpeed="showPlaybackSpeedModal = true"
|
@selectPlaybackSpeed="showPlaybackSpeedModal = true"
|
||||||
@selectChapter="clickChapterBtn"
|
|
||||||
@updateTime="(t) => (currentTime = t)"
|
@updateTime="(t) => (currentTime = t)"
|
||||||
@showSleepTimer="showSleepTimer"
|
@showSleepTimer="showSleepTimer"
|
||||||
@showBookmarks="showBookmarks"
|
@showBookmarks="showBookmarks"
|
||||||
|
@ -23,14 +17,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-rate.sync="playbackSpeed" @update:playbackRate="updatePlaybackSpeed" @change="changePlaybackSpeed" />
|
<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-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" :audiobook-id="audiobookId" :bookmarks="bookmarks" :current-time="currentTime" @select="selectBookmark" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Dialog } from '@capacitor/dialog'
|
|
||||||
import MyNativeAudio from '@/plugins/my-native-audio'
|
import MyNativeAudio from '@/plugins/my-native-audio'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -40,12 +32,10 @@ export default {
|
||||||
audioPlayerReady: false,
|
audioPlayerReady: false,
|
||||||
stream: null,
|
stream: null,
|
||||||
download: null,
|
download: null,
|
||||||
lastProgressTimeUpdate: 0,
|
|
||||||
showPlaybackSpeedModal: false,
|
showPlaybackSpeedModal: false,
|
||||||
showBookmarksModal: false,
|
showBookmarksModal: false,
|
||||||
showSleepTimerModal: false,
|
showSleepTimerModal: false,
|
||||||
playbackSpeed: 1,
|
playbackSpeed: 1,
|
||||||
showChapterModal: false,
|
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
isSleepTimerRunning: false,
|
isSleepTimerRunning: false,
|
||||||
sleepTimerEndTime: 0,
|
sleepTimerEndTime: 0,
|
||||||
|
@ -66,96 +56,13 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
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() {
|
bookmarks() {
|
||||||
if (!this.userAudiobook) return []
|
// return this.$store.getters['user/getUserBookmarksForItem'](this.)
|
||||||
return this.userAudiobook.bookmarks || []
|
return []
|
||||||
},
|
|
||||||
currentChapter() {
|
|
||||||
if (!this.audiobook || !this.chapters.length) return null
|
|
||||||
return this.chapters.find((ch) => Number(ch.start) <= this.currentTime && Number(ch.end) > this.currentTime)
|
|
||||||
},
|
},
|
||||||
socketConnected() {
|
socketConnected() {
|
||||||
return this.$store.state.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: {
|
methods: {
|
||||||
showBookmarks() {
|
showBookmarks() {
|
||||||
|
@ -174,7 +81,7 @@ export default {
|
||||||
if (currentPosition) {
|
if (currentPosition) {
|
||||||
console.log('Sleep Timer Ended Current Position: ' + currentPosition)
|
console.log('Sleep Timer Ended Current Position: ' + currentPosition)
|
||||||
var currentTime = Math.floor(currentPosition / 1000)
|
var currentTime = Math.floor(currentPosition / 1000)
|
||||||
this.updateTime(currentTime)
|
// TODO: Was syncing to the server here before
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSleepTimerSet({ value: sleepTimeRemaining }) {
|
onSleepTimerSet({ value: sleepTimeRemaining }) {
|
||||||
|
@ -189,8 +96,8 @@ export default {
|
||||||
this.sleepTimeRemaining = sleepTimeRemaining
|
this.sleepTimeRemaining = sleepTimeRemaining
|
||||||
},
|
},
|
||||||
showSleepTimer() {
|
showSleepTimer() {
|
||||||
if (this.currentChapter) {
|
if (this.$refs.audioPlayer && this.$refs.audioPlayer.currentChapter) {
|
||||||
this.currentEndOfChapterTime = Math.floor(this.currentChapter.end)
|
this.currentEndOfChapterTime = Math.floor(this.$refs.audioPlayer.currentChapter.end)
|
||||||
} else {
|
} else {
|
||||||
this.currentEndOfChapterTime = 0
|
this.currentEndOfChapterTime = 0
|
||||||
}
|
}
|
||||||
|
@ -214,85 +121,11 @@ export default {
|
||||||
console.log('Canceling sleep timer')
|
console.log('Canceling sleep timer')
|
||||||
await MyNativeAudio.cancelSleepTimer()
|
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) {
|
setTotalDuration(duration) {
|
||||||
this.totalDuration = duration
|
this.totalDuration = duration
|
||||||
},
|
},
|
||||||
streamClosed(audiobookId) {
|
streamClosed() {
|
||||||
console.log('Stream Closed')
|
console.log('Stream Closed')
|
||||||
if (this.stream.audiobook.id === audiobookId || audiobookId === 'n/a') {
|
|
||||||
this.$store.commit('setStreamAudiobook', null)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
streamProgress(data) {
|
streamProgress(data) {
|
||||||
if (!data.numSegments) return
|
if (!data.numSegments) return
|
||||||
|
@ -308,131 +141,13 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
streamReset({ streamId, startTime }) {
|
streamReset({ streamId, startTime }) {
|
||||||
|
console.log('received stream reset', streamId, startTime)
|
||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
if (this.stream && this.stream.id === streamId) {
|
if (this.stream && this.stream.id === streamId) {
|
||||||
this.$refs.audioPlayer.resetStream(startTime)
|
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) {
|
updatePlaybackSpeed(speed) {
|
||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
console.log(`[AudioPlayerContainer] Update Playback Speed: ${speed}`)
|
console.log(`[AudioPlayerContainer] Update Playback Speed: ${speed}`)
|
||||||
|
@ -463,39 +178,24 @@ export default {
|
||||||
this.$server.socket.on('stream_reset', this.streamReset)
|
this.$server.socket.on('stream_reset', this.streamReset)
|
||||||
},
|
},
|
||||||
closeStreamOnly() {
|
closeStreamOnly() {
|
||||||
// If user logs out or disconnects from server, close audio if streaming
|
// If user logs out or disconnects from server and not playing local
|
||||||
if (!this.download) {
|
if (this.$refs.audioPlayer && !this.$refs.audioPlayer.isLocalPlayMethod) {
|
||||||
this.$store.commit('setStreamAudiobook', null)
|
this.$refs.audioPlayer.terminateStream()
|
||||||
if (this.$refs.audioPlayer) {
|
|
||||||
this.$refs.audioPlayer.terminateStream()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async playLibraryItem(libraryItemId) {
|
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 })
|
MyNativeAudio.prepareLibraryItem({ libraryItemId, playWhenReady: true })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log('TEST library item play response', JSON.stringify(data))
|
console.log('TEST library item play response', JSON.stringify(data))
|
||||||
var mediaEntity = data.mediaEntity
|
|
||||||
this.$store.commit('globals/setMediaEntityPlaying', mediaEntity)
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('TEST failed', error)
|
console.error('TEST failed', error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async playLocalItem(localMediaItemId) {
|
async playLocalItem(localMediaItemId) {
|
||||||
console.log('Called play local media item for lmi', localMediaItemId)
|
|
||||||
MyNativeAudio.playLocalLibraryItem({ localMediaItemId, playWhenReady: true })
|
MyNativeAudio.playLocalLibraryItem({ localMediaItemId, playWhenReady: true })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log('TEST library item play response', JSON.stringify(data))
|
console.log('TEST library item play response', JSON.stringify(data))
|
||||||
var mediaEntity = data.mediaEntity
|
|
||||||
this.$store.commit('globals/setMediaEntityPlaying', mediaEntity)
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('TEST failed', error)
|
console.error('TEST failed', error)
|
||||||
|
@ -512,7 +212,7 @@ export default {
|
||||||
this.setListeners()
|
this.setListeners()
|
||||||
this.$eventBus.$on('play-item', this.playLibraryItem)
|
this.$eventBus.$on('play-item', this.playLibraryItem)
|
||||||
this.$eventBus.$on('play-local-item', this.playLocalItem)
|
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 })
|
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
@ -527,8 +227,8 @@ export default {
|
||||||
this.$server.socket.off('stream_reset', this.streamReset)
|
this.$server.socket.off('stream_reset', this.streamReset)
|
||||||
}
|
}
|
||||||
this.$eventBus.$off('play-item', this.playLibraryItem)
|
this.$eventBus.$off('play-item', this.playLibraryItem)
|
||||||
this.$eventBus.$off('play-local-item', this.playLocalItem)
|
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')
|
this.$store.commit('user/removeSettingsListener', 'streamContainer')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<div class="absolute cover-bg" ref="coverBg" />
|
<div class="absolute cover-bg" ref="coverBg" />
|
||||||
</div>
|
</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">
|
<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>
|
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||||
<div class="absolute top-2 right-2">
|
<div class="absolute top-2 right-2">
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="relative w-full" v-click-outside="clickOutsideObj">
|
<div class="relative w-full" v-click-outside="clickOutsideObj">
|
||||||
<p class="text-sm font-semibold" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
<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">
|
<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 class="block truncate" :class="small ? 'text-sm' : ''">{{ selectedText || placeholder || '' }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
<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() {
|
userLoggedOut() {
|
||||||
// Only cancels stream if streamining not playing downloaded
|
// Only cancels stream if streamining not playing downloaded
|
||||||
this.$eventBus.$emit('close_stream')
|
this.$eventBus.$emit('close-stream')
|
||||||
},
|
},
|
||||||
initSocketListeners() {
|
initSocketListeners() {
|
||||||
if (this.$server.socket) {
|
if (this.$server.socket) {
|
||||||
|
|
|
@ -102,8 +102,8 @@ export default {
|
||||||
// When all items are up-to-date then local media items are not returned
|
// When all items are up-to-date then local media items are not returned
|
||||||
if (response.localMediaItems.length) {
|
if (response.localMediaItems.length) {
|
||||||
this.localMediaItems = response.localMediaItems.map((mi) => {
|
this.localMediaItems = response.localMediaItems.map((mi) => {
|
||||||
if (mi.coverPath) {
|
if (mi.coverContentUrl) {
|
||||||
mi.coverPathSrc = Capacitor.convertFileSrc(mi.coverPath)
|
mi.coverPathSrc = Capacitor.convertFileSrc(mi.coverContentUrl)
|
||||||
}
|
}
|
||||||
return mi
|
return mi
|
||||||
})
|
})
|
||||||
|
@ -123,7 +123,7 @@ export default {
|
||||||
this.localMediaItems = items.map((lmi) => {
|
this.localMediaItems = items.map((lmi) => {
|
||||||
return {
|
return {
|
||||||
...lmi,
|
...lmi,
|
||||||
coverPathSrc: lmi.coverPath ? Capacitor.convertFileSrc(lmi.coverPath) : null
|
coverPathSrc: lmi.coverContentUrl ? Capacitor.convertFileSrc(lmi.coverContentUrl) : null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (this.shouldScan) {
|
if (this.shouldScan) {
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<div v-if="!localFolders.length" class="flex justify-center">
|
<div v-if="!localFolders.length" class="flex justify-center">
|
||||||
<p class="text-center">No Media Folders</p>
|
<p class="text-center">No Media Folders</p>
|
||||||
</div>
|
</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">
|
<div class="flex-grow pr-1">
|
||||||
<ui-dropdown v-model="newFolderMediaType" placeholder="Select media type" :items="mediaTypeItems" />
|
<ui-dropdown v-model="newFolderMediaType" placeholder="Select media type" :items="mediaTypeItems" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,7 +56,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
async selectFolder() {
|
async selectFolder() {
|
||||||
if (!this.newFolderMediaType) {
|
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 })
|
var folderObj = await StorageManager.selectFolder({ mediaType: this.newFolderMediaType })
|
||||||
if (folderObj.error) {
|
if (folderObj.error) {
|
||||||
|
|
|
@ -15,10 +15,18 @@ const BookCoverAspectRatio = {
|
||||||
SQUARE: 1
|
SQUARE: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PlayMethod = {
|
||||||
|
DIRECTPLAY: 0,
|
||||||
|
DIRECTSTREAM: 1,
|
||||||
|
TRANSCODE: 2,
|
||||||
|
LOCAL: 3
|
||||||
|
}
|
||||||
|
|
||||||
const Constants = {
|
const Constants = {
|
||||||
DownloadStatus,
|
DownloadStatus,
|
||||||
CoverDestination,
|
CoverDestination,
|
||||||
BookCoverAspectRatio
|
BookCoverAspectRatio,
|
||||||
|
PlayMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ app }, inject) => {
|
export default ({ app }, inject) => {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
libraryItemPlaying: null,
|
|
||||||
mediaEntityPlaying: null
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
|
@ -29,10 +28,5 @@ export const actions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
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
|
if (!state.user.libraryItemProgress) return null
|
||||||
return state.user.libraryItemProgress.find(li => li.id == libraryItemId)
|
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) => {
|
getUserAudiobookData: (state, getters) => (audiobookId) => {
|
||||||
return getters.getUserAudiobook(audiobookId)
|
return getters.getUserAudiobook(audiobookId)
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue