mirror of
https://github.com/sudoxnym/audiobookshelf-atv.git
synced 2026-07-19 02:04:19 +00:00
Merge pull request #1002 from benonymity/iosChapterTrack
iOS Now Playing Chapter Track (updated #372)
This commit is contained in:
commit
08ad216fe2
12 changed files with 103 additions and 14 deletions
|
|
@ -695,8 +695,8 @@ export default {
|
|||
this.updateTimestamp()
|
||||
this.updateTrack()
|
||||
this.updateReadyTrack()
|
||||
this.updateUseChapterTrack()
|
||||
this.$localStore.setUseTotalTrack(this.useTotalTrack)
|
||||
this.$localStore.setUseChapterTrack(this.useChapterTrack)
|
||||
} else if (action === 'total_track') {
|
||||
this.useTotalTrack = !this.useTotalTrack
|
||||
this.useChapterTrack = !this.useTotalTrack || this.useChapterTrack
|
||||
|
|
@ -704,13 +704,20 @@ export default {
|
|||
this.updateTimestamp()
|
||||
this.updateTrack()
|
||||
this.updateReadyTrack()
|
||||
this.updateUseChapterTrack()
|
||||
this.$localStore.setUseTotalTrack(this.useTotalTrack)
|
||||
this.$localStore.setUseChapterTrack(this.useChapterTrack)
|
||||
} else if (action === 'close') {
|
||||
this.closePlayback()
|
||||
}
|
||||
})
|
||||
},
|
||||
updateUseChapterTrack() {
|
||||
this.$localStore.setUseChapterTrack(this.useChapterTrack)
|
||||
// Chapter track in NowPlaying only supported on iOS for now
|
||||
if (this.$platform === 'ios') {
|
||||
AbsAudioPlayer.setChapterTrack({ enabled: this.useChapterTrack })
|
||||
}
|
||||
},
|
||||
forceCloseDropdownMenu() {
|
||||
if (this.$refs.dropdownMenu && this.$refs.dropdownMenu.closeMenu) {
|
||||
this.$refs.dropdownMenu.closeMenu()
|
||||
|
|
|
|||
|
|
@ -4,17 +4,17 @@ import RealmSwift
|
|||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
|
||||
private let logger = AppLogger(category: "AppDelegate")
|
||||
|
||||
lazy var window: UIWindow? = UIWindow(frame: UIScreen.main.bounds)
|
||||
var backgroundCompletionHandler: (() -> Void)?
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
|
||||
|
||||
let configuration = Realm.Configuration(
|
||||
schemaVersion: 15,
|
||||
schemaVersion: 16,
|
||||
migrationBlock: { [weak self] migration, oldSchemaVersion in
|
||||
if (oldSchemaVersion < 1) {
|
||||
self?.logger.log("Realm schema version was \(oldSchemaVersion)")
|
||||
|
|
@ -48,10 +48,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
newObject?["languageCode"] = "en-us"
|
||||
}
|
||||
}
|
||||
if (oldSchemaVersion < 16) {
|
||||
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding chapterTrack setting")
|
||||
migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in
|
||||
newObject?["chapterTrack"] = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
Realm.Configuration.defaultConfiguration = configuration
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +100,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
// tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
|
||||
|
||||
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
|
||||
// Stores the completion handler for background downloads
|
||||
// The identifier of this method can be ignored at this time as we only have one background url session
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ CAP_PLUGIN(AbsAudioPlayer, "AbsAudioPlayer",
|
|||
CAP_PLUGIN_METHOD(closePlayback, CAPPluginReturnPromise);
|
||||
|
||||
CAP_PLUGIN_METHOD(setPlaybackSpeed, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(setChapterTrack, CAPPluginReturnPromise);
|
||||
|
||||
CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise);
|
||||
|
|
|
|||
|
|
@ -119,6 +119,17 @@ public class AbsAudioPlayer: CAPPlugin {
|
|||
PlayerHandler.setPlaybackSpeed(speed: settings.playbackRate)
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func setChapterTrack(_ call: CAPPluginCall) {
|
||||
let chapterTrack = call.getBool("enabled", true)
|
||||
logger.log(String(chapterTrack))
|
||||
let settings = PlayerSettings.main()
|
||||
try? settings.update {
|
||||
settings.chapterTrack = chapterTrack
|
||||
}
|
||||
PlayerHandler.setChapterTrack()
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func playPlayer(_ call: CAPPluginCall) {
|
||||
PlayerHandler.paused = false
|
||||
|
|
|
|||
|
|
@ -160,3 +160,11 @@ class PlaybackSession: Object, Codable, Deletable {
|
|||
try container.encode(localMediaProgressId, forKey: .localMediaProgressId)
|
||||
}
|
||||
}
|
||||
|
||||
extension PlaybackSession {
|
||||
func getCurrentChapter() -> Chapter? {
|
||||
return chapters.first { chapter in
|
||||
chapter.start <= self.currentTime && chapter.end > self.currentTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ class PlayerSettings: Object {
|
|||
// The webapp has a persisted setting for playback speed, but it's not always available to the native code
|
||||
// Lets track it natively as well, so we never have a situation where the UI and native player are out of sync
|
||||
@Persisted var playbackRate: Float = 1.0
|
||||
@Persisted var chapterTrack: Bool = true
|
||||
|
||||
|
||||
// Singleton pattern for Realm objects
|
||||
static func main() -> PlayerSettings {
|
||||
|
|
|
|||
|
|
@ -39,3 +39,13 @@ class Chapter: EmbeddedObject, Codable {
|
|||
try container.encode(title, forKey: .title)
|
||||
}
|
||||
}
|
||||
|
||||
extension Chapter {
|
||||
func getRelativeChapterCurrentTime(sessionCurrentTime: Double) -> Double {
|
||||
return sessionCurrentTime - self.start
|
||||
}
|
||||
|
||||
func getRelativeChapterEndTime() -> Double {
|
||||
return self.end - self.start
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -224,6 +224,8 @@ class AudioPlayer: NSObject {
|
|||
// Update the UI
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
|
||||
}
|
||||
// Update the now playing and chapter info
|
||||
self.updateNowPlaying()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -443,6 +445,10 @@ class AudioPlayer: NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
public func setChapterTrack() {
|
||||
self.updateNowPlaying()
|
||||
}
|
||||
|
||||
public func getCurrentTime() -> Double? {
|
||||
guard let playbackSession = self.getPlaybackSession() else { return nil }
|
||||
let currentTrackTime = self.audioPlayer.currentTime().seconds
|
||||
|
|
@ -662,7 +668,14 @@ class AudioPlayer: NSObject {
|
|||
return .noSuchContent
|
||||
}
|
||||
|
||||
self?.seek(event.positionTime, from: "remote")
|
||||
// Adjust seek time if chapter track is being used
|
||||
var seekTime = event.positionTime
|
||||
if PlayerSettings.main().chapterTrack {
|
||||
if let session = self?.getPlaybackSession(), let currentChapter = session.getCurrentChapter() {
|
||||
seekTime += currentChapter.start
|
||||
}
|
||||
}
|
||||
self?.seek(seekTime, from: "remote")
|
||||
return .success
|
||||
}
|
||||
|
||||
|
|
@ -679,7 +692,16 @@ class AudioPlayer: NSObject {
|
|||
}
|
||||
private func updateNowPlaying() {
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
|
||||
if let duration = self.getDuration(), let currentTime = self.getCurrentTime() {
|
||||
if let session = self.getPlaybackSession(), let currentChapter = session.getCurrentChapter(), PlayerSettings.main().chapterTrack {
|
||||
NowPlayingInfo.shared.update(
|
||||
duration: currentChapter.getRelativeChapterEndTime(),
|
||||
currentTime: currentChapter.getRelativeChapterCurrentTime(sessionCurrentTime: session.currentTime),
|
||||
rate: rate,
|
||||
chapterName: currentChapter.title,
|
||||
chapterNumber: (session.chapters.firstIndex(of: currentChapter) ?? 0) + 1,
|
||||
chapterCount: session.chapters.count
|
||||
)
|
||||
} else if let duration = self.getDuration(), let currentTime = self.getCurrentTime() {
|
||||
NowPlayingInfo.shared.update(duration: duration, currentTime: currentTime, rate: rate)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@ class PlayerHandler {
|
|||
self.player?.setPlaybackRate(speed)
|
||||
}
|
||||
|
||||
public static func setChapterTrack() {
|
||||
self.player?.setChapterTrack()
|
||||
}
|
||||
|
||||
public static func getSleepTimeRemaining() -> Double? {
|
||||
return self.player?.getSleepTimeRemaining()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class NowPlayingInfo {
|
|||
self.setMetadata(artwork: artwork, metadata: metadata)
|
||||
}
|
||||
}
|
||||
public func update(duration: Double, currentTime: Double, rate: Float) {
|
||||
public func update(duration: Double, currentTime: Double, rate: Float, chapterName: String? = nil, chapterNumber: Int? = nil, chapterCount: Int? = nil) {
|
||||
// Update on the main to prevent access collisions
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
if let self = self {
|
||||
|
|
@ -62,6 +62,18 @@ class NowPlayingInfo {
|
|||
self.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate
|
||||
self.nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0
|
||||
|
||||
|
||||
if let chapterName = chapterName, let chapterNumber = chapterNumber, let chapterCount = chapterCount {
|
||||
self.nowPlayingInfo[MPMediaItemPropertyTitle] = chapterName
|
||||
self.nowPlayingInfo[MPNowPlayingInfoPropertyChapterNumber] = chapterNumber
|
||||
self.nowPlayingInfo[MPNowPlayingInfoPropertyChapterCount] = chapterCount
|
||||
} else {
|
||||
// Set the title back to the book title
|
||||
self.nowPlayingInfo[MPMediaItemPropertyTitle] = self.nowPlayingInfo[MPMediaItemPropertyAlbumTitle]
|
||||
self.nowPlayingInfo[MPNowPlayingInfoPropertyChapterNumber] = nil
|
||||
self.nowPlayingInfo[MPNowPlayingInfoPropertyChapterCount] = nil
|
||||
}
|
||||
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = self.nowPlayingInfo
|
||||
}
|
||||
}
|
||||
|
|
@ -89,7 +101,7 @@ class NowPlayingInfo {
|
|||
|
||||
nowPlayingInfo[MPMediaItemPropertyTitle] = metadata!.title
|
||||
nowPlayingInfo[MPMediaItemPropertyArtist] = metadata!.author ?? "unknown"
|
||||
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.series
|
||||
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.title
|
||||
}
|
||||
|
||||
private func shouldFetchCover(id: String) -> Bool {
|
||||
|
|
|
|||
|
|
@ -141,6 +141,11 @@ class AbsAudioPlayerWeb extends WebPlugin {
|
|||
if (this.player) this.player.playbackRate = value
|
||||
}
|
||||
|
||||
// PluginMethod
|
||||
setChapterTrack({ enabled }) {
|
||||
this.useChapterTrack = enabled
|
||||
}
|
||||
|
||||
// PluginMethod
|
||||
async getCurrentTime() {
|
||||
return {
|
||||
|
|
@ -262,4 +267,4 @@ export { AbsAudioPlayer }
|
|||
export default ({ app, store }, inject) => {
|
||||
$axios = app.$axios
|
||||
vuexStore = store
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -184,4 +184,4 @@ class LocalStorage {
|
|||
|
||||
export default ({ app, store }, inject) => {
|
||||
inject('localStore', new LocalStorage(store))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue