capacitor-plugin-playlist 0.8.6 → 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.
@@ -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
@@ -52,21 +53,38 @@ final class RmxAudioPlayer: NSObject {
52
53
  print("RmxAudioPlayer.execute=initialize")
53
54
 
54
55
  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)
56
+ // Guard against duplicate KVO registration (e.g. called more than once without a
57
+ // matching releaseResources() between calls would otherwise crash with an
58
+ // "Cannot remove observer" or duplicate-key exception).
59
+ if !kvoObserversRegistered {
60
+ avQueuePlayer.addObserver(self, forKeyPath: "currentItem", options: .new, context: nil)
61
+ avQueuePlayer.addObserver(self, forKeyPath: "rate", options: .new, context: nil)
62
+ avQueuePlayer.addObserver(self, forKeyPath: "timeControlStatus", options: .new, context: nil)
63
+ kvoObserversRegistered = true
64
+ }
65
+
66
+ installPlaybackTimeObserverIfNeeded()
58
67
 
68
+ onStatus(.rmxstatus_REGISTER, trackId: "INIT", param: nil)
69
+ }
70
+
71
+ /// Re-arms the periodic time observer if it is not already installed.
72
+ /// Safe to call multiple times; no-ops when an observer is already active.
73
+ /// Must be called on the main thread.
74
+ private func installPlaybackTimeObserverIfNeeded() {
75
+ guard playbackTimeObserver == nil else { return }
59
76
  let interval = CMTimeMakeWithSeconds(Float64(1.0), preferredTimescale: Int32(Double(NSEC_PER_SEC)))
60
77
  playbackTimeObserver = avQueuePlayer.addPeriodicTimeObserver(forInterval: interval, queue: .main, using: { [weak self] time in
61
78
  self?.executePeriodicUpdate(time)
62
79
  })
63
-
64
- onStatus(.rmxstatus_REGISTER, trackId: "INIT", param: nil)
65
80
  }
66
81
 
