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.
- package/android/src/main/java/org/dwbn/plugins/playlist/OnStatusCallback.kt +3 -13
- package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistPlugin.kt +120 -32
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioPlayer.java +41 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/StatusBridgePolicyTest.kt +56 -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 +42 -17
- package/ios/Plugin/RmxAudioPlayer.swift +101 -8
- package/ios/PluginTests/PluginTests.swift +12 -24
- package/package.json +1 -1
- package/android/.gradle/8.11.1/checksums/checksums.lock +0 -0
- package/android/.gradle/8.11.1/checksums/md5-checksums.bin +0 -0
- package/android/.gradle/8.11.1/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/8.11.1/executionHistory/executionHistory.bin +0 -0
- package/android/.gradle/8.11.1/executionHistory/executionHistory.lock +0 -0
- package/android/.gradle/8.11.1/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.11.1/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/8.11.1/fileHashes/fileHashes.lock +0 -0
- 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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
func testPlaybackPositionSuppressedWhenWebViewInactive() {
|
|
7
|
+
let player = RmxAudioPlayer()
|
|
8
|
+
player.setWebViewActive(false)
|
|
9
|
+
XCTAssertFalse(player.shouldEmitStatusToBridge(.rmxstatus_PLAYBACK_POSITION))
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
func testPlaybackPositionEmittedWhenWebViewActive() {
|
|
13
|
+
let player = RmxAudioPlayer()
|
|
14
|
+
player.setWebViewActive(true)
|
|
15
|
+
XCTAssertTrue(player.shouldEmitStatusToBridge(.rmxstatus_PLAYBACK_POSITION))
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
func
|
|
18
|
-
|
|
19
|
-
|
|
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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|