@capgo/native-audio 8.2.15 → 8.3.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.
@@ -12,7 +12,7 @@ enum MyError: Error {
12
12
  @objc(NativeAudio)
13
13
  // swiftlint:disable:next type_body_length
14
14
  public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
15
- private let pluginVersion: String = "8.2.15"
15
+ private let pluginVersion: String = "8.3.1"
16
16
  public let identifier = "NativeAudio"
17
17
  public let jsName = "NativeAudio"
18
18
  public let pluginMethods: [CAPPluginMethod] = [
@@ -49,6 +49,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
49
49
  }
50
50
  }
51
51
  private let queueKey = DispatchSpecificKey<Bool>()
52
+ /// Set while executing a block on the audio queue so getAudioAsset/endSession can avoid reentrant sync (deadlock).
53
+ private let audioQueueContextKey = DispatchSpecificKey<Bool?>()
52
54
  var session = AVAudioSession.sharedInstance()
53
55
 
54
56
  // Track if audio session has been initialized
@@ -187,7 +189,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
187
189
 
188
190
  if !asset.isPlaying() {
189
191
  asset.resume()
190
- self.updatePlaybackState(isPlaying: true)
192
+ self.updateNowPlayingInfo(audioId: assetId, audioAsset: asset)
191
193
  }
192
194
  }
193
195
  return .success
@@ -222,9 +224,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
222
224
  }
223
225
 
224
226
  asset.stop()
225
- self.clearNowPlayingInfo()
226
227
  self.currentlyPlayingAssetId = nil
227
- self.updatePlaybackState(isPlaying: false)
228
228
  }
229
229
  return .success
230
230
  }
@@ -245,7 +245,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
245
245
  self.updatePlaybackState(isPlaying: false)
246
246
  } else {
247
247
  asset.resume()
248
- self.updatePlaybackState(isPlaying: true)
248
+ self.updateNowPlayingInfo(audioId: assetId, audioAsset: asset)
249
249
  }
250
250
  }
251
251
  return .success
@@ -273,7 +273,11 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
273
273
  let focus = call.getBool(Constant.FocusAudio) ?? false
274
274
  let background = call.getBool(Constant.Background) ?? false
275
275
  let ignoreSilent = call.getBool(Constant.IgnoreSilent) ?? true
276
- self.showNotification = call.getBool(Constant.ShowNotification) ?? false
276
+ // Only update showNotification when explicitly provided so repeated configure() calls
277
+ // (e.g. when switching assets) don't reset it to false and break Now Playing for the next play
278
+ if let showNotification = call.getBool(Constant.ShowNotification) {
279
+ self.showNotification = showNotification
280
+ }
277
281
 
278
282
  logger.info("Configuring audio session with focus=%@ background=%@ ignoreSilent=%@", "\(focus)", "\(background)", "\(ignoreSilent)")
279
283
 
@@ -415,9 +419,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
415
419
  self.playOnceAssets.remove(assetId)
416
420
  self.notificationMetadataMap.removeValue(forKey: assetId)
417
421
 
418
- // Clear notification if this was the currently playing asset
422
+ // Reset current track if this was the currently playing asset (next play will overwrite Now Playing)
419
423
  if self.currentlyPlayingAssetId == assetId {
420
- self.clearNowPlayingInfo()
421
424
  self.currentlyPlayingAssetId = nil
422
425
  }
423
426
 
@@ -605,14 +608,24 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
605
608
 
