@mustafaj/capacitor-plugin-playlist 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CapacitorPluginPlaylist.podspec +17 -0
  2. package/README.md +248 -0
  3. package/android/.project +34 -0
  4. package/android/build.gradle +69 -0
  5. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  6. package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  7. package/android/gradle.properties +22 -0
  8. package/android/gradlew +251 -0
  9. package/android/gradlew.bat +94 -0
  10. package/android/proguard-rules.pro +21 -0
  11. package/android/settings.gradle +2 -0
  12. package/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java +26 -0
  13. package/android/src/main/AndroidManifest.xml +4 -0
  14. package/android/src/main/java/org/dwbn/plugins/playlist/App.kt +19 -0
  15. package/android/src/main/java/org/dwbn/plugins/playlist/FakeR.kt +39 -0
  16. package/android/src/main/java/org/dwbn/plugins/playlist/OnStatusCallback.kt +34 -0
  17. package/android/src/main/java/org/dwbn/plugins/playlist/OnStatusReportListener.java +7 -0
  18. package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistItemOptions.java +52 -0
  19. package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistPlugin.kt +447 -0
  20. package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioErrorType.java +13 -0
  21. package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioPlayer.java +487 -0
  22. package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioStatusMessage.java +35 -0
  23. package/android/src/main/java/org/dwbn/plugins/playlist/RmxConstants.java +42 -0
  24. package/android/src/main/java/org/dwbn/plugins/playlist/TrackRemovalItem.java +12 -0
  25. package/android/src/main/java/org/dwbn/plugins/playlist/data/AudioTrack.kt +94 -0
  26. package/android/src/main/java/org/dwbn/plugins/playlist/manager/MediaControlsListener.kt +13 -0
  27. package/android/src/main/java/org/dwbn/plugins/playlist/manager/Options.kt +77 -0
  28. package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaylistManager.kt +308 -0
  29. package/android/src/main/java/org/dwbn/plugins/playlist/notification/PlaylistNotificationProvider.kt +26 -0
  30. package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioApi.kt +114 -0
  31. package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioPlaylistHandler.java +146 -0
  32. package/android/src/main/java/org/dwbn/plugins/playlist/playlist/BaseMediaApi.kt +36 -0
  33. package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaImageProvider.kt +83 -0
  34. package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaService.kt +98 -0
  35. package/android/src/main/res/.gitkeep +0 -0
  36. package/android/src/main/res/drawable/ic_closed_caption_white_24dp.xml +9 -0
  37. package/android/src/main/res/drawable/ic_demo_icon_adaptive.xml +15 -0
  38. package/android/src/main/res/drawable/ic_launcher_background.xml +48 -0
  39. package/android/src/main/res/drawable/ic_launcher_foreground.xml +22 -0
  40. package/android/src/main/res/drawable/ic_notification_icon.png +0 -0
  41. package/android/src/main/res/layout/bridge_layout_main.xml +15 -0
  42. package/android/src/main/res/values/colors.xml +3 -0
  43. package/android/src/main/res/values/strings.xml +3 -0
  44. package/android/src/main/res/values/styles.xml +3 -0
  45. package/android/src/test/java/com/getcapacitor/ExampleUnitTest.java +18 -0
  46. package/dist/docs.json +2071 -0
  47. package/dist/esm/Constants.d.ts +164 -0
  48. package/dist/esm/Constants.js +175 -0
  49. package/dist/esm/Constants.js.map +1 -0
  50. package/dist/esm/RmxAudioPlayer.d.ts +181 -0
  51. package/dist/esm/RmxAudioPlayer.js +344 -0
  52. package/dist/esm/RmxAudioPlayer.js.map +1 -0
  53. package/dist/esm/definitions.d.ts +78 -0
  54. package/dist/esm/definitions.js +2 -0
  55. package/dist/esm/definitions.js.map +1 -0
  56. package/dist/esm/index.d.ts +5 -0
  57. package/dist/esm/index.js +6 -0
  58. package/dist/esm/index.js.map +1 -0
  59. package/dist/esm/interfaces.d.ts +246 -0
  60. package/dist/esm/interfaces.js +2 -0
  61. package/dist/esm/interfaces.js.map +1 -0
  62. package/dist/esm/plugin.d.ts +3 -0
  63. package/dist/esm/plugin.js +13 -0
  64. package/dist/esm/plugin.js.map +1 -0
  65. package/dist/esm/utils.d.ts +15 -0
  66. package/dist/esm/utils.js +48 -0
  67. package/dist/esm/utils.js.map +1 -0
  68. package/dist/esm/web.d.ts +54 -0
  69. package/dist/esm/web.js +409 -0
  70. package/dist/esm/web.js.map +1 -0
  71. package/dist/plugin.cjs.js +993 -0
  72. package/dist/plugin.cjs.js.map +1 -0
  73. package/dist/plugin.js +996 -0
  74. package/dist/plugin.js.map +1 -0
  75. package/ios/Plugin/AVBidirectionalQueuePlayer.swift +269 -0
  76. package/ios/Plugin/AudioTrack.swift +63 -0
  77. package/ios/Plugin/Constants.swift +39 -0
  78. package/ios/Plugin/DispatchQueue.swift +47 -0
  79. package/ios/Plugin/Info.plist +24 -0
  80. package/ios/Plugin/Plugin.h +10 -0
  81. package/ios/Plugin/Plugin.m +30 -0
  82. package/ios/Plugin/Plugin.swift +208 -0
  83. package/ios/Plugin/RmxAudioPlayer.swift +1150 -0
  84. package/ios/Plugin.xcodeproj/project.pbxproj +574 -0
  85. package/ios/Plugin.xcworkspace/contents.xcworkspacedata +10 -0
  86. package/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  87. package/ios/PluginTests/Info.plist +22 -0
  88. package/ios/PluginTests/PluginTests.swift +35 -0
  89. package/ios/Podfile +16 -0
  90. package/package.json +89 -0
