@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
package/ios/Plugin/Plugin.swift
CHANGED
@@ -42,17 +42,85 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
42
42
|
var fadeMusic = false
|
43
43
|
var session = AVAudioSession.sharedInstance()
|
44
44
|
|
45
|
-
|
45
|
+
// Add observer for audio session interruptions
|
46
|
+
private var interruptionObserver: Any?
|
47
|
+
|
48
|
+
@objc override public func load() {
|
46
49
|
super.load()
|
47
50
|
audioQueue.setSpecific(key: queueKey, value: true)
|
48
51
|
|
49
52
|
self.fadeMusic = false
|
50
53
|
|
54
|
+
setupAudioSession()
|
55
|
+
setupInterruptionHandling()
|
56
|
+
|
57
|
+
NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { [weak self] _ in
|
58
|
+
guard let strongSelf = self else { return }
|
59
|
+
|
60
|
+
// When entering background, automatically deactivate audio session if not playing any audio
|
61
|
+
strongSelf.audioQueue.sync {
|
62
|
+
// Check if there are any playing assets
|
63
|
+
let hasPlayingAssets = strongSelf.audioList.values.contains { asset in
|
64
|
+
if let audioAsset = asset as? AudioAsset {
|
65
|
+
return audioAsset.isPlaying()
|
66
|
+
}
|
67
|
+
return false
|
68
|
+
}
|
69
|
+
|
70
|
+
if !hasPlayingAssets {
|
71
|
+
strongSelf.endSession()
|
72
|
+
}
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
// Clean up on deinit
|
78
|
+
deinit {
|
79
|
+
if let observer = interruptionObserver {
|
80
|
+
NotificationCenter.default.removeObserver(observer)
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
private func setupAudioSession() {
|
51
85
|
do {
|
52
86
|
try self.session.setCategory(AVAudioSession.Category.playback, options: .mixWithOthers)
|
87
|
+
try self.session.setActive(true)
|
53
88
|
try self.session.setActive(false)
|
54
89
|
} catch {
|
55
|
-
print("Failed to
|
90
|
+
print("Failed to setup audio session: \(error)")
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
private func setupInterruptionHandling() {
|
95
|
+
// Handle audio session interruptions
|
96
|
+
interruptionObserver = NotificationCenter.default.addObserver(
|
97
|
+
forName: AVAudioSession.interruptionNotification,
|
98
|
+
object: nil,
|
99
|
+
queue: nil) { [weak self] notification in
|
100
|
+
guard let strongSelf = self else { return }
|
101
|
+
|
102
|
+
guard let userInfo = notification.userInfo,
|
103
|
+
let typeInt = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
|
104
|
+
let type = AVAudioSession.InterruptionType(rawValue: typeInt) else {
|
105
|
+
return
|
106
|
+
}
|
107
|
+
|
108
|
+
switch type {
|
109
|
+
case .began:
|
110
|
+
// Audio was interrupted - we could pause all playing audio here
|
111
|
+
strongSelf.notifyListeners("interrupt", data: ["interrupted": true])
|
112
|
+
case .ended:
|
113
|
+
// Interruption ended - we could resume audio here if appropriate
|
114
|
+
if let optionsInt = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt,
|
115
|
+
AVAudioSession.InterruptionOptions(rawValue: optionsInt).contains(.shouldResume) {
|
116
|
+
// Resume playback if appropriate (user wants to resume)
|
117
|
+
strongSelf.notifyListeners("interrupt", data: ["interrupted": false, "shouldResume": true])
|
118
|
+
} else {
|
119
|
+
strongSelf.notifyListeners("interrupt", data: ["interrupted": false, "shouldResume": false])
|
120
|
+
}
|
121
|
+
@unknown default:
|
122
|
+
break
|
123
|
+
}
|
56
124
|
}
|
57
125
|
}
|
58
126
|
|
@@ -62,65 +130,29 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
62
130
|
}
|
63
131
|
|
64
132
|
let focus = call.getBool(Constant.FocusAudio) ?? false
|
65
|
-
do {
|
66
|
-
if focus {
|
67
|
-
try self.session.setCategory(AVAudioSession.Category.playback, options: .duckOthers)
|
68
|
-
|
69
|
-
}
|
70
|
-
|
71
|
-
} catch {
|
72
|
-
|
73
|
-
print("Failed to set setCategory audio")
|
74
|
-
|
75
|
-
}
|
76
|
-
|
77
133
|
let background = call.getBool(Constant.Background) ?? false
|
134
|
+
let ignoreSilent = call.getBool(Constant.IgnoreSilent) ?? true
|
78
135
|
|
136
|
+
// Use a single audio session configuration block for better atomicity
|
79
137
|
do {
|
138
|
+
try self.session.setActive(true)
|
80
139
|
|
81
|
-
if
|
82
|
-
|
83
|
-
|
140
|
+
if focus {
|
141
|
+
try self.session.setCategory(AVAudioSession.Category.playback, options: .duckOthers)
|
142
|
+
} else if !ignoreSilent {
|
143
|
+
try self.session.setCategory(AVAudioSession.Category.ambient, options: focus ? .duckOthers : .mixWithOthers)
|
144
|
+
} else {
|
145
|
+
try self.session.setCategory(AVAudioSession.Category.playback, options: .mixWithOthers)
|
146
|
+
}
|
84
147
|
|
148
|
+
if !background {
|
149
|
+
try self.session.setActive(false)
|
85
150
|
}
|
86
151
|
|
87
152
|
} catch {
|
88
|
-
|
89
|
-
print("Failed to set setSession true")
|
90
|
-
|
153
|
+
print("Failed to configure audio session: \(error)")
|
91
154
|
}
|
92
155
|
|
93
|
-
let ignoreSilent = call.getBool(Constant.IgnoreSilent) ?? true
|
94
|
-
|
95
|
-
do {
|
96
|
-
|
97
|
-
if ignoreSilent == false {
|
98
|
-
|
99
|
-
if let focus = call.getBool(Constant.FocusAudio) {
|
100
|
-
|
101
|
-
do {
|
102
|
-
|
103
|
-
if focus {
|
104
|
-
|
105
|
-
try self.session.setCategory(AVAudioSession.Category.ambient, options: .duckOthers)
|
106
|
-
|
107
|
-
} else {
|
108
|
-
|
109
|
-
try self.session.setCategory(
|
110
|
-
AVAudioSession.Category.ambient, options: .mixWithOthers)
|
111
|
-
|
112
|
-
}
|
113
|
-
|
114
|
-
} catch {
|
115
|
-
|
116
|
-
print("Failed to set setCategory audio")
|
117
|
-
|
118
|
-
}
|
119
|
-
|
120
|
-
}
|
121
|
-
|
122
|
-
}
|
123
|
-
}
|
124
156
|
call.resolve()
|
125
157
|
}
|
126
158
|
|
@@ -145,7 +177,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
145
177
|
do {
|
146
178
|
try self.session.setActive(true)
|
147
179
|
} catch {
|
148
|
-
print("Failed to set session active")
|
180
|
+
print("Failed to set session active: \(error)")
|
149
181
|
}
|
150
182
|
}
|
151
183
|
|
@@ -153,7 +185,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
153
185
|
do {
|
154
186
|
try self.session.setActive(false, options: .notifyOthersOnDeactivation)
|
155
187
|
} catch {
|
156
|
-
print("Failed to deactivate audio session")
|
188
|
+
print("Failed to deactivate audio session: \(error)")
|
157
189
|
}
|
158
190
|
}
|
159
191
|
|
@@ -163,10 +195,11 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
163
195
|
|
164
196
|
@objc func play(_ call: CAPPluginCall) {
|
165
197
|
let audioId = call.getString(Constant.AssetIdKey) ?? ""
|
166
|
-
let time = call.getDouble("time") ?? 0
|
167
|
-
let delay = call.getDouble("delay") ?? 0
|
198
|
+
let time = max(call.getDouble("time") ?? 0, 0) // Ensure non-negative time
|
199
|
+
let delay = max(call.getDouble("delay") ?? 0, 0) // Ensure non-negative delay
|
168
200
|
|
169
|
-
|
201
|
+
// Use sync for operations that need to be blocking
|
202
|
+
audioQueue.sync {
|
170
203
|
guard !audioList.isEmpty else {
|
171
204
|
call.reject("Audio list is empty")
|
172
205
|
return
|
@@ -197,27 +230,28 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
197
230
|
|
198
231
|
@objc private func getAudioAsset(_ call: CAPPluginCall) -> AudioAsset? {
|
199
232
|
var asset: AudioAsset?
|
200
|
-
audioQueue.sync {
|
233
|
+
audioQueue.sync { // Read operations should use sync
|
201
234
|
asset = self.audioList[call.getString(Constant.AssetIdKey) ?? ""] as? AudioAsset
|
202
235
|
}
|
203
236
|
return asset
|
204
237
|
}
|
205
238
|
|
206
239
|
@objc func setCurrentTime(_ call: CAPPluginCall) {
|
207
|
-
audioQueue.
|
240
|
+
// Consistent use of audioQueue.sync for all operations
|
241
|
+
audioQueue.sync {
|
208
242
|
guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
|
209
243
|
call.reject("Failed to get audio asset")
|
210
244
|
return
|
211
245
|
}
|
212
246
|
|
213
|
-
let time = call.getDouble("time") ?? 0
|
247
|
+
let time = max(call.getDouble("time") ?? 0, 0) // Ensure non-negative time
|
214
248
|
audioAsset.setCurrentTime(time: time)
|
215
249
|
call.resolve()
|
216
250
|
}
|
217
251
|
}
|
218
252
|
|
219
253
|
@objc func getDuration(_ call: CAPPluginCall) {
|
220
|
-
audioQueue.
|
254
|
+
audioQueue.sync {
|
221
255
|
guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
|
222
256
|
call.reject("Failed to get audio asset")
|
223
257
|
return
|
@@ -230,7 +264,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
230
264
|
}
|
231
265
|
|
232
266
|
@objc func getCurrentTime(_ call: CAPPluginCall) {
|
233
|
-
audioQueue.
|
267
|
+
audioQueue.sync {
|
234
268
|
guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
|
235
269
|
call.reject("Failed to get audio asset")
|
236
270
|
return
|
@@ -243,7 +277,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
243
277
|
}
|
244
278
|
|
245
279
|
@objc func resume(_ call: CAPPluginCall) {
|
246
|
-
audioQueue.
|
280
|
+
audioQueue.sync {
|
247
281
|
guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
|
248
282
|
call.reject("Failed to get audio asset")
|
249
283
|
return
|
@@ -255,7 +289,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
255
289
|
}
|
256
290
|
|
257
291
|
@objc func pause(_ call: CAPPluginCall) {
|
258
|
-
audioQueue.
|
292
|
+
audioQueue.sync {
|
259
293
|
guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
|
260
294
|
call.reject("Failed to get audio asset")
|
261
295
|
return
|
@@ -270,7 +304,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
270
304
|
@objc func stop(_ call: CAPPluginCall) {
|
271
305
|
let audioId = call.getString(Constant.AssetIdKey) ?? ""
|
272
306
|
|
273
|
-
audioQueue.
|
307
|
+
audioQueue.sync {
|
274
308
|
guard !self.audioList.isEmpty else {
|
275
309
|
call.reject("Audio list is empty")
|
276
310
|
return
|
@@ -281,13 +315,13 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
281
315
|
self.endSession()
|
282
316
|
call.resolve()
|
283
317
|
} catch {
|
284
|
-
call.reject(
|
318
|
+
call.reject(error.localizedDescription)
|
285
319
|
}
|
286
320
|
}
|
287
321
|
}
|
288
322
|
|
289
323
|
@objc func loop(_ call: CAPPluginCall) {
|
290
|
-
audioQueue.
|
324
|
+
audioQueue.sync {
|
291
325
|
guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
|
292
326
|
call.reject("Failed to get audio asset")
|
293
327
|
return
|
@@ -301,7 +335,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
301
335
|
@objc func unload(_ call: CAPPluginCall) {
|
302
336
|
let audioId = call.getString(Constant.AssetIdKey) ?? ""
|
303
337
|
|
304
|
-
audioQueue.
|
338
|
+
audioQueue.sync(flags: .barrier) { // Use barrier for writing operations
|
305
339
|
guard !self.audioList.isEmpty else {
|
306
340
|
call.reject("Audio list is empty")
|
307
341
|
return
|
@@ -311,6 +345,11 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
311
345
|
asset.unload()
|
312
346
|
self.audioList[audioId] = nil
|
313
347
|
call.resolve()
|
348
|
+
} else if let audioNumber = self.audioList[audioId] as? NSNumber {
|
349
|
+
// Also handle unloading system sounds
|
350
|
+
AudioServicesDisposeSystemSoundID(SystemSoundID(audioNumber.intValue))
|
351
|
+
self.audioList[audioId] = nil
|
352
|
+
call.resolve()
|
314
353
|
} else {
|
315
354
|
call.reject("Cannot cast to AudioAsset")
|
316
355
|
}
|
@@ -318,33 +357,33 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
318
357
|
}
|
319
358
|
|
320
359
|
@objc func setVolume(_ call: CAPPluginCall) {
|
321
|
-
audioQueue.
|
360
|
+
audioQueue.sync {
|
322
361
|
guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
|
323
362
|
call.reject("Failed to get audio asset")
|
324
363
|
return
|
325
364
|
}
|
326
365
|
|
327
|
-
let volume = call.getFloat(Constant.Volume) ??
|
366
|
+
let volume = min(max(call.getFloat(Constant.Volume) ?? Constant.DefaultVolume, Constant.MinVolume), Constant.MaxVolume)
|
328
367
|
audioAsset.setVolume(volume: volume as NSNumber)
|
329
368
|
call.resolve()
|
330
369
|
}
|
331
370
|
}
|
332
371
|
|
333
372
|
@objc func setRate(_ call: CAPPluginCall) {
|
334
|
-
audioQueue.
|
373
|
+
audioQueue.sync {
|
335
374
|
guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
|
336
375
|
call.reject("Failed to get audio asset")
|
337
376
|
return
|
338
377
|
}
|
339
378
|
|
340
|
-
let rate = call.getFloat(Constant.Rate) ??
|
379
|
+
let rate = min(max(call.getFloat(Constant.Rate) ?? Constant.DefaultRate, Constant.MinRate), Constant.MaxRate)
|
341
380
|
audioAsset.setRate(rate: rate as NSNumber)
|
342
381
|
call.resolve()
|
343
382
|
}
|
344
383
|
}
|
345
384
|
|
346
385
|
@objc func isPlaying(_ call: CAPPluginCall) {
|
347
|
-
audioQueue.
|
386
|
+
audioQueue.sync {
|
348
387
|
guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
|
349
388
|
call.reject("Failed to get audio asset")
|
350
389
|
return
|
@@ -357,13 +396,14 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
357
396
|
}
|
358
397
|
|
359
398
|
@objc func clearCache(_ call: CAPPluginCall) {
|
360
|
-
|
399
|
+
DispatchQueue.global(qos: .background).async {
|
361
400
|
RemoteAudioAsset.clearCache()
|
362
401
|
call.resolve()
|
363
402
|
}
|
364
403
|
}
|
365
404
|
|
366
405
|
@objc private func preloadAsset(_ call: CAPPluginCall, isComplex complex: Bool) {
|
406
|
+
// Common default values to ensure consistency
|
367
407
|
let audioId = call.getString(Constant.AssetIdKey) ?? ""
|
368
408
|
let channels: Int?
|
369
409
|
let volume: Float?
|
@@ -376,14 +416,19 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
376
416
|
}
|
377
417
|
var assetPath: String = call.getString(Constant.AssetPathKey) ?? ""
|
378
418
|
|
419
|
+
if assetPath == "" {
|
420
|
+
call.reject(Constant.ErrorAssetPath)
|
421
|
+
return
|
422
|
+
}
|
423
|
+
|
379
424
|
if complex {
|
380
|
-
volume = call.getFloat("volume") ??
|
381
|
-
channels = call.getInt("channels") ?? 1
|
382
|
-
delay = call.getFloat("delay") ??
|
425
|
+
volume = min(max(call.getFloat("volume") ?? Constant.DefaultVolume, Constant.MinVolume), Constant.MaxVolume)
|
426
|
+
channels = max(call.getInt("channels") ?? Constant.DefaultChannels, 1)
|
427
|
+
delay = max(call.getFloat("delay") ?? Constant.DefaultFadeDelay, 0.0)
|
383
428
|
} else {
|
384
|
-
channels =
|
385
|
-
volume =
|
386
|
-
delay =
|
429
|
+
channels = Constant.DefaultChannels
|
430
|
+
volume = Constant.DefaultVolume
|
431
|
+
delay = Constant.DefaultFadeDelay
|
387
432
|
isLocalUrl = false
|
388
433
|
}
|
389
434
|
|
@@ -408,7 +453,12 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
408
453
|
// Handle public folder
|
409
454
|
assetPath = assetPath.starts(with: "public/") ? assetPath : "public/" + assetPath
|
410
455
|
let assetPathSplit = assetPath.components(separatedBy: ".")
|
411
|
-
|
456
|
+
if assetPathSplit.count >= 2 {
|
457
|
+
basePath = Bundle.main.path(forResource: assetPathSplit[0], ofType: assetPathSplit[1])
|
458
|
+
} else {
|
459
|
+
call.reject("Invalid asset path format: \(assetPath)")
|
460
|
+
return
|
461
|
+
}
|
412
462
|
} else {
|
413
463
|
// Handle local file URL
|
414
464
|
let fileURL = URL(fileURLWithPath: assetPath)
|
@@ -419,8 +469,13 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
419
469
|
if !complex {
|
420
470
|
let soundFileUrl = URL(fileURLWithPath: basePath)
|
421
471
|
var soundId = SystemSoundID()
|
422
|
-
AudioServicesCreateSystemSoundID(soundFileUrl as CFURL, &soundId)
|
423
|
-
|
472
|
+
let result = AudioServicesCreateSystemSoundID(soundFileUrl as CFURL, &soundId)
|
473
|
+
if result == kAudioServicesNoError {
|
474
|
+
self.audioList[audioId] = NSNumber(value: Int32(soundId))
|
475
|
+
} else {
|
476
|
+
call.reject("Failed to create system sound: \(result)")
|
477
|
+
return
|
478
|
+
}
|
424
479
|
} else {
|
425
480
|
let audioAsset = AudioAsset(
|
426
481
|
owner: self,
|
@@ -437,8 +492,13 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
437
492
|
if !complex {
|
438
493
|
let soundFileUrl = URL(fileURLWithPath: assetPath)
|
439
494
|
var soundId = SystemSoundID()
|
440
|
-
AudioServicesCreateSystemSoundID(soundFileUrl as CFURL, &soundId)
|
441
|
-
|
495
|
+
let result = AudioServicesCreateSystemSoundID(soundFileUrl as CFURL, &soundId)
|
496
|
+
if result == kAudioServicesNoError {
|
497
|
+
self.audioList[audioId] = NSNumber(value: Int32(soundId))
|
498
|
+
} else {
|
499
|
+
call.reject("Failed to create system sound: \(result)")
|
500
|
+
return
|
501
|
+
}
|
442
502
|
} else {
|
443
503
|
let audioAsset = AudioAsset(
|
444
504
|
owner: self,
|
@@ -480,17 +540,14 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
480
540
|
}
|
481
541
|
|
482
542
|
@objc func notifyCurrentTime(_ asset: AudioAsset) {
|
483
|
-
NSLog("notifyCurrentTime called in Plugin")
|
484
543
|
audioQueue.sync {
|
485
544
|
let rawTime = asset.getCurrentTime()
|
486
545
|
// Round to nearest 100ms (0.1 seconds)
|
487
546
|
let currentTime = round(rawTime * 10) / 10
|
488
|
-
NSLog("About to notify listeners with time: \(currentTime)")
|
489
547
|
notifyListeners("currentTime", data: [
|
490
548
|
"currentTime": currentTime,
|
491
549
|
"assetId": asset.assetId
|
492
550
|
])
|
493
|
-
NSLog("Listeners notified")
|
494
551
|
}
|
495
552
|
}
|
496
553
|
}
|