diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt
index 714098fd..b81c67da 100644
--- a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt
+++ b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt
@@ -37,9 +37,9 @@ data class DeviceSettings(
}
@get:JsonIgnore
- val jumpBackwardsTimeMs get() = jumpBackwardsTime * 1000L
+ val jumpBackwardsTimeMs get() = (jumpBackwardsTime ?: default().jumpBackwardsTime) * 1000L
@get:JsonIgnore
- val jumpForwardTimeMs get() = jumpForwardTime * 1000L
+ val jumpForwardTimeMs get() = (jumpForwardTime ?: default().jumpBackwardsTime) * 1000L
}
data class DeviceData(
diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt
index 3809286d..63460461 100644
--- a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt
+++ b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt
@@ -7,10 +7,8 @@ import android.os.Handler
import android.os.Looper
import android.os.Message
import android.support.v4.media.session.MediaSessionCompat
-import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import android.view.KeyEvent
-import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.LibraryItemWrapper
import com.audiobookshelf.app.data.PodcastEpisode
import java.util.*
@@ -21,7 +19,6 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
private var mediaButtonClickCount: Int = 0
var mediaButtonClickTimeout: Long = 1000 //ms
- var seekAmount: Long = 20000 //ms
override fun onPrepare() {
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
@@ -75,19 +72,19 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
}
override fun onSkipToPrevious() {
- playerNotificationService.seekBackward(seekAmount)
+ playerNotificationService.skipToPrevious()
}
override fun onSkipToNext() {
- playerNotificationService.seekForward(seekAmount)
+ playerNotificationService.skipToNext()
}
override fun onFastForward() {
- playerNotificationService.seekForward(seekAmount)
+ playerNotificationService.jumpForward()
}
override fun onRewind() {
- playerNotificationService.seekForward(seekAmount)
+ playerNotificationService.jumpBackward()
}
override fun onSeekTo(pos: Long) {
@@ -179,10 +176,10 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
handleMediaButtonClickCount()
}
KeyEvent.KEYCODE_MEDIA_NEXT -> {
- playerNotificationService.seekForward(seekAmount)
+ playerNotificationService.jumpForward()
}
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
- playerNotificationService.seekBackward(seekAmount)
+ playerNotificationService.jumpBackward()
}
KeyEvent.KEYCODE_MEDIA_STOP -> {
playerNotificationService.closePlayback()
@@ -226,22 +223,22 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
if (2 == msg.what) {
- playerNotificationService.seekBackward(seekAmount)
+ playerNotificationService.jumpBackward()
playerNotificationService.play()
}
else if (msg.what >= 3) {
- playerNotificationService.seekForward(seekAmount)
+ playerNotificationService.jumpForward()
playerNotificationService.play()
}
}
}
- // Example Using a custom action in android auto
-// override fun onCustomAction(action: String?, extras: Bundle?) {
-// super.onCustomAction(action, extras)
-//
-// if ("com.audiobookshelf.app.PLAYBACK_RATE" == action) {
-//
-// }
-// }
+ override fun onCustomAction(action: String?, extras: Bundle?) {
+ super.onCustomAction(action, extras)
+
+ when (action) {
+ CUSTOM_ACTION_JUMP_FORWARD -> onFastForward()
+ CUSTOM_ACTION_JUMP_BACKWARD -> onRewind()
+ }
+ }
}
diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerConstants.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerConstants.kt
new file mode 100644
index 00000000..407425f8
--- /dev/null
+++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerConstants.kt
@@ -0,0 +1,4 @@
+package com.audiobookshelf.app.player
+
+const val CUSTOM_ACTION_JUMP_FORWARD = "com.audiobookshelf.customAction.jump_forward";
+const val CUSTOM_ACTION_JUMP_BACKWARD = "com.audiobookshelf.customAction.jump_backward";
diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt
index eb313b6c..92a0cdde 100644
--- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt
+++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt
@@ -20,7 +20,6 @@ import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
-import androidx.core.content.res.ResourcesCompat
import androidx.media.MediaBrowserServiceCompat
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.BuildConfig
@@ -30,9 +29,11 @@ import com.audiobookshelf.app.data.DeviceInfo
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.media.MediaManager
import com.audiobookshelf.app.server.ApiHandler
+import com.fasterxml.jackson.annotation.JsonIgnore
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
+import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector.CustomActionProvider
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
@@ -42,6 +43,7 @@ import com.google.android.exoplayer2.upstream.*
import java.util.*
import kotlin.concurrent.schedule
+
const val SLEEP_TIMER_WAKE_UP_EXPIRATION = 120000L // 2m
class PlayerNotificationService : MediaBrowserServiceCompat() {
@@ -294,17 +296,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
mediaSessionConnector.setQueueNavigator(queueNavigator)
mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this))
- // Example adding custom action with icon in android auto
-// mediaSessionConnector.setCustomActionProviders(object : MediaSessionConnector.CustomActionProvider {
-// override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
-// }
-// override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
-// var icon = R.drawable.exo_icon_rewind
-// return PlaybackStateCompat.CustomAction.Builder(
-// "com.audiobookshelf.app.PLAYBACK_RATE", "Playback Rate", icon)
-// .build()
-// }
-// })
+ mediaSessionConnector.setCustomActionProviders(
+ JumpForwardCustomActionProvider(),
+ JumpBackwardCustomActionProvider(),
+ )
mediaSession.setCallback(MediaSessionCallback(this))
@@ -320,13 +315,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
1000 * 20 // 20s playback rebuffer
).build()
- val seekBackTime = DeviceManager.deviceData.deviceSettings?.jumpBackwardsTimeMs ?: 10000
- val seekForwardTime = DeviceManager.deviceData.deviceSettings?.jumpForwardTimeMs ?: 10000
-
mPlayer = ExoPlayer.Builder(this)
.setLoadControl(customLoadControl)
- .setSeekBackIncrementMs(seekBackTime)
- .setSeekForwardIncrementMs(seekForwardTime)
+ .setSeekBackIncrementMs(deviceSettings.jumpBackwardsTimeMs)
+ .setSeekForwardIncrementMs(deviceSettings.jumpForwardTimeMs)
.build()
mPlayer.setHandleAudioBecomingNoisy(true)
mPlayer.addListener(PlayerListener(this))
@@ -701,6 +693,22 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
}
+ fun skipToPrevious() {
+ currentPlayer.seekToPrevious()
+ }
+
+ fun skipToNext() {
+ currentPlayer.seekToNext()
+ }
+
+ fun jumpForward() {
+ seekForward(deviceSettings.jumpForwardTimeMs)
+ }
+
+ fun jumpBackward() {
+ seekBackward(deviceSettings.jumpBackwardsTimeMs)
+ }
+
fun seekForward(amount: Long) {
seekPlayer(getCurrentTime() + amount)
}
@@ -757,6 +765,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
return DeviceInfo(Build.MANUFACTURER, Build.MODEL, Build.BRAND, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
}
+ @get:JsonIgnore
+ val deviceSettings get() = DeviceManager.deviceData.deviceSettings ?: DeviceSettings.default()
+
fun getPlayItemRequestPayload(forceTranscode:Boolean):PlayItemRequestPayload {
return PlayItemRequestPayload(getMediaPlayer(), !forceTranscode, forceTranscode, getDeviceInfo())
}
@@ -966,5 +977,39 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
clientEventEmitter?.onNetworkMeteredChanged(unmetered)
}
}
+
+ inner class JumpBackwardCustomActionProvider : CustomActionProvider {
+ override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
+ /*
+ This does not appear to ever get called. Instead, MediaSessionCallback.onCustomAction() is
+ responsible to reacting to a custom action.
+ */
+ }
+
+ override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
+ return PlaybackStateCompat.CustomAction.Builder(
+ CUSTOM_ACTION_JUMP_BACKWARD,
+ getContext().getString(R.string.action_jump_backward),
+ R.drawable.exo_icon_rewind
+ ).build()
+ }
+ }
+
+ inner class JumpForwardCustomActionProvider : CustomActionProvider {
+ override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
+ /*
+ This does not appear to ever get called. Instead, MediaSessionCallback.onCustomAction() is
+ responsible to reacting to a custom action.
+ */
+ }
+
+ override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
+ return PlaybackStateCompat.CustomAction.Builder(
+ CUSTOM_ACTION_JUMP_FORWARD,
+ getContext().getString(R.string.action_jump_forward),
+ R.drawable.exo_icon_fastforward
+ ).build()
+ }
+ }
}
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index b0944308..dee17c07 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -6,4 +6,6 @@
{{ manualTimeoutMin }} min
+{{ timeRemainingPretty }}
@@ -45,7 +67,10 @@ export default { currentEndOfChapterTime: Number }, data() { - return {} + return { + manualTimerModal: null, + manualTimeoutMin: 1 + } }, computed: { show: { @@ -57,7 +82,7 @@ export default { } }, timeouts() { - return [1, 5, 10, 15, 30, 45, 60, 90] + return [5, 10, 15, 30, 45, 60, 90] }, timeRemainingPretty() { return this.$secondsToTimestamp(this.currentTime) @@ -71,6 +96,7 @@ export default { clickedOption(timeoutMin) { var timeout = timeoutMin * 1000 * 60 this.show = false + this.manualTimerModal = false this.$nextTick(() => this.$emit('change', { time: timeout, isChapterTime: false })) }, cancelSleepTimer() { @@ -82,6 +108,12 @@ export default { }, decreaseSleepTime() { this.$emit('decrease') + }, + increaseManualTimeout() { + this.manualTimeoutMin++ + }, + decreaseManualTimeout() { + if (this.manualTimeoutMin > 1) this.manualTimeoutMin-- } }, mounted() {} diff --git a/components/tables/podcast/EpisodeRow.vue b/components/tables/podcast/EpisodeRow.vue index bd2b4a6c..673613f2 100644 --- a/components/tables/podcast/EpisodeRow.vue +++ b/components/tables/podcast/EpisodeRow.vue @@ -193,7 +193,7 @@ export default { episodeId: this.episode.id } if (localFolder) { - this.localFolderId = localFolder.id + payload.localFolderId = localFolder.id } var downloadRes = await AbsDownloader.downloadLibraryItem(payload) if (downloadRes && downloadRes.error) { diff --git a/ios/App/App/plugins/AbsAudioPlayer.swift b/ios/App/App/plugins/AbsAudioPlayer.swift index 6a2a40d5..c376d66a 100644 --- a/ios/App/App/plugins/AbsAudioPlayer.swift +++ b/ios/App/App/plugins/AbsAudioPlayer.swift @@ -38,7 +38,7 @@ public class AbsAudioPlayer: CAPPlugin { do { // Fetch the most recent active session - let activeSession = try await Realm().objects(PlaybackSession.self).where({ $0.isActiveSession == true }).last + let activeSession = try await Realm().objects(PlaybackSession.self).where({ $0.isActiveSession == true }).last?.freeze() if let activeSession = activeSession { await PlayerProgress.shared.syncFromServer() try self.startPlaybackSession(activeSession, playWhenReady: false, playbackRate: PlayerSettings.main().playbackRate) diff --git a/ios/App/Podfile b/ios/App/Podfile index dcdae730..89125e80 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -9,12 +9,12 @@ install! 'cocoapods', :disable_input_output_paths => true def capacitor_pods pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' - pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' - pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog' - pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics' - pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network' - pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' - pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage' + pod 'CapacitorApp', :path => '..\..\node_modules\@capacitor\app' + pod 'CapacitorDialog', :path => '..\..\node_modules\@capacitor\dialog' + pod 'CapacitorHaptics', :path => '..\..\node_modules\@capacitor\haptics' + pod 'CapacitorNetwork', :path => '..\..\node_modules\@capacitor\network' + pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar' + pod 'CapacitorStorage', :path => '..\..\node_modules\@capacitor\storage' end target 'App' do diff --git a/ios/App/Shared/models/local/LocalMediaProgress.swift b/ios/App/Shared/models/local/LocalMediaProgress.swift index 746b2e75..8962673d 100644 --- a/ios/App/Shared/models/local/LocalMediaProgress.swift +++ b/ios/App/Shared/models/local/LocalMediaProgress.swift @@ -155,8 +155,13 @@ extension LocalMediaProgress { static func fetchOrCreateLocalMediaProgress(localMediaProgressId: String?, localLibraryItemId: String?, localEpisodeId: String?) -> LocalMediaProgress? { if let localMediaProgressId = localMediaProgressId { - return Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) - } else if let localLibraryItemId = localLibraryItemId { + // Check if it existing in the database, if not, we need to create it + if let progress = Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) { + return progress + } + } + + if let localLibraryItemId = localLibraryItemId { guard let localLibraryItem = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) else { return nil } let episode = localLibraryItem.getPodcastEpisode(episodeId: localEpisodeId) return LocalMediaProgress(localLibraryItem: localLibraryItem, episode: episode) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 5f9cbc18..15e1f81f 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -220,6 +220,8 @@ class AudioPlayer: NSObject { // MARK: - Methods public func play(allowSeekBack: Bool = false) { + guard self.isInitialized() else { return } + if allowSeekBack { let diffrence = Date.timeIntervalSinceReferenceDate - lastPlayTime var time: Int? @@ -262,6 +264,8 @@ class AudioPlayer: NSObject { } public func pause() { + guard self.isInitialized() else { return } + self.audioPlayer.pause() self.status = 0 self.rate = 0.0 diff --git a/ios/App/Shared/player/PlayerProgress.swift b/ios/App/Shared/player/PlayerProgress.swift index f7f534bc..6b86861f 100644 --- a/ios/App/Shared/player/PlayerProgress.swift +++ b/ios/App/Shared/player/PlayerProgress.swift @@ -123,17 +123,26 @@ class PlayerProgress { } } - private func updateLocalSessionFromServerMediaProgress() async { - NSLog("checkCurrentSessionProgress: Checking if local media progress was updated on server") - guard let session = PlayerHandler.getPlaybackSession()?.freeze() else { return } + private static func updateLocalSessionFromServerMediaProgress() async { + NSLog("updateLocalSessionFromServerMediaProgress: Checking if local media progress was updated on server") + guard let session = try! await Realm().objects(PlaybackSession.self).last(where: { $0.isActiveSession == true })?.freeze() else { + NSLog("updateLocalSessionFromServerMediaProgress: Failed to get session") + return + } // Fetch the current progress let progress = await ApiClient.getMediaProgress(libraryItemId: session.libraryItemId!, episodeId: session.episodeId) - guard let progress = progress else { return } + guard let progress = progress else { + NSLog("updateLocalSessionFromServerMediaProgress: No progress object") + return + } // Determine which session is newer let serverLastUpdate = progress.lastUpdate - guard let localLastUpdate = session.updatedAt else { return } + guard let localLastUpdate = session.updatedAt else { + NSLog("updateLocalSessionFromServerMediaProgress: No local session updatedAt") + return + } let serverCurrentTime = progress.currentTime let localCurrentTime = session.currentTime @@ -142,12 +151,16 @@ class PlayerProgress { // Update the session, if needed if serverIsNewerThanLocal && currentTimeIsDifferent { + NSLog("updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate=\(serverLastUpdate) localLastUpdate=\(localLastUpdate)") guard let session = session.thaw() else { return } session.update { session.currentTime = serverCurrentTime session.updatedAt = serverLastUpdate } + NSLog("updateLocalSessionFromServerMediaProgress: Updated session currentTime newCurrentTime=\(serverCurrentTime) previousCurrentTime=\(localCurrentTime)") PlayerHandler.seek(amount: session.currentTime) + } else { + NSLog("updateLocalSessionFromServerMediaProgress: Local session does not need updating; local has latest progress") } } diff --git a/pages/bookshelf/index.vue b/pages/bookshelf/index.vue index e7cb3855..0806ca74 100644 --- a/pages/bookshelf/index.vue +++ b/pages/bookshelf/index.vue @@ -122,7 +122,7 @@ export default { }) categories = categories.map((cat) => { console.log('[breadcrumb] Personalized category from server', cat.type) - if (cat.type == 'book' || cat.type == 'podcast') { + if (cat.type == 'book' || cat.type == 'podcast' || cat.type == 'episode') { // Map localLibraryItem to entities cat.entities = cat.entities.map((entity) => { var localLibraryItem = this.localLibraryItems.find((lli) => { diff --git a/pages/downloads.vue b/pages/downloads.vue new file mode 100644 index 00000000..52863e3e --- /dev/null +++ b/pages/downloads.vue @@ -0,0 +1,82 @@ + +Downloads ({{ localLibraryItems.length }})
+ +{{ mediaItem.media.metadata.title }}
+{{ mediaItem.media.tracks.length }} Track{{ mediaItem.media.tracks.length == 1 ? '' : 's' }}
+{{ mediaItem.media.episodes.length }} Episode{{ mediaItem.media.episodes.length == 1 ? '' : 's' }}
+{{ $bytesPretty(mediaItem.size) }}
+