Add:iOS audio player

This commit is contained in:
advplyr
2021-12-31 16:57:53 -06:00
parent 4496b1170c
commit 4bf75c606a
24 changed files with 467 additions and 129 deletions
+2
View File
@@ -357,6 +357,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -378,6 +379,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+2
View File
@@ -30,6 +30,7 @@
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
@@ -44,6 +45,7 @@
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
+5 -2
View File
@@ -6,8 +6,11 @@ CAP_PLUGIN(MyNativeAudio, "MyNativeAudio",
CAP_PLUGIN_METHOD(initPlayer, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(seekForward10, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(seekBackward10, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(seekForward, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(seekBackward, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(seekPlayer, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(terminateStream, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getStreamSyncData, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getCurrentTime, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(setPlaybackSpeed, CAPPluginReturnPromise);
)
+278 -15
View File
@@ -1,8 +1,26 @@
import Foundation
import Capacitor
import MediaPlayer
import AVKit
extension UIImageView {
public func imageFromUrl(urlString: String) {
if let url = NSURL(string: urlString) {
let request = NSURLRequest(url: url as URL)
NSURLConnection.sendAsynchronousRequest(request as URLRequest, queue: OperationQueue.main) {
(response: URLResponse?, data: Data?, error: Error?) -> Void in
if let imageData = data as Data? {
self.image = UIImage(data: imageData)
}
}
}
}
}
struct Audiobook {
var streamId = ""
var audiobookId = ""
var title = "No Title"
var author = "Unknown"
var playWhenReady = false
@@ -16,18 +34,43 @@ struct Audiobook {
@objc(MyNativeAudio)
public class MyNativeAudio: CAPPlugin {
var avPlayer: AVPlayer!
var currentCall: CAPPluginCall?
var audioPlayer: AVPlayer!
var audiobook: Audiobook?
enum PlayerState {
case stopped
case playing
case paused
}
private var playerState: PlayerState = .stopped
// Key-value observing context
private var playerItemContext = 0
override public func load() {
NSLog("Load MyNativeAudio")
NotificationCenter.default.addObserver(self, selector: #selector(stop),
name:Notification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(appDidEnterBackground),
name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(appWillEnterForeground),
name: UIApplication.willEnterForegroundNotification, object: nil)
setupRemoteTransportControls()
}
@objc func initPlayer(_ call: CAPPluginCall) {
NSLog("Init Player")
audiobook = Audiobook(
streamId: call.getString("id") ?? "",
audiobookId: call.getString("audiobookId") ?? "",
title: call.getString("title") ?? "No Title",
author: call.getString("author") ?? "Unknown",
playWhenReady: call.getBool("playWhenReady", false),
@@ -50,51 +93,85 @@ public class MyNativeAudio: CAPPlugin {
url: url!,
options: ["AVURLAssetHTTPHeaderFieldsKey": headers]
)
print("Playing audiobook url \(String(describing: url))")
// For play in background
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers, .allowAirPlay])
NSLog("[TEST] Playback OK")
try AVAudioSession.sharedInstance().setActive(true)
NSLog("[TEST] Session is Active")
} catch {
NSLog("[TEST] Failed to set BG Data")
print(error)
}
let playerItem = AVPlayerItem(asset: asset)
self.audioPlayer = AVPlayer(playerItem: playerItem)
// self.audioPlayer = AVPlayer(url: url)
// Register as an observer of the player item's status property
playerItem.addObserver(self,
forKeyPath: #keyPath(AVPlayerItem.status),
options: [.old, .new],
context: &playerItemContext)
self.audioPlayer.play()
self.audioPlayer = AVPlayer(playerItem: playerItem)
let time = self.audioPlayer.currentItem?.currentTime()
print("Audio Player Initialized \(String(describing: time))")
call.resolve(["success": true])
}
@objc func seekForward10() {
@objc func seekForward(_ call: CAPPluginCall) {
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
let duration = self.audioPlayer.currentItem?.duration.seconds ?? 0
let currentTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
var destinationTime = currentTime + 10
var destinationTime = currentTime + amount
if (destinationTime > duration) { destinationTime = duration }
let time = CMTime(seconds:destinationTime,preferredTimescale: 1000)
self.audioPlayer.seek(to: time)
call.resolve()
}
@objc func seekBackward10() {
@objc func seekBackward(_ call: CAPPluginCall) {
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
let currentTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
var destinationTime = currentTime - 10
var destinationTime = currentTime - amount
if (destinationTime < 0) { destinationTime = 0 }
let time = CMTime(seconds:destinationTime,preferredTimescale: 1000)
self.audioPlayer.seek(to: time)
call.resolve()
}
@objc func seekPlayer(_ call: CAPPluginCall) {
var seekTime = call.getInt("timeMs") ?? 0
seekTime /= 1000
var seekTime = (Int(call.getString("timeMs", "0")) ?? 0) / 1000
NSLog("Seek Player \(seekTime)")
if (seekTime < 0) { seekTime = 0 }
let time = CMTime(seconds:Double(seekTime),preferredTimescale: 1000)
self.audioPlayer.seek(to: time)
call.resolve()
}
@objc func pausePlayer() {
self.audioPlayer.pause()
@objc func pausePlayer(_ call: CAPPluginCall) {
pause()
call.resolve()
}
@objc func playPlayer() {
self.audioPlayer.play()
@objc func playPlayer(_ call: CAPPluginCall) {
play()
call.resolve()
}
@objc func terminateStream() {
self.audioPlayer.pause()
@objc func terminateStream(_ call: CAPPluginCall) {
pause()
call.resolve()
}
@objc func stop() {
@@ -103,4 +180,190 @@ public class MyNativeAudio: CAPPlugin {
call.resolve([ "result": true])
}
}
@objc func getCurrentTime(_ call: CAPPluginCall) {
let currTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
let buffTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
NSLog("AVPlayer getCurrentTime \(currTime)")
call.resolve([ "value": currTime * 1000, "bufferedTime": buffTime * 1000 ])
}
@objc func getStreamSyncData(_ call: CAPPluginCall) {
let streamId = audiobook?.streamId ?? ""
call.resolve([ "isPlaying": false, "lastPauseTime": 0, "id": streamId ])
}
@objc func setPlaybackSpeed(_ call: CAPPluginCall) {
let speed = call.getFloat("speed") ?? 0
NSLog("[TEST] Set Playback Speed \(speed)")
audioPlayer.rate = speed
call.resolve()
}
func play() {
audioPlayer.play()
self.notifyListeners("onPlayingUpdate", data: [
"value": true
])
playerState = .playing
setupNowPlaying()
}
func pause() {
audioPlayer.pause()
self.notifyListeners("onPlayingUpdate", data: [
"value": false
])
playerState = .paused
}
func currentTime() -> Double {
return self.audioPlayer.currentItem?.currentTime().seconds ?? 0
}
func duration() -> Double {
return self.audioPlayer.currentItem?.duration.seconds ?? 0
}
func playbackRate() -> Float {
return self.audioPlayer.rate
}
func sendMetadata() {
let currTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
let duration = self.audioPlayer.currentItem?.duration.seconds ?? 0
self.notifyListeners("onMetadata", data: [
"duration": duration * 1000,
"currentTime": currTime * 1000,
"stateName": "unknown"
])
}
public override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
// Only handle observations for the playerItemContext
guard context == &playerItemContext else {
super.observeValue(forKeyPath: keyPath,
of: object,
change: change,
context: context)
return
}
if keyPath == #keyPath(AVPlayerItem.status) {
let status: AVPlayerItem.Status
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayerItem.Status(rawValue: statusNumber.intValue)!
print("AVPlayer Status Change \(String(status.rawValue))")
} else {
status = .unknown
}
// Switch over status value
switch status {
case .readyToPlay:
// Player item is ready to play.
NSLog("AVPlayer ready to play")
setNowPlayingMetadata()
sendMetadata()
if (audiobook?.playWhenReady == true) {
NSLog("AVPlayer playWhenReady == true")
play()
}
break
case .failed:
// Player item failed. See error.
break
case .unknown:
// Player item is not yet ready
break
@unknown default:
break
}
}
}
@objc func appDidEnterBackground() {
setupNowPlaying()
NSLog("[TEST] App Enter Backround")
}
@objc func appWillEnterForeground() {
NSLog("[TEST] App Will Enter Foreground")
}
func setupRemoteTransportControls() {
// Get the shared MPRemoteCommandCenter
let commandCenter = MPRemoteCommandCenter.shared()
// Add handler for Play Command
commandCenter.playCommand.addTarget { [unowned self] event in
NSLog("[TEST] Play Command \(playbackRate())")
if playbackRate() == 0.0 {
play()
return .success
}
return .commandFailed
}
// Add handler for Pause Command
commandCenter.pauseCommand.addTarget { [unowned self] event in
NSLog("[TEST] Pause Command \(playbackRate())")
if playbackRate() == 1.0 {
pause()
return .success
}
return .commandFailed
}
}
func setNowPlayingMetadata() {
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
var nowPlayingInfo = [String: Any]()
NSLog("%@", "**** Set track metadata: title \(audiobook?.title ?? "")")
nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = audiobook?.playlistUrl ?? ""
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = "hls"
nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false
nowPlayingInfo[MPMediaItemPropertyTitle] = audiobook?.title ?? ""
nowPlayingInfo[MPMediaItemPropertyArtist] = audiobook?.author ?? ""
if (audiobook?.cover != nil) {
let myImageView = UIImageView()
myImageView.imageFromUrl(urlString: audiobook?.cover ?? "")
nowPlayingInfo[MPMediaItemPropertyArtwork] = myImageView.image
}
nowPlayingInfo[MPMediaItemPropertyAlbumArtist] = audiobook?.author ?? ""
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = audiobook?.title ?? ""
nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo
}
func setupNowPlaying() {
if (playerState != .playing) {
NSLog("[TEST] Not current playing so not updating now playing info")
return
}
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
var nowPlayingInfo = nowPlayingInfoCenter.nowPlayingInfo ?? [String: Any]()
NSLog("%@", "**** Set playback info: rate \(playbackRate()), position \(currentTime()), duration \(duration())")
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration()
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime()
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = playbackRate()
nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0
nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo
}
}
+7
View File
@@ -9,7 +9,14 @@ 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 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite'
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog'
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage'
pod 'RobingenzCapacitorAppUpdate', :path => '../../node_modules/@robingenz/capacitor-app-update'
pod 'CapacitorDataStorageSqlite', :path => '../../node_modules/capacitor-data-storage-sqlite'
end
target 'App' do