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.
- package/README.md +28 -0
- package/android/build.gradle +6 -18
- package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistPlugin.kt +72 -32
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioPlayer.java +32 -0
- package/dist/docs.json +72 -0
- package/dist/esm/RmxAudioPlayer.js +14 -3
- package/dist/esm/RmxAudioPlayer.js.map +1 -1
- package/dist/esm/definitions.d.ts +19 -0
- package/dist/esm/web.d.ts +8 -0
- package/dist/esm/web.js +39 -24
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +55 -27
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +55 -27
- package/dist/plugin.js.map +1 -1
- package/ios/Plugin/AudioTrack.swift +6 -1
- package/ios/Plugin/Plugin.m +0 -1
- package/ios/Plugin/Plugin.swift +33 -16
- package/ios/Plugin/RmxAudioPlayer.swift +79 -8
- package/package.json +1 -1
|
@@ -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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|