capacitor-plugin-playlist 0.8.7 → 0.9.1

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 (30) hide show
  1. package/android/src/main/java/org/dwbn/plugins/playlist/OnStatusCallback.kt +3 -13
  2. package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistPlugin.kt +120 -32
  3. package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioPlayer.java +41 -0
  4. package/android/src/test/java/org/dwbn/plugins/playlist/StatusBridgePolicyTest.kt +56 -0
  5. package/dist/docs.json +72 -0
  6. package/dist/esm/RmxAudioPlayer.js +14 -3
  7. package/dist/esm/RmxAudioPlayer.js.map +1 -1
  8. package/dist/esm/definitions.d.ts +19 -0
  9. package/dist/esm/web.d.ts +8 -0
  10. package/dist/esm/web.js +39 -24
  11. package/dist/esm/web.js.map +1 -1
  12. package/dist/plugin.cjs.js +55 -27
  13. package/dist/plugin.cjs.js.map +1 -1
  14. package/dist/plugin.js +55 -27
  15. package/dist/plugin.js.map +1 -1
  16. package/ios/Plugin/AudioTrack.swift +6 -1
  17. package/ios/Plugin/Plugin.m +0 -1
  18. package/ios/Plugin/Plugin.swift +42 -17
  19. package/ios/Plugin/RmxAudioPlayer.swift +101 -8
  20. package/ios/PluginTests/PluginTests.swift +12 -24
  21. package/package.json +1 -1
  22. package/android/.gradle/8.11.1/checksums/checksums.lock +0 -0
  23. package/android/.gradle/8.11.1/checksums/md5-checksums.bin +0 -0
  24. package/android/.gradle/8.11.1/checksums/sha1-checksums.bin +0 -0
  25. package/android/.gradle/8.11.1/executionHistory/executionHistory.bin +0 -0
  26. package/android/.gradle/8.11.1/executionHistory/executionHistory.lock +0 -0
  27. package/android/.gradle/8.11.1/fileChanges/last-build.bin +0 -0
  28. package/android/.gradle/8.11.1/fileHashes/fileHashes.bin +0 -0
  29. package/android/.gradle/8.11.1/fileHashes/fileHashes.lock +0 -0
  30. package/android/.gradle/8.11.1/gc.properties +0 -0
