@capgo/capacitor-native-audio 8.4.3

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.
Files changed (46) hide show
  1. package/CapgoCapacitorNativeAudio.podspec +16 -0
  2. package/LICENSE +373 -0
  3. package/Package.swift +31 -0
  4. package/README.md +1229 -0
  5. package/android/build.gradle +89 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/java/ee/forgr/audio/AudioAsset.java +611 -0
  8. package/android/src/main/java/ee/forgr/audio/AudioCompletionListener.java +5 -0
  9. package/android/src/main/java/ee/forgr/audio/AudioDispatcher.java +208 -0
  10. package/android/src/main/java/ee/forgr/audio/Constant.java +36 -0
  11. package/android/src/main/java/ee/forgr/audio/HlsAvailabilityChecker.java +84 -0
  12. package/android/src/main/java/ee/forgr/audio/Logger.java +55 -0
  13. package/android/src/main/java/ee/forgr/audio/NativeAudio.java +2022 -0
  14. package/android/src/main/java/ee/forgr/audio/RemoteAudioAsset.java +886 -0
  15. package/android/src/main/java/ee/forgr/audio/StreamAudioAsset.java +708 -0
  16. package/android/src/main/res/values/colors.xml +3 -0
  17. package/android/src/main/res/values/strings.xml +3 -0
  18. package/android/src/main/res/values/styles.xml +3 -0
  19. package/dist/docs.json +1470 -0
  20. package/dist/esm/audio-asset.d.ts +4 -0
  21. package/dist/esm/audio-asset.js +6 -0
  22. package/dist/esm/audio-asset.js.map +1 -0
  23. package/dist/esm/definitions.d.ts +597 -0
  24. package/dist/esm/definitions.js +2 -0
  25. package/dist/esm/definitions.js.map +1 -0
  26. package/dist/esm/index.d.ts +4 -0
  27. package/dist/esm/index.js +7 -0
  28. package/dist/esm/index.js.map +1 -0
  29. package/dist/esm/web.d.ts +82 -0
  30. package/dist/esm/web.js +553 -0
  31. package/dist/esm/web.js.map +1 -0
  32. package/dist/plugin.cjs.js +571 -0
  33. package/dist/plugin.cjs.js.map +1 -0
  34. package/dist/plugin.js +574 -0
  35. package/dist/plugin.js.map +1 -0
  36. package/ios/Sources/NativeAudioPlugin/AudioAsset+Fade.swift +157 -0
  37. package/ios/Sources/NativeAudioPlugin/AudioAsset.swift +403 -0
  38. package/ios/Sources/NativeAudioPlugin/Constant.swift +52 -0
  39. package/ios/Sources/NativeAudioPlugin/Logger.swift +43 -0
  40. package/ios/Sources/NativeAudioPlugin/Plugin.swift +1786 -0
  41. package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset+Fade.swift +152 -0
  42. package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset.swift +405 -0
  43. package/ios/Tests/NativeAudioPluginTests/PluginTests.swift +648 -0
  44. package/ios/Tests/README.md +39 -0
  45. package/package.json +101 -0
  46. package/scripts/configure-dependencies.js +251 -0
