@capgo/native-audio 7.3.18 → 7.3.19

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.
@@ -67,7 +67,8 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
67
67
  // Limit channels to a reasonable maximum to prevent resource issues
68
68
  let channelCount = min(max(channels ?? 1, 1), MAX_CHANNELS)
69
69
 
70
- owner.executeOnAudioQueue { [self] in
70
+ owner.executeOnAudioQueue { [weak self] in
71
+ guard let self = self else { return }
71
72
  for _ in 0..<channelCount {
72
73
  do {
73
74
  let player = try AVAudioPlayer(contentsOf: pathUrl)
@@ -88,10 +89,10 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
88
89
  deinit {
89
90
  currentTimeTimer?.invalidate()
90
91
  currentTimeTimer = nil
91
-
92
+
92
93
  fadeTimer?.invalidate()
93
94
  fadeTimer = nil
94
-
95
+
95
96
  // Clean up any players that might still be playing
96
97
  for player in channels {
97
98
  if player.isPlaying {
@@ -107,7 +108,9 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
107
108
  */
108
109
  func getCurrentTime() -> TimeInterval {
109
110
  var result: TimeInterval = 0
110
- owner?.executeOnAudioQueue { [self] in
111
+ owner?.executeOnAudioQueue { [weak self] in
112
+ guard let self = self else { return }
113
+
111
114
  if channels.isEmpty || playIndex >= channels.count {
112
115
  result = 0
113
116
  return
@@ -123,7 +126,9 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
123
126
  * - Parameter time: Time in seconds
124
127
  */
125
128
  func setCurrentTime(time: TimeInterval) {
126
- owner?.executeOnAudioQueue { [self] in
129
+ owner?.executeOnAudioQueue { [weak self] in
130
+ guard let self = self else { return }
131
+
127
132
  if channels.isEmpty || playIndex >= channels.count {
128
133
  return
129
134
  }
@@ -140,7 +145,9 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
140
145
  */
141
146
  func getDuration() -> TimeInterval {
142
147
  var result: TimeInterval = 0
143
- owner?.executeOnAudioQueue { [self] in
148
+ owner?.executeOnAudioQueue { [weak self] in
149
+ guard let self = self else { return }
150
+
144
151
  if channels.isEmpty || playIndex >= channels.count {
145
152
  result = 0
146
153
  return
@@ -161,7 +168,9 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
161
168
  stopCurrentTimeUpdates()
162
169
  stopFadeTimer()
163
170
 
164
- owner?.executeOnAudioQueue { [self] in
171
+ owner?.executeOnAudioQueue { [weak self] in
172
+ guard let self = self else { return }
173
+
165
174
  guard !channels.isEmpty else { return }
166
175
 
167
176
  // Reset play index if it's out of bounds
@@ -186,14 +195,16 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
186
195
  } else {
187
196
  player.play()
188
197
  }
189
-
198
+
190
199
  playIndex = (playIndex + 1) % channels.count
191
200
  startCurrentTimeUpdates()
192
201
  }
193
202
  }
194
203
 
195
204
  func playWithFade(time: TimeInterval) {
196
- owner?.executeOnAudioQueue { [self] in
205
+ owner?.executeOnAudioQueue { [weak self] in
206
+ guard let self = self else { return }
207
+
197
208
  guard !channels.isEmpty else { return }
198
209
 
199
210
  // Reset play index if it's out of bounds
@@ -234,36 +245,58 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
234
245
 
235
246
  player.volume = startVolume
236
247
 
237
- fadeTimer = Timer.scheduledTimer(withTimeInterval: TimeInterval(timeInterval), repeats: true) { [weak self, weak player] timer in
238
- guard let strongSelf = self, let strongPlayer = player else {
239
- timer.invalidate()
240
- return
241
- }
248
+ // Create timer on main thread
249
+ DispatchQueue.main.async { [weak self, weak player] in
250
+ guard let self = self else { return }
242
251
 
243
- currentStep += 1
244
- let progress = Float(currentStep) / Float(totalSteps)
245
- let newVolume = startVolume + progress * (endVolume - startVolume)
252
+ let timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(timeInterval), repeats: true) { [weak self, weak player] timer in
253
+ guard let strongSelf = self, let strongPlayer = player else {
254
+ timer.invalidate()
255
+ return
256
+ }
246
257
 
247
- strongPlayer.volume = newVolume
258
+ currentStep += 1
259
+ let progress = Float(currentStep) / Float(totalSteps)
260
+ let newVolume = startVolume + progress * (endVolume - startVolume)
248
261
 
249
- if currentStep >= totalSteps {
250
- strongPlayer.volume = endVolume
251
- timer.invalidate()
252
- strongSelf.fadeTimer = nil
262
+ // Update player on audio queue
263
+ strongSelf.owner?.executeOnAudioQueue {
264
+ strongPlayer.volume = newVolume
265
+ }
266
+
267
+ if currentStep >= totalSteps {
268
+ strongSelf.owner?.executeOnAudioQueue {
269
+ strongPlayer.volume = endVolume
270
+ }
271
+ timer.invalidate()
272
+
273
+ // Update timer reference on main thread
274
+ DispatchQueue.main.async {
275
+ strongSelf.fadeTimer = nil
276
+ }
277
+ }
253
278
  }
279
+
280
+ self.fadeTimer = timer
281
+ RunLoop.current.add(timer, forMode: .common)
254
282
  }
255
- RunLoop.current.add(fadeTimer!, forMode: .common)
256
283
  }
257
284
 
258
285
  internal func stopFadeTimer() {
259
286
  DispatchQueue.main.async { [weak self] in
260
- self?.fadeTimer?.invalidate()
261
- self?.fadeTimer = nil
287
+ guard let self = self else { return }
288
+
289
+ if let timer = self.fadeTimer {
290
+ timer.invalidate()
291
+ self.fadeTimer = nil
292
+ }
262
293
  }
263
294
  }
264
295
 
265
296
  func pause() {
266
- owner?.executeOnAudioQueue { [self] in
297
+ owner?.executeOnAudioQueue { [weak self] in
298
+ guard let self = self else { return }
299
+
267
300
  stopCurrentTimeUpdates()
268
301
 
269
302
  // Check for valid playIndex
@@ -275,7 +308,9 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
275
308
  }
276
309
 
277
310
  func resume() {
278
- owner?.executeOnAudioQueue { [self] in
311
+ owner?.executeOnAudioQueue { [weak self] in
312
+ guard let self = self else { return }
313
+
279
314
  // Check for valid playIndex
280
315
  guard !channels.isEmpty && playIndex < channels.count else { return }
281
316
 
@@ -287,7 +322,9 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
287
322
  }
288
323
 
289
324
  func stop() {
290
- owner?.executeOnAudioQueue { [self] in
325
+ owner?.executeOnAudioQueue { [weak self] in
326
+ guard let self = self else { return }
327
+
291
328
  stopCurrentTimeUpdates()
292
329
  stopFadeTimer()
293
330
 
@@ -304,7 +341,9 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
304
341
 
305
342
  func stopWithFade() {
306
343
  // Store current player locally to avoid race conditions with playIndex
307
- owner?.executeOnAudioQueue { [self] in
344
+ owner?.executeOnAudioQueue { [weak self] in
345
+ guard let self = self else { return }
346
+
308
347
  guard !channels.isEmpty && playIndex < channels.count else {
309
348
  stop()
310
349
  return
@@ -329,7 +368,9 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
329
368
  }
330
369
 
331
370
  func loop() {
332
- owner?.executeOnAudioQueue { [self] in
371
+ owner?.executeOnAudioQueue { [weak self] in
372
+ guard let self = self else { return }
373
+
333
374
  self.stop()
334
375
 
335
376
  guard !channels.isEmpty && playIndex < channels.count else { return }
@@ -344,7 +385,9 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
344
385
  }
345
386
 
346
387
  func unload() {
347
- owner?.executeOnAudioQueue { [self] in
388
+ owner?.executeOnAudioQueue { [weak self] in
389
+ guard let self = self else { return }
390
+
348
391
  self.stop()
349
392
  stopCurrentTimeUpdates()
350
393
  stopFadeTimer()
@@ -357,7 +400,9 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
357
400
  * - Parameter volume: Volume level (0.0-1.0)
358
401
  */
359
402
  func setVolume(volume: NSNumber!) {
360
- owner?.executeOnAudioQueue { [self] in
403
+ owner?.executeOnAudioQueue { [weak self] in
404
+ guard let self = self else { return }
405
+
361
406
  // Ensure volume is in valid range
362
407
  let validVolume = min(max(volume.floatValue, Constant.MinVolume), Constant.MaxVolume)
363
408
  for player in channels {
@@ -371,7 +416,9 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
371
416
  * - Parameter rate: Playback rate (0.5-2.0 is typical range)
372
417
  */
373
418
  func setRate(rate: NSNumber!) {
374
- owner?.executeOnAudioQueue { [self] in
419
+ owner?.executeOnAudioQueue { [weak self] in
420
+ guard let self = self else { return }
421
+
375
422
  // Ensure rate is in valid range
376
423
  let validRate = min(max(rate.floatValue, Constant.MinRate), Constant.MaxRate)
377
424
  for player in channels {
@@ -384,11 +431,13 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
384
431
  * AVAudioPlayerDelegate method called when playback finishes
385
432
  */
386
433
  public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
387
- owner?.executeOnAudioQueue { [self] in
434
+ owner?.executeOnAudioQueue { [weak self] in
435
+ guard let self = self else { return }
436
+
388
437
  self.owner?.notifyListeners("complete", data: [
389
438
  "assetId": self.assetId
390
439
  ])
391
-
440
+
392
441
  // Notify the owner that this player finished
393
442
  // The owner will check if any other assets are still playing
394
443
  owner?.audioPlayerDidFinishPlaying(player, successfully: flag)
@@ -403,7 +452,9 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
403
452
 
404
453
  func isPlaying() -> Bool {
405
454
  var result: Bool = false
406
- owner?.executeOnAudioQueue { [self] in
455
+ owner?.executeOnAudioQueue { [weak self] in
456
+ guard let self = self else { return }
457
+
407
458
  if channels.isEmpty || playIndex >= channels.count {
408
459
  result = false
409
460
  return
@@ -440,8 +491,10 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
440
491
 
441
492
  internal func stopCurrentTimeUpdates() {
442
493
  DispatchQueue.main.async { [weak self] in
443
- self?.currentTimeTimer?.invalidate()
444
- self?.currentTimeTimer = nil
494
+ guard let self = self else { return }
495
+
496
+ self.currentTimeTimer?.invalidate()
497
+ self.currentTimeTimer = nil
445
498
  }
446
499
  }
447
500
  }
@@ -143,7 +143,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
143
143
  } else {
144
144
  try self.session.setCategory(AVAudioSession.Category.playback, options: .mixWithOthers)
145
145
  }
146
-
146
+
147
147
  // Only activate if needed (background mode)
148
148
  if background {
149
149
  try self.session.setActive(true)
@@ -195,7 +195,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
195
195
  return false
196
196
  }
197
197
  }
198
-
198
+
199
199
  // Only deactivate if no assets are playing
200
200
  if !hasPlayingAssets {
201
201
  try self.session.setActive(false, options: .notifyOthersOnDeactivation)
@@ -210,7 +210,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
210
210
  // Instead, check if all players are done
211
211
  audioQueue.async { [weak self] in
212
212
  guard let self = self else { return }
213
-
213
+
214
214
  // Avoid recursive calls by checking if the asset is still in the list
215
215
  let hasPlayingAssets = self.audioList.values.contains { asset in
216
216
  if let audioAsset = asset as? AudioAsset {
@@ -219,7 +219,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
219
219
  }
220
220
  return false
221
221
  }
222
-
222
+
223
223
  // Only end the session if no more assets are playing
224
224
  if !hasPlayingAssets {
225
225
  self.endSession()
@@ -478,11 +478,28 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
478
478
 
479
479
  var basePath: String?
480
480
  if let url = URL(string: assetPath), url.scheme != nil {
481
- // Handle remote URL
482
- let remoteAudioAsset = RemoteAudioAsset(owner: self, withAssetId: audioId, withPath: assetPath, withChannels: channels, withVolume: volume, withFadeDelay: delay)
483
- self.audioList[audioId] = remoteAudioAsset
484
- call.resolve()
485
- return
481
+ // Check if it's a local file URL or a remote URL
482
+ if url.isFileURL {
483
+ // Handle local file URL
484
+ let fileURL = url
485
+ basePath = fileURL.path
486
+
487
+ if let basePath = basePath, FileManager.default.fileExists(atPath: basePath) {
488
+ let audioAsset = AudioAsset(
489
+ owner: self,
490
+ withAssetId: audioId, withPath: basePath, withChannels: channels,
491
+ withVolume: volume, withFadeDelay: delay)
492
+ self.audioList[audioId] = audioAsset
493
+ call.resolve()
494
+ return
495
+ }
496
+ } else {
497
+ // Handle remote URL
498
+ let remoteAudioAsset = RemoteAudioAsset(owner: self, withAssetId: audioId, withPath: assetPath, withChannels: channels, withVolume: volume, withFadeDelay: delay)
499
+ self.audioList[audioId] = remoteAudioAsset
500
+ call.resolve()
501
+ return
502
+ }
486
503
  } else if isLocalUrl == false {
487
504
  // Handle public folder
488
505
  assetPath = assetPath.starts(with: "public/") ? assetPath : "public/" + assetPath
@@ -15,7 +15,9 @@ public class RemoteAudioAsset: AudioAsset {
15
15
  override init(owner: NativeAudio, withAssetId assetId: String, withPath path: String!, withChannels channels: Int!, withVolume volume: Float!, withFadeDelay delay: Float!) {
16
16
  super.init(owner: owner, withAssetId: assetId, withPath: path, withChannels: channels ?? 1, withVolume: volume ?? 1.0, withFadeDelay: delay ?? 0.0)
17
17
 
18
- owner.executeOnAudioQueue { [self] in
18
+ owner.executeOnAudioQueue { [weak self] in
19
+ guard let self = self else { return }
20
+
19
21
  guard let url = URL(string: path ?? "") else {
20
22
  print("Invalid URL: \(String(describing: path))")
21
23
  return
@@ -86,7 +88,9 @@ public class RemoteAudioAsset: AudioAsset {
86
88
  }
87
89
 
88
90
  func playerDidFinishPlaying(player: AVPlayer) {
89
- owner?.executeOnAudioQueue { [self] in
91
+ owner?.executeOnAudioQueue { [weak self] in
92
+ guard let self = self else { return }
93
+
90
94
  self.owner?.notifyListeners("complete", data: [
91
95
  "assetId": self.assetId
92
96
  ])
@@ -94,7 +98,9 @@ public class RemoteAudioAsset: AudioAsset {
94
98
  }
95
99
 
96
100
  override func play(time: TimeInterval, delay: TimeInterval) {
97
- owner?.executeOnAudioQueue { [self] in
101
+ owner?.executeOnAudioQueue { [weak self] in
102
+ guard let self = self else { return }
103
+
98
104
  guard !players.isEmpty else { return }
99
105
 
100
106
  // Reset play index if it's out of bounds
@@ -124,7 +130,9 @@ public class RemoteAudioAsset: AudioAsset {
124
130
  }
125
131
 
126
132
  override func pause() {
127
- owner?.executeOnAudioQueue { [self] in
133
+ owner?.executeOnAudioQueue { [weak self] in
134
+ guard let self = self else { return }
135
+
128
136
  guard !players.isEmpty && playIndex < players.count else { return }
129
137
 
130
138
  let player = players[playIndex]
@@ -134,7 +142,9 @@ public class RemoteAudioAsset: AudioAsset {
134
142
  }
135
143
 
136
144
  override func resume() {
137
- owner?.executeOnAudioQueue { [self] in
145
+ owner?.executeOnAudioQueue { [weak self] in
146
+ guard let self = self else { return }
147
+
138
148
  guard !players.isEmpty && playIndex < players.count else { return }
139
149
 
140
150
  let player = players[playIndex]
@@ -143,24 +153,27 @@ public class RemoteAudioAsset: AudioAsset {
143
153
  // Add notification observer for when playback stops
144
154
  cleanupNotificationObservers()
145
155
 
156
+ // Capture weak reference to self
146
157
  let observer = NotificationCenter.default.addObserver(
147
158
  forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
148
159
  object: player.currentItem,
149
- queue: nil) { [weak self] notification in
150
- guard let strongSelf = self else { return }
160
+ queue: OperationQueue.main) { [weak self, weak player] notification in
161
+ guard let strongSelf = self, let strongPlayer = player else { return }
151
162
 
152
- if let currentItem = notification.object as? AVPlayerItem,
153
- currentItem == strongSelf.playerItems[strongSelf.playIndex] {
154
- strongSelf.playerDidFinishPlaying(player: strongSelf.players[strongSelf.playIndex])
163
+ if let currentItem = notification.object as? AVPlayerItem,
164
+ strongPlayer.currentItem == currentItem {
165
+ strongSelf.playerDidFinishPlaying(player: strongPlayer)
166
+ }
155
167
  }
156
- }
157
168
  notificationObservers.append(observer)
158
169
  startCurrentTimeUpdates()
159
170
  }
160
171
  }
161
172
 
162
173
  override func stop() {
163
- owner?.executeOnAudioQueue { [self] in
174
+ owner?.executeOnAudioQueue { [weak self] in
175
+ guard let self = self else { return }
176
+
164
177
  stopCurrentTimeUpdates()
165
178
 
166
179
  for player in players {
@@ -178,7 +191,9 @@ public class RemoteAudioAsset: AudioAsset {
178
191
  }
179
192
 
180
193
  override func loop() {
181
- owner?.executeOnAudioQueue { [self] in
194
+ owner?.executeOnAudioQueue { [weak self] in
195
+ guard let self = self else { return }
196
+
182
197
  cleanupNotificationObservers()
183
198
 
184
199
  for (index, player) in players.enumerated() {
@@ -189,14 +204,15 @@ public class RemoteAudioAsset: AudioAsset {
189
204
  let observer = NotificationCenter.default.addObserver(
190
205
  forName: .AVPlayerItemDidPlayToEndTime,
191
206
  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
- }
207
+ queue: OperationQueue.main) { [weak self, weak player] notification in
208
+ guard let strongPlayer = player,
209
+ let strongSelf = self,
210
+ let item = notification.object as? AVPlayerItem,
211
+ strongPlayer.currentItem === item else { return }
212
+
213
+ strongPlayer.seek(to: .zero)
214
+ strongPlayer.play()
215
+ }
200
216
 
201
217
  notificationObservers.append(observer)
202
218
 
@@ -218,7 +234,9 @@ public class RemoteAudioAsset: AudioAsset {
218
234
  }
219
235
 
220
236
  @objc func playerItemDidReachEnd(notification: Notification) {
221
- owner?.executeOnAudioQueue { [self] in
237
+ owner?.executeOnAudioQueue { [weak self] in
238
+ guard let self = self else { return }
239
+
222
240
  if let playerItem = notification.object as? AVPlayerItem,
223
241
  let player = players.first(where: { $0.currentItem == playerItem }) {
224
242
  player.seek(to: .zero)
@@ -228,7 +246,9 @@ public class RemoteAudioAsset: AudioAsset {
228
246
  }
229
247
 
230
248
  override func unload() {
231
- owner?.executeOnAudioQueue { [self] in
249
+ owner?.executeOnAudioQueue { [weak self] in
250
+ guard let self = self else { return }
251
+
232
252
  stopCurrentTimeUpdates()
233
253
  stop()
234
254
 
@@ -245,7 +265,9 @@ public class RemoteAudioAsset: AudioAsset {
245
265
  }
246
266
 
247
267
  override func setVolume(volume: NSNumber!) {
248
- owner?.executeOnAudioQueue { [self] in
268
+ owner?.executeOnAudioQueue { [weak self] in
269
+ guard let self = self else { return }
270
+
249
271
  // Ensure volume is in valid range (0.0-1.0)
250
272
  let validVolume = min(max(volume.floatValue, Constant.MinVolume), Constant.MaxVolume)
251
273
  for player in players {
@@ -255,7 +277,9 @@ public class RemoteAudioAsset: AudioAsset {
255
277
  }
256
278
 
257
279
  override func setRate(rate: NSNumber!) {
258
- owner?.executeOnAudioQueue { [self] in
280
+ owner?.executeOnAudioQueue { [weak self] in
281
+ guard let self = self else { return }
282
+
259
283
  // Ensure rate is in valid range
260
284
  let validRate = min(max(rate.floatValue, Constant.MinRate), Constant.MaxRate)
261
285
  for player in players {
@@ -266,7 +290,9 @@ public class RemoteAudioAsset: AudioAsset {
266
290
 
267
291
  override func isPlaying() -> Bool {
268
292
  var result = false
269
- owner?.executeOnAudioQueue { [self] in
293
+ owner?.executeOnAudioQueue { [weak self] in
294
+ guard let self = self else { return }
295
+
270
296
  guard !players.isEmpty && playIndex < players.count else {
271
297
  result = false
272
298
  return
@@ -279,7 +305,9 @@ public class RemoteAudioAsset: AudioAsset {
279
305
 
280
306
  override func getCurrentTime() -> TimeInterval {
281
307
  var result: TimeInterval = 0
282
- owner?.executeOnAudioQueue { [self] in
308
+ owner?.executeOnAudioQueue { [weak self] in
309
+ guard let self = self else { return }
310
+
283
311
  guard !players.isEmpty && playIndex < players.count else {
284
312
  result = 0
285
313
  return
@@ -292,7 +320,9 @@ public class RemoteAudioAsset: AudioAsset {
292
320
 
293
321
  override func getDuration() -> TimeInterval {
294
322
  var result: TimeInterval = 0
295
- owner?.executeOnAudioQueue { [self] in
323
+ owner?.executeOnAudioQueue { [weak self] in
324
+ guard let self = self else { return }
325
+
296
326
  guard !players.isEmpty && playIndex < players.count else {
297
327
  result = 0
298
328
  return
@@ -308,7 +338,9 @@ public class RemoteAudioAsset: AudioAsset {
308
338
  }
309
339
 
310
340
  override func playWithFade(time: TimeInterval) {
311
- owner?.executeOnAudioQueue { [self] in
341
+ owner?.executeOnAudioQueue { [weak self] in
342
+ guard let self = self else { return }
343
+
312
344
  guard !players.isEmpty && playIndex < players.count else { return }
313
345
 
314
346
  let player = players[playIndex]
@@ -332,7 +364,9 @@ public class RemoteAudioAsset: AudioAsset {
332
364
  }
333
365
 
334
366
  override func stopWithFade() {
335
- owner?.executeOnAudioQueue { [self] in
367
+ owner?.executeOnAudioQueue { [weak self] in
368
+ guard let self = self else { return }
369
+
336
370
  guard !players.isEmpty && playIndex < players.count else {
337
371
  stop()
338
372
  return
@@ -390,24 +424,36 @@ public class RemoteAudioAsset: AudioAsset {
390
424
 
391
425
  stopFadeTimer()
392
426
 
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
- }
427
+ // Ensure timer creation happens on main thread
428
+ DispatchQueue.main.async { [weak self, weak player] in
429
+ guard let self = self else { return }
398
430
 
399
- currentStep += 1
400
- let progress = Float(currentStep) / Float(totalSteps)
401
- let newVolume = startVolume + progress * (endVolume - startVolume)
431
+ self.fadeTimer = Timer.scheduledTimer(withTimeInterval: TimeInterval(timeInterval), repeats: true) { [weak self, weak player] timer in
432
+ guard let strongPlayer = player, let strongSelf = self else {
433
+ timer.invalidate()
434
+ return
435
+ }
436
+
437
+ currentStep += 1
438
+ let progress = Float(currentStep) / Float(totalSteps)
439
+ let newVolume = startVolume + progress * (endVolume - startVolume)
440
+
441
+ strongPlayer.volume = newVolume
442
+
443
+ if currentStep >= totalSteps {
444
+ strongPlayer.volume = endVolume
445
+ timer.invalidate()
402
446
 
403
- strongPlayer.volume = newVolume
447
+ // Update timer reference on main thread
448
+ DispatchQueue.main.async {
449
+ strongSelf.fadeTimer = nil
450
+ }
451
+ }
452
+ }
404
453
 
405
- if currentStep >= totalSteps {
406
- strongPlayer.volume = endVolume
407
- timer.invalidate()
408
- self.fadeTimer = nil
454
+ if let timer = self.fadeTimer {
455
+ RunLoop.current.add(timer, forMode: .common)
409
456
  }
410
457
  }
411
- RunLoop.current.add(fadeTimer!, forMode: .common)
412
458
  }
413
459
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/native-audio",
3
- "version": "7.3.18",
3
+ "version": "7.3.19",
4
4
  "description": "A native plugin for native audio engine",
5
5
  "license": "MIT",
6
6
  "main": "dist/plugin.cjs.js",