@@ -19,6 +19,7 @@ final class RmxAudioPlayer: NSObject {
19
19
  var statusUpdater: StatusUpdater? = nil
20
20
 
21
21
  private var playbackTimeObserver: Any?
22
+ private var kvoObserversRegistered = false
22
23
  private var wasPlayingInterrupted = false
23
24
  private var commandCenterRegistered = false
24
25
  private var resetStreamOnPause = false
@@ -27,6 +28,7 @@ final class RmxAudioPlayer: NSObject {
27
28
  private var isReplacingItems = false
28
29
  private var isWaitingToStartPlayback = false
29
30
  private var loop = false
31
+ private var isWebViewActive = true
30
32
 
31
33
  let avQueuePlayer = AVBidirectionalQueuePlayer(items: [])
32
34
 
@@ -52,21 +54,38 @@ final class RmxAudioPlayer: NSObject {
52
54
  print("RmxAudioPlayer.execute=initialize")
53
55
 
54
56
  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)
57
+ // Guard against duplicate KVO registration (e.g. called more than once without a
58
+ // matching releaseResources() between calls would otherwise crash with an
59
+ // "Cannot remove observer" or duplicate-key exception).
60
+ if !kvoObserversRegistered {
61
+ avQueuePlayer.addObserver(self, forKeyPath: "currentItem", options: .new, context: nil)
62
+ avQueuePlayer.addObserver(self, forKeyPath: "rate", options: .new, context: nil)
63
+ avQueuePlayer.addObserver(self, forKeyPath: "timeControlStatus", options: .new, context: nil)
64
+ kvoObserversRegistered = true
65
+ }
66
+
67
+ installPlaybackTimeObserverIfNeeded()
68
+
69
+ onStatus(.rmxstatus_REGISTER, trackId: "INIT", param: nil)
70
+ }
58
71
 
72
+ /// Re-arms the periodic time observer if it is not already installed.
73
+ /// Safe to call multiple times; no-ops when an observer is already active.
74
+ /// Must be called on the main thread.
75
+ private func installPlaybackTimeObserverIfNeeded() {
76
+ guard playbackTimeObserver == nil else { return }
59
77
  let interval = CMTimeMakeWithSeconds(Float64(1.0), preferredTimescale: Int32(Double(NSEC_PER_SEC)))
60
78
  playbackTimeObserver = avQueuePlayer.addPeriodicTimeObserver(forInterval: interval, queue: .main, using: { [weak self] time in
61
79
  self?.executePeriodicUpdate(time)
62
80
  })
63
-
64
- onStatus(.rmxstatus_REGISTER, trackId: "INIT", param: nil)
65
81
  }
66
82
 
67
83
  func setPlaylistItems(_ items: [AudioTrack], options: [String:Any]) {
68
84
  print("RmxAudioPlayer.execute=setPlaylistItems, \(options), \(items.count)")
69
85
 
86
+ // Re-arm the periodic observer in case it was removed by a prior releaseResources() call.
87
+ installPlaybackTimeObserverIfNeeded()
88
+
70
89
  var seekToPosition: Float = 0.0
71
90
  let retainPosition = options["retainPosition"] != nil ? (options["retainPosition"] as? Bool) ?? false : false
72
91
  let playFromPosition = options["playFromPosition"] != nil ? (options["playFromPosition"] as? Float) ?? 0.0 : 0.0
@@ -202,7 +221,7 @@ final class RmxAudioPlayer: NSObject {
202
221
  ///
203
222
  /// These functions don't really do anything interesting by themselves.
204
223
  func selectTrack(index: Int) throws {
205
- guard index >= 0 || index < avQueuePlayer.queuedAudioTracks.count else {
224
+ guard index >= 0 && index < avQueuePlayer.queuedAudioTracks.count else {
206
225
  throw "Index out of Playlist bounds"
207
226
  }
208
227
  avQueuePlayer.setCurrentIndex(index)
@@ -256,6 +275,8 @@ final class RmxAudioPlayer: NSObject {
256
275
  func playCommand(_ isCommand: Bool) {
257
276
  wasPlayingInterrupted = false
258
277
  initializeMPCommandCenter()
278
+ // Re-arm the periodic observer if it was removed by a prior releaseResources() call.
279
+ installPlaybackTimeObserverIfNeeded()
259
280
 
260
281
  // Ensure audio session is active before playing
261
282
  // This is critical when resuming after video player has deactivated the session
@@ -590,7 +611,13 @@ final class RmxAudioPlayer: NSObject {
590
611
  let trackStatus = getStatusItem(playerItem)
591
612
  print("Playback rate changed: \(String(describing: change[.newKey])), is playing: \(player?.isPlaying ?? false)")
592
613
 
593
- if player?.isPlaying ?? false {
614
+ // Use the new rate value to determine playing/paused state.
615
+ // player?.isPlaying (= timeControlStatus == .playing) is false during the
616
+ // .waitingToPlayAtSpecifiedRate transition right after play() is called, which
617
+ // would emit a spurious PAUSE event and leave JS stuck in PAUSED state until the
618
+ // periodic PLAYBACK_POSITION event corrects it ~1 second later.
619
+ let newRate = change[.newKey] as? Float ?? 0
620
+ if newRate != 0 {
594
621
  onStatus(.rmxstatus_PLAYING, trackId: playerItem.trackId, param: trackStatus)
595
622
  } else {
596
623
  onStatus(.rmxstatus_PAUSE, trackId: playerItem.trackId, param: trackStatus)
@@ -1101,6 +1128,10 @@ final class RmxAudioPlayer: NSObject {
1101
1128
  }
1102
1129
 
1103
1130
  func onStatus(_ what: RmxAudioStatusMessage, trackId: String?, param: [String:Any]?) {
1131
+ if what == .rmxstatus_PLAYBACK_POSITION && !isWebViewActive {
1132
+ return
1133
+ }
1134
+
1104
1135
  var status: [String : Any] = [:]
1105
1136
  status["msgType"] = NSNumber(value: what.rawValue)
1106
1137
  // in the error case contains a dict with "code" and "message", otherwise a NSNumber
@@ -1139,11 +1170,73 @@ final class RmxAudioPlayer: NSObject {
1139
1170
  if let playbackTimeObserver = playbackTimeObserver {
1140
1171
  avQueuePlayer.removeTimeObserver(playbackTimeObserver)
1141
1172
  }
1173
+ playbackTimeObserver = nil
1174
+
1175
+ // Remove the queue-level KVO observers added in initialize() so that a subsequent
1176
+ // initialize() call does not crash with a duplicate-observer exception.
1177
+ if kvoObserversRegistered {
1178
+ avQueuePlayer.removeObserver(self, forKeyPath: "currentItem")
1179
+ avQueuePlayer.removeObserver(self, forKeyPath: "rate")
1180
+ avQueuePlayer.removeObserver(self, forKeyPath: "timeControlStatus")
1181
+ kvoObserversRegistered = false
1182
+ }
1183
+
1142
1184
  deregisterMusicControlsEventListener()
1185
+ // commandCenterRegistered is already reset inside deregisterMusicControlsEventListener()
1143
1186
 
1144
1187
  removeAllTracks()
1145
1188
 
1146
- playbackTimeObserver = nil
1147
1189
  isWaitingToStartPlayback = false
1148
1190
  }
1191
+
1192
+ // MARK: - Epic 45 video handoff
1193
+
1194
+ private var lastKnownHandoffPosition: Float = 0
1195
+
1196
+ func prepareForVideoHandoff() {
1197
+ pauseCommand(false)
1198
+ // Capture position after pausing so lastKnownHandoffPosition reflects the
1199
+ // true stopped head, not a value that may have ticked during the pause call.
1200
+ if let track = avQueuePlayer.currentAudioTrack {
1201
+ lastKnownHandoffPosition = getTrackCurrentTime(track)
1202
+ } else {
1203
+ lastKnownHandoffPosition = 0
1204
+ }
1205
+ do {
1206
+ try AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation])
1207
+ } catch {
1208
+ print("prepareForVideoHandoff: setActive(false) failed: \(error.localizedDescription)")
1209
+ }
1210
+ }
1211
+
1212
+ func resumeAfterVideoHandoff(position: Float) {
1213
+ lastKnownHandoffPosition = position
1214
+ activateAudioSession()
1215
+ // Reset lastTrackId so the timeControlStatus KVO guard does not suppress the PLAYING
1216
+ // event on same-track non-index-0 resume. The guard `lastTrackId != trackId || isAtBeginning`
1217
+ // (where isAtBeginning = currentIndex() == 0) would silently drop the PLAYING transition
1218
+ // for any audio track at playlist index > 0, leaving JS stuck in PAUSED.
1219
+ lastTrackId = nil
1220
+ }
1221
+
1222
+ func getLastKnownPosition() -> Float {
1223
+ lastKnownHandoffPosition
1224
+ }
1225
+
1226
+ func setWebViewActive(_ active: Bool) {
1227
+ isWebViewActive = active
1228
+ }
1229
+
1230
+ func shouldEmitStatusToBridge(_ what: RmxAudioStatusMessage) -> Bool {
1231
+ if what == .rmxstatus_PLAYBACK_POSITION && !isWebViewActive {
1232
+ return false
1233
+ }
1234
+ return true
1235
+ }
1236
+
1237
+ func emitPlaybackSnapshot() {
1238
+ guard let playerItem = avQueuePlayer.currentAudioTrack else { return }
1239
+ let trackStatus = getStatusItem(playerItem)
1240
+ onStatus(.rmxstatus_PLAYBACK_POSITION, trackId: playerItem.trackId, param: trackStatus)
1241
+ }
1149
1242
  }
@@ -1,35 +1,23 @@
1
1
  import XCTest
2
- import Capacitor
3
2
  @testable import Plugin
4
3
 
5
4
  class PluginTests: XCTestCase {
6
5
 
7
- override func setUp() {
8
- super.setUp()
9
- // Put setup code here. This method is called before the invocation of each test method in the class.
6
+ func testPlaybackPositionSuppressedWhenWebViewInactive() {
7
+ let player = RmxAudioPlayer()
8
+ player.setWebViewActive(false)
9
+ XCTAssertFalse(player.shouldEmitStatusToBridge(.rmxstatus_PLAYBACK_POSITION))
10
10
  }
11
11
 
12
- override func tearDown() {
13
- // Put teardown code here. This method is called after the invocation of each test method in the class.
14
- super.tearDown()
12
+ func testPlaybackPositionEmittedWhenWebViewActive() {
13
+ let player = RmxAudioPlayer()
14
+ player.setWebViewActive(true)
15
+ XCTAssertTrue(player.shouldEmitStatusToBridge(.rmxstatus_PLAYBACK_POSITION))
15
16
  }
16
17
 
17
- func testEcho() {
18
- // This is an example of a functional test case for a plugin.
19
- // Use XCTAssert and related functions to verify your tests produce the correct results.
20
-
21
- let value = "Hello, World!"
22
- let plugin = MyPlugin()
23
-
24
- let call = CAPPluginCall(callbackId: "test", options: [
25
- "value": value
26
- ], success: { (result, _) in
27
- let resultValue = result!.data["value"] as? String
28
- XCTAssertEqual(value, resultValue)
29
- }, error: { (_) in
30
- XCTFail("Error shouldn't have been called")
31
- })
32
-
33
- plugin.echo(call!)
18
+ func testPlayingEmittedWhenWebViewInactive() {
19
+ let player = RmxAudioPlayer()
20
+ player.setWebViewActive(false)
21
+ XCTAssertTrue(player.shouldEmitStatusToBridge(.rmxstatus_PLAYING))
34
22
  }
35
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-plugin-playlist",
3
- "version": "0.8.7",
3
+ "version": "0.9.1",
4
4
  "description": "Playlist ",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
File without changes