diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 042cc7ac..561ca0ae 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; }; 3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A200C1427D64D7E00CBF02E /* AudioPlayer.swift */; }; 3AB34053280829BF0039308B /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB34052280829BF0039308B /* Extensions.swift */; }; + 3AB34055280832720039308B /* PlayerEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB34054280832720039308B /* PlayerEvents.swift */; }; 3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */; }; 3ABF580B2805A837005DFBE5 /* PlaybackReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABF580A2805A837005DFBE5 /* PlaybackReport.swift */; }; 3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABF618E2804325C0070250E /* PlayerHandler.swift */; }; @@ -35,6 +36,7 @@ 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = ""; }; 3A200C1427D64D7E00CBF02E /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = ""; }; 3AB34052280829BF0039308B /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + 3AB34054280832720039308B /* PlayerEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerEvents.swift; sourceTree = ""; }; 3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSession.swift; sourceTree = ""; }; 3ABF580A2805A837005DFBE5 /* PlaybackReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackReport.swift; sourceTree = ""; }; 3ABF618E2804325C0070250E /* PlayerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerHandler.swift; sourceTree = ""; }; @@ -129,6 +131,7 @@ 3AD4FCEC28044E6C006DB301 /* Store.swift */, 3AF1970B2806E2590096F747 /* ApiClient.swift */, 3AB34052280829BF0039308B /* Extensions.swift */, + 3AB34054280832720039308B /* PlayerEvents.swift */, ); path = util; sourceTree = ""; @@ -306,6 +309,7 @@ 3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */, 3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */, 3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */, + 3AB34055280832720039308B /* PlayerEvents.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/App/App/plugins/AbsAudioPlayer.m b/ios/App/App/plugins/AbsAudioPlayer.m index 653b0551..2bb2c0c2 100644 --- a/ios/App/App/plugins/AbsAudioPlayer.m +++ b/ios/App/App/plugins/AbsAudioPlayer.m @@ -10,5 +10,17 @@ CAP_PLUGIN(AbsAudioPlayer, "AbsAudioPlayer", CAP_PLUGIN_METHOD(prepareLibraryItem, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(closePlayback, CAPPluginReturnPromise); + + CAP_PLUGIN_METHOD(setPlaybackSpeed, CAPPluginReturnPromise); + + CAP_PLUGIN_METHOD(playPause, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise); + + CAP_PLUGIN_METHOD(seek, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(seekForward, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(seekBackward, CAPPluginReturnPromise); + + CAP_PLUGIN_METHOD(getCurrentTime, CAPPluginReturnPromise); ) - diff --git a/ios/App/App/plugins/AbsAudioPlayer.swift b/ios/App/App/plugins/AbsAudioPlayer.swift index 79bbd2fa..0a5b8f0c 100644 --- a/ios/App/App/plugins/AbsAudioPlayer.swift +++ b/ios/App/App/plugins/AbsAudioPlayer.swift @@ -10,6 +10,11 @@ import Capacitor @objc(AbsAudioPlayer) public class AbsAudioPlayer: CAPPlugin { + override public func load() { + NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(sendPlaybackClosedEvent), name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil) + } + @objc func prepareLibraryItem(_ call: CAPPluginCall) { let libraryItemId = call.getString("libraryItemId") let episodeId = call.getString("episodeId") @@ -24,9 +29,12 @@ public class AbsAudioPlayer: CAPPlugin { return call.resolve() } + sendPrepareMetadataEvent(itemId: libraryItemId!, playWhenReady: playWhenReady) ApiClient.startPlaybackSession(libraryItemId: libraryItemId!, episodeId: episodeId) { session in PlayerHandler.startPlayback(session: session, playWhenReady: playWhenReady) + do { + self.sendPlaybackSession(session: try session.asDictionary()) call.resolve(try session.asDictionary()) } catch(let exception) { NSLog("failed to convert session to json") @@ -34,6 +42,95 @@ public class AbsAudioPlayer: CAPPlugin { call.resolve([:]) } + + self.sendMetadata() } } + @objc func closePlayback(_ call: CAPPluginCall) { + PlayerHandler.stopPlayback() + call.resolve() + } + + @objc func getCurrentTime(_ call: CAPPluginCall) { + call.resolve([ + "value": PlayerHandler.getCurrentTime() ?? 0, + "bufferedTime": PlayerHandler.getCurrentTime() ?? 0, + ]) + } + @objc func setPlaybackSpeed(_ call: CAPPluginCall) { + PlayerHandler.setPlaybackSpeed(speed: call.getFloat("value", 1.0)) + call.resolve() + } + + @objc func playPause(_ call: CAPPluginCall) { + PlayerHandler.playPause() + call.resolve([ "playing": !PlayerHandler.paused() ]) + } + @objc func playPlayer(_ call: CAPPluginCall) { + PlayerHandler.play() + call.resolve() + } + @objc func pausePlayer(_ call: CAPPluginCall) { + PlayerHandler.pause() + call.resolve() + } + + @objc func seek(_ call: CAPPluginCall) { + PlayerHandler.seek(amount: call.getDouble("value", 0.0)) + call.resolve() + } + @objc func seekForward(_ call: CAPPluginCall) { + PlayerHandler.seekForward(amount: call.getDouble("value", 0.0)) + call.resolve() + } + @objc func seekBackward(_ call: CAPPluginCall) { + PlayerHandler.seekBackward(amount: call.getDouble("value", 0.0)) + call.resolve() + } + + @objc func sendMetadata() { + self.notifyListeners("onPlayingUpdate", data: [ "value": !PlayerHandler.paused() ]) + self.notifyListeners("onMetadata", data: PlayerHandler.getMetdata()) + } + @objc func sendPlaybackClosedEvent() { + self.notifyListeners("onPlaybackClosed", data: [ "value": true ]) + } + + @objc func sendPrepareMetadataEvent(itemId: String, playWhenReady: Bool) { + self.notifyListeners("onPrepareMedia", data: [ + "audiobookId": itemId, + "playWhenReady": playWhenReady, + ]) + } + @objc func sendPlaybackSession(session: [String: Any]) { + self.notifyListeners("onPlaybackSession", data: session) + } + + /* + IMPLEMENTED: + + cancelSleepTimer + decreaseSleepTime + increaseSleepTime + getSleepTimerTime + setSleepTimer + * closePlayback + * setPlaybackSpeed + * seekBackward + * seekForward + * seek + * playPause + * playPlayer + * pausePlayer + * getCurrentTime + + * onPlaybackSession + * onPrepareMedia + + * onPlaybackClosed + * onPlayingUpdate + * onMetadata + onSleepTimerEnded + onSleepTimerSet + */ } diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index a6a28dff..26640333 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -78,6 +78,8 @@ class AudioPlayer: NSObject { // DispatchQueue.main.sync { UIApplication.shared.endReceivingRemoteControlEvents() // } + + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil) } // MARK: - Methods @@ -239,6 +241,7 @@ class AudioPlayer: NSObject { } } private func updateNowPlaying() { + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil) NowPlayingInfo.update(duration: getDuration(), currentTime: getCurrentTime(), rate: rate) } diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index e52628f7..eef1cf45 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -21,4 +21,64 @@ class PlayerHandler { self.session = session player = AudioPlayer(playbackSession: session, playWhenReady: playWhenReady) } + public static func stopPlayback() { + player?.destroy() + player = nil + + NowPlayingInfo.reset() + } + + public static func getCurrentTime() -> Double? { + self.player?.getCurrentTime() + } + public static func setPlaybackSpeed(speed: Float) { + self.player?.setPlaybackRate(speed) + } + + public static func play() { + self.player?.play() + } + public static func pause() { + self.player?.play() + } + public static func playPause() { + if paused() { + self.player?.play() + } else { + self.player?.pause() + } + } + + public static func seekForward(amount: Double) { + if player == nil { + return + } + + let destinationTime = player!.getCurrentTime() + amount + player!.seek(destinationTime) + } + public static func seekBackward(amount: Double) { + if player == nil { + return + } + + let destinationTime = player!.getCurrentTime() - amount + player!.seek(destinationTime) + } + public static func seek(amount: Double) { + player?.seek(amount) + } + + public static func paused() -> Bool { + player?.rate == 0.0 + } + + public static func getMetdata() -> [String: Any] { + return [ + "duration": player?.getDuration() ?? 0, + "currentTime": player?.getCurrentTime() ?? 0, + "playerState": !paused(), + "currentRate": player?.rate ?? 0, + ] + } } diff --git a/ios/App/Shared/util/ApiClient.swift b/ios/App/Shared/util/ApiClient.swift index 37faf6a9..b546d244 100644 --- a/ios/App/Shared/util/ApiClient.swift +++ b/ios/App/Shared/util/ApiClient.swift @@ -47,6 +47,13 @@ class ApiClient { ApiClient.postResource(endpoint: endpoint, parameters: [ "forceTranscode": "true", // TODO: direct play "mediaPlayer": "AVPlayer", - ], decodable: PlaybackSession.self, callback: callback) + ], decodable: PlaybackSession.self) { obj in + var session = obj + + session.serverConnectionConfigId = Store.serverConfig.id + session.serverAddress = Store.serverConfig.address + + callback(session) + } } } diff --git a/ios/App/Shared/util/PlayerEvents.swift b/ios/App/Shared/util/PlayerEvents.swift new file mode 100644 index 00000000..5c794e92 --- /dev/null +++ b/ios/App/Shared/util/PlayerEvents.swift @@ -0,0 +1,13 @@ +// +// PlayerEvents.swift +// App +// +// Created by Rasmus Krämer on 14.04.22. +// + +import Foundation + +enum PlayerEvents: String { + case update = "com.audiobookshelf.app.player.update" + case closed = "com.audiobookshelf.app.player.closed" +}