@capgo/native-audio 8.2.12 → 8.2.14

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.
@@ -3,16 +3,25 @@ package ee.forgr.audio;
3
3
  import static ee.forgr.audio.Constant.ASSET_ID;
4
4
  import static ee.forgr.audio.Constant.ASSET_PATH;
5
5
  import static ee.forgr.audio.Constant.AUDIO_CHANNEL_NUM;
6
+ import static ee.forgr.audio.Constant.DELAY;
7
+ import static ee.forgr.audio.Constant.DURATION;
6
8
  import static ee.forgr.audio.Constant.ERROR_ASSET_NOT_LOADED;
7
9
  import static ee.forgr.audio.Constant.ERROR_ASSET_PATH_MISSING;
8
10
  import static ee.forgr.audio.Constant.ERROR_AUDIO_ASSET_MISSING;
9
11
  import static ee.forgr.audio.Constant.ERROR_AUDIO_EXISTS;
10
12
  import static ee.forgr.audio.Constant.ERROR_AUDIO_ID_MISSING;
13
+ import static ee.forgr.audio.Constant.FADE_IN;
14
+ import static ee.forgr.audio.Constant.FADE_IN_DURATION;
15
+ import static ee.forgr.audio.Constant.FADE_OUT;
16
+ import static ee.forgr.audio.Constant.FADE_OUT_DURATION;
17
+ import static ee.forgr.audio.Constant.FADE_OUT_START_TIME;
11
18
  import static ee.forgr.audio.Constant.LOOP;
12
19
  import static ee.forgr.audio.Constant.NOTIFICATION_METADATA;
13
20
  import static ee.forgr.audio.Constant.OPT_FOCUS_AUDIO;
21
+ import static ee.forgr.audio.Constant.PLAY;
14
22
  import static ee.forgr.audio.Constant.RATE;
15
23
  import static ee.forgr.audio.Constant.SHOW_NOTIFICATION;
24
+ import static ee.forgr.audio.Constant.TIME;
16
25
  import static ee.forgr.audio.Constant.VOLUME;
17
26
 
18
27
  import android.Manifest;
@@ -55,6 +64,8 @@ import java.util.Iterator;
55
64
  import java.util.Map;
56
65
  import java.util.Set;
57
66
  import java.util.UUID;
67
+ import java.util.concurrent.ConcurrentHashMap;
68
+ import java.util.concurrent.CopyOnWriteArrayList;
58
69
 
59
70
  @UnstableApi