@@ -0,0 +1,708 @@
1
+ package ee.forgr.audio;
2
+
3
+ import android.net.Uri;
4
+ import android.os.Handler;
5
+ import android.os.Looper;
6
+ import android.util.Log;
7
+ import androidx.media3.common.MediaItem;
8
+ import androidx.media3.common.PlaybackException;
9
+ import androidx.media3.common.PlaybackParameters;
10
+ import androidx.media3.common.Player;
11
+ import androidx.media3.common.util.UnstableApi;
12
+ import androidx.media3.datasource.DefaultHttpDataSource;
13
+ import androidx.media3.exoplayer.DefaultLivePlaybackSpeedControl;
14
+ import androidx.media3.exoplayer.DefaultLoadControl;
15
+ import androidx.media3.exoplayer.ExoPlayer;
16
+ import androidx.media3.exoplayer.hls.HlsMediaSource;
17
+
18
+ @UnstableApi
19
+ public class StreamAudioAsset extends AudioAsset {
20
+
21
+ private static final String TAG = "StreamAudioAsset";
22
+ private ExoPlayer player;
23
+ private final Uri uri;
24
+ private float volume;
25
+ private boolean isPrepared = false;
26
+ private final float initialVolume;
27
+ private static final long LIVE_OFFSET_MS = 5000; // 5 seconds behind live
28
+ private final java.util.Map<String, String> headers;
29
+
30
+ public StreamAudioAsset(NativeAudio owner, String assetId, Uri uri, float volume, java.util.Map<String, String> headers)
31
+ throws Exception {
32
+ super(owner, assetId, null, 0, volume);
33
+ this.uri = uri;
34
+ this.volume = volume;
35
+ this.initialVolume = volume;
36
+ this.headers = headers;
37
+
38
+ createPlayer();
39
+ }
40
+
41
+ private void createPlayer() {
42
+ // Adjust buffer settings for smoother playback
43
+ DefaultLoadControl loadControl = new DefaultLoadControl.Builder()
44
+ .setBufferDurationsMs(
45
+ 60000, // Increase min buffer to 60s
46
+ 180000, // Increase max buffer to 180s
47
+ 5000, // Increase buffer for playback
48
+ 10000 // Increase buffer to start playback
49
+ )
50
+ .setPrioritizeTimeOverSizeThresholds(true)
51
+ .setBackBuffer(60000, true) // Increase back buffer
52
+ .build();
53
+
54
+ player = new ExoPlayer.Builder(owner.getContext())
55
+ .setLoadControl(loadControl)
56
+ .setLivePlaybackSpeedControl(
57
+ new DefaultLivePlaybackSpeedControl.Builder()
58
+ .setFallbackMaxPlaybackSpeed(1.04f)
59
+ .setMaxLiveOffsetErrorMsForUnitSpeed(LIVE_OFFSET_MS)
60
+ .build()
61
+ )
62
+ .build();
63
+
64
+ player.setVolume(volume);
65
+ initializePlayer();
66
+ }
67
+
68
+ private void initializePlayer() {
69
+ logger.debug("Initializing stream player with volume: " + volume);
70
+
71
+ // Configure HLS source with better settings for live streaming
72
+ DefaultHttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory()
73
+ .setAllowCrossProtocolRedirects(true)
74
+ .setConnectTimeoutMs(15000)
75
+ .setReadTimeoutMs(15000)
76
+ .setUserAgent("ExoPlayer");
77
+
78
+ // Add custom headers if provided
79
+ if (headers != null && !headers.isEmpty()) {
80
+ httpDataSourceFactory.setDefaultRequestProperties(headers);
81
+ }
82
+
83
+ HlsMediaSource mediaSource = new HlsMediaSource.Factory(httpDataSourceFactory)
84
+ .setAllowChunklessPreparation(true)
85
+ .setTimestampAdjusterInitializationTimeoutMs(LIVE_OFFSET_MS) // 30 seconds timeout
86
+ .createMediaSource(MediaItem.fromUri(uri));
87
+
88
+ player.setMediaSource(mediaSource);
89
+ player.setVolume(volume);
90
+ player.prepare();
91
+
92
+ player.addListener(
93
+ new Player.Listener() {
94
+ @Override
95
+ public void onPlaybackStateChanged(int state) {
96
+ logger.debug("Stream state changed to: " + getStateString(state));
97
+ if (state == Player.STATE_READY && !isPrepared) {
98
+ isPrepared = true;
99
+ if (player.isCurrentMediaItemLive()) {
100
+ player.seekToDefaultPosition();
101
+ }
102
+ }
103
+ }
104
+
105
+ @Override
106
+ public void onIsLoadingChanged(boolean isLoading) {
107
+ logger.debug("Loading state changed: " + isLoading);
108
+ }
109
+
110
+ @Override
111
+ public void onIsPlayingChanged(boolean isPlaying) {
112
+ logger.debug("Playing state changed: " + isPlaying);
113
+ }
114
+
115
+ @Override
116
+ public void onPlayerError(PlaybackException error) {
117
+ logger.error("Player error: " + error.getMessage());
118
+ isPrepared = false;
119
+ // Try to recover by recreating the player
120
+ owner
121
+ .getActivity()
122
+ .runOnUiThread(() -> {
123
+ player.release();
124
+ createPlayer();
125
+ });
126
+ }
127
+ }
128
+ );
129
+ }
130
+
131
+ private String getStateString(int state) {
132
+ switch (state) {
133
+ case Player.STATE_IDLE:
134
+ return "IDLE";
135
+ case Player.STATE_BUFFERING:
136
+ return "BUFFERING";
137
+ case Player.STATE_READY:
138
+ return "READY";
139
+ case Player.STATE_ENDED:
140
+ return "ENDED";
141
+ default:
142
+ return "UNKNOWN(" + state + ")";
143
+ }
144
+ }
145
+
146
+ @Override
147
+ public void play(double time, float volume) throws Exception {
148
+ logger.debug("Play called with time: " + time + ", isPrepared: " + isPrepared);
149
+ owner
150
+ .getActivity()
151
+ .runOnUiThread(() -> {
152
+ if (!isPrepared) {
153
+ // If not prepared, wait for preparation
154
+ player.addListener(
155
+ new Player.Listener() {
156
+ @Override
157
+ public void onPlaybackStateChanged(int state) {
158
+ logger.debug("Play-wait state changed to: " + getStateString(state));
159
+ if (state == Player.STATE_READY) {
160
+ startPlayback(time, volume);
161
+ startCurrentTimeUpdates();
162
+ player.removeListener(this);
163
+ }
164
+ }
165
+ }
166
+ );
167
+ } else {
168
+ startPlayback(time, volume);
169
+ }
170
+ });
171
+ }
172
+
173
+ private void startPlayback(double time, float volume) {
174
+ logger.debug("Starting playback with time: " + time);
175
+ if (time != 0) {
176
+ player.seekTo(Math.round(time * 1000));
177
+ } else if (player.isCurrentMediaItemLive()) {
178
+ player.seekToDefaultPosition();
179
+ }
180
+ player.setPlaybackParameters(new PlaybackParameters(1.0f));
181
+ player.setVolume(volume);
182
+ player.setPlayWhenReady(true);
183
+ startCurrentTimeUpdates();
184
+ }
185
+
186
+ @Override
187
+ public boolean pause() throws Exception {
188
+ final boolean[] wasPlaying = { false };
189
+ owner
190
+ .getActivity()
191
+ .runOnUiThread(() -> {
192
+ cancelFade();
193
+ if (player != null && player.isPlaying()) {
194
+ player.setPlayWhenReady(false);
195
+ stopCurrentTimeUpdates();
196
+ wasPlaying[0] = true;
197
+ }
198
+ });
199
+ return wasPlaying[0];
200
+ }
201
+
202
+ @Override
203
+ public void resume() throws Exception {
204
+ owner
205
+ .getActivity()
206
+ .runOnUiThread(() -> {
207
+ player.setPlayWhenReady(true);
208
+ startCurrentTimeUpdates();
209
+ });
210
+ }
211
+
212
+ @Override
213
+ public void stop() throws Exception {
214
+ owner
215
+ .getActivity()
216
+ .runOnUiThread(() -> {
217
+ cancelFade();
218
+ // First stop playback
219
+ player.stop();
220
+ // Reset player state
221
+ player.clearMediaItems();
222
+ isPrepared = false;
223
+
224
+ // Create new media source
225
+ DefaultHttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory()
226
+ .setAllowCrossProtocolRedirects(true)
227
+ .setConnectTimeoutMs(15000)
228
+ .setReadTimeoutMs(15000)
229
+ .setUserAgent("ExoPlayer");
230
+
231
+ // Add custom headers if provided
232
+ if (headers != null && !headers.isEmpty()) {
233
+ httpDataSourceFactory.setDefaultRequestProperties(headers);
234
+ }
235
+
236
+ HlsMediaSource mediaSource = new HlsMediaSource.Factory(httpDataSourceFactory)
237
+ .setAllowChunklessPreparation(true)
238
+ .setTimestampAdjusterInitializationTimeoutMs(LIVE_OFFSET_MS)
239
+ .createMediaSource(MediaItem.fromUri(uri));
240
+
241
+ // Set new media source and prepare
242
+ player.setMediaSource(mediaSource);
243
+ player.prepare();
244
+
245
+ // Add listener for preparation completion
246
+ player.addListener(
247
+ new Player.Listener() {
248
+ @Override
249
+ public void onPlaybackStateChanged(int state) {
250
+ logger.debug("Stop-reinit state changed to: " + getStateString(state));
251
+ if (state == Player.STATE_READY) {
252
+ isPrepared = true;
253
+ player.removeListener(this);
254
+ } else if (state == Player.STATE_IDLE) {
255
+ // Retry preparation if it fails
256
+ player.prepare();
257
+ }
258
+ }
259
+ }
260
+ );
261
+ });
262
+ }
263
+
264
+ @Override
265
+ public void loop() throws Exception {
266
+ owner
267
+ .getActivity()
268
+ .runOnUiThread(() -> {
269
+ player.setRepeatMode(Player.REPEAT_MODE_ONE);
270
+ player.setPlayWhenReady(true);
271
+ startCurrentTimeUpdates();
272
+ });
273
+ }
274
+
275
+ @Override
276
+ public void unload() throws Exception {
277
+ owner
278
+ .getActivity()
279
+ .runOnUiThread(() -> {
280
+ cancelFade();
281
+ player.stop();
282
+ player.clearMediaItems();
283
+ player.release();
284
+ isPrepared = false;
285
+ close(); // Ensure fadeExecutor is shutdown
286
+ });
287
+ }
288
+
289
+ @Override
290
+ public void close() {
291
+ if (fadeExecutor != null && !fadeExecutor.isShutdown()) {
292
+ fadeExecutor.shutdown();
293
+ }
294
+ }
295
+
296
+ @Override
297
+ protected void finalize() throws Throwable {
298
+ try {
299
+ close();
300
+ } finally {
301
+ super.finalize();
302
+ }
303
+ }
304
+
305
+ @Override
306
+ public void setVolume(float volume, double duration) throws Exception {
307
+ this.volume = volume;
308
+ owner
309
+ .getActivity()
310
+ .runOnUiThread(() -> {
311
+ cancelFade();
312
+ try {
313
+ if (this.isPlaying() && duration > 0) {
314
+ fadeTo(duration, volume);
315
+ } else {
316
+ player.setVolume(volume);
317
+ }
318
+ } catch (Exception e) {
319
+ logger.error("Error setting volume", e);
320
+ }
321
+ });
322
+ }
323
+
324
+ @Override
325
+ public float getVolume() throws Exception {
326
+ if (player != null) {
327
+ return player.getVolume();
328
+ }
329
+ return 0;
330
+ }
331
+
332
+ @Override
333
+ public boolean isPlaying() throws Exception {
334
+ return player != null && player.isPlaying();
335
+ }
336
+
337
+ @Override
338
+ public double getDuration() {
339
+ if (isPrepared) {
340
+ final double[] duration = { 0 };
341
+ owner
342
+ .getActivity()
343
+ .runOnUiThread(() -> {
344
+ if (player.getPlaybackState() == Player.STATE_READY) {
345
+ long rawDuration = player.getDuration();
346
+ if (rawDuration != androidx.media3.common.C.TIME_UNSET) {
347
+ duration[0] = rawDuration / 1000.0;
348
+ }
349
+ }
350
+ });
351
+ return duration[0];
352
+ }
353
+ return 0;
354
+ }
355
+
356
+ @Override
357
+ public double getCurrentPosition() {
358
+ if (isPrepared) {
359
+ final double[] position = { 0 };
360
+ owner
361
+ .getActivity()
362
+ .runOnUiThread(() -> {
363
+ if (player.getPlaybackState() == Player.STATE_READY) {
364
+ position[0] = player.getCurrentPosition() / 1000.0;
365
+ }
366
+ });
367
+ return position[0];
368
+ }
369
+ return 0;
370
+ }
371
+
372
+ @Override
373
+ public void setCurrentTime(double time) throws Exception {
374
+ owner
375
+ .getActivity()
376
+ .runOnUiThread(() -> {
377
+ player.seekTo(Math.round(time * 1000));
378
+ });
379
+ }
380
+
381
+ @Override
382
+ public void playWithFadeIn(double time, float volume, double fadeInDurationMs) throws Exception {
383
+ logger.debug("playWithFadeIn called with time: " + time);
384
+ owner
385
+ .getActivity()
386
+ .runOnUiThread(() -> {
387
+ if (!isPrepared) {
388
+ // If not prepared, wait for preparation
389
+ player.addListener(
390
+ new Player.Listener() {
391
+ @Override
392
+ public void onPlaybackStateChanged(int state) {
393
+ if (state == Player.STATE_READY) {
394
+ startPlaybackWithFade(time, volume, fadeInDurationMs);
395
+ player.removeListener(this);
396
+ }
397
+ }
398
+ }
399
+ );
400
+ } else {
401
+ startPlaybackWithFade(time, volume, fadeInDurationMs);
402
+ }
403
+ });
404
+ }
405
+
406
+ private void startPlaybackWithFade(Double time, float targetVolume, double fadeInDurationMs) {
407
+ if (!player.isPlayingAd()) {
408
+ // Make sure we're not in an ad
409
+ if (time != null) {
410
+ player.seekTo(Math.round(time * 1000));
411
+ } else if (player.isCurrentMediaItemLive()) {
412
+ long liveEdge = player.getCurrentLiveOffset();
413
+ if (liveEdge > 0) {
414
+ player.seekTo(liveEdge - LIVE_OFFSET_MS);
415
+ }
416
+ }
417
+
418
+ // Wait for buffering to complete before starting playback
419
+ player.addListener(
420
+ new Player.Listener() {
421
+ @Override
422
+ public void onPlaybackStateChanged(int state) {
423
+ if (state == Player.STATE_READY) {
424
+ player.removeListener(this);
425
+ // Ensure playback rate is normal
426
+ player.setPlaybackParameters(new PlaybackParameters(1.0f));
427
+ // Start with volume 0
428
+ player.setVolume(0);
429
+ player.setPlayWhenReady(true);
430
+ startCurrentTimeUpdates();
431
+ // Start fade after ensuring we're actually playing
432
+ checkAndStartFade(fadeInDurationMs, targetVolume);
433
+ }
434
+ }
435
+ }
436
+ );
437
+ }
438
+ }
439
+
440
+ private void checkAndStartFade(double fadeInDurationMs, float volume) {
441
+ final Handler handler = new Handler(Looper.getMainLooper());
442
+ handler.postDelayed(
443
+ new Runnable() {
444
+ int attempts = 0;
445
+
446
+ @Override
447
+ public void run() {
448
+ if (player.isPlaying()) {
449
+ fadeIn(fadeInDurationMs, volume);
450
+ } else if (attempts < 10) {
451
+ // Try for 5 seconds (10 * 500ms)
452
+ attempts++;
453
+ handler.postDelayed(this, 500);
454
+ }
455
+ }
456
+ },
457
+ 500
458
+ );
459
+ }
460
+
461
+ private void fadeIn(double fadeInDurationMs, float targetVolume) {
462
+ cancelFade();
463
+ fadeState = FadeState.FADE_IN;
464
+
465
+ final int steps = Math.max(1, (int) (fadeInDurationMs / FADE_DELAY_MS));
466
+ final float fadeStep = targetVolume / steps;
467
+
468
+ fadeTask = fadeExecutor.scheduleWithFixedDelay(
469
+ new Runnable() {
470
+ float currentVolume = 0;
471
+
472
+ @Override
473
+ public void run() {
474
+ if (fadeState != FadeState.FADE_IN || player == null || !player.isPlaying() || currentVolume >= targetVolume) {
475
+ fadeState = FadeState.NONE;
476
+ cancelFade();
477
+ return;
478
+ }
479
+
480
+ final float nextVolume = Math.min(currentVolume + fadeStep, targetVolume);
481
+ owner
482
+ .getActivity()
483
+ .runOnUiThread(() -> {
484
+ if (player != null && player.isPlaying()) {
485
+ player.setVolume(nextVolume);
486
+ }
487
+ });
488
+ currentVolume = nextVolume;
489
+ }
490
+ },
491
+ 0,
492
+ FADE_DELAY_MS,
493
+ java.util.concurrent.TimeUnit.MILLISECONDS
494
+ );
495
+ }
496
+
497
+ private void fadeTo(double fadeDurationMs, float targetVolume) {
498
+ cancelFade();
499
+ fadeState = FadeState.FADE_TO;
500
+
501
+ if (player == null) return;
502
+
503
+ final int steps = Math.max(1, (int) (fadeDurationMs / FADE_DELAY_MS));
504
+ final float minVolume = zeroVolume;
505
+ final float initialVolume = Math.max(player.getVolume(), minVolume);
506
+ final float finalTargetVolume = Math.max(targetVolume, minVolume);
507
+ final double ratio = Math.pow(finalTargetVolume / initialVolume, 1.0 / steps);
508
+ if (Double.isNaN(ratio) || Double.isInfinite(ratio)) {
509
+ player.setVolume(finalTargetVolume);
510
+ fadeState = FadeState.NONE;
511
+ return;
512
+ }
513
+
514
+ fadeTask = fadeExecutor.scheduleWithFixedDelay(
515
+ new Runnable() {
516
+ int currentStep = 0;
517
+ float currentVolume = initialVolume;
518
+
519
+ @Override
520
+ public void run() {
521
+ if (fadeState != FadeState.FADE_TO || player == null || !player.isPlaying() || currentStep >= steps) {
522
+ fadeState = FadeState.NONE;
523
+ cancelFade();
524
+ return;
525
+ }
526
+
527
+ currentVolume *= (float) ratio;
528
+ final float nextVolume = Math.min(Math.max(currentVolume, minVolume), maxVolume);
529
+ owner
530
+ .getActivity()
531
+ .runOnUiThread(() -> {
532
+ if (player != null && player.isPlaying()) {
533
+ player.setVolume(nextVolume);
534
+ }
535
+ });
536
+ currentStep++;
537
+ }
538
+ },
539
+ 0,
540
+ FADE_DELAY_MS,
541
+ java.util.concurrent.TimeUnit.MILLISECONDS
542
+ );
543
+ }
544
+
545
+ @Override
546
+ public void stopWithFade(double fadeOutDurationMs, boolean toPause) throws Exception {
547
+ owner
548
+ .getActivity()
549
+ .runOnUiThread(() -> {
550
+ if (player != null && player.isPlaying()) {
551
+ fadeOut(fadeOutDurationMs, toPause);
552
+ } else if (!toPause) {
553
+ try {
554
+ stop();
555
+ } catch (Exception e) {
556
+ logger.error("Error stopping stream asset", e);
557
+ }
558
+ }
559
+ });
560
+ }
561
+
562
+ @Override
563
+ public void stopWithFade() throws Exception {
564
+ stopWithFade(DEFAULT_FADE_DURATION_MS, false);
565
+ }
566
+
567
+ private void fadeOut(double fadeOutDurationMs, boolean toPause) {
568
+ cancelFade();
569
+ fadeState = FadeState.FADE_OUT;
570
+
571
+ if (player == null) return;
572
+
573
+ final int steps = Math.max(1, (int) (fadeOutDurationMs / FADE_DELAY_MS));
574
+ final float initialVolume = player.getVolume();
575
+ final float fadeStep = initialVolume / steps;
576
+
577
+ fadeTask = fadeExecutor.scheduleWithFixedDelay(
578
+ new Runnable() {
579
+ float currentVolume = initialVolume;
580
+
581
+ @Override
582
+ public void run() {
583
+ if (fadeState != FadeState.FADE_OUT || player == null || currentVolume <= 0) {
584
+ fadeState = FadeState.NONE;
585
+ cancelFade();
586
+ owner
587
+ .getActivity()
588
+ .runOnUiThread(() -> {
589
+ if (player == null) {
590
+ return;
591
+ }
592
+ if (toPause) {
593
+ player.setPlayWhenReady(false);
594
+ stopCurrentTimeUpdates();
595
+ } else {
596
+ try {
597
+ stop();
598
+ } catch (Exception e) {
599
+ logger.error("Error stopping stream asset after fade out", e);
600
+ }
601
+ }
602
+ });
603
+ return;
604
+ }
605
+
606
+ final float nextVolume = Math.max(currentVolume - fadeStep, 0f);
607
+ owner
608
+ .getActivity()
609
+ .runOnUiThread(() -> {
610
+ if (player != null) {
611
+ player.setVolume(nextVolume);
612
+ }
613
+ });
614
+ currentVolume = nextVolume;
615
+ }
616
+ },
617
+ 0,
618
+ FADE_DELAY_MS,
619
+ java.util.concurrent.TimeUnit.MILLISECONDS
620
+ );
621
+ }
622
+
623
+ @Override
624
+ public void setRate(float rate) throws Exception {
625
+ owner
626
+ .getActivity()
627
+ .runOnUiThread(() -> {
628
+ logger.debug("Setting playback rate to: " + rate);
629
+ player.setPlaybackParameters(new PlaybackParameters(rate));
630
+ });
631
+ }
632
+
633
+ @Override
634
+ protected void startCurrentTimeUpdates() {
635
+ logger.debug("Starting timer updates");
636
+ if (currentTimeHandler == null) {
637
+ currentTimeHandler = new Handler(Looper.getMainLooper());
638
+ }
639
+ // Reset completion status for this assetId
640
+ dispatchedCompleteMap.put(assetId, false);
641
+
642
+ // Wait for player to be truly ready
643
+ currentTimeHandler.postDelayed(
644
+ new Runnable() {
645
+ @Override
646
+ public void run() {
647
+ if (player != null && player.getPlaybackState() == Player.STATE_READY) {
648
+ startTimeUpdateLoop();
649
+ } else {
650
+ // Check again in 100ms
651
+ currentTimeHandler.postDelayed(this, 100);
652
+ }
653
+ }
654
+ },
655
+ 100
656
+ );
657
+ }
658
+
659
+ private void startTimeUpdateLoop() {
660
+ currentTimeRunnable = new Runnable() {
661
+ @Override
662
+ public void run() {
663
+ try {
664
+ boolean isPaused = false;
665
+ if (player != null && player.getPlaybackState() == Player.STATE_READY) {
666
+ if (player.isPlaying()) {
667
+ double currentTime = player.getCurrentPosition() / 1000.0; // Get time directly
668
+ logger.debug("Play timer update: currentTime = " + currentTime);
669
+ if (owner != null) owner.notifyCurrentTime(assetId, currentTime);
670
+ currentTimeHandler.postDelayed(this, 100);
671
+ return;
672
+ } else if (!player.getPlayWhenReady()) {
673
+ isPaused = true;
674
+ }
675
+ }
676
+ logger.debug("Stopping play timer - not playing or not ready");
677
+ stopCurrentTimeUpdates();
678
+ if (isPaused) {
679
+ logger.verbose("Playback is paused, not dispatching complete");
680
+ } else {
681
+ logger.verbose("Playback is stopped, dispatching complete");
682
+ dispatchComplete();
683
+ }
684
+ } catch (Exception e) {
685
+ logger.error("Error getting current time", e);
686
+ stopCurrentTimeUpdates();
687
+ }
688
+ }
689
+ };
690
+ try {
691
+ if (currentTimeHandler == null) {
692
+ currentTimeHandler = new Handler(Looper.getMainLooper());
693
+ }
694
+ currentTimeHandler.post(currentTimeRunnable);
695
+ } catch (Exception e) {
696
+ logger.error("Error starting current time updates", e);
697
+ }
698
+ }
699
+
700
+ @Override
701
+ void stopCurrentTimeUpdates() {
702
+ logger.debug("Stopping play timer updates");
703
+ if (currentTimeHandler != null) {
704
+ currentTimeHandler.removeCallbacks(currentTimeRunnable);
705
+ currentTimeHandler = null;
706
+ }
707
+ }
708
+ }
@@ -0,0 +1,3 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ </resources>