@@ -0,0 +1,1150 @@
1
+ // Converted to Swift 5.3 by Swiftify v5.3.19197 - https://swiftify.com/
2
+ //
3
+ // RmxAudioPlayer.swift
4
+ // Music Controls Capacitor Plugin
5
+ //
6
+ // Created by Juan Gonzalez on 12/16/16.
7
+ //
8
+
9
+
10
+ import AVFoundation
11
+ import Capacitor
12
+ import MediaPlayer
13
+ import UIKit
14
+
15
+ extension String: Error {}
16
+
17
+ final class RmxAudioPlayer: NSObject {
18
+
19
+ var statusUpdater: StatusUpdater? = nil
20
+
21
+ private var playbackTimeObserver: Any?
22
+ private var wasPlayingInterrupted = false
23
+ private var commandCenterRegistered = false
24
+ private var resetStreamOnPause = false
25
+ private var updatedNowPlayingInfo: [String : Any]?
26
+ private let nowPlayingInfoQueue = DispatchQueue(label: "RMXAudioPlayerNowPlayingQueue")
27
+ private var isReplacingItems = false
28
+ private var isWaitingToStartPlayback = false
29
+ private var loop = false
30
+
31
+ let avQueuePlayer = AVBidirectionalQueuePlayer(items: [])
32
+
33
+ private var lastTrackId: String? = nil
34
+ private var lastRate: Float? = nil
35
+ override init() {
36
+ super.init()
37
+
38
+ activateAudioSession()
39
+ observeLifeCycle()
40
+ }
41
+
42
+ deinit {
43
+ releaseResources()
44
+ }
45
+
46
+ func setOptions(_ options: [String:Any]) {
47
+ print("RmxAudioPlayer.execute=setOptions, \(options)")
48
+ resetStreamOnPause = (options["resetStreamOnPause"] as? NSNumber)?.boolValue ?? false
49
+ }
50
+
51
+ func initialize() {
52
+ print("RmxAudioPlayer.execute=initialize")
53
+
54
+ avQueuePlayer.actionAtItemEnd = .advance
55
+ avQueuePlayer.addObserver(self, forKeyPath: "currentItem", options: .new, context: nil)
56
+ avQueuePlayer.addObserver(self, forKeyPath: "rate", options: .new, context: nil)
57
+ avQueuePlayer.addObserver(self, forKeyPath: "timeControlStatus", options: .new, context: nil)
58
+
59
+ let interval = CMTimeMakeWithSeconds(Float64(1.0), preferredTimescale: Int32(Double(NSEC_PER_SEC)))
60
+ playbackTimeObserver = avQueuePlayer.addPeriodicTimeObserver(forInterval: interval, queue: .main, using: { [weak self] time in
61
+ self?.executePeriodicUpdate(time)
62
+ })
63
+
64
+ onStatus(.rmxstatus_REGISTER, trackId: "INIT", param: nil)
65
+ }
66
+
67
+ func setPlaylistItems(_ items: [AudioTrack], options: [String:Any]) {
68
+ print("RmxAudioPlayer.execute=setPlaylistItems, \(options), \(items.count)")
69
+
70
+ var seekToPosition: Float = 0.0
71
+ let retainPosition = options["retainPosition"] != nil ? (options["retainPosition"] as? Bool) ?? false : false
72
+ let playFromPosition = options["playFromPosition"] != nil ? (options["playFromPosition"] as? Float) ?? 0.0 : 0.0
73
+
74
+ let playFromId = ((options["playFromId"] != nil) ? options["playFromId"] : nil) as? String
75
+
76
+ let startPaused = options["startPaused"] != nil ? (options["startPaused"] as? Bool) ?? false : true
77
+
78
+ if playFromPosition > 0.0 {
79
+ seekToPosition = playFromPosition
80
+ }
81
+ else if retainPosition {
82
+ seekToPosition = getTrackCurrentTime(nil)
83
+ }
84
+
85
+ // Index needs to come from new tracks, so we find it in 'items'
86
+ let result = findTrackIndex(byId: playFromId, items)
87
+ let idx = (result?["index"] as? NSNumber)?.intValue ?? 0
88
+
89
+ setTracks(items, startIndex: idx, startPosition: seekToPosition)
90
+
91
+ // This will wait for the AVPlayerItemStatusReadyToPlay status change, and then trigger playback.
92
+ isWaitingToStartPlayback = !startPaused
93
+ if isWaitingToStartPlayback {
94
+ print("RmxAudioPlayer[setPlaylistItems] will wait for ready event to begin playback")
95
+ }
96
+
97
+ if isWaitingToStartPlayback {
98
+ playCommand(false) // but we will try to preempt it to avoid the button blinking paused.
99
+ }
100
+ }
101
+
102
+ func addItem(_ item: AudioTrack) {
103
+ print("RmxAudioPlayer.execute=addItem, \(item)")
104
+
105
+ let tempArr = [item]
106
+ addTracks(tempArr, startPosition: -1)
107
+ }
108
+ func addAllItems(_ items: [AudioTrack]) {
109
+ addTracks(items, startPosition: -1)
110
+ }
111
+
112
+ func removeItems(_ items: JSArray) -> Int {
113
+ print("RmxAudioPlayer.execute=removeItems, \(items)")
114
+
115
+ var removed = 0
116
+ if items.count > 0 {
117
+ for item in items {
118
+ guard let item = item as? [String: Any] else {
119
+ continue
120
+ }
121
+
122
+ if let id = item["id"] as? String {
123
+ do {
124
+ try removeItem(id)
125
+ removed += 1
126
+ } catch {}
127
+ }
128
+ else if let index = (item["index"] as? NSNumber)?.intValue {
129
+ do {
130
+ try removeItem(index)
131
+ removed += 1
132
+ } catch {}
133
+ }
134
+
135
+ }
136
+ }
137
+
138
+ return removed
139
+ }
140
+
141
+ func clearAllItems() {
142
+ print("RmxAudioPlayer.execute=clearAllItems")
143
+ removeAllTracks()
144
+ }
145
+
146
+ func playTrack(index: Int, positionTime: Float?) throws {
147
+ guard (0..<avQueuePlayer.queuedAudioTracks.count).contains(index) else {
148
+ throw "Provided index is out of bounds"
149
+ }
150
+
151
+ if avQueuePlayer.currentIndex() != index {
152
+ avQueuePlayer.setCurrentIndex(index)
153
+ }
154
+ playCommand(false)
155
+
156
+ if positionTime != nil {
157
+ seek(to: positionTime!, isCommand: false)
158
+ }
159
+ }
160
+
161
+ func playTrack(_ trackId: String, positionTime: Float?) throws {
162
+ guard !avQueuePlayer.queuedAudioTracks.isEmpty else {
163
+ throw "The playlist is empty!"
164
+ }
165
+
166
+ if avQueuePlayer.currentAudioTrack?.trackId != trackId {
167
+ let result = findTrack(byId: trackId)
168
+ let idx = result?["index"] as? Int ?? -1
169
+ guard idx >= 0 else {
170
+ throw "Track ID not found"
171
+ }
172
+ avQueuePlayer.setCurrentIndex(idx)
173
+ }
174
+ playCommand(false)
175
+
176
+ if positionTime != nil {
177
+ seek(to: positionTime!, isCommand: false)
178
+ }
179
+ }
180
+
181
+ func setPlaybackRate(_ rate: Float) {
182
+ avQueuePlayer.rate = rate
183
+ }
184
+
185
+ // Not supporten in IOS ?https://developer.apple.com/documentation/avfoundation/avplayer/1390127-volume
186
+ func setPlaybackVolume(_ volume: Float) {
187
+ avQueuePlayer.volume = volume
188
+ }
189
+
190
+ func setLoopAll(_ loop: Bool) {
191
+ self.loop = loop
192
+
193
+ print("RmxAudioPlayer.execute=setLoopAll, \(loop)")
194
+ }
195
+
196
+
197
+ // MARK: - Capacitor interface
198
+
199
+ ///
200
+ /// Capacitor interface
201
+ ///
202
+ /// These are basically just passing through to the core functionality of the queue and this player.
203
+ ///
204
+ /// These functions don't really do anything interesting by themselves.
205
+ func selectTrack(index: Int) throws {
206
+ guard index >= 0 && index < avQueuePlayer.queuedAudioTracks.count else {
207
+ throw "Index out of Playlist bounds"
208
+ }
209
+ avQueuePlayer.setCurrentIndex(index)
210
+ }
211
+
212
+ func selectTrack(id: String) throws {
213
+ guard !avQueuePlayer.queuedAudioTracks.isEmpty else {
214
+ throw "Queue is Empty"
215
+ }
216
+ let result = findTrack(byId: id)
217
+ let idx = (result?["index"] as? NSNumber)?.intValue ?? 0
218
+
219
+ if idx >= 0 {
220
+ avQueuePlayer.setCurrentIndex(idx)
221
+ }
222
+ }
223
+
224
+ func removeItem(_ index: Int) throws {
225
+ guard index > -1 && index < avQueuePlayer.queuedAudioTracks.count else {
226
+ throw "Index not found"
227
+ }
228
+ let item = avQueuePlayer.queuedAudioTracks[index]
229
+ removeTrackObservers(item)
230
+ avQueuePlayer.remove(item)
231
+ onStatus(.rmxstatus_ITEM_REMOVED, trackId: item.trackId, param: item.toDict())
232
+ }
233
+
234
+ func removeItem(_ id: String) throws {
235
+ let result = findTrack(byId: id)
236
+ let idx = (result?["index"] as? NSNumber)?.intValue ?? 0
237
+ let track = result?["track"] as? AudioTrack
238
+
239
+ guard idx >= 0 else {
240
+ throw "Could not find track by id" + id
241
+ }
242
+ // AudioTrack* item = [self avQueuePlayer].itemsForPlayer[idx];
243
+ removeTrackObservers(track)
244
+
245
+ if let track = track {
246
+ avQueuePlayer.remove(track)
247
+ }
248
+ onStatus(.rmxstatus_ITEM_REMOVED, trackId: track?.trackId, param: track?.toDict())
249
+ }
250
+
251
+ // MARK: - player actions
252
+
253
+ ///
254
+ /// Player actions.
255
+ ///
256
+ /// These are the public API for the player and wrap most of the complexity of the queue.
257
+ func playCommand(_ isCommand: Bool) {
258
+ wasPlayingInterrupted = false
259
+ initializeMPCommandCenter()
260
+
261
+ // Ensure audio session is active before playing
262
+ // This is critical when resuming after video player has deactivated the session
263
+ let audioSession = AVAudioSession.sharedInstance()
264
+ if !audioSession.isOtherAudioPlaying {
265
+ // Only reactivate if no other audio is playing
266
+ do {
267
+ try audioSession.setActive(true)
268
+ } catch {
269
+ print("Warning: Could not activate audio session: \(error.localizedDescription)")
270
+ // Try to reactivate with category setup
271
+ activateAudioSession()
272
+ }
273
+ } else {
274
+ // If other audio is playing, ensure our category is set correctly
275
+ activateAudioSession()
276
+ }
277
+
278
+ if resetStreamOnPause,
279
+ let currentTrack = avQueuePlayer.currentAudioTrack,
280
+ currentTrack.isStream {
281
+ print( "music-stream-play")
282
+ avQueuePlayer.seek(to: .positiveInfinity, toleranceBefore: .zero, toleranceAfter: .zero)
283
+ currentTrack.seek(to: .positiveInfinity, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: nil)
284
+ }
285
+
286
+ print( "music-controls-play ")
287
+
288
+ avQueuePlayer.play()
289
+ }
290
+
291
+ func pauseCommand(_ isCommand: Bool) {
292
+ wasPlayingInterrupted = false
293
+ initializeMPCommandCenter()
294
+ avQueuePlayer.pause()
295
+
296
+ // When the track is a stream, we do not want it to hold the buffer at the current location;
297
+ // it does in fact continue buffering afterwards but the buffer on iOS is rather small, so you'll end up
298
+ // reaching a point where you jump forward in time however long you were paused.
299
+ // The correct behavior for streams is to pick up at the current LIVE point in the stream, which we accomplish
300
+ // by seeking to the "end" of the stream.
301
+ if resetStreamOnPause,
302
+ let currentTrack = avQueuePlayer.currentAudioTrack,
303
+ currentTrack.isStream {
304
+ avQueuePlayer.seek(to: .positiveInfinity, toleranceBefore: .zero, toleranceAfter: .zero)
305
+ currentTrack.seek(to: .positiveInfinity, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: nil)
306
+ }
307
+
308
+ if isCommand {
309
+ let action = "music-controls-pause"
310
+ print("\(action)")
311
+ }
312
+ }
313
+
314
+ func playPrevious(_ isCommand: Bool) {
315
+ wasPlayingInterrupted = false
316
+ initializeMPCommandCenter()
317
+
318
+ avQueuePlayer.playPreviousItem()
319
+
320
+ if isCommand {
321
+ let action = "music-controls-previous"
322
+ print("\(action)")
323
+
324
+ let playerItem = avQueuePlayer.currentAudioTrack
325
+ var param: [String : Any]? = nil
326
+ if let to = playerItem?.toDict() {
327
+ param = [
328
+ "currentIndex": NSNumber(value: avQueuePlayer.currentIndex() ?? 0),
329
+ "currentItem": to
330
+ ]
331
+ }
332
+ onStatus(.rmx_STATUS_SKIP_BACK, trackId: playerItem?.trackId, param: param)
333
+ }
334
+ }
335
+
336
+ func playNext(_ isCommand: Bool) {
337
+ wasPlayingInterrupted = false
338
+ initializeMPCommandCenter()
339
+
340
+ avQueuePlayer.advanceToNextItem()
341
+
342
+ if isCommand {
343
+ let action = "music-controls-next"
344
+ print("\(action)")
345
+
346
+ let playerItem = avQueuePlayer.currentAudioTrack
347
+ var param: [String : Any]? = nil
348
+ if let to = playerItem?.toDict() {
349
+ param = [
350
+ "currentIndex": NSNumber(value: avQueuePlayer.currentIndex() ?? 0),
351
+ "currentItem": to
352
+ ]
353
+ }
354
+ onStatus(.rmx_STATUS_SKIP_FORWARD, trackId: playerItem?.trackId, param: param)
355
+ }
356
+ }
357
+
358
+ func seek(to positionTime: Float, isCommand: Bool) {
359
+ //Handle seeking with the progress slider on lockscreen or control center
360
+ wasPlayingInterrupted = false
361
+ initializeMPCommandCenter()
362
+
363
+ let seekToTime = CMTimeMakeWithSeconds(Float64(positionTime), preferredTimescale: 1000)
364
+ avQueuePlayer.seek(to: seekToTime, toleranceBefore: .zero, toleranceAfter: .zero)
365
+
366
+ let action = "music-controls-seek-to"
367
+ print(String(format: "%@ %.3f", action, positionTime))
368
+
369
+ // Always fire seek event, not just for commands
370
+ let playerItem = avQueuePlayer.currentAudioTrack
371
+ onStatus(.rmxstatus_SEEK, trackId: playerItem?.trackId, param: [
372
+ "position": NSNumber(value: positionTime)
373
+ ])
374
+ }
375
+
376
+ func setVolume(_ volume: Float) {
377
+ avQueuePlayer.volume = volume
378
+ }
379
+
380
+ func addTracks(_ tracks: [AudioTrack], startPosition: Float) {
381
+ for playerItem in tracks {
382
+ addTrackObservers(playerItem)
383
+ }
384
+
385
+ avQueuePlayer.appendItems(tracks)
386
+
387
+ if startPosition > 0 {
388
+ seek(to: startPosition, isCommand: false)
389
+ }
390
+ }
391
+
392
+ func setTracks(_ tracks: [AudioTrack], startIndex: Int, startPosition: Float) {
393
+ avQueuePlayer.removeAllTrackObservers()
394
+
395
+ isReplacingItems = true
396
+ print("RmxAudioPlayer[setTracks] replacing tracks ")
397
+ avQueuePlayer.replaceAllItems(with: tracks)
398
+
399
+ print("RmxAudioPlayer[setTracks] replacing finished ")
400
+ for playerItem in tracks {
401
+ addTrackObservers(playerItem)
402
+ }
403
+
404
+ isReplacingItems = false
405
+ print("RmxAudioPlayer[setTracks] added track observers ")
406
+ if !avQueuePlayer.queuedAudioTracks.isEmpty {
407
+ if startIndex >= 0 {
408
+ avQueuePlayer.setCurrentIndex(startIndex)
409
+ }
410
+ }
411
+
412
+ if startPosition > 0 {
413
+ seek(to: startPosition, isCommand: false)
414
+ }
415
+ }
416
+
417
+ func removeAllTracks() {
418
+ for item in avQueuePlayer.queuedAudioTracks {
419
+ removeTrackObservers(item)
420
+ onStatus(.rmxstatus_ITEM_REMOVED, trackId: item.trackId, param: item.toDict())
421
+ }
422
+
423
+ avQueuePlayer.removeAllItems()
424
+ wasPlayingInterrupted = false
425
+
426
+ // Clear lock screen player info when playlist is cleared
427
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
428
+ }
429
+
430
+ // MARK: - remote control events
431
+
432
+ ///
433
+ /// Events - receive events from the iOS remote controls and command center.
434
+ @objc func play(_ event: MPRemoteCommandEvent?) -> MPRemoteCommandHandlerStatus {
435
+ playCommand(true)
436
+ return .success
437
+ }
438
+
439
+ @objc func pause(_ event: MPRemoteCommandEvent?) -> MPRemoteCommandHandlerStatus {
440
+ pauseCommand(true)
441
+ return .success
442
+ }
443
+
444
+ @objc func togglePlayPauseTrackEvent(_ event: MPRemoteCommandEvent?) -> MPRemoteCommandHandlerStatus {
445
+ if avQueuePlayer.isPlaying {
446
+ pauseCommand(true)
447
+ } else {
448
+ playCommand(true)
449
+ }
450
+ return .success
451
+ }
452
+
453
+ @objc func prevTrackEvent(_ event: MPRemoteCommandEvent?) -> MPRemoteCommandHandlerStatus {
454
+ playPrevious(true)
455
+ return .success
456
+ }
457
+
458
+ @objc func nextTrackEvent(_ event: MPRemoteCommandEvent?) -> MPRemoteCommandHandlerStatus {
459
+ playNext(true)
460
+ return .success
461
+ }
462
+
463
+ @objc func changedThumbSlider(onLockScreen event: MPChangePlaybackPositionCommandEvent?) -> MPRemoteCommandHandlerStatus {
464
+ seek(to: Float(event?.positionTime ?? 0.0), isCommand: true)
465
+ return .success
466
+ }
467
+
468
+ // MARK: - notifications
469
+
470
+ ///
471
+ /// Notifications
472
+ ///
473
+ /// These handle the events raised by the queue and the player items.
474
+ @objc func itemStalledPlaying(_ notification: Notification?) {
475
+ // This happens when the network is insufficient to continue playback.
476
+ let playerItem = avQueuePlayer.currentAudioTrack
477
+ let trackStatus = getStatusItem(playerItem)
478
+
479
+ onStatus(.rmxstatus_STALLED, trackId: playerItem?.trackId, param: trackStatus)
480
+ }
481
+
482
+ @objc func playerItemDidReachEnd(_ notification: Notification?) {
483
+ if let object = notification?.object {
484
+ print("Player item reached end: \(object)")
485
+ }
486
+ let playerItem = notification?.object as? AudioTrack
487
+ // When an item finishes, immediately scrub it back to the beginning
488
+ // so that the visual indicators show you can "play again" or whatever.
489
+ // Might make sense to have a flag for this behavior.
490
+ playerItem?.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: nil)
491
+
492
+ let trackStatus = getStatusItem(playerItem)
493
+ onStatus(.rmxstatus_COMPLETED, trackId: playerItem?.trackId, param: trackStatus)
494
+ if (avQueuePlayer.isAtEnd) {
495
+ onStatus(.rmxstatus_PLAYLIST_COMPLETED, trackId: "INVALID", param: nil)
496
+ }
497
+
498
+ if loop && avQueuePlayer.isAtEnd {
499
+ print("Last music in playlist play ended, loop back.")
500
+ avQueuePlayer.setCurrentIndex(0)
501
+ avQueuePlayer.play()
502
+ }
503
+ }
504
+
505
+ @objc func handleAudioSessionInterruption(_ interruptionNotification: Notification?) {
506
+ if let interruptionNotification = interruptionNotification {
507
+ print("Audio session interruption received: \(interruptionNotification)")
508
+ }
509
+ guard let userInfo = interruptionNotification?.userInfo,
510
+ let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
511
+ let interruptionType = AVAudioSession.InterruptionType(rawValue: typeValue) else {
512
+ print("notification.userInfo?[AVAudioSessionInterruptionTypeKey]",
513
+ interruptionNotification?.userInfo?[AVAudioSessionInterruptionTypeKey] as Any)
514
+ return
515
+ }
516
+
517
+ switch interruptionType {
518
+ case AVAudioSession.InterruptionType.began:
519
+ let suspended = (interruptionNotification?.userInfo?[AVAudioSessionInterruptionWasSuspendedKey] as? NSNumber)?.boolValue ?? false
520
+ print("AVAudioSessionInterruptionTypeBegan. Was suspended: \(suspended)")
521
+ if avQueuePlayer.isPlaying {
522
+ wasPlayingInterrupted = true
523
+ }
524
+
525
+ // [[self avQueuePlayer] pause];
526
+ pauseCommand(false)
527
+ case AVAudioSession.InterruptionType.ended:
528
+ print("AVAudioSessionInterruptionTypeEnded")
529
+ guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
530
+ let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
531
+ if options.contains(.shouldResume) {
532
+ if wasPlayingInterrupted {
533
+ avQueuePlayer.play()
534
+ }
535
+ } else {
536
+ // Interruption ended. Playback should not resume.
537
+ }
538
+ wasPlayingInterrupted = false
539
+ default:
540
+ break
541
+ }
542
+ }
543
+
544
+ /*
545
+ * This method only executes while the queue is playing, so we can use the playback position event.
546
+ */
547
+ func executePeriodicUpdate(_ time: CMTime) {
548
+ guard let playerItem = avQueuePlayer.currentAudioTrack else { return }
549
+
550
+ if !CMTIME_IS_INDEFINITE(playerItem.currentTime()) {
551
+ updateNowPlayingTrackInfo(playerItem, updateTrackData: false)
552
+ if avQueuePlayer.isPlaying {
553
+ let trackStatus = getStatusItem(playerItem)
554
+ onStatus(.rmxstatus_PLAYBACK_POSITION, trackId: playerItem.trackId, param: trackStatus)
555
+ }
556
+ }
557
+
558
+ return
559
+ }
560
+
561
+ override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
562
+ guard let change = change else {
563
+ return
564
+ }
565
+
566
+ switch keyPath {
567
+ case "currentItem":
568
+ // only fire on real change!
569
+ let player = object as? AVBidirectionalQueuePlayer
570
+ let playerItem = player?.currentAudioTrack
571
+ if playerItem != nil {
572
+ guard !isReplacingItems && self.lastTrackId != playerItem?.trackId else {
573
+ return
574
+ }
575
+ print("observe change currentItem: lastTrackId \(self.lastTrackId ?? "nil") playerItem: \(playerItem?.trackId ?? "nil")")
576
+ self.lastTrackId = playerItem?.trackId
577
+ handleCurrentItemChanged(playerItem)
578
+ } else {
579
+ self.lastTrackId = nil
580
+ }
581
+
582
+ case "rate":
583
+ guard lastRate != change[.newKey] as? Float else {
584
+ return // todo should call super instead or?
585
+ }
586
+ self.lastRate = change[.newKey] as? Float
587
+ let player = object as? AVBidirectionalQueuePlayer
588
+
589
+ guard let playerItem = player?.currentAudioTrack else { return }
590
+
591
+ let trackStatus = getStatusItem(playerItem)
592
+ print("Playback rate changed: \(String(describing: change[.newKey])), is playing: \(player?.isPlaying ?? false)")
593
+
594
+ if player?.isPlaying ?? false {
595
+ onStatus(.rmxstatus_PLAYING, trackId: playerItem.trackId, param: trackStatus)
596
+ } else {
597
+ onStatus(.rmxstatus_PAUSE, trackId: playerItem.trackId, param: trackStatus)
598
+ }
599
+ case "status":
600
+ DispatchQueue.main.debounce(interval: 0.2, context: self, action: { [self] in
601
+ let playerItem = object as? AudioTrack
602
+ handleTrackStatusEvent(playerItem)
603
+ })
604
+ case "timeControlStatus":
605
+ let player = object as? AVBidirectionalQueuePlayer
606
+
607
+ guard let playerItem = player?.currentAudioTrack else {
608
+ return
609
+ }
610
+ guard lastTrackId != playerItem.trackId || player?.isAtBeginning ?? false else {
611
+ return // todo should call super instead or?
612
+ }
613
+
614
+ let trackStatus = getStatusItem(playerItem)
615
+ print("TCSPlayback rate changed: \(String(describing: change[.newKey])), is playing: \(player?.isPlaying ?? false)")
616
+
617
+ if player?.timeControlStatus == .playing {
618
+ onStatus(.rmxstatus_PLAYING, trackId: playerItem.trackId, param: trackStatus)
619
+ } else if player?.timeControlStatus == .waitingToPlayAtSpecifiedRate {
620
+ onStatus(.rmxstatus_BUFFERING, trackId: playerItem.trackId, param: trackStatus)
621
+ } else {
622
+ onStatus(.rmxstatus_PAUSE, trackId: playerItem.trackId, param: trackStatus)
623
+ }
624
+ case "duration":
625
+ DispatchQueue.main.debounce(interval: 0.5, context: self, action: { [self] in
626
+ let playerItem = object as? AudioTrack
627
+ handleTrackDuration(playerItem)
628
+ })
629
+ case "loadedTimeRanges":
630
+ let playerItem = object as? AudioTrack
631
+ handleTrackBuffering(playerItem)
632
+ default:
633
+ super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
634
+ }
635
+ }
636
+
637
+ func updateNowPlayingTrackInfo(_ playerItem: AudioTrack?, updateTrackData: Bool) {
638
+ let currentItem = playerItem ?? avQueuePlayer.currentAudioTrack
639
+ let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
640
+ self.nowPlayingInfoQueue.sync {
641
+ if updatedNowPlayingInfo == nil {
642
+ let nowPlayingInfo = nowPlayingInfoCenter.nowPlayingInfo
643
+ updatedNowPlayingInfo = nowPlayingInfo ?? [:]
644
+ }
645
+ }
646
+
647
+ var currentTime: Float? = nil
648
+ if let currentTime1 = currentItem?.currentTime() {
649
+ currentTime = Float(CMTimeGetSeconds(currentTime1))
650
+ }
651
+ var duration: Float? = nil
652
+ if let duration1 = currentItem?.duration {
653
+ duration = Float(CMTimeGetSeconds(duration1))
654
+ }
655
+ if CMTIME_IS_INDEFINITE(currentItem!.duration) {
656
+ duration = 0
657
+ }
658
+
659
+ self.nowPlayingInfoQueue.sync {
660
+ if updateTrackData {
661
+ updatedNowPlayingInfo![MPMediaItemPropertyArtist] = currentItem?.artist
662
+ updatedNowPlayingInfo![MPMediaItemPropertyTitle] = currentItem?.title
663
+ updatedNowPlayingInfo![MPMediaItemPropertyAlbumTitle] = currentItem?.album
664
+
665
+ if let mediaItemArtwork = createCoverArtwork(currentItem?.albumArt?.absoluteString) {
666
+ updatedNowPlayingInfo![MPMediaItemPropertyArtwork] = mediaItemArtwork
667
+ }
668
+ }
669
+ updatedNowPlayingInfo![MPMediaItemPropertyPlaybackDuration] = duration ?? 0.0
670
+ updatedNowPlayingInfo![MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime ?? 0.0
671
+ updatedNowPlayingInfo![MPNowPlayingInfoPropertyPlaybackRate] = 1.0
672
+
673
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = updatedNowPlayingInfo
674
+ }
675
+
676
+ let commandCenter = MPRemoteCommandCenter.shared()
677
+ commandCenter.nextTrackCommand.isEnabled = !avQueuePlayer.isAtEnd
678
+ commandCenter.previousTrackCommand.isEnabled = !avQueuePlayer.isAtBeginning
679
+ }
680
+
681
+ func createCoverArtwork(_ coverUriOrNil: String?) -> MPMediaItemArtwork? {
682
+ guard let coverUri = coverUriOrNil else {
683
+ return nil
684
+ }
685
+ var coverImage: UIImage? = nil
686
+ if coverUri.hasPrefix("http://") || coverUri.hasPrefix("https://") {
687
+ let coverImageUrl = URL(string: coverUri)!
688
+
689
+ do {
690
+ let coverImageData = try Data(contentsOf: coverImageUrl)
691
+ coverImage = UIImage(data: coverImageData)
692
+ } catch {
693
+ print("Error creating the coverImageData");
694
+ }
695
+ } else {
696
+ if FileManager.default.fileExists(atPath: coverUri) {
697
+ coverImage = UIImage(contentsOfFile: coverUri)
698
+ }
699
+ }
700
+
701
+ if isCoverImageValid(coverImage) {
702
+ return MPMediaItemArtwork.init(boundsSize: coverImage!.size, requestHandler: { (size) -> UIImage in
703
+ return coverImage!
704
+ })
705
+ }
706
+ return nil;
707
+ }
708
+
709
+ func downloadImage(url: URL, completion: @escaping ((_ image: UIImage?) -> Void)){
710
+ print("Started downloading \"\(url.deletingPathExtension().lastPathComponent)\".")
711
+ self.getImageDataFromUrl(url) { (_ data: Data?) in
712
+ DispatchQueue.main.async {
713
+ print("Finished downloading \"\(url.deletingPathExtension().lastPathComponent)\".")
714
+ completion(UIImage(data: data!))
715
+ }
716
+ }
717
+ }
718
+
719
+ func getImageDataFromUrl(_ url: URL, completion: @escaping ((_ data: Data?) -> Void)) {
720
+ URLSession.shared.dataTask(with: url) { (data, response, error) in
721
+ completion(data)
722
+ }.resume()
723
+ }
724
+
725
+ func isCoverImageValid(_ coverImage: UIImage?) -> Bool {
726
+ return coverImage != nil && (coverImage?.ciImage != nil || coverImage?.cgImage != nil)
727
+ }
728
+
729
+ func handleCurrentItemChanged(_ playerItem: AudioTrack?) {
730
+ if let playerItem = playerItem {
731
+ print("Queue changed current item to: \(playerItem.trackId ?? "nil")")
732
+ // NSLog(@"New music name: %@", ((AVURLAsset*)playerItem.asset).URL.pathComponents.lastObject);
733
+ print("New item ID: \(playerItem.trackId ?? "")")
734
+ print("Queue is at end: \(avQueuePlayer.isAtEnd ? "YES" : "NO")")
735
+ // When an item starts, immediately scrub it back to the beginning
736
+ //playerItem.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: nil)
737
+ // Update the command center
738
+ updateNowPlayingTrackInfo(playerItem, updateTrackData: true)
739
+ } else if loop {
740
+ return
741
+ }
742
+
743
+ var info: [String: Any] = [:]
744
+ if let to = playerItem != nil ? playerItem?.toDict() : [:] {
745
+ info = [
746
+ "currentItem": to,
747
+ "currentIndex": NSNumber(value: avQueuePlayer.currentIndex() ?? 0),
748
+ "isAtEnd": NSNumber(value: avQueuePlayer.isAtEnd),
749
+ "isAtBeginning": NSNumber(value: avQueuePlayer.isAtBeginning),
750
+ "hasNext": NSNumber(value: !avQueuePlayer.isAtEnd),
751
+ "hasPrevious": NSNumber(value: !avQueuePlayer.isAtBeginning)
752
+ ]
753
+ }
754
+ let trackId = playerItem != nil ? playerItem?.trackId : "NONE"
755
+
756
+ print("Update Track changed: \(info)")
757
+ onStatus(.rmxstatus_TRACK_CHANGED, trackId: trackId, param: info)
758
+
759
+ if avQueuePlayer.isAtEnd && avQueuePlayer.currentItem == nil {
760
+ if !loop {
761
+ avQueuePlayer.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero)
762
+ }
763
+
764
+ if !avQueuePlayer.queuedAudioTracks.isEmpty && !isReplacingItems {
765
+ onStatus(.rmxstatus_PLAYLIST_COMPLETED, trackId: "INVALID", param: nil)
766
+ }
767
+
768
+ if loop && !avQueuePlayer.queuedAudioTracks.isEmpty {
769
+ avQueuePlayer.setCurrentIndex(0)
770
+ // not playing here
771
+ } else {
772
+ onStatus(.rmxstatus_STOPPED, trackId: "INVALID", param: nil)
773
+ }
774
+ }
775
+ }
776
+
777
+ func handleTrackStatusEvent(_ playerItem: AudioTrack?) {
778
+ guard let playerItem = playerItem else {
779
+ return
780
+ }
781
+ // NSString* name = ((AVURLAsset*)playerItem.asset).URL.pathComponents.lastObject;
782
+ let name = playerItem.trackId
783
+ let status = playerItem.status
784
+
785
+ // Switch over the status
786
+ switch status {
787
+ case .readyToPlay:
788
+ print("PlayerItem status changed to AVPlayerItemStatusReadyToPlay [\(name ?? "")]")
789
+ let trackStatus = getStatusItem(playerItem)
790
+ onStatus(.rmxstatus_CANPLAY, trackId: playerItem.trackId, param: trackStatus)
791
+
792
+ if isWaitingToStartPlayback {
793
+ isWaitingToStartPlayback = false
794
+ print("RmxAudioPlayer[setPlaylistItems] is beginning playback after waiting for ReadyToPlay event")
795
+ playCommand(false)
796
+ }
797
+ case .failed:
798
+ // Failed. Examine AVPlayerItem.error
799
+ isWaitingToStartPlayback = false
800
+ var errorMsg = ""
801
+ if let error = playerItem.error {
802
+ print("\(error)")
803
+ errorMsg = "Error playing audio track: \((error as NSError).localizedFailureReason ?? "")"
804
+ }
805
+ print("AVPlayerItemStatusFailed: \(errorMsg)")
806
+ let errorParam = createError(withCode: .rmxerr_DECODE, message: errorMsg)
807
+ onStatus(.rmxstatus_ERROR, trackId: playerItem.trackId, param: errorParam)
808
+ case .unknown:
809
+ isWaitingToStartPlayback = false
810
+ print("PlayerItem status changed to AVPlayerItemStatusUnknown [\(name ?? "")]")
811
+ // Not ready
812
+ default:
813
+ break
814
+ }
815
+ }
816
+
817
+ func handleTrackDuration(_ playerItem: AudioTrack?) {
818
+ // This function isn't all that useful really in terms of state management.
819
+ // It doesn't always fire, and it is not needed because the queue's periodic update can also
820
+ // deliver this info.
821
+ //NSString* name = ((AVURLAsset*)playerItem.asset).URL.pathComponents.lastObject;
822
+
823
+ guard let playerItem = playerItem else { return }
824
+
825
+ if !CMTIME_IS_INDEFINITE(playerItem.duration) {
826
+ let duration = CMTimeGetSeconds(playerItem.duration)
827
+ print("The track duration was changed [\(playerItem.trackId ?? "")]: \(duration)")
828
+
829
+ // We will still report the duration though.
830
+ let trackStatus = getStatusItem(playerItem)
831
+ onStatus(.rmxstatus_DURATION, trackId: playerItem.trackId, param: trackStatus)
832
+ } else if let url = (playerItem.asset as? AVURLAsset)?.url {
833
+ print("Item duration is indefinite (unknown): \(url)")
834
+ }
835
+ }
836
+
837
+ func handleTrackBuffering(_ playerItem: AudioTrack?) {
838
+ //NSString* name = ((AVURLAsset*)playerItem.asset).URL.pathComponents.lastObject;
839
+ let name = playerItem?.trackId
840
+ let trackStatus = getStatusItem(playerItem)
841
+
842
+ print(
843
+ String(format: " . . . %.5f -> %.5f (%.1f %%) [%@]",
844
+ (trackStatus?["bufferStart"] as? NSNumber)?.floatValue ?? 0.0,
845
+ (trackStatus?["bufferStart"] as? NSNumber)?.floatValue ?? 0.0 + (trackStatus?["bufferEnd"] as? NSNumber)!.floatValue ,
846
+ (trackStatus?["bufferPercent"] as? NSNumber)?.floatValue ?? 0.0, name ?? ""
847
+ )
848
+ )
849
+
850
+ onStatus(.rmxstatus_BUFFERING, trackId: playerItem?.trackId, param: trackStatus)
851
+
852
+ if (trackStatus?["bufferPercent"] as? NSNumber)?.floatValue ?? 0.0 >= 100.0 {
853
+ onStatus(.rmxstatus_LOADED, trackId: playerItem?.trackId, param: trackStatus)
854
+ }
855
+ }
856
+
857
+ ///
858
+ /// Status utilities
859
+ ///
860
+ /// These provide the statis objects and data for the player items when they update.
861
+ ///
862
+ /// It is largely this data that is actually reported to the consumers.
863
+
864
+ // Not really needed, the dicts do this themselves but, blah.
865
+ func getNumberFor(_ str: String?) -> NSNumber? {
866
+ let f = NumberFormatter()
867
+ f.numberStyle = .decimal
868
+ f.locale = NSLocale.current
869
+ return f.number(from: str ?? "")
870
+ }
871
+
872
+ func getStatusItem(_ playerItem: AudioTrack?) -> [String : Any]? {
873
+ guard let currentItem = playerItem ?? avQueuePlayer.currentAudioTrack else {
874
+ return nil
875
+ }
876
+
877
+ let bufferInfo = getTrackBufferInfo(currentItem)
878
+ var position = getTrackCurrentTime(currentItem)
879
+ let duration = (bufferInfo?["duration"] as? NSNumber)?.floatValue ?? 0.0
880
+
881
+ // Correct this value here, so that playbackPercent is not set to INFINITY
882
+ if position.isNaN || position.isInfinite {
883
+ position = 0.0
884
+ }
885
+
886
+ let playbackPercent = duration > 0 ? (position / duration) * 100.0 : 0.0
887
+
888
+ var status: String
889
+ switch currentItem.status {
890
+ case .readyToPlay:
891
+ status = "ready"
892
+ case .failed:
893
+ status = "error"
894
+ default:
895
+ status = "unknown"
896
+ }
897
+
898
+ if avQueuePlayer.currentItem == currentItem {
899
+ if avQueuePlayer.rate != 0.0 {
900
+ status = "playing"
901
+
902
+ if position <= 0 && (bufferInfo?["bufferPercent"] as? NSNumber)?.floatValue ?? 0.0 == 0.0 {
903
+ status = "loading"
904
+ }
905
+ } else {
906
+ status = "paused"
907
+ }
908
+ }
909
+
910
+ return [
911
+ "trackId": currentItem.trackId ?? "",
912
+ "isStream": currentItem.isStream ? NSNumber(value: 1) : NSNumber(value: 0),
913
+ "currentIndex": NSNumber(value: avQueuePlayer.currentIndex() ?? 0),
914
+ "status": status,
915
+ "currentPosition": NSNumber(value: position),
916
+ "duration": NSNumber(value: duration),
917
+ "playbackPercent": NSNumber(value: playbackPercent),
918
+ "bufferPercent": NSNumber(value: (bufferInfo?["bufferPercent"] as? NSNumber)?.floatValue ?? 0.0),
919
+ "bufferStart": NSNumber(value: (bufferInfo?["start"] as? NSNumber)?.floatValue ?? 0.0),
920
+ "bufferEnd": NSNumber(value: (bufferInfo?["end"] as? NSNumber)?.floatValue ?? 0.0)
921
+ ]
922
+ }
923
+
924
+ func getTrackCurrentTime(_ playerItem: AudioTrack?) -> Float {
925
+ guard let currentItem = playerItem ?? avQueuePlayer.currentAudioTrack else {
926
+ return 0
927
+ }
928
+
929
+ if !CMTIME_IS_INDEFINITE(currentItem.currentTime()) && CMTIME_IS_VALID(currentItem.currentTime()) {
930
+ return Float(CMTimeGetSeconds(currentItem.currentTime()))
931
+ } else {
932
+ return 0
933
+ }
934
+ }
935
+
936
+ func getTrackBufferInfo(_ playerItem: AudioTrack?) -> [String : Any]? {
937
+ guard let playerItem = playerItem, !CMTIME_IS_INDEFINITE(playerItem.duration) else {
938
+ return [
939
+ "start": NSNumber(value: 0.0),
940
+ "end": NSNumber(value: 0.0),
941
+ "bufferPercent": NSNumber(value: 0.0),
942
+ "duration": NSNumber(value: 0.0)
943
+ ]
944
+ }
945
+
946
+ let duration = Float(CMTimeGetSeconds(playerItem.duration))
947
+ let timeRanges = playerItem.loadedTimeRanges
948
+
949
+ guard !timeRanges.isEmpty else {
950
+ return [
951
+ "start": NSNumber(value: 0.0),
952
+ "end": NSNumber(value: 0.0),
953
+ "bufferPercent": NSNumber(value: 0.0),
954
+ "duration": NSNumber(value: duration)
955
+ ]
956
+ }
957
+
958
+ let timerange = timeRanges[0].timeRangeValue
959
+ let start = Float(CMTimeGetSeconds(timerange.start))
960
+ let rangeEnd = Float(CMTimeGetSeconds(timerange.duration))
961
+ let bufferPercent = (rangeEnd / duration) * 100.0
962
+
963
+ return [
964
+ "start": NSNumber(value: start),
965
+ "end": NSNumber(value: rangeEnd),
966
+ "bufferPercent": NSNumber(value: bufferPercent),
967
+ "duration": NSNumber(value: duration)
968
+ ]
969
+ }
970
+
971
+ // MARK: - plugin initialization
972
+
973
+ ///
974
+ /// Object initialization. Mostly boring plumbing to initialize the objects and wire everything up.
975
+ func initializeMPCommandCenter() {
976
+ if !commandCenterRegistered {
977
+ let commandCenter = MPRemoteCommandCenter.shared()
978
+ commandCenter.playCommand.isEnabled = true
979
+ commandCenter.playCommand.addTarget(self, action: #selector(play(_:)))
980
+ commandCenter.pauseCommand.isEnabled = true
981
+ commandCenter.pauseCommand.addTarget(self, action: #selector(pause(_:)))
982
+ commandCenter.nextTrackCommand.isEnabled = true
983
+ commandCenter.nextTrackCommand.addTarget(self, action: #selector(nextTrackEvent(_:)))
984
+ commandCenter.previousTrackCommand.isEnabled = true
985
+ commandCenter.previousTrackCommand.addTarget(self, action: #selector(prevTrackEvent(_:)))
986
+ commandCenter.togglePlayPauseCommand.isEnabled = true
987
+ commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(togglePlayPauseTrackEvent(_:)))
988
+ commandCenter.changePlaybackPositionCommand.isEnabled = true
989
+ commandCenter.changePlaybackPositionCommand.addTarget(self, action: #selector(changedThumbSlider(onLockScreen:)))
990
+
991
+ commandCenterRegistered = true
992
+ }
993
+ }
994
+
995
+ func findTrackIndex(byId trackId: String?, _ tracks: [AudioTrack]) -> [String: Any]? {
996
+ let trackInformation: (Int, AudioTrack)? = tracks
997
+ .enumerated()
998
+ .first(where: { _, track in
999
+ track.trackId == trackId
1000
+ })
1001
+
1002
+ guard
1003
+ let index = trackInformation?.0,
1004
+ let track = trackInformation?.1
1005
+ else {
1006
+ return nil
1007
+ }
1008
+
1009
+ return [
1010
+ "track": track,
1011
+ "index": NSNumber(value: index)
1012
+ ]
1013
+ }
1014
+
1015
+ func findTrack(byId trackId: String?) -> [String: Any]? {
1016
+ let trackInformation: (Int, AudioTrack)? = avQueuePlayer.queuedAudioTracks
1017
+ .enumerated()
1018
+ .first(where: { _, track in
1019
+ track.trackId == trackId
1020
+ })
1021
+
1022
+ guard
1023
+ let index = trackInformation?.0,
1024
+ let track = trackInformation?.1
1025
+ else {
1026
+ return nil
1027
+ }
1028
+
1029
+ return [
1030
+ "track": track,
1031
+ "index": NSNumber(value: index)
1032
+ ]
1033
+ }
1034
+
1035
+ func addTrackObservers(_ playerItem: AudioTrack?) {
1036
+ let options: NSKeyValueObservingOptions = [.old, .new]
1037
+ playerItem?.addObserver(self, forKeyPath: "status", options: options, context: nil)
1038
+ playerItem?.addObserver(self, forKeyPath: "duration", options: options, context: nil)
1039
+ playerItem?.addObserver(self, forKeyPath: "loadedTimeRanges", options: options, context: nil)
1040
+
1041
+ // We don't need this one because we get the currentItem notification from the queue.
1042
+ // But we will wire it up anyway...
1043
+ let listener = NotificationCenter.default
1044
+ listener.addObserver(self, selector: #selector(playerItemDidReachEnd(_:)), name: .AVPlayerItemDidPlayToEndTime, object: playerItem)
1045
+ // Subscribe to the AVPlayerItem's PlaybackStalledNotification notification.
1046
+ listener.addObserver(self, selector: #selector(itemStalledPlaying(_:)), name: .AVPlayerItemPlaybackStalled, object: playerItem)
1047
+
1048
+ onStatus(.rmxstatus_ITEM_ADDED, trackId: playerItem?.trackId, param: playerItem?.toDict())
1049
+ }
1050
+
1051
+ @objc func queueCleared(_ notification: Notification?) {
1052
+ isReplacingItems = false // is this necessary ?
1053
+ print("RmxAudioPlayer, queuePlayerCleared")
1054
+ onStatus(.rmxstatus_PLAYLIST_CLEARED, trackId: "INVALID", param: nil)
1055
+ }
1056
+
1057
+ func removeTrackObservers(_ playerItem: AudioTrack?) {
1058
+ avQueuePlayer.removeTrackObservers(playerItem)
1059
+ }
1060
+
1061
+ func activateAudioSession() {
1062
+ let avSession = AVAudioSession.sharedInstance()
1063
+
1064
+ // If no devices are connected, play audio through the default speaker (rather than the earpiece).
1065
+ var options: AVAudioSession.CategoryOptions = .defaultToSpeaker
1066
+
1067
+ // If both Bluetooth streaming options are enabled, the low quality stream is preferred; enable A2DP only.
1068
+ options.insert(.allowBluetoothA2DP)
1069
+
1070
+ do {
1071
+ // Always set category first, even if session is already active
1072
+ // This ensures we have the correct category after video player exits
1073
+ try avSession.setCategory(.playAndRecord, options: options)
1074
+ } catch {
1075
+ print("Error setting category! \(error.localizedDescription)")
1076
+ }
1077
+
1078
+ do {
1079
+ // Activate the session (will activate if not active, or update if already active)
1080
+ try AVAudioSession.sharedInstance().setActive(true)
1081
+ } catch {
1082
+ print("Could not activate audio session. \(error.localizedDescription)")
1083
+ }
1084
+ }
1085
+
1086
+ /// Register the listener for pause and resume events.
1087
+ func observeLifeCycle() {
1088
+ let listener = NotificationCenter.default
1089
+
1090
+ // We do need these.
1091
+ listener.addObserver(self, selector: #selector(handleAudioSessionInterruption(_:)), name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance())
1092
+
1093
+ // Listen to when the queue player tells us it emptied the items list
1094
+ listener.addObserver(self, selector: #selector(queueCleared(_:)), name: NSNotification.Name(AVBidirectionalQueueCleared), object: avQueuePlayer)
1095
+ }
1096
+
1097
+ func createError(withCode code: RmxAudioErrorType, message: String?) -> [String : Any]? {
1098
+ [
1099
+ "code": NSNumber(value: code.rawValue),
1100
+ "message": message ?? ""
1101
+ ]
1102
+ }
1103
+
1104
+ func onStatus(_ what: RmxAudioStatusMessage, trackId: String?, param: [String:Any]?) {
1105
+ var status: [String : Any] = [:]
1106
+ status["msgType"] = NSNumber(value: what.rawValue)
1107
+ // in the error case contains a dict with "code" and "message", otherwise a NSNumber
1108
+ if let param = param {
1109
+ status["value"] = param
1110
+ }
1111
+ status["trackId"] = trackId ?? ""
1112
+
1113
+ var dict: [String : Any] = [:]
1114
+ dict["action"] = "status"
1115
+ dict["status"] = status
1116
+
1117
+ statusUpdater?.onStatus(dict)
1118
+ }
1119
+
1120
+ /// Cleanup
1121
+ func deregisterMusicControlsEventListener() {
1122
+ let commandCenter = MPRemoteCommandCenter.shared()
1123
+ commandCenter.playCommand.removeTarget(self)
1124
+ commandCenter.pauseCommand.removeTarget(self)
1125
+ commandCenter.nextTrackCommand.removeTarget(self)
1126
+ commandCenter.previousTrackCommand.removeTarget(self)
1127
+ commandCenter.togglePlayPauseCommand.removeTarget(self)
1128
+ commandCenter.changePlaybackPositionCommand.isEnabled = false
1129
+ commandCenter.changePlaybackPositionCommand.removeTarget(self, action: nil)
1130
+
1131
+ commandCenterRegistered = false
1132
+ }
1133
+
1134
+ func onReset() {
1135
+ // Override to cancel any long-running requests when the WebView navigates or refreshes.
1136
+ releaseResources()
1137
+ }
1138
+
1139
+ func releaseResources() {
1140
+ if let playbackTimeObserver = playbackTimeObserver {
1141
+ avQueuePlayer.removeTimeObserver(playbackTimeObserver)
1142
+ }
1143
+ deregisterMusicControlsEventListener()
1144
+
1145
+ removeAllTracks()
1146
+
1147
+ playbackTimeObserver = nil
1148
+ isWaitingToStartPlayback = false
1149
+ }
1150
+ }