67
82
  func setPlaylistItems(_ items: [AudioTrack], options: [String:Any]) {
68
83
  print("RmxAudioPlayer.execute=setPlaylistItems, \(options), \(items.count)")
69
84
 
85
+ // Re-arm the periodic observer in case it was removed by a prior releaseResources() call.
86
+ installPlaybackTimeObserverIfNeeded()
87
+
70
88
  var seekToPosition: Float = 0.0
71
89
  let retainPosition = options["retainPosition"] != nil ? (options["retainPosition"] as? Bool) ?? false : false
72
90
  let playFromPosition = options["playFromPosition"] != nil ? (options["playFromPosition"] as? Float) ?? 0.0 : 0.0
@@ -202,7 +220,7 @@ final class RmxAudioPlayer: NSObject {
202
220
  ///
203
221
  /// These functions don't really do anything interesting by themselves.
204
222
  func selectTrack(index: Int) throws {
205
- guard index >= 0 || index < avQueuePlayer.queuedAudioTracks.count else {
223
+ guard index >= 0 && index < avQueuePlayer.queuedAudioTracks.count else {
206
224
  throw "Index out of Playlist bounds"
207
225
  }
208
226
  avQueuePlayer.setCurrentIndex(index)
@@ -256,6 +274,8 @@ final class RmxAudioPlayer: NSObject {
256
274
  func playCommand(_ isCommand: Bool) {
257
275
  wasPlayingInterrupted = false
258
276
  initializeMPCommandCenter()
277
+ // Re-arm the periodic observer if it was removed by a prior releaseResources() call.
278
+ installPlaybackTimeObserverIfNeeded()
259
279
 
260
280
  // Ensure audio session is active before playing
261
281
  // This is critical when resuming after video player has deactivated the session
@@ -590,7 +610,13 @@ final class RmxAudioPlayer: NSObject {
590
610
  let trackStatus = getStatusItem(playerItem)
591
611
  print("Playback rate changed: \(String(describing: change[.newKey])), is playing: \(player?.isPlaying ?? false)")
592
612
 
593
- if player?.isPlaying ?? false {
613
+ // Use the new rate value to determine playing/paused state.
614
+ // player?.isPlaying (= timeControlStatus == .playing) is false during the
615
+ // .waitingToPlayAtSpecifiedRate transition right after play() is called, which
616
+ // would emit a spurious PAUSE event and leave JS stuck in PAUSED state until the
617
+ // periodic PLAYBACK_POSITION event corrects it ~1 second later.
618
+ let newRate = change[.newKey] as? Float ?? 0
619
+ if newRate != 0 {
594
620
  onStatus(.rmxstatus_PLAYING, trackId: playerItem.trackId, param: trackStatus)
595
621
  } else {
596
622
  onStatus(.rmxstatus_PAUSE, trackId: playerItem.trackId, param: trackStatus)
@@ -1139,11 +1165,56 @@ final class RmxAudioPlayer: NSObject {
1139
1165
  if let playbackTimeObserver = playbackTimeObserver {
1140
1166
  avQueuePlayer.removeTimeObserver(playbackTimeObserver)
1141
1167
  }
1168
+ playbackTimeObserver = nil
1169
+
1170
+ // Remove the queue-level KVO observers added in initialize() so that a subsequent
1171
+ // initialize() call does not crash with a duplicate-observer exception.
1172
+ if kvoObserversRegistered {
1173
+ avQueuePlayer.removeObserver(self, forKeyPath: "currentItem")
1174
+ avQueuePlayer.removeObserver(self, forKeyPath: "rate")
1175
+ avQueuePlayer.removeObserver(self, forKeyPath: "timeControlStatus")
1176
+ kvoObserversRegistered = false
1177
+ }
1178
+
1142
1179
  deregisterMusicControlsEventListener()
1180
+ // commandCenterRegistered is already reset inside deregisterMusicControlsEventListener()
1143
1181
 
1144
1182
  removeAllTracks()
1145
1183
 
1146
- playbackTimeObserver = nil
1147
1184
  isWaitingToStartPlayback = false
1148
1185
  }
1186
+
1187
+ // MARK: - Epic 45 video handoff
1188
+
1189
+ private var lastKnownHandoffPosition: Float = 0
1190
+
1191
+ func prepareForVideoHandoff() {
1192
+ pauseCommand(false)
1193
+ // Capture position after pausing so lastKnownHandoffPosition reflects the
1194
+ // true stopped head, not a value that may have ticked during the pause call.
1195
+ if let track = avQueuePlayer.currentAudioTrack {
1196
+ lastKnownHandoffPosition = getTrackCurrentTime(track)
1197
+ } else {
1198
+ lastKnownHandoffPosition = 0
1199
+ }
1200
+ do {
1201
+ try AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation])
1202
+ } catch {
1203
+ print("prepareForVideoHandoff: setActive(false) failed: \(error.localizedDescription)")
1204
+ }
1205
+ }
1206
+
1207
+ func resumeAfterVideoHandoff(position: Float) {
1208
+ lastKnownHandoffPosition = position
1209
+ activateAudioSession()
1210
+ // Reset lastTrackId so the timeControlStatus KVO guard does not suppress the PLAYING
1211
+ // event on same-track non-index-0 resume. The guard `lastTrackId != trackId || isAtBeginning`
1212
+ // (where isAtBeginning = currentIndex() == 0) would silently drop the PLAYING transition
1213
+ // for any audio track at playlist index > 0, leaving JS stuck in PAUSED.
1214
+ lastTrackId = nil
1215
+ }
1216
+
1217
+ func getLastKnownPosition() -> Float {
1218
+ lastKnownHandoffPosition
1219
+ }
1149
1220
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-plugin-playlist",
3
- "version": "0.8.6",
3
+ "version": "0.9.0",
4
4
  "description": "Playlist ",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",