606
609
  func endSession() {
607
610
  do {
608
- // Check if any audio assets are still playing before deactivating
609
- let hasPlayingAssets = audioQueue.sync {
610
- return self.audioList.values.contains { asset in
611
+ // Avoid reentrant sync when already on audio queue (e.g. from pause()) to prevent deadlock
612
+ let hasPlayingAssets: Bool
613
+ if DispatchQueue.getSpecific(key: audioQueueContextKey) == true {
614
+ hasPlayingAssets = self.audioList.values.contains { asset in
611
615
  if let audioAsset = asset as? AudioAsset {
612
616
  return audioAsset.isPlaying()
613
617
  }
614
618
  return false
615
619
  }
620
+ } else {
621
+ hasPlayingAssets = audioQueue.sync {
622
+ return self.audioList.values.contains { asset in
623
+ if let audioAsset = asset as? AudioAsset {
624
+ return audioAsset.isPlaying()
625
+ }
626
+ return false
627
+ }
628
+ }
616
629
  }
617
630
 
618
631
  // Only deactivate if no assets are playing AND no other audio is active
@@ -627,10 +640,21 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
627
640
 
628
641
  public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
629
642
  // Don't immediately end the session here, as other players might still be active
630
- // Instead, check if all players are done
643
+ // Instead, check if all players are done and clear Now Playing if this asset was current
631
644
  audioQueue.async { [weak self] in
632
645
  guard let self = self else { return }
633
646
 
647
+ // Find which asset this player belongs to; if it was the currently playing one, clear notification
648
+ for (audioId, asset) in self.audioList {
649
+ if let audioAsset = asset as? AudioAsset, audioAsset.channels.contains(player) {
650
+ if self.currentlyPlayingAssetId == audioId {
651
+ self.currentlyPlayingAssetId = nil
652
+ self.clearNowPlayingInfo()
653
+ }
654
+ break
655
+ }
656
+ }
657
+
634
658
  // Avoid recursive calls by checking if the asset is still in the list
635
659
  let hasPlayingAssets = self.audioList.values.contains { asset in
636
660
  if let audioAsset = asset as? AudioAsset {
@@ -642,6 +666,11 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
642
666
 
643
667
  // Only end the session if no more assets are playing
644
668
  if !hasPlayingAssets {
669
+ // If we didn't find the asset above (e.g. playOnce already removed it), clear notification when nothing is playing
670
+ if self.currentlyPlayingAssetId != nil {
671
+ self.currentlyPlayingAssetId = nil
672
+ self.clearNowPlayingInfo()
673
+ }
645
674
  self.endSession()
646
675
  }
647
676
  }
@@ -727,16 +756,21 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
727
756
  }
728
757
 
729
758
  @objc private func getAudioAsset(_ call: CAPPluginCall) -> AudioAsset? {
759
+ // Avoid reentrant sync when already on audio queue (e.g. from pause()) to prevent deadlock
760
+ if DispatchQueue.getSpecific(key: audioQueueContextKey) == true {
761
+ return self.audioList[call.getString(Constant.AssetIdKey) ?? ""] as? AudioAsset
762
+ }
730
763
  var asset: AudioAsset?
731
- audioQueue.sync { // Read operations should use sync
764
+ audioQueue.sync {
732
765
  asset = self.audioList[call.getString(Constant.AssetIdKey) ?? ""] as? AudioAsset
733
766
  }
734
767
  return asset
735
768
  }
736
769
 
737
770
  @objc func setCurrentTime(_ call: CAPPluginCall) {
738
- // Consistent use of audioQueue.sync for all operations
739
771
  audioQueue.sync {
772
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
773
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
740
774
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
741
775
  call.reject("Failed to get audio asset")
742
776
  return
@@ -752,6 +786,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
752
786
 
753
787
  @objc func getDuration(_ call: CAPPluginCall) {
754
788
  audioQueue.sync {
789
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
790
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
755
791
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
756
792
  call.reject("Failed to get audio asset")
757
793
  return
@@ -765,6 +801,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
765
801
 
766
802
  @objc func getCurrentTime(_ call: CAPPluginCall) {
767
803
  audioQueue.sync {
804
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
805
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
768
806
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
769
807
  call.reject("Failed to get audio asset")
770
808
  return
@@ -777,7 +815,10 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
777
815
  }
778
816
 
779
817
  @objc func resume(_ call: CAPPluginCall) {
818
+ let audioId = call.getString(Constant.AssetIdKey) ?? ""
780
819
  audioQueue.sync {
820
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
821
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
781
822
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
782
823
  call.reject("Failed to get audio asset")
783
824
  return
@@ -807,7 +848,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
807
848
 
808
849
  // Update notification when resumed
809
850
  if self.showNotification {
810
- self.updatePlaybackState(isPlaying: true)
851
+ self.updateNowPlayingInfo(audioId: audioId, audioAsset: audioAsset)
811
852
  }
812
853
 
813
854
  call.resolve()
@@ -816,6 +857,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
816
857
 
817
858
  @objc func pause(_ call: CAPPluginCall) {
818
859
  audioQueue.sync {
860
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
861
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
819
862
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
820
863
  call.reject("Failed to get audio asset")
821
864
  return
@@ -853,6 +896,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
853
896
  let fadeOutDuration = call.getDouble(Constant.FadeOutDuration) ?? Double(Constant.DefaultFadeDuration)
854
897
 
855
898
  audioQueue.sync {
899
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
900
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
856
901
  guard !self.audioList.isEmpty else {
857
902
  call.reject("Audio list is empty")
858
903
  return
@@ -861,9 +906,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
861
906
  do {
862
907
  try self.stopAudio(audioId: audioId, fadeOut: fadeOut, fadeOutDuration: fadeOutDuration)
863
908
 
864
- // Clear notification when stopped
865
- if self.showNotification {
866
- self.clearNowPlayingInfo()
909
+ // Reset current track when stopping the asset that was playing (internal state, not just for notifications)
910
+ if self.currentlyPlayingAssetId == audioId {
867
911
  self.currentlyPlayingAssetId = nil
868
912
  }
869
913
 
@@ -883,6 +927,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
883
927
 
884
928
  @objc func loop(_ call: CAPPluginCall) {
885
929
  audioQueue.sync {
930
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
931
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
886
932
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
887
933
  call.reject("Failed to get audio asset")
888
934
  return
@@ -909,6 +955,11 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
909
955
  asset.unload()
910
956
  self.audioList[audioId] = nil
911
957
 
958
+ // Reset current track if this was the currently playing asset (internal state tracking)
959
+ if self.currentlyPlayingAssetId == audioId {
960
+ self.currentlyPlayingAssetId = nil
961
+ }
962
+
912
963
  // Clean up playOnce tracking if this was a playOnce asset
913
964
  if self.playOnceAssets.contains(audioId) {
914
965
  self.playOnceAssets.remove(audioId)
@@ -936,6 +987,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
936
987
 
937
988
  @objc func setVolume(_ call: CAPPluginCall) {
938
989
  audioQueue.sync {
990
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
991
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
939
992
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
940
993
  call.reject("Failed to get audio asset")
941
994
  return
@@ -950,6 +1003,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
950
1003
 
951
1004
  @objc func setRate(_ call: CAPPluginCall) {
952
1005
  audioQueue.sync {
1006
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
1007
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
953
1008
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
954
1009
  call.reject("Failed to get audio asset")
955
1010
  return
@@ -963,6 +1018,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
963
1018
 
964
1019
  @objc func isPlaying(_ call: CAPPluginCall) {
965
1020
  audioQueue.sync {
1021
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
1022
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
966
1023
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
967
1024
  call.reject("Failed to get audio asset")
968
1025
  return
@@ -1320,6 +1377,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
1320
1377
  }
1321
1378
  }
1322
1379
 
1380
+ /// Clears the Now Playing info. Only used when tearing down (deinit); stop/unload do not clear
1381
+ /// so that the next play can overwrite the notification without a race.
1323
1382
  private func clearNowPlayingInfo() {
1324
1383
  DispatchQueue.main.async {
1325
1384
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/native-audio",
3
- "version": "8.2.15",
3
+ "version": "8.3.1",
4
4
  "description": "A native plugin for native audio engine",
5
5
  "license": "MPL-2.0",
6
6
  "main": "dist/plugin.cjs.js",