@capgo/native-audio 7.3.0 → 7.3.9
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 +27 -6
- package/dist/docs.json +7 -0
- package/dist/esm/definitions.d.ts +4 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Plugin/AudioAsset.swift +235 -117
- package/ios/Plugin/Constant.swift +13 -0
- package/ios/Plugin/Plugin.swift +144 -87
- package/ios/Plugin/RemoteAudioAsset.swift +226 -86
- package/package.json +5 -3
@@ -1,9 +1,14 @@
|
|
1
1
|
import AVFoundation
|
2
2
|
|
3
|
+
/**
|
4
|
+
* RemoteAudioAsset extends AudioAsset to handle remote (URL-based) audio files
|
5
|
+
* Provides network audio playback using AVPlayer instead of AVAudioPlayer
|
6
|
+
*/
|
3
7
|
public class RemoteAudioAsset: AudioAsset {
|
4
8
|
var playerItems: [AVPlayerItem] = []
|
5
9
|
var players: [AVPlayer] = []
|
6
10
|
var playerObservers: [NSKeyValueObservation] = []
|
11
|
+
var notificationObservers: [NSObjectProtocol] = []
|
7
12
|
var duration: TimeInterval = 0
|
8
13
|
var asset: AVURLAsset?
|
9
14
|
|
@@ -11,73 +16,117 @@ public class RemoteAudioAsset: AudioAsset {
|
|
11
16
|
super.init(owner: owner, withAssetId: assetId, withPath: path, withChannels: channels ?? 1, withVolume: volume ?? 1.0, withFadeDelay: delay ?? 0.0)
|
12
17
|
|
13
18
|
owner.executeOnAudioQueue { [self] in
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
19
|
+
guard let url = URL(string: path ?? "") else {
|
20
|
+
print("Invalid URL: \(String(describing: path))")
|
21
|
+
return
|
22
|
+
}
|
23
|
+
|
24
|
+
let asset = AVURLAsset(url: url, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true])
|
25
|
+
self.asset = asset
|
26
|
+
|
27
|
+
// Limit channels to a reasonable maximum to prevent resource issues
|
28
|
+
let channelCount = min(max(channels ?? Constant.DefaultChannels, 1), Constant.MaxChannels)
|
29
|
+
|
30
|
+
for _ in 0..<channelCount {
|
31
|
+
let playerItem = AVPlayerItem(asset: asset)
|
32
|
+
let player = AVPlayer(playerItem: playerItem)
|
33
|
+
// Apply volume constraints consistent with AudioAsset
|
34
|
+
player.volume = self.initialVolume
|
35
|
+
player.rate = 1.0
|
36
|
+
self.playerItems.append(playerItem)
|
37
|
+
self.players.append(player)
|
38
|
+
|
39
|
+
// Add observer for duration
|
40
|
+
let durationObserver = playerItem.observe(\.status) { [weak self] item, _ in
|
41
|
+
guard let strongSelf = self else { return }
|
42
|
+
strongSelf.owner?.executeOnAudioQueue {
|
43
|
+
if item.status == .readyToPlay {
|
44
|
+
strongSelf.duration = item.duration.seconds
|
32
45
|
}
|
33
46
|
}
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
47
|
+
}
|
48
|
+
self.playerObservers.append(durationObserver)
|
49
|
+
|
50
|
+
// Add observer for playback finished
|
51
|
+
let observer = player.observe(\.timeControlStatus) { [weak self, weak player] observedPlayer, _ in
|
52
|
+
guard let strongSelf = self,
|
53
|
+
let strongPlayer = player,
|
54
|
+
strongPlayer === observedPlayer else { return }
|
55
|
+
|
56
|
+
if strongPlayer.timeControlStatus == .paused &&
|
57
|
+
(strongPlayer.currentItem?.currentTime() == strongPlayer.currentItem?.duration ||
|
58
|
+
strongPlayer.currentItem?.duration == .zero) {
|
59
|
+
strongSelf.playerDidFinishPlaying(player: strongPlayer)
|
43
60
|
}
|
44
|
-
self.playerObservers.append(observer)
|
45
61
|
}
|
62
|
+
self.playerObservers.append(observer)
|
46
63
|
}
|
47
64
|
}
|
48
65
|
}
|
49
66
|
|
67
|
+
deinit {
|
68
|
+
// Clean up observers
|
69
|
+
for observer in playerObservers {
|
70
|
+
observer.invalidate()
|
71
|
+
}
|
72
|
+
|
73
|
+
for observer in notificationObservers {
|
74
|
+
NotificationCenter.default.removeObserver(observer)
|
75
|
+
}
|
76
|
+
|
77
|
+
// Clean up players
|
78
|
+
for player in players {
|
79
|
+
player.pause()
|
80
|
+
}
|
81
|
+
|
82
|
+
playerItems = []
|
83
|
+
players = []
|
84
|
+
playerObservers = []
|
85
|
+
notificationObservers = []
|
86
|
+
}
|
87
|
+
|
50
88
|
func playerDidFinishPlaying(player: AVPlayer) {
|
51
|
-
owner
|
52
|
-
self.owner
|
89
|
+
owner?.executeOnAudioQueue { [self] in
|
90
|
+
self.owner?.notifyListeners("complete", data: [
|
53
91
|
"assetId": self.assetId
|
54
92
|
])
|
55
93
|
}
|
56
94
|
}
|
57
95
|
|
58
96
|
override func play(time: TimeInterval, delay: TimeInterval) {
|
59
|
-
owner
|
97
|
+
owner?.executeOnAudioQueue { [self] in
|
60
98
|
guard !players.isEmpty else { return }
|
99
|
+
|
100
|
+
// Reset play index if it's out of bounds
|
101
|
+
if playIndex >= players.count {
|
102
|
+
playIndex = 0
|
103
|
+
}
|
104
|
+
|
61
105
|
let player = players[playIndex]
|
62
|
-
|
106
|
+
|
107
|
+
// Ensure non-negative values for time and delay
|
108
|
+
let validTime = max(time, 0)
|
109
|
+
let validDelay = max(delay, 0)
|
110
|
+
|
111
|
+
if validDelay > 0 {
|
63
112
|
// Convert delay to CMTime and add to current time
|
64
113
|
let currentTime = player.currentTime()
|
65
|
-
let delayTime = CMTimeMakeWithSeconds(
|
114
|
+
let delayTime = CMTimeMakeWithSeconds(validDelay, preferredTimescale: currentTime.timescale)
|
66
115
|
let timeToPlay = CMTimeAdd(currentTime, delayTime)
|
67
116
|
player.seek(to: timeToPlay)
|
68
117
|
} else {
|
69
|
-
player.seek(to: CMTimeMakeWithSeconds(
|
118
|
+
player.seek(to: CMTimeMakeWithSeconds(validTime, preferredTimescale: 1))
|
70
119
|
}
|
71
120
|
player.play()
|
72
121
|
playIndex = (playIndex + 1) % players.count
|
73
|
-
NSLog("RemoteAudioAsset: About to start timer updates")
|
74
122
|
startCurrentTimeUpdates()
|
75
123
|
}
|
76
124
|
}
|
77
125
|
|
78
126
|
override func pause() {
|
79
|
-
owner
|
80
|
-
guard !players.isEmpty else { return }
|
127
|
+
owner?.executeOnAudioQueue { [self] in
|
128
|
+
guard !players.isEmpty && playIndex < players.count else { return }
|
129
|
+
|
81
130
|
let player = players[playIndex]
|
82
131
|
player.pause()
|
83
132
|
stopCurrentTimeUpdates()
|
@@ -85,18 +134,35 @@ public class RemoteAudioAsset: AudioAsset {
|
|
85
134
|
}
|
86
135
|
|
87
136
|
override func resume() {
|
88
|
-
owner
|
89
|
-
guard !players.isEmpty else { return }
|
137
|
+
owner?.executeOnAudioQueue { [self] in
|
138
|
+
guard !players.isEmpty && playIndex < players.count else { return }
|
139
|
+
|
90
140
|
let player = players[playIndex]
|
91
141
|
player.play()
|
92
|
-
|
93
|
-
|
142
|
+
|
143
|
+
// Add notification observer for when playback stops
|
144
|
+
cleanupNotificationObservers()
|
145
|
+
|
146
|
+
let observer = NotificationCenter.default.addObserver(
|
147
|
+
forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
|
148
|
+
object: player.currentItem,
|
149
|
+
queue: nil) { [weak self] notification in
|
150
|
+
guard let strongSelf = self else { return }
|
151
|
+
|
152
|
+
if let currentItem = notification.object as? AVPlayerItem,
|
153
|
+
currentItem == strongSelf.playerItems[strongSelf.playIndex] {
|
154
|
+
strongSelf.playerDidFinishPlaying(player: strongSelf.players[strongSelf.playIndex])
|
155
|
+
}
|
156
|
+
}
|
157
|
+
notificationObservers.append(observer)
|
158
|
+
startCurrentTimeUpdates()
|
94
159
|
}
|
95
160
|
}
|
96
161
|
|
97
162
|
override func stop() {
|
98
|
-
owner
|
99
|
-
stopCurrentTimeUpdates()
|
163
|
+
owner?.executeOnAudioQueue { [self] in
|
164
|
+
stopCurrentTimeUpdates()
|
165
|
+
|
100
166
|
for player in players {
|
101
167
|
// First pause
|
102
168
|
player.pause()
|
@@ -112,26 +178,47 @@ public class RemoteAudioAsset: AudioAsset {
|
|
112
178
|
}
|
113
179
|
|
114
180
|
override func loop() {
|
115
|
-
owner
|
116
|
-
|
181
|
+
owner?.executeOnAudioQueue { [self] in
|
182
|
+
cleanupNotificationObservers()
|
183
|
+
|
184
|
+
for (index, player) in players.enumerated() {
|
117
185
|
player.actionAtItemEnd = .none
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
NotificationCenter.default.addObserver(
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
186
|
+
|
187
|
+
guard let playerItem = player.currentItem else { continue }
|
188
|
+
|
189
|
+
let observer = NotificationCenter.default.addObserver(
|
190
|
+
forName: .AVPlayerItemDidPlayToEndTime,
|
191
|
+
object: playerItem,
|
192
|
+
queue: nil) { [weak player] notification in
|
193
|
+
guard let strongPlayer = player,
|
194
|
+
let item = notification.object as? AVPlayerItem,
|
195
|
+
strongPlayer.currentItem === item else { return }
|
196
|
+
|
197
|
+
strongPlayer.seek(to: .zero)
|
198
|
+
strongPlayer.play()
|
199
|
+
}
|
200
|
+
|
201
|
+
notificationObservers.append(observer)
|
202
|
+
|
203
|
+
if index == playIndex {
|
204
|
+
player.seek(to: .zero)
|
205
|
+
player.play()
|
206
|
+
}
|
127
207
|
}
|
128
|
-
|
129
|
-
startCurrentTimeUpdates()
|
208
|
+
|
209
|
+
startCurrentTimeUpdates()
|
210
|
+
}
|
211
|
+
}
|
212
|
+
|
213
|
+
private func cleanupNotificationObservers() {
|
214
|
+
for observer in notificationObservers {
|
215
|
+
NotificationCenter.default.removeObserver(observer)
|
130
216
|
}
|
217
|
+
notificationObservers = []
|
131
218
|
}
|
132
219
|
|
133
220
|
@objc func playerItemDidReachEnd(notification: Notification) {
|
134
|
-
owner
|
221
|
+
owner?.executeOnAudioQueue { [self] in
|
135
222
|
if let playerItem = notification.object as? AVPlayerItem,
|
136
223
|
let player = players.first(where: { $0.currentItem == playerItem }) {
|
137
224
|
player.seek(to: .zero)
|
@@ -141,10 +228,12 @@ public class RemoteAudioAsset: AudioAsset {
|
|
141
228
|
}
|
142
229
|
|
143
230
|
override func unload() {
|
144
|
-
owner
|
231
|
+
owner?.executeOnAudioQueue { [self] in
|
145
232
|
stopCurrentTimeUpdates()
|
146
233
|
stop()
|
147
|
-
|
234
|
+
|
235
|
+
cleanupNotificationObservers()
|
236
|
+
|
148
237
|
// Remove KVO observers
|
149
238
|
for observer in playerObservers {
|
150
239
|
observer.invalidate()
|
@@ -156,25 +245,29 @@ public class RemoteAudioAsset: AudioAsset {
|
|
156
245
|
}
|
157
246
|
|
158
247
|
override func setVolume(volume: NSNumber!) {
|
159
|
-
owner
|
248
|
+
owner?.executeOnAudioQueue { [self] in
|
249
|
+
// Ensure volume is in valid range (0.0-1.0)
|
250
|
+
let validVolume = min(max(volume.floatValue, Constant.MinVolume), Constant.MaxVolume)
|
160
251
|
for player in players {
|
161
|
-
player.volume =
|
252
|
+
player.volume = validVolume
|
162
253
|
}
|
163
254
|
}
|
164
255
|
}
|
165
256
|
|
166
257
|
override func setRate(rate: NSNumber!) {
|
167
|
-
owner
|
258
|
+
owner?.executeOnAudioQueue { [self] in
|
259
|
+
// Ensure rate is in valid range
|
260
|
+
let validRate = min(max(rate.floatValue, Constant.MinRate), Constant.MaxRate)
|
168
261
|
for player in players {
|
169
|
-
player.rate =
|
262
|
+
player.rate = validRate
|
170
263
|
}
|
171
264
|
}
|
172
265
|
}
|
173
266
|
|
174
267
|
override func isPlaying() -> Bool {
|
175
268
|
var result = false
|
176
|
-
owner
|
177
|
-
guard !players.isEmpty else {
|
269
|
+
owner?.executeOnAudioQueue { [self] in
|
270
|
+
guard !players.isEmpty && playIndex < players.count else {
|
178
271
|
result = false
|
179
272
|
return
|
180
273
|
}
|
@@ -186,8 +279,8 @@ public class RemoteAudioAsset: AudioAsset {
|
|
186
279
|
|
187
280
|
override func getCurrentTime() -> TimeInterval {
|
188
281
|
var result: TimeInterval = 0
|
189
|
-
owner
|
190
|
-
guard !players.isEmpty else {
|
282
|
+
owner?.executeOnAudioQueue { [self] in
|
283
|
+
guard !players.isEmpty && playIndex < players.count else {
|
191
284
|
result = 0
|
192
285
|
return
|
193
286
|
}
|
@@ -199,8 +292,8 @@ public class RemoteAudioAsset: AudioAsset {
|
|
199
292
|
|
200
293
|
override func getDuration() -> TimeInterval {
|
201
294
|
var result: TimeInterval = 0
|
202
|
-
owner
|
203
|
-
guard !players.isEmpty else {
|
295
|
+
owner?.executeOnAudioQueue { [self] in
|
296
|
+
guard !players.isEmpty && playIndex < players.count else {
|
204
297
|
result = 0
|
205
298
|
return
|
206
299
|
}
|
@@ -215,42 +308,52 @@ public class RemoteAudioAsset: AudioAsset {
|
|
215
308
|
}
|
216
309
|
|
217
310
|
override func playWithFade(time: TimeInterval) {
|
218
|
-
owner
|
219
|
-
guard !players.isEmpty else { return }
|
311
|
+
owner?.executeOnAudioQueue { [self] in
|
312
|
+
guard !players.isEmpty && playIndex < players.count else { return }
|
313
|
+
|
220
314
|
let player = players[playIndex]
|
221
315
|
|
222
316
|
if player.timeControlStatus != .playing {
|
223
317
|
player.seek(to: CMTimeMakeWithSeconds(time, preferredTimescale: 1))
|
224
|
-
player.volume =
|
318
|
+
player.volume = 0 // Start with volume at 0
|
225
319
|
player.play()
|
226
320
|
playIndex = (playIndex + 1) % players.count
|
227
|
-
NSLog("RemoteAudioAsset PlayWithFade: About to start timer updates")
|
228
321
|
startCurrentTimeUpdates()
|
322
|
+
|
323
|
+
// Start fade-in using the parent class method
|
324
|
+
startVolumeRamp(from: 0, to: initialVolume, player: player)
|
229
325
|
} else {
|
230
326
|
if player.volume < initialVolume {
|
231
|
-
|
327
|
+
// Continue fade-in if already in progress
|
328
|
+
startVolumeRamp(from: player.volume, to: initialVolume, player: player)
|
232
329
|
}
|
233
330
|
}
|
234
331
|
}
|
235
332
|
}
|
236
333
|
|
237
334
|
override func stopWithFade() {
|
238
|
-
owner
|
239
|
-
guard !players.isEmpty else {
|
335
|
+
owner?.executeOnAudioQueue { [self] in
|
336
|
+
guard !players.isEmpty && playIndex < players.count else {
|
337
|
+
stop()
|
338
|
+
return
|
339
|
+
}
|
340
|
+
|
240
341
|
let player = players[playIndex]
|
241
342
|
|
242
343
|
if player.timeControlStatus == .playing {
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
344
|
+
// Use parent class fade method
|
345
|
+
startVolumeRamp(from: player.volume, to: 0, player: player)
|
346
|
+
|
347
|
+
// Schedule the stop when fade is complete
|
348
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(self.FADEDELAY * 1000))) { [weak self, weak player] in
|
349
|
+
guard let strongSelf = self, let strongPlayer = player else { return }
|
350
|
+
|
351
|
+
if strongPlayer.volume < strongSelf.FADESTEP {
|
352
|
+
strongSelf.stop()
|
248
353
|
}
|
249
|
-
} else {
|
250
|
-
// Volume is near 0, actually stop
|
251
|
-
player.volume = 0
|
252
|
-
self.stop()
|
253
354
|
}
|
355
|
+
} else {
|
356
|
+
stop()
|
254
357
|
}
|
255
358
|
}
|
256
359
|
}
|
@@ -261,7 +364,9 @@ public class RemoteAudioAsset: AudioAsset {
|
|
261
364
|
if let cachePath = urls.first {
|
262
365
|
do {
|
263
366
|
let fileURLs = try FileManager.default.contentsOfDirectory(at: cachePath, includingPropertiesForKeys: nil)
|
264
|
-
|
367
|
+
// Clear all audio file types
|
368
|
+
let audioExtensions = ["mp3", "wav", "aac", "m4a", "ogg", "mp4", "caf", "aiff"]
|
369
|
+
for fileURL in fileURLs where audioExtensions.contains(fileURL.pathExtension.lowercased()) {
|
265
370
|
try FileManager.default.removeItem(at: fileURL)
|
266
371
|
}
|
267
372
|
} catch {
|
@@ -270,4 +375,39 @@ public class RemoteAudioAsset: AudioAsset {
|
|
270
375
|
}
|
271
376
|
}
|
272
377
|
}
|
378
|
+
|
379
|
+
// Add helper method for parent class
|
380
|
+
private func startVolumeRamp(from startVolume: Float, to endVolume: Float, player: AVPlayer) {
|
381
|
+
player.volume = startVolume
|
382
|
+
|
383
|
+
// Calculate steps
|
384
|
+
let steps = abs(endVolume - startVolume) / FADESTEP
|
385
|
+
guard steps > 0 else { return }
|
386
|
+
|
387
|
+
let timeInterval = FADEDELAY / steps
|
388
|
+
var currentStep = 0
|
389
|
+
let totalSteps = Int(ceil(steps))
|
390
|
+
|
391
|
+
stopFadeTimer()
|
392
|
+
|
393
|
+
fadeTimer = Timer.scheduledTimer(withTimeInterval: TimeInterval(timeInterval), repeats: true) { [weak player] timer in
|
394
|
+
guard let strongPlayer = player else {
|
395
|
+
timer.invalidate()
|
396
|
+
return
|
397
|
+
}
|
398
|
+
|
399
|
+
currentStep += 1
|
400
|
+
let progress = Float(currentStep) / Float(totalSteps)
|
401
|
+
let newVolume = startVolume + progress * (endVolume - startVolume)
|
402
|
+
|
403
|
+
strongPlayer.volume = newVolume
|
404
|
+
|
405
|
+
if currentStep >= totalSteps {
|
406
|
+
strongPlayer.volume = endVolume
|
407
|
+
timer.invalidate()
|
408
|
+
self.fadeTimer = nil
|
409
|
+
}
|
410
|
+
}
|
411
|
+
RunLoop.current.add(fadeTimer!, forMode: .common)
|
412
|
+
}
|
273
413
|
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@capgo/native-audio",
|
3
|
-
"version": "7.3.
|
3
|
+
"version": "7.3.9",
|
4
4
|
"description": "A native plugin for native audio engine",
|
5
5
|
"license": "MIT",
|
6
6
|
"main": "dist/plugin.cjs.js",
|
@@ -17,10 +17,10 @@
|
|
17
17
|
"author": "Martin Donadieu",
|
18
18
|
"repository": {
|
19
19
|
"type": "git",
|
20
|
-
"url": "git+https://github.com/Cap-go/native-audio.git"
|
20
|
+
"url": "git+https://github.com/Cap-go/capacitor-native-audio.git"
|
21
21
|
},
|
22
22
|
"bugs": {
|
23
|
-
"url": "https://github.com/Cap-go/native-audio/issues"
|
23
|
+
"url": "https://github.com/Cap-go/capacitor-native-audio/native-audio/issues"
|
24
24
|
},
|
25
25
|
"keywords": [
|
26
26
|
"capacitor",
|
@@ -35,6 +35,8 @@
|
|
35
35
|
"verify:ios": "cd ios && pod install && xcodebuild -workspace Plugin.xcworkspace -scheme Plugin && cd ..",
|
36
36
|
"verify:android": "cd android && ./gradlew clean build test && cd ..",
|
37
37
|
"verify:web": "npm run build",
|
38
|
+
"test": "npm run test:ios",
|
39
|
+
"test:ios": "cd ios && xcodebuild test -workspace Plugin.xcworkspace -scheme Plugin -destination 'platform=iOS Simulator,name=iPhone 16' && cd ..",
|
38
40
|
"lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
|
39
41
|
"fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --autocorrect --format",
|
40
42
|
"eslint": "eslint . --ext .ts",
|