@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.
@@ -42,17 +42,85 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
42
42
  var fadeMusic = false
43
43
  var session = AVAudioSession.sharedInstance()
44
44
 
45
- override public func load() {
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 set session category")
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 background {
82
-
83
- try self.session.setActive(true)
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
- audioQueue.sync { [self] in // Changed from async to sync for consistency
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 { [self] in // Read operation
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.async {
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.async {
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.async {
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.async {
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.async {
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.async {
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(Constant.ErrorAssetNotFound)
318
+ call.reject(error.localizedDescription)
285
319
  }
286
320
  }
287
321
  }
288
322
 
289
323
  @objc func loop(_ call: CAPPluginCall) {
290
- audioQueue.async {
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.async {
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.async {
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) ?? 1.0
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.async {
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) ?? 1.0
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.async {
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
- audioQueue.async {
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") ?? 1.0
381
- channels = call.getInt("channels") ?? 1
382
- delay = call.getFloat("delay") ?? 1.0
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 = 1
385
- volume = 1.0
386
- delay = 0.0
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
- basePath = Bundle.main.path(forResource: assetPathSplit[0], ofType: assetPathSplit[1])
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
- self.audioList[audioId] = NSNumber(value: Int32(soundId))
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
- self.audioList[audioId] = NSNumber(value: Int32(soundId))
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
  }