@capgo/native-audio 8.2.14 → 8.3.0

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.
@@ -79,7 +79,7 @@ dependencies {
79
79
  }
80
80
 
81
81
  implementation 'androidx.media3:media3-session:1.9.2'
82
- implementation 'androidx.media3:media3-transformer:1.9.0'
82
+ implementation 'androidx.media3:media3-transformer:1.9.2'
83
83
  implementation 'androidx.media3:media3-ui:1.9.0'
84
84
  implementation 'androidx.media3:media3-database:1.9.2'
85
85
  implementation 'androidx.media3:media3-common:1.9.2'
@@ -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.14"
15
+ private let pluginVersion: String = "8.3.0"
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
@@ -727,16 +740,21 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
727
740
  }
728
741
 
729
742
  @objc private func getAudioAsset(_ call: CAPPluginCall) -> AudioAsset? {
743
+ // Avoid reentrant sync when already on audio queue (e.g. from pause()) to prevent deadlock
744
+ if DispatchQueue.getSpecific(key: audioQueueContextKey) == true {
745
+ return self.audioList[call.getString(Constant.AssetIdKey) ?? ""] as? AudioAsset
746
+ }
730
747
  var asset: AudioAsset?
731
- audioQueue.sync { // Read operations should use sync
748
+ audioQueue.sync {
732
749
  asset = self.audioList[call.getString(Constant.AssetIdKey) ?? ""] as? AudioAsset
733
750
  }
734
751
  return asset
735
752
  }
736
753
 
737
754
  @objc func setCurrentTime(_ call: CAPPluginCall) {
738
- // Consistent use of audioQueue.sync for all operations
739
755
  audioQueue.sync {
756
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
757
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
740
758
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
741
759
  call.reject("Failed to get audio asset")
742
760
  return
@@ -752,6 +770,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
752
770
 
753
771
  @objc func getDuration(_ call: CAPPluginCall) {
754
772
  audioQueue.sync {
773
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
774
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
755
775
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
756
776
  call.reject("Failed to get audio asset")
757
777
  return
@@ -765,6 +785,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
765
785
 
766
786
  @objc func getCurrentTime(_ call: CAPPluginCall) {
767
787
  audioQueue.sync {
788
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
789
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
768
790
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
769
791
  call.reject("Failed to get audio asset")
770
792
  return
@@ -777,7 +799,10 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
777
799
  }
778
800
 
779
801
  @objc func resume(_ call: CAPPluginCall) {
802
+ let audioId = call.getString(Constant.AssetIdKey) ?? ""
780
803
  audioQueue.sync {
804
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
805
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
781
806
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
782
807
  call.reject("Failed to get audio asset")
783
808
  return
@@ -807,7 +832,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
807
832
 
808
833
  // Update notification when resumed
809
834
  if self.showNotification {
810
- self.updatePlaybackState(isPlaying: true)
835
+ self.updateNowPlayingInfo(audioId: audioId, audioAsset: audioAsset)
811
836
  }
812
837
 
813
838
  call.resolve()
@@ -816,6 +841,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
816
841
 
817
842
  @objc func pause(_ call: CAPPluginCall) {
818
843
  audioQueue.sync {
844
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
845
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
819
846
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
820
847
  call.reject("Failed to get audio asset")
821
848
  return
@@ -853,6 +880,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
853
880
  let fadeOutDuration = call.getDouble(Constant.FadeOutDuration) ?? Double(Constant.DefaultFadeDuration)
854
881
 
855
882
  audioQueue.sync {
883
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
884
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
856
885
  guard !self.audioList.isEmpty else {
857
886
  call.reject("Audio list is empty")
858
887
  return
@@ -861,9 +890,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
861
890
  do {
862
891
  try self.stopAudio(audioId: audioId, fadeOut: fadeOut, fadeOutDuration: fadeOutDuration)
863
892
 
864
- // Clear notification when stopped
865
- if self.showNotification {
866
- self.clearNowPlayingInfo()
893
+ // Reset current track when stopping the asset that was playing (internal state, not just for notifications)
894
+ if self.currentlyPlayingAssetId == audioId {
867
895
  self.currentlyPlayingAssetId = nil
868
896
  }
869
897
 
@@ -883,6 +911,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
883
911
 
884
912
  @objc func loop(_ call: CAPPluginCall) {
885
913
  audioQueue.sync {
914
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
915
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
886
916
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
887
917
  call.reject("Failed to get audio asset")
888
918
  return
@@ -909,6 +939,11 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
909
939
  asset.unload()
910
940
  self.audioList[audioId] = nil
911
941
 
942
+ // Reset current track if this was the currently playing asset (internal state tracking)
943
+ if self.currentlyPlayingAssetId == audioId {
944
+ self.currentlyPlayingAssetId = nil
945
+ }
946
+
912
947
  // Clean up playOnce tracking if this was a playOnce asset
913
948
  if self.playOnceAssets.contains(audioId) {
914
949
  self.playOnceAssets.remove(audioId)
@@ -936,6 +971,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
936
971
 
937
972
  @objc func setVolume(_ call: CAPPluginCall) {
938
973
  audioQueue.sync {
974
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
975
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
939
976
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
940
977
  call.reject("Failed to get audio asset")
941
978
  return
@@ -950,6 +987,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
950
987
 
951
988
  @objc func setRate(_ call: CAPPluginCall) {
952
989
  audioQueue.sync {
990
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
991
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
953
992
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
954
993
  call.reject("Failed to get audio asset")
955
994
  return
@@ -963,6 +1002,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
963
1002
 
964
1003
  @objc func isPlaying(_ call: CAPPluginCall) {
965
1004
  audioQueue.sync {
1005
+ self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: true)
1006
+ defer { self.audioQueue.setSpecific(key: self.audioQueueContextKey, value: nil) }
966
1007
  guard let audioAsset: AudioAsset = self.getAudioAsset(call) else {
967
1008
  call.reject("Failed to get audio asset")
968
1009
  return
@@ -1320,6 +1361,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
1320
1361
  }
1321
1362
  }
1322
1363
 
1364
+ /// Clears the Now Playing info. Only used when tearing down (deinit); stop/unload do not clear
1365
+ /// so that the next play can overwrite the notification without a race.
1323
1366
  private func clearNowPlayingInfo() {
1324
1367
  DispatchQueue.main.async {
1325
1368
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/native-audio",
3
- "version": "8.2.14",
3
+ "version": "8.3.0",
4
4
  "description": "A native plugin for native audio engine",
5
5
  "license": "MPL-2.0",
6
6
  "main": "dist/plugin.cjs.js",