60
71
  @CapacitorPlugin(
@@ -69,19 +80,23 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
69
80
  private final String pluginVersion = "";
70
81
 
71
82
  public static final String TAG = "NativeAudio";
83
+ private static final Logger logger = new Logger(TAG);
84
+ public static boolean debugEnabled = false;
72
85
 
73
- private static HashMap<String, AudioAsset> audioAssetList = new HashMap<>();
74
- private static ArrayList<AudioAsset> resumeList;
86
+ private static ConcurrentHashMap<String, AudioAsset> audioAssetList = new ConcurrentHashMap<>();
87
+ private static CopyOnWriteArrayList<AudioAsset> resumeList;
75
88
  private AudioManager audioManager;
76
- private boolean fadeMusic = false;
77
89
  private boolean audioFocusRequested = false;
78
90
  private int originalAudioMode = AudioManager.MODE_INVALID;
79
91
 
80
- private final Map<String, PluginCall> pendingDurationCalls = new HashMap<>();
92
+ private final Map<String, PluginCall> pendingDurationCalls = new ConcurrentHashMap<>();
93
+ private final Map<String, Handler> pendingPlayHandlers = new ConcurrentHashMap<>();
94
+ private final Map<String, Runnable> pendingPlayRunnables = new ConcurrentHashMap<>();
95
+ private final Map<String, JSObject> audioData = new ConcurrentHashMap<>();
81
96
 
82
97
  // Notification center support
83
98
  private boolean showNotification = false;
84
- private Map<String, Map<String, String>> notificationMetadataMap = new HashMap<>();
99
+ private Map<String, Map<String, String>> notificationMetadataMap = new ConcurrentHashMap<>();
85
100
  private MediaSessionCompat mediaSession;
86
101
  private String currentlyPlayingAssetId;
87
102
  private static final int NOTIFICATION_ID = 1001;
@@ -103,7 +118,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
103
118
 
104
119
  this.audioManager = (AudioManager) this.getActivity().getSystemService(Context.AUDIO_SERVICE);
105
120
 
106
- audioAssetList = new HashMap<>();
121
+ audioAssetList = new ConcurrentHashMap<>();
107
122
 
108
123
  // Store the original audio mode but don't request focus yet
109
124
  if (this.audioManager != null) {
@@ -154,7 +169,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
154
169
 
155
170
  try {
156
171
  if (audioAssetList != null) {
157
- for (HashMap.Entry<String, AudioAsset> entry : audioAssetList.entrySet()) {
172
+ for (Map.Entry<String, AudioAsset> entry : audioAssetList.entrySet()) {
158
173
  AudioAsset audio = entry.getValue();
159
174
 
160
175
  if (audio != null) {
@@ -196,6 +211,16 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
196
211
  }
197
212
  }
198
213
 
214
+ @PluginMethod
215
+ public void setDebugMode(PluginCall call) {
216
+ boolean enabled = Boolean.TRUE.equals(call.getBoolean("enabled", false));
217
+ debugEnabled = enabled;
218
+ if (enabled) {
219
+ logger.info("Debug mode enabled");
220
+ }
221
+ call.resolve();
222
+ }
223
+
199
224
  @PluginMethod
200
225
  public void configure(PluginCall call) {
201
226
  initSoundPool();
@@ -212,7 +237,6 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
212
237
 
213
238
  boolean focus = call.getBoolean(OPT_FOCUS_AUDIO, false);
214
239
  boolean background = call.getBoolean("background", false);
215
- this.fadeMusic = call.getBoolean("fade", false);
216
240
  this.showNotification = call.getBoolean(SHOW_NOTIFICATION, false);
217
241
  this.backgroundPlayback = call.getBoolean("backgroundPlayback", false);
218
242
 
@@ -478,12 +502,125 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
478
502
  new Runnable() {
479
503
  @Override
480
504
  public void run() {
481
- playOrLoop("play", call);
505
+ try {
506
+ final String audioId = call.getString(ASSET_ID);
507
+ if (!isStringValid(audioId)) {
508
+ call.reject(ERROR_AUDIO_ID_MISSING + " - " + audioId);
509
+ return;
510
+ }
511
+
512
+ final double time = call.getDouble(TIME, 0.0);
513
+ final double delaySecs = call.getDouble(DELAY, 0.0);
514
+ final float volume = call.getFloat(VOLUME, 1F);
515
+ final boolean fadeIn = call.getBoolean(FADE_IN, false);
516
+ final double fadeInDurationMs =
517
+ call.getDouble(FADE_IN_DURATION, AudioAsset.DEFAULT_FADE_DURATION_MS / 1000.0) * 1000.0;
518
+ final boolean fadeOut = call.getBoolean(FADE_OUT, false);
519
+ final double fadeOutDurationMs =
520
+ call.getDouble(FADE_OUT_DURATION, AudioAsset.DEFAULT_FADE_DURATION_MS / 1000.0) * 1000.0;
521
+ final double fadeOutStartTimeSecs = call.getDouble(FADE_OUT_START_TIME, 0.0);
522
+
523
+ cancelPendingPlay(audioId);
524
+ clearFadeOutToStopTimer(audioId);
525
+
526
+ if (delaySecs > 0) {
527
+ final Handler handler = new Handler(Looper.getMainLooper());
528
+ final Runnable runnable = new Runnable() {
529
+ @Override
530
+ public void run() {
531
+ pendingPlayHandlers.remove(audioId);
532
+ pendingPlayRunnables.remove(audioId);
533
+ executePlay(
534
+ call,
535
+ audioId,
536
+ time,
537
+ volume,
538
+ fadeIn,
539
+ fadeInDurationMs,
540
+ fadeOut,
541
+ fadeOutDurationMs,
542
+ fadeOutStartTimeSecs
543
+ );
544
+ }
545
+ };
546
+ pendingPlayHandlers.put(audioId, handler);
547
+ pendingPlayRunnables.put(audioId, runnable);
548
+ handler.postDelayed(runnable, Math.max(0L, (long) (delaySecs * 1000)));
549
+ return;
550
+ }
551
+
552
+ executePlay(
553
+ call,
554
+ audioId,
555
+ time,
556
+ volume,
557
+ fadeIn,
558
+ fadeInDurationMs,
559
+ fadeOut,
560
+ fadeOutDurationMs,
561
+ fadeOutStartTimeSecs
562
+ );
563
+ } catch (Exception ex) {
564
+ call.reject(ex.getMessage());
565
+ }
482
566
  }
483
567
  }
484
568
  );
485
569
  }
486
570
 
571
+ private void executePlay(
572
+ PluginCall call,
573
+ String audioId,
574
+ double time,
575
+ float volume,
576
+ boolean fadeIn,
577
+ double fadeInDurationMs,
578
+ boolean fadeOut,
579
+ double fadeOutDurationMs,
580
+ double fadeOutStartTimeSecs
581
+ ) {
582
+ try {
583
+ if (!audioAssetList.containsKey(audioId)) {
584
+ call.reject(ERROR_ASSET_NOT_LOADED + " - " + audioId);
585
+ return;
586
+ }
587
+
588
+ AudioAsset asset = audioAssetList.get(audioId);
589
+ if (asset == null) {
590
+ call.reject(ERROR_AUDIO_ASSET_MISSING + " - " + audioId);
591
+ return;
592
+ }
593
+
594
+ if (fadeIn) {
595
+ asset.playWithFadeIn(time, volume, fadeInDurationMs);
596
+ } else {
597
+ asset.play(time, volume);
598
+ }
599
+
600
+ if (fadeOut) {
601
+ handleFadeOut(asset, audioId, fadeOutDurationMs, fadeOutStartTimeSecs);
602
+ }
603
+
604
+ if (showNotification) {
605
+ currentlyPlayingAssetId = audioId;
606
+ updateNotification(audioId);
607
+ }
608
+
609
+ call.resolve();
610
+ } catch (Exception ex) {
611
+ call.reject(ex.getMessage());
612
+ }
613
+ }
614
+
615
+ private void cancelPendingPlay(String audioId) {
616
+ if (audioId == null) return;
617
+ Handler handler = pendingPlayHandlers.remove(audioId);
618
+ Runnable runnable = pendingPlayRunnables.remove(audioId);
619
+ if (handler != null && runnable != null) {
620
+ handler.removeCallbacks(runnable);
621
+ }
622
+ }
623
+
487
624
  @PluginMethod
488
625
  public void getCurrentTime(final PluginCall call) {
489
626
  try {
@@ -517,21 +654,15 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
517
654
  call.reject(ERROR_AUDIO_ID_MISSING + " - " + audioId);
518
655
  return;
519
656
  }
520
-
521
- if (audioAssetList.containsKey(audioId)) {
522
- AudioAsset asset = audioAssetList.get(audioId);
523
- if (asset != null) {
524
- double duration = asset.getDuration();
525
- if (duration > 0) {
526
- JSObject ret = new JSObject();
527
- ret.put("duration", duration);
528
- call.resolve(ret);
529
- } else {
530
- // Save the call to resolve it later when duration is available
531
- saveDurationCall(audioId, call);
532
- }
657
+ AudioAsset asset = audioAssetList.get(audioId);
658
+ if (asset != null) {
659
+ double duration = asset.getDuration();
660
+ if (duration > 0) {
661
+ JSObject ret = new JSObject();
662
+ ret.put("duration", duration);
663
+ call.resolve(ret);
533
664
  } else {
534
- call.reject(ERROR_ASSET_NOT_LOADED + " - " + audioId);
665
+ saveDurationCall(audioId, call);
535
666
  }
536
667
  } else {
537
668
  call.reject(ERROR_ASSET_NOT_LOADED + " - " + audioId);
@@ -547,6 +678,9 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
547
678
  new Runnable() {
548
679
  @Override
549
680
  public void run() {
681
+ String audioId = call.getString(ASSET_ID);
682
+ cancelPendingPlay(audioId);
683
+ clearFadeOutToStopTimer(audioId);
550
684
  playOrLoop("loop", call);
551
685
  }
552
686
  }
@@ -558,11 +692,25 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
558
692
  try {
559
693
  initSoundPool();
560
694
  String audioId = call.getString(ASSET_ID);
695
+ final boolean fadeOut = call.getBoolean(FADE_OUT, false);
696
+ final double fadeOutDurationMs = call.getDouble(FADE_OUT_DURATION, AudioAsset.DEFAULT_FADE_DURATION_MS / 1000.0) * 1000.0;
697
+
698
+ cancelPendingPlay(audioId);
561
699
 
562
700
  if (audioAssetList.containsKey(audioId)) {
563
701
  AudioAsset asset = audioAssetList.get(audioId);
564
702
  if (asset != null) {
565
- boolean wasPlaying = asset.pause();
703
+ boolean wasPlaying = asset.isPlaying();
704
+
705
+ JSObject data = getAudioAssetData(audioId);
706
+ data.put("volumeBeforePause", asset.getVolume());
707
+ setAudioAssetData(audioId, data);
708
+
709
+ if (fadeOut) {
710
+ asset.stopWithFade(fadeOutDurationMs, true);
711
+ } else {
712
+ asset.pause();
713
+ }
566
714
 
567
715
  if (wasPlaying) {
568
716
  resumeList.add(asset);
@@ -590,11 +738,26 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
590
738
  try {
591
739
  initSoundPool();
592
740
  String audioId = call.getString(ASSET_ID);
741
+ final boolean fadeIn = call.getBoolean(FADE_IN, false);
742
+ final double fadeInDurationMs = call.getDouble(FADE_IN_DURATION, AudioAsset.DEFAULT_FADE_DURATION_MS / 1000.0) * 1000.0;
593
743
 
594
744
  if (audioAssetList.containsKey(audioId)) {
595
745
  AudioAsset asset = audioAssetList.get(audioId);
596
746
  if (asset != null) {
597
- asset.resume();
747
+ JSObject data = getAudioAssetData(audioId);
748
+ float volumeBeforePause = (float) data.optDouble("volumeBeforePause", asset.getVolume());
749
+
750
+ if (fadeIn) {
751
+ asset.setVolume(0f, 0);
752
+ asset.resume();
753
+ asset.setVolume(volumeBeforePause, fadeInDurationMs);
754
+ } else {
755
+ asset.setVolume(volumeBeforePause, 0);
756
+ asset.resume();
757
+ }
758
+
759
+ data.remove("volumeBeforePause");
760
+ setAudioAssetData(audioId, data);
598
761
  resumeList.add(asset);
599
762
 
600
763
  // Update notification when resumed
@@ -622,11 +785,18 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
622
785
  public void run() {
623
786
  try {
624
787
  String audioId = call.getString(ASSET_ID);
788
+ boolean fadeOut = call.getBoolean(FADE_OUT, false);
789
+ double fadeOutDurationMs = call.getDouble(FADE_OUT_DURATION, AudioAsset.DEFAULT_FADE_DURATION_MS / 1000.0) * 1000.0;
790
+
625
791
  if (!isStringValid(audioId)) {
626
792
  call.reject(ERROR_AUDIO_ID_MISSING + " - " + audioId);
627
793
  return;
628
794
  }
629
- stopAudio(audioId);
795
+
796
+ cancelPendingPlay(audioId);
797
+ clearFadeOutToStopTimer(audioId);
798
+ stopAudio(audioId, fadeOut, fadeOutDurationMs);
799
+ audioData.remove(audioId);
630
800
 
631
801
  // Clear notification when stopped
632
802
  if (showNotification) {
@@ -649,19 +819,18 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
649
819
  initSoundPool();
650
820
  new JSObject();
651
821
  JSObject status;
652
-
653
822
  if (isStringValid(call.getString(ASSET_ID))) {
654
823
  String audioId = call.getString(ASSET_ID);
655
-
656
- if (audioAssetList.containsKey(audioId)) {
657
- AudioAsset asset = audioAssetList.get(audioId);
658
- if (asset != null) {
659
- asset.unload();
660
- audioAssetList.remove(audioId);
661
- call.resolve();
662
- } else {
663
- call.reject(ERROR_AUDIO_ASSET_MISSING + " - " + audioId);
664
- }
824
+ cancelPendingPlay(audioId);
825
+ pendingPlayHandlers.remove(audioId);
826
+ pendingPlayRunnables.remove(audioId);
827
+ audioData.remove(audioId);
828
+ AudioAsset asset = audioAssetList.get(audioId);
829
+ if (asset != null) {
830
+ clearFadeOutToStopTimer(audioId);
831
+ asset.unload();
832
+ audioAssetList.remove(audioId);
833
+ call.resolve();
665
834
  } else {
666
835
  call.reject(ERROR_AUDIO_ASSET_MISSING + " - " + audioId);
667
836
  }
@@ -669,6 +838,12 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
669
838
  call.reject(ERROR_AUDIO_ID_MISSING);
670
839
  }
671
840
  } catch (Exception ex) {
841
+ String audioId = call.getString(ASSET_ID);
842
+ if (audioId != null) {
843
+ pendingPlayHandlers.remove(audioId);
844
+ pendingPlayRunnables.remove(audioId);
845
+ audioData.remove(audioId);
846
+ }
672
847
  call.reject(ex.getMessage());
673
848
  }
674
849
  }
@@ -680,11 +855,19 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
680
855
 
681
856
  String audioId = call.getString(ASSET_ID);
682
857
  float volume = call.getFloat(VOLUME, 1F);
858
+ double durationSecs = call.getDouble(DURATION, 0.0);
859
+
860
+ if (durationSecs > 0) {
861
+ logger.debug("setVolume " + volume + " over duration " + durationSecs + " seconds");
862
+ } else {
863
+ logger.debug("setVolume " + volume);
864
+ }
683
865
 
684
866
  if (audioAssetList.containsKey(audioId)) {
685
867
  AudioAsset asset = audioAssetList.get(audioId);
686
868
  if (asset != null) {
687
- asset.setVolume(volume);
869
+ double durationMs = durationSecs * 1000;
870
+ asset.setVolume(volume, durationMs);
688
871
  call.resolve();
689
872
  } else {
690
873
  call.reject(ERROR_AUDIO_ASSET_MISSING);
@@ -748,8 +931,12 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
748
931
 
749
932
  @PluginMethod
750
933
  public void clearCache(PluginCall call) {
751
- RemoteAudioAsset.clearCache(getContext());
752
- call.resolve();
934
+ try {
935
+ RemoteAudioAsset.clearCache(getContext());
936
+ call.resolve();
937
+ } catch (Exception ex) {
938
+ call.reject(ex.getMessage());
939
+ }
753
940
  }
754
941
 
755
942
  @PluginMethod
@@ -758,7 +945,10 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
758
945
  initSoundPool();
759
946
 
760
947
  String audioId = call.getString(ASSET_ID);
761
- Double time = call.getDouble("time", 0.0);
948
+ clearFadeOutToStopTimer(audioId);
949
+ double time = call.getDouble(TIME, 0.0);
950
+
951
+ cancelPendingPlay(audioId);
762
952
 
763
953
  if (!isStringValid(audioId)) {
764
954
  call.reject(ERROR_AUDIO_ID_MISSING + " - " + audioId);
@@ -813,6 +1003,23 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
813
1003
  ret.put("currentTime", roundedTime);
814
1004
  ret.put("assetId", assetId);
815
1005
  notifyListeners("currentTime", ret);
1006
+
1007
+ JSObject data = getAudioAssetData(assetId);
1008
+ if (data.optBoolean("fadeOut", false)) {
1009
+ double fadeOutStartTime = data.optDouble("fadeOutStartTime", -1);
1010
+ if (fadeOutStartTime >= 0 && currentTime >= fadeOutStartTime) {
1011
+ double fadeOutDuration = data.optDouble("fadeOutDuration", AudioAsset.DEFAULT_FADE_DURATION_MS);
1012
+ try {
1013
+ AudioAsset asset = audioAssetList.get(assetId);
1014
+ if (asset != null) {
1015
+ asset.stopWithFade(fadeOutDuration, false);
1016
+ }
1017
+ } catch (Exception e) {
1018
+ logger.error("Error triggering scheduled fade-out", e);
1019
+ }
1020
+ clearFadeOutToStopTimer(assetId);
1021
+ }
1022
+ }
816
1023
  }
817
1024
 
818
1025
  /**
@@ -966,7 +1173,10 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
966
1173
  boolean isLocalUrl = call.getBoolean("isUrl", false);
967
1174
  boolean isComplex = call.getBoolean("isComplex", false);
968
1175
 
969
- Log.d("AudioPlugin", "Debug: audioId = " + audioId + ", assetPath = " + assetPath + ", isLocalUrl = " + isLocalUrl);
1176
+ Log.d(
1177
+ TAG,
1178
+ "Preloading asset: " + audioId + ", path: " + assetPath + ", isLocalUrl: " + isLocalUrl + ", isComplex: " + isComplex
1179
+ );
970
1180
 
971
1181
  if (audioAssetList.containsKey(audioId)) {
972
1182
  call.reject(ERROR_AUDIO_EXISTS + " - " + audioId);
@@ -1024,11 +1234,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
1024
1234
  if (LOOP.equals(action)) {
1025
1235
  asset.loop();
1026
1236
  } else {
1027
- if (fadeMusic) {
1028
- asset.playWithFade(time);
1029
- } else {
1030
- asset.play(time);
1031
- }
1237
+ asset.play(time);
1032
1238
  }
1033
1239
 
1034
1240
  // Update notification if enabled
@@ -1045,18 +1251,79 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
1045
1251
  call.reject("Asset not found: " + audioId);
1046
1252
  }
1047
1253
  } catch (Exception ex) {
1048
- Log.e(TAG, "Error in playOrLoop", ex);
1254
+ logger.error("Error in playOrLoop", ex);
1049
1255
  call.reject(ex.getMessage());
1050
1256
  }
1051
1257
  }
1052
1258
 
1259
+ private void scheduleFadeOut(AudioAsset asset, double fadeOutDurationMs, double fadeOutStartTimeMs) {
1260
+ try {
1261
+ double duration = asset.getDuration();
1262
+ if (duration > 0) {
1263
+ double fadeOutStartTime = duration - (fadeOutDurationMs / 1000.0);
1264
+ if (fadeOutStartTimeMs > 0) {
1265
+ fadeOutStartTime = fadeOutStartTimeMs / 1000.0;
1266
+ }
1267
+
1268
+ logger.debug("Scheduling fade-out for asset: " + asset.assetId + ", start time: " + fadeOutStartTime + " seconds");
1269
+
1270
+ // Store fade-out parameters in asset data
1271
+ JSObject data = getAudioAssetData(asset.assetId);
1272
+ data.put("fadeOut", true);
1273
+ data.put("fadeOutStartTime", fadeOutStartTime);
1274
+ data.put("fadeOutDuration", fadeOutDurationMs);
1275
+ setAudioAssetData(asset.assetId, data);
1276
+ } else {
1277
+ logger.warning("Duration not available, skipping fade-out scheduling");
1278
+ }
1279
+ } catch (Exception e) {
1280
+ logger.error("Error handling fade-out", e);
1281
+ }
1282
+ }
1283
+
1284
+ private void handleFadeOut(AudioAsset asset, String audioId, double fadeOutDurationMs, double fadeOutStartTimeSecs) {
1285
+ try {
1286
+ double duration = asset.getDuration();
1287
+ if (duration <= 0) {
1288
+ logger.warning("Duration not available, skipping fade-out scheduling");
1289
+ return;
1290
+ }
1291
+
1292
+ double fadeOutStartTime = duration - (fadeOutDurationMs / 1000.0);
1293
+ if (fadeOutStartTimeSecs > 0) {
1294
+ fadeOutStartTime = fadeOutStartTimeSecs;
1295
+ }
1296
+ fadeOutStartTime = Math.max(fadeOutStartTime, 0);
1297
+
1298
+ JSObject data = getAudioAssetData(audioId);
1299
+ data.put("fadeOut", true);
1300
+ data.put("fadeOutStartTime", fadeOutStartTime);
1301
+ data.put("fadeOutDuration", fadeOutDurationMs);
1302
+ setAudioAssetData(audioId, data);
1303
+ } catch (Exception e) {
1304
+ logger.error("Error scheduling fade-out", e);
1305
+ }
1306
+ }
1307
+
1308
+ private void clearFadeOutToStopTimer(String audioId) {
1309
+ JSObject data = getAudioAssetData(audioId);
1310
+ if (data.has("fadeOut")) {
1311
+ logger.debug("Cancelling fade-out for asset: " + audioId);
1312
+ data.remove("fadeOut");
1313
+ data.remove("fadeOutStartTime");
1314
+ data.remove("fadeOutDuration");
1315
+ setAudioAssetData(audioId, data);
1316
+ }
1317
+ }
1318
+
1053
1319
  private void initSoundPool() {
1054
1320
  if (audioAssetList == null) {
1055
- audioAssetList = new HashMap<>();
1321
+ logger.debug("Initializing audio asset list");
1322
+ audioAssetList = new ConcurrentHashMap<>();
1056
1323
  }
1057
-
1058
1324
  if (resumeList == null) {
1059
- resumeList = new ArrayList<>();
1325
+ logger.debug("Initializing resume list");
1326
+ resumeList = new CopyOnWriteArrayList<>();
1060
1327
  }
1061
1328
  }
1062
1329
 
@@ -1121,15 +1388,15 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
1121
1388
  }
1122
1389
  }
1123
1390
 
1124
- private void stopAudio(String audioId) throws Exception {
1391
+ private void stopAudio(String audioId, boolean fadeOut, double fadeOutDurationMs) throws Exception {
1125
1392
  if (!audioAssetList.containsKey(audioId)) {
1126
1393
  throw new Exception(ERROR_ASSET_NOT_LOADED);
1127
1394
  }
1128
1395
 
1129
1396
  AudioAsset asset = audioAssetList.get(audioId);
1130
1397
  if (asset != null) {
1131
- if (fadeMusic) {
1132
- asset.stopWithFade();
1398
+ if (fadeOut) {
1399
+ asset.stopWithFade(fadeOutDurationMs, false);
1133
1400
  } else {
1134
1401
  asset.stop();
1135
1402
  }
@@ -1137,12 +1404,12 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
1137
1404
  }
1138
1405
 
1139
1406
  private void saveDurationCall(String audioId, PluginCall call) {
1140
- Log.d(TAG, "Saving duration call for later: " + audioId);
1407
+ logger.debug("Saving duration call for later: " + audioId);
1141
1408
  pendingDurationCalls.put(audioId, call);
1142
1409
  }
1143
1410
 
1144
1411
  public void notifyDurationAvailable(String assetId, double duration) {
1145
- Log.d(TAG, "Duration available for " + assetId + ": " + duration);
1412
+ logger.debug("Duration available for " + assetId + ": " + duration);
1146
1413
  PluginCall savedCall = pendingDurationCalls.remove(assetId);
1147
1414
  if (savedCall != null) {
1148
1415
  JSObject ret = new JSObject();
@@ -1151,6 +1418,18 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
1151
1418
  }
1152
1419
  }
1153
1420
 
1421
+ private JSObject getAudioAssetData(String audioId) {
1422
+ JSObject data = audioData.get(audioId);
1423
+ if (data == null) {
1424
+ data = new JSObject();
1425
+ }
1426
+ return data;
1427
+ }
1428
+
1429
+ private void setAudioAssetData(String audioId, JSObject data) {
1430
+ audioData.put(audioId, data);
1431
+ }
1432
+
1154
1433
  @PluginMethod
1155
1434
  public void getPluginVersion(final PluginCall call) {
1156
1435
  try {
@@ -1253,7 +1532,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
1253
1532
  public void onStop() {
1254
1533
  if (currentlyPlayingAssetId != null) {
1255
1534
  try {
1256
- stopAudio(currentlyPlayingAssetId);
1535
+ stopAudio(currentlyPlayingAssetId, false, 0);
1257
1536
  clearNotification();
1258
1537
  currentlyPlayingAssetId = null;
1259
1538
  } catch (Exception e) {