@capgo/native-audio 7.1.8 → 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.
package/README.md CHANGED
@@ -30,7 +30,9 @@
30
30
  # Capacitor Native Audio Plugin
31
31
 
32
32
  Capacitor plugin for native audio engine.
33
- Capacitor V6 - ✅ Support!
33
+ Capacitor V7 - ✅ Support!
34
+
35
+ Support local file, remote URL, and m3u8 stream
34
36
 
35
37
  Click on video to see example 💥
36
38
 
@@ -481,7 +483,7 @@ Check if an audio file is playing
481
483
  ### addListener('complete', ...)
482
484
 
483
485
  ```typescript
484
- addListener(eventName: "complete", listenerFunc: CompletedListener) => Promise<PluginListenerHandle>
486
+ addListener(eventName: 'complete', listenerFunc: CompletedListener) => Promise<PluginListenerHandle>
485
487
  ```
486
488
 
487
489
  Listen for complete event
@@ -499,27 +501,63 @@ return {@link CompletedEvent}
499
501
  --------------------
500
502
 
501
503
 
504
+ ### addListener('currentTime', ...)
505
+
506
+ ```typescript
507
+ addListener(eventName: 'currentTime', listenerFunc: CurrentTimeListener) => Promise<PluginListenerHandle>
508
+ ```
509
+
510
+ Listen for current time updates
511
+ Emits every 100ms while audio is playing
512
+
513
+ | Param | Type |
514
+ | ------------------ | ------------------------------------------------------------------- |
515
+ | **`eventName`** | <code>'currentTime'</code> |
516
+ | **`listenerFunc`** | <code><a href="#currenttimelistener">CurrentTimeListener</a></code> |
517
+
518
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
519
+
520
+ **Since:** 6.5.0
521
+ return {@link CurrentTimeEvent}
522
+
523
+ --------------------
524
+
525
+
526
+ ### clearCache()
527
+
528
+ ```typescript
529
+ clearCache() => Promise<void>
530
+ ```
531
+
532
+ Clear the audio cache for remote audio files
533
+
534
+ **Since:** 6.5.0
535
+
536
+ --------------------
537
+
538
+
502
539
  ### Interfaces
503
540
 
504
541
 
505
542
  #### ConfigureOptions
506
543
 
507
- | Prop | Type | Description |
508
- | ---------------- | -------------------- | ------------------------------------------------------- |
509
- | **`fade`** | <code>boolean</code> | Play the audio with Fade effect, only available for IOS |
510
- | **`focus`** | <code>boolean</code> | focus the audio with Audio Focus |
511
- | **`background`** | <code>boolean</code> | Play the audio in the background |
544
+ | Prop | Type | Description |
545
+ | ------------------ | -------------------- | ----------------------------------------------------------------------------- |
546
+ | **`fade`** | <code>boolean</code> | Play the audio with Fade effect, only available for IOS |
547
+ | **`focus`** | <code>boolean</code> | focus the audio with Audio Focus |
548
+ | **`background`** | <code>boolean</code> | Play the audio in the background |
549
+ | **`ignoreSilent`** | <code>boolean</code> | Ignore silent mode, works only on iOS setting this will nuke other audio apps |
512
550
 
513
551
 
514
552
  #### PreloadOptions
515
553
 
516
- | Prop | Type | Description |
517
- | --------------------- | -------------------- | -------------------------------------------------------------------------------------------------- |
518
- | **`assetPath`** | <code>string</code> | Path to the audio file, relative path of the file, absolute url (file://) or remote url (https://) |
519
- | **`assetId`** | <code>string</code> | Asset Id, unique identifier of the file |
520
- | **`volume`** | <code>number</code> | Volume of the audio, between 0.1 and 1.0 |
521
- | **`audioChannelNum`** | <code>number</code> | Audio channel number, default is 1 |
522
- | **`isUrl`** | <code>boolean</code> | Is the audio file a URL, pass true if assetPath is a `file://` url |
554
+ | Prop | Type | Description |
555
+ | --------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
556
+ | **`assetPath`** | <code>string</code> | Path to the audio file, relative path of the file, absolute url (file://) or remote url (https://) Supported formats: - MP3, WAV (all platforms) - M3U8/HLS streams (iOS and Android) |
557
+ | **`assetId`** | <code>string</code> | Asset Id, unique identifier of the file |
558
+ | **`volume`** | <code>number</code> | Volume of the audio, between 0.1 and 1.0 |
559
+ | **`audioChannelNum`** | <code>number</code> | Audio channel number, default is 1 |
560
+ | **`isUrl`** | <code>boolean</code> | Is the audio file a URL, pass true if assetPath is a `file://` url or a streaming URL (m3u8) |
523
561
 
524
562
 
525
563
  #### Assets
@@ -543,6 +581,14 @@ return {@link CompletedEvent}
543
581
  | **`assetId`** | <code>string</code> | Emit when a play completes | 5.0.0 |
544
582
 
545
583
 
584
+ #### CurrentTimeEvent
585
+
586
+ | Prop | Type | Description | Since |
587
+ | ----------------- | ------------------- | ------------------------------------ | ----- |
588
+ | **`currentTime`** | <code>number</code> | Current time of the audio in seconds | 6.5.0 |
589
+ | **`assetId`** | <code>string</code> | Asset Id of the audio | 6.5.0 |
590
+
591
+
546
592
  ### Type Aliases
547
593
 
548
594
 
@@ -550,4 +596,27 @@ return {@link CompletedEvent}
550
596
 
551
597
  <code>(state: <a href="#completedevent">CompletedEvent</a>): void</code>
552
598
 
599
+
600
+ #### CurrentTimeListener
601
+
602
+ <code>(state: <a href="#currenttimeevent">CurrentTimeEvent</a>): void</code>
603
+
553
604
  </docgen-api>
605
+
606
+ ## Development and Testing
607
+
608
+ ### Building
609
+
610
+ ```bash
611
+ npm run build
612
+ ```
613
+
614
+ ### Testing
615
+
616
+ This plugin includes a comprehensive test suite for iOS:
617
+
618
+ 1. Open the iOS project in Xcode: `npx cap open ios`
619
+ 2. Navigate to the `PluginTests` directory
620
+ 3. Run tests using Product > Test (⌘+U)
621
+
622
+ The tests cover core functionality including audio asset initialization, playback, volume control, fade effects, and more. See the [test documentation](ios/PluginTests/README.md) for more details.
@@ -1,8 +1,8 @@
1
1
  ext {
2
2
  junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
3
+ androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
3
4
  androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1'
4
5
  androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1'
5
- androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
6
6
  }
7
7
 
8
8
  buildscript {
@@ -11,7 +11,7 @@ buildscript {
11
11
  mavenCentral()
12
12
  }
13
13
  dependencies {
14
- classpath 'com.android.tools.build:gradle:8.7.2'
14
+ classpath 'com.android.tools.build:gradle:8.7.3'
15
15
  }
16
16
  }
17
17
 
@@ -35,8 +35,10 @@ android {
35
35
  }
36
36
  lintOptions {
37
37
  abortOnError false
38
- disable "UnsafeExperimentalUsageError",
39
- "UnsafeExperimentalUsageWarning"
38
+ }
39
+ compileOptions {
40
+ sourceCompatibility JavaVersion.VERSION_21
41
+ targetCompatibility JavaVersion.VERSION_21
40
42
  }
41
43
  }
42
44
 
@@ -47,10 +49,17 @@ repositories {
47
49
 
48
50
 
49
51
  dependencies {
50
- implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
51
52
  implementation fileTree(dir: 'libs', include: ['*.jar'])
52
53
  implementation project(':capacitor-android')
54
+ implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
53
55
  testImplementation "junit:junit:$junitVersion"
54
56
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
55
57
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
58
+ implementation 'androidx.media3:media3-exoplayer:1.5.1'
59
+ implementation 'androidx.media3:media3-exoplayer-hls:1.5.1'
60
+ implementation 'androidx.media3:media3-session:1.5.1'
61
+ implementation 'androidx.media3:media3-transformer:1.5.1'
62
+ implementation 'androidx.media3:media3-ui:1.5.1'
63
+ implementation 'androidx.media3:media3-database:1.5.1'
64
+ implementation 'androidx.media3:media3-common:1.5.1'
56
65
  }
@@ -2,190 +2,333 @@ package ee.forgr.audio;
2
2
 
3
3
  import android.content.res.AssetFileDescriptor;
4
4
  import android.os.Build;
5
+ import android.os.Handler;
6
+ import android.os.Looper;
7
+ import android.util.Log;
5
8
  import androidx.annotation.RequiresApi;
6
9
  import java.util.ArrayList;
7
10
 
8
11
  public class AudioAsset {
9
12
 
10
- private final String TAG = "AudioAsset";
11
-
12
- private final ArrayList<AudioDispatcher> audioList;
13
- private int playIndex = 0;
14
- private final String assetId;
15
- protected final NativeAudio owner;
16
- protected AudioCompletionListener completionListener;
17
-
18
- AudioAsset(
19
- NativeAudio owner,
20
- String assetId,
21
- AssetFileDescriptor assetFileDescriptor,
22
- int audioChannelNum,
23
- float volume
24
- ) throws Exception {
25
- audioList = new ArrayList<>();
26
- this.owner = owner;
27
- this.assetId = assetId;
13
+ private final String TAG = "AudioAsset";
14
+
15
+ private final ArrayList<AudioDispatcher> audioList;
16
+ private int playIndex = 0;
17
+ protected final NativeAudio owner;
18
+ protected AudioCompletionListener completionListener;
19
+ protected String assetId;
20
+ private Handler currentTimeHandler;
21
+ private Runnable currentTimeRunnable;
22
+ private static final float FADE_STEP = 0.05f;
23
+ private static final int FADE_DELAY_MS = 80;
24
+ private float initialVolume;
25
+
26
+ AudioAsset(NativeAudio owner, String assetId, AssetFileDescriptor assetFileDescriptor, int audioChannelNum, float volume)
27
+ throws Exception {
28
+ audioList = new ArrayList<>();
29
+ this.owner = owner;
30
+ this.assetId = assetId;
31
+ this.initialVolume = volume;
32
+
33
+ if (audioChannelNum < 0) {
34
+ audioChannelNum = 1;
35
+ }
36
+
37
+ for (int x = 0; x < audioChannelNum; x++) {
38
+ AudioDispatcher audioDispatcher = new AudioDispatcher(assetFileDescriptor, volume);
39
+ audioList.add(audioDispatcher);
40
+ if (audioChannelNum == 1) audioDispatcher.setOwner(this);
41
+ }
42
+ }
28
43
 
29
- if (audioChannelNum < 0) {
30
- audioChannelNum = 1;
44
+ public void dispatchComplete() {
45
+ this.owner.dispatchComplete(this.assetId);
31
46
  }
32
47
 
33
- for (int x = 0; x < audioChannelNum; x++) {
34
- AudioDispatcher audioDispatcher = new AudioDispatcher(
35
- assetFileDescriptor,
36
- volume
37
- );
38
- audioList.add(audioDispatcher);
39
- if (audioChannelNum == 1) audioDispatcher.setOwner(this);
48
+ public void play(Double time) throws Exception {
49
+ AudioDispatcher audio = audioList.get(playIndex);
50
+ if (audio != null) {
51
+ audio.play(time);
52
+ playIndex++;
53
+ playIndex = playIndex % audioList.size();
54
+ Log.d(TAG, "Starting timer from play"); // Debug log
55
+ startCurrentTimeUpdates(); // Make sure this is called
56
+ } else {
57
+ throw new Exception("AudioDispatcher is null");
58
+ }
40
59
  }
41
- }
42
60
 
43
- public void dispatchComplete() {
44
- this.owner.dispatchComplete(this.assetId);
45
- }
61
+ public double getDuration() {
62
+ if (audioList.size() != 1) return 0;
46
63
 
47
- public void play(Double time) throws Exception {
48
- AudioDispatcher audio = audioList.get(playIndex);
64
+ AudioDispatcher audio = audioList.get(playIndex);
49
65
 
50
- if (audio != null) {
51
- audio.play(time);
52
- playIndex++;
53
- playIndex = playIndex % audioList.size();
54
- } else {
55
- throw new Exception("AudioDispatcher is null");
66
+ if (audio != null) {
67
+ return audio.getDuration();
68
+ }
69
+ return 0;
56
70
  }
57
- }
58
71
 
59
- public double getDuration() {
60
- if (audioList.size() != 1) return 0;
72
+ public void setCurrentPosition(double time) {
73
+ if (audioList.size() != 1) return;
61
74
 
62
- AudioDispatcher audio = audioList.get(playIndex);
75
+ AudioDispatcher audio = audioList.get(playIndex);
63
76
 
64
- if (audio != null) {
65
- return audio.getDuration();
77
+ if (audio != null) {
78
+ audio.setCurrentPosition(time);
79
+ }
66
80
  }
67
- return 0;
68
- }
69
81
 
70
- public void setCurrentPosition(double time) {
71
- if (audioList.size() != 1) return;
82
+ public double getCurrentPosition() {
83
+ if (audioList.size() != 1) return 0;
72
84
 
73
- AudioDispatcher audio = audioList.get(playIndex);
85
+ AudioDispatcher audio = audioList.get(playIndex);
74
86
 
75
- if (audio != null) {
76
- audio.setCurrentPosition(time);
87
+ if (audio != null) {
88
+ return audio.getCurrentPosition();
89
+ }
90
+ return 0;
77
91
  }
78
- }
79
92
 
80
- public double getCurrentPosition() {
81
- if (audioList.size() != 1) return 0;
93
+ public boolean pause() throws Exception {
94
+ stopCurrentTimeUpdates(); // Stop updates when pausing
95
+ boolean wasPlaying = false;
96
+
97
+ for (int x = 0; x < audioList.size(); x++) {
98
+ AudioDispatcher audio = audioList.get(x);
99
+ wasPlaying |= audio.pause();
100
+ }
82
101
 
83
- AudioDispatcher audio = audioList.get(playIndex);
102
+ return wasPlaying;
103
+ }
84
104
 
85
- if (audio != null) {
86
- return audio.getCurrentPosition();
105
+ public void resume() throws Exception {
106
+ if (!audioList.isEmpty()) {
107
+ AudioDispatcher audio = audioList.get(0);
108
+ if (audio != null) {
109
+ audio.resume();
110
+ Log.d(TAG, "Starting timer from resume"); // Debug log
111
+ startCurrentTimeUpdates(); // Make sure this is called
112
+ } else {
113
+ throw new Exception("AudioDispatcher is null");
114
+ }
115
+ }
87
116
  }
88
- return 0;
89
- }
90
117
 
91
- public boolean pause() throws Exception {
92
- boolean wasPlaying = false;
118
+ public void stop() throws Exception {
119
+ stopCurrentTimeUpdates(); // Stop updates when stopping
120
+ for (int x = 0; x < audioList.size(); x++) {
121
+ AudioDispatcher audio = audioList.get(x);
122
+
123
+ if (audio != null) {
124
+ audio.stop();
125
+ } else {
126
+ throw new Exception("AudioDispatcher is null");
127
+ }
128
+ }
129
+ }
93
130
 
94
- for (int x = 0; x < audioList.size(); x++) {
95
- AudioDispatcher audio = audioList.get(x);
96
- wasPlaying |= audio.pause();
131
+ public void loop() throws Exception {
132
+ AudioDispatcher audio = audioList.get(playIndex);
133
+ if (audio != null) {
134
+ audio.loop();
135
+ playIndex++;
136
+ playIndex = playIndex % audioList.size();
137
+ startCurrentTimeUpdates(); // Add timer start
138
+ } else {
139
+ throw new Exception("AudioDispatcher is null");
140
+ }
97
141
  }
98
142
 
99
- return wasPlaying;
100
- }
143
+ public void unload() throws Exception {
144
+ this.stop();
101
145
 
102
- public void resume() throws Exception {
103
- if (!audioList.isEmpty()) {
104
- AudioDispatcher audio = audioList.get(0);
146
+ for (int x = 0; x < audioList.size(); x++) {
147
+ AudioDispatcher audio = audioList.get(x);
105
148
 
106
- if (audio != null) {
107
- audio.resume();
108
- } else {
109
- throw new Exception("AudioDispatcher is null");
110
- }
149
+ if (audio != null) {
150
+ audio.unload();
151
+ } else {
152
+ throw new Exception("AudioDispatcher is null");
153
+ }
154
+ }
155
+
156
+ audioList.clear();
111
157
  }
112
- }
113
158
 
114
- public void stop() throws Exception {
115
- for (int x = 0; x < audioList.size(); x++) {
116
- AudioDispatcher audio = audioList.get(x);
159
+ public void setVolume(float volume) throws Exception {
160
+ for (int x = 0; x < audioList.size(); x++) {
161
+ AudioDispatcher audio = audioList.get(x);
162
+
163
+ if (audio != null) {
164
+ audio.setVolume(volume);
165
+ } else {
166
+ throw new Exception("AudioDispatcher is null");
167
+ }
168
+ }
169
+ }
117
170
 
118
- if (audio != null) {
119
- audio.stop();
120
- } else {
121
- throw new Exception("AudioDispatcher is null");
122
- }
171
+ @RequiresApi(api = Build.VERSION_CODES.M)
172
+ public void setRate(float rate) throws Exception {
173
+ for (int x = 0; x < audioList.size(); x++) {
174
+ AudioDispatcher audio = audioList.get(x);
175
+ if (audio != null) {
176
+ audio.setRate(rate);
177
+ }
178
+ }
123
179
  }
124
- }
125
180
 
126
- public void loop() throws Exception {
127
- AudioDispatcher audio = audioList.get(playIndex);
181
+ public boolean isPlaying() throws Exception {
182
+ if (audioList.size() != 1) return false;
128
183
 
129
- if (audio != null) {
130
- audio.loop();
131
- playIndex++;
132
- playIndex = playIndex % audioList.size();
133
- } else {
134
- throw new Exception("AudioDispatcher is null");
184
+ return audioList.get(playIndex).isPlaying();
135
185
  }
136
- }
137
186
 
138
- public void unload() throws Exception {
139
- this.stop();
187
+ public void setCompletionListener(AudioCompletionListener listener) {
188
+ this.completionListener = listener;
189
+ }
140
190
 
141
- for (int x = 0; x < audioList.size(); x++) {
142
- AudioDispatcher audio = audioList.get(x);
191
+ protected void notifyCompletion() {
192
+ if (completionListener != null) {
193
+ completionListener.onCompletion(this.assetId);
194
+ }
195
+ }
143
196
 
144
- if (audio != null) {
145
- audio.unload();
146
- } else {
147
- throw new Exception("AudioDispatcher is null");
148
- }
197
+ protected String getAssetId() {
198
+ return assetId;
149
199
  }
150
200
 
151
- audioList.clear();
152
- }
201
+ public void setCurrentTime(double time) throws Exception {
202
+ owner
203
+ .getActivity()
204
+ .runOnUiThread(
205
+ new Runnable() {
206
+ @Override
207
+ public void run() {
208
+ if (audioList.size() != 1) {
209
+ return;
210
+ }
211
+ AudioDispatcher audio = audioList.get(playIndex);
212
+ if (audio != null) {
213
+ audio.setCurrentPosition(time);
214
+ }
215
+ }
216
+ }
217
+ );
218
+ }
153
219
 
154
- public void setVolume(float volume) throws Exception {
155
- for (int x = 0; x < audioList.size(); x++) {
156
- AudioDispatcher audio = audioList.get(x);
220
+ protected void startCurrentTimeUpdates() {
221
+ Log.d(TAG, "Starting timer updates in AudioAsset");
222
+ if (currentTimeHandler == null) {
223
+ currentTimeHandler = new Handler(Looper.getMainLooper());
224
+ }
225
+
226
+ // Add small delay to let audio start playing
227
+ currentTimeHandler.postDelayed(
228
+ new Runnable() {
229
+ @Override
230
+ public void run() {
231
+ startTimeUpdateLoop();
232
+ }
233
+ },
234
+ 100
235
+ ); // 100ms delay
236
+ }
157
237
 
158
- if (audio != null) {
159
- audio.setVolume(volume);
160
- } else {
161
- throw new Exception("AudioDispatcher is null");
162
- }
238
+ private void startTimeUpdateLoop() {
239
+ currentTimeRunnable = new Runnable() {
240
+ @Override
241
+ public void run() {
242
+ try {
243
+ AudioDispatcher audio = audioList.get(playIndex);
244
+ if (audio != null && audio.isPlaying()) {
245
+ double currentTime = getCurrentPosition();
246
+ Log.d(TAG, "Timer update: currentTime = " + currentTime);
247
+ owner.notifyCurrentTime(assetId, currentTime);
248
+ currentTimeHandler.postDelayed(this, 100);
249
+ } else {
250
+ Log.d(TAG, "Stopping timer - not playing");
251
+ stopCurrentTimeUpdates();
252
+ }
253
+ } catch (Exception e) {
254
+ Log.e(TAG, "Error getting current time", e);
255
+ stopCurrentTimeUpdates();
256
+ }
257
+ }
258
+ };
259
+ currentTimeHandler.post(currentTimeRunnable);
163
260
  }
164
- }
165
261
 
166
- @RequiresApi(api = Build.VERSION_CODES.M)
167
- public void setRate(float rate) throws Exception {
168
- for (int x = 0; x < audioList.size(); x++) {
169
- AudioDispatcher audio = audioList.get(x);
170
- if (audio != null) {
171
- audio.setRate(rate);
172
- }
262
+ void stopCurrentTimeUpdates() {
263
+ Log.d(TAG, "Stopping timer updates in AudioAsset");
264
+ if (currentTimeHandler != null && currentTimeRunnable != null) {
265
+ currentTimeHandler.removeCallbacks(currentTimeRunnable);
266
+ }
173
267
  }
174
- }
175
268
 
176
- public boolean isPlaying() throws Exception {
177
- if (audioList.size() != 1) return false;
269
+ public void playWithFade(Double time) throws Exception {
270
+ AudioDispatcher audio = audioList.get(playIndex);
271
+ if (audio != null) {
272
+ audio.setVolume(0);
273
+ audio.play(time);
274
+ fadeIn(audio);
275
+ startCurrentTimeUpdates();
276
+ }
277
+ }
178
278
 
179
- return audioList.get(playIndex).isPlaying();
180
- }
279
+ private void fadeIn(final AudioDispatcher audio) {
280
+ final Handler handler = new Handler(Looper.getMainLooper());
281
+ final Runnable fadeRunnable = new Runnable() {
282
+ float currentVolume = 0;
283
+
284
+ @Override
285
+ public void run() {
286
+ if (currentVolume < initialVolume) {
287
+ currentVolume += FADE_STEP;
288
+ try {
289
+ audio.setVolume(currentVolume);
290
+ handler.postDelayed(this, FADE_DELAY_MS);
291
+ } catch (Exception e) {
292
+ Log.e(TAG, "Error during fade in", e);
293
+ }
294
+ }
295
+ }
296
+ };
297
+ handler.post(fadeRunnable);
298
+ }
181
299
 
182
- public void setCompletionListener(AudioCompletionListener listener) {
183
- this.completionListener = listener;
184
- }
300
+ public void stopWithFade() throws Exception {
301
+ AudioDispatcher audio = audioList.get(playIndex);
302
+ if (audio != null && audio.isPlaying()) {
303
+ fadeOut(audio);
304
+ }
305
+ }
185
306
 
186
- protected void notifyCompletion() {
187
- if (completionListener != null) {
188
- completionListener.onCompletion(this.assetId);
307
+ private void fadeOut(final AudioDispatcher audio) {
308
+ final Handler handler = new Handler(Looper.getMainLooper());
309
+ final Runnable fadeRunnable = new Runnable() {
310
+ float currentVolume = initialVolume;
311
+
312
+ @Override
313
+ public void run() {
314
+ if (currentVolume > FADE_STEP) {
315
+ currentVolume -= FADE_STEP;
316
+ try {
317
+ audio.setVolume(currentVolume);
318
+ handler.postDelayed(this, FADE_DELAY_MS);
319
+ } catch (Exception e) {
320
+ Log.e(TAG, "Error during fade out", e);
321
+ }
322
+ } else {
323
+ try {
324
+ audio.setVolume(0);
325
+ stop();
326
+ } catch (Exception e) {
327
+ Log.e(TAG, "Error stopping after fade", e);
328
+ }
329
+ }
330
+ }
331
+ };
332
+ handler.post(fadeRunnable);
189
333
  }
190
- }
191
334
  }
@@ -1,5 +1,5 @@
1
1
  package ee.forgr.audio;
2
2
 
3
3
  public interface AudioCompletionListener {
4
- void onCompletion(String assetId);
4
+ void onCompletion(String assetId);
5
5
  }