@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.
@@ -24,8 +24,6 @@ public class StreamAudioAsset extends AudioAsset {
24
24
  private float volume;
25
25
  private boolean isPrepared = false;
26
26
  private final float initialVolume;
27
- private static final float FADE_STEP = 0.05f;
28
- private static final int FADE_DELAY_MS = 80; // 80ms between steps
29
27
  private static final long LIVE_OFFSET_MS = 5000; // 5 seconds behind live
30
28
  private final java.util.Map<String, String> headers;
31
29
 
@@ -68,7 +66,7 @@ public class StreamAudioAsset extends AudioAsset {
68
66
  }
69
67
 
70
68
  private void initializePlayer() {
71
- Log.d(TAG, "Initializing stream player with volume: " + volume);
69
+ logger.debug("Initializing stream player with volume: " + volume);
72
70
 
73
71
  // Configure HLS source with better settings for live streaming
74
72
  DefaultHttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory()
@@ -95,7 +93,7 @@ public class StreamAudioAsset extends AudioAsset {
95
93
  new Player.Listener() {
96
94
  @Override
97
95
  public void onPlaybackStateChanged(int state) {
98
- Log.d(TAG, "Stream state changed to: " + getStateString(state));
96
+ logger.debug("Stream state changed to: " + getStateString(state));
99
97
  if (state == Player.STATE_READY && !isPrepared) {
100
98
  isPrepared = true;
101
99
  if (player.isCurrentMediaItemLive()) {
@@ -106,17 +104,17 @@ public class StreamAudioAsset extends AudioAsset {
106
104
 
107
105
  @Override
108
106
  public void onIsLoadingChanged(boolean isLoading) {
109
- Log.d(TAG, "Loading state changed: " + isLoading);
107
+ logger.debug("Loading state changed: " + isLoading);
110
108
  }
111
109
 
112
110
  @Override
113
111
  public void onIsPlayingChanged(boolean isPlaying) {
114
- Log.d(TAG, "Playing state changed: " + isPlaying);
112
+ logger.debug("Playing state changed: " + isPlaying);
115
113
  }
116
114
 
117
115
  @Override
118
116
  public void onPlayerError(PlaybackException error) {
119
- Log.e(TAG, "Player error: " + error.getMessage());
117
+ logger.error("Player error: " + error.getMessage());
120
118
  isPrepared = false;
121
119
  // Try to recover by recreating the player
122
120
  owner
@@ -146,8 +144,8 @@ public class StreamAudioAsset extends AudioAsset {
146
144
  }
147
145
 
148
146
  @Override
149
- public void play(Double time) throws Exception {
150
- Log.d(TAG, "Play called with time: " + time + ", isPrepared: " + isPrepared);
147
+ public void play(double time, float volume) throws Exception {
148
+ logger.debug("Play called with time: " + time + ", isPrepared: " + isPrepared);
151
149
  owner
152
150
  .getActivity()
153
151
  .runOnUiThread(() -> {
@@ -157,23 +155,24 @@ public class StreamAudioAsset extends AudioAsset {
157
155
  new Player.Listener() {
158
156
  @Override
159
157
  public void onPlaybackStateChanged(int state) {
160
- Log.d(TAG, "Play-wait state changed to: " + getStateString(state));
158
+ logger.debug("Play-wait state changed to: " + getStateString(state));
161
159
  if (state == Player.STATE_READY) {
162
- startPlayback(time);
160
+ startPlayback(time, volume);
161
+ startCurrentTimeUpdates();
163
162
  player.removeListener(this);
164
163
  }
165
164
  }
166
165
  }
167
166
  );
168
167
  } else {
169
- startPlayback(time);
168
+ startPlayback(time, volume);
170
169
  }
171
170
  });
172
171
  }
173
172
 
174
- private void startPlayback(Double time) {
175
- Log.d(TAG, "Starting playback with time: " + time);
176
- if (time != null) {
173
+ private void startPlayback(double time, float volume) {
174
+ logger.debug("Starting playback with time: " + time);
175
+ if (time != 0) {
177
176
  player.seekTo(Math.round(time * 1000));
178
177
  } else if (player.isCurrentMediaItemLive()) {
179
178
  player.seekToDefaultPosition();
@@ -181,6 +180,7 @@ public class StreamAudioAsset extends AudioAsset {
181
180
  player.setPlaybackParameters(new PlaybackParameters(1.0f));
182
181
  player.setVolume(volume);
183
182
  player.setPlayWhenReady(true);
183
+ startCurrentTimeUpdates();
184
184
  }
185
185
 
186
186
  @Override
@@ -189,8 +189,10 @@ public class StreamAudioAsset extends AudioAsset {
189
189
  owner
190
190
  .getActivity()
191
191
  .runOnUiThread(() -> {
192
- if (player.isPlaying()) {
192
+ cancelFade();
193
+ if (player != null && player.isPlaying()) {
193
194
  player.setPlayWhenReady(false);
195
+ stopCurrentTimeUpdates();
194
196
  wasPlaying[0] = true;
195
197
  }
196
198
  });
@@ -203,6 +205,7 @@ public class StreamAudioAsset extends AudioAsset {
203
205
  .getActivity()
204
206
  .runOnUiThread(() -> {
205
207
  player.setPlayWhenReady(true);
208
+ startCurrentTimeUpdates();
206
209
  });
207
210
  }
208
211
 
@@ -211,6 +214,7 @@ public class StreamAudioAsset extends AudioAsset {
211
214
  owner
212
215
  .getActivity()
213
216
  .runOnUiThread(() -> {
217
+ cancelFade();
214
218
  // First stop playback
215
219
  player.stop();
216
220
  // Reset player state
@@ -243,7 +247,7 @@ public class StreamAudioAsset extends AudioAsset {
243
247
  new Player.Listener() {
244
248
  @Override
245
249
  public void onPlaybackStateChanged(int state) {
246
- Log.d(TAG, "Stop-reinit state changed to: " + getStateString(state));
250
+ logger.debug("Stop-reinit state changed to: " + getStateString(state));
247
251
  if (state == Player.STATE_READY) {
248
252
  isPrepared = true;
249
253
  player.removeListener(this);
@@ -264,6 +268,7 @@ public class StreamAudioAsset extends AudioAsset {
264
268
  .runOnUiThread(() -> {
265
269
  player.setRepeatMode(Player.REPEAT_MODE_ONE);
266
270
  player.setPlayWhenReady(true);
271
+ startCurrentTimeUpdates();
267
272
  });
268
273
  }
269
274
 
@@ -272,24 +277,58 @@ public class StreamAudioAsset extends AudioAsset {
272
277
  owner
273
278
  .getActivity()
274
279
  .runOnUiThread(() -> {
280
+ cancelFade();
275
281
  player.stop();
276
282
  player.clearMediaItems();
277
283
  player.release();
278
284
  isPrepared = false;
285
+ close(); // Ensure fadeExecutor is shutdown
279
286
  });
280
287
  }
281
288
 
282
289
  @Override
283
- public void setVolume(float volume) throws Exception {
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 {
284
307
  this.volume = volume;
285
308
  owner
286
309
  .getActivity()
287
310
  .runOnUiThread(() -> {
288
- Log.d(TAG, "Setting volume to: " + volume);
289
- player.setVolume(volume);
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
+ }
290
321
  });
291
322
  }
292
323
 
324
+ @Override
325
+ public float getVolume() throws Exception {
326
+ if (player != null) {
327
+ return player.getVolume();
328
+ }
329
+ return 0;
330
+ }
331
+
293
332
  @Override
294
333
  public boolean isPlaying() throws Exception {
295
334
  return player != null && player.isPlaying();
@@ -340,8 +379,8 @@ public class StreamAudioAsset extends AudioAsset {
340
379
  }
341
380
 
342
381
  @Override
343
- public void playWithFade(Double time) throws Exception {
344
- Log.d(TAG, "PlayWithFade called with time: " + time);
382
+ public void playWithFadeIn(double time, float volume, double fadeInDurationMs) throws Exception {
383
+ logger.debug("playWithFadeIn called with time: " + time);
345
384
  owner
346
385
  .getActivity()
347
386
  .runOnUiThread(() -> {
@@ -352,19 +391,19 @@ public class StreamAudioAsset extends AudioAsset {
352
391
  @Override
353
392
  public void onPlaybackStateChanged(int state) {
354
393
  if (state == Player.STATE_READY) {
355
- startPlaybackWithFade(time);
394
+ startPlaybackWithFade(time, volume, fadeInDurationMs);
356
395
  player.removeListener(this);
357
396
  }
358
397
  }
359
398
  }
360
399
  );
361
400
  } else {
362
- startPlaybackWithFade(time);
401
+ startPlaybackWithFade(time, volume, fadeInDurationMs);
363
402
  }
364
403
  });
365
404
  }
366
405
 
367
- private void startPlaybackWithFade(Double time) {
406
+ private void startPlaybackWithFade(Double time, float targetVolume, double fadeInDurationMs) {
368
407
  if (!player.isPlayingAd()) {
369
408
  // Make sure we're not in an ad
370
409
  if (time != null) {
@@ -388,8 +427,9 @@ public class StreamAudioAsset extends AudioAsset {
388
427
  // Start with volume 0
389
428
  player.setVolume(0);
390
429
  player.setPlayWhenReady(true);
430
+ startCurrentTimeUpdates();
391
431
  // Start fade after ensuring we're actually playing
392
- checkAndStartFade();
432
+ checkAndStartFade(fadeInDurationMs, targetVolume);
393
433
  }
394
434
  }
395
435
  }
@@ -397,7 +437,7 @@ public class StreamAudioAsset extends AudioAsset {
397
437
  }
398
438
  }
399
439
 
400
- private void checkAndStartFade() {
440
+ private void checkAndStartFade(double fadeInDurationMs, float volume) {
401
441
  final Handler handler = new Handler(Looper.getMainLooper());
402
442
  handler.postDelayed(
403
443
  new Runnable() {
@@ -406,7 +446,7 @@ public class StreamAudioAsset extends AudioAsset {
406
446
  @Override
407
447
  public void run() {
408
448
  if (player.isPlaying()) {
409
- fadeIn();
449
+ fadeIn(fadeInDurationMs, volume);
410
450
  } else if (attempts < 10) {
411
451
  // Try for 5 seconds (10 * 500ms)
412
452
  attempts++;
@@ -418,93 +458,166 @@ public class StreamAudioAsset extends AudioAsset {
418
458
  );
419
459
  }
420
460
 
421
- private void fadeIn() {
422
- final Handler handler = new Handler(Looper.getMainLooper());
423
- final Runnable fadeRunnable = new Runnable() {
424
- float currentVolume = 0;
461
+ private void fadeIn(double fadeInDurationMs, float targetVolume) {
462
+ cancelFade();
463
+ fadeState = FadeState.FADE_IN;
425
464
 
426
- @Override
427
- public void run() {
428
- if (player != null && player.isPlaying() && currentVolume < volume) {
429
- currentVolume += FADE_STEP;
430
- if (currentVolume > volume) currentVolume = volume;
431
- player.setVolume(currentVolume);
432
- Log.d(TAG, "Fading in: volume = " + currentVolume);
433
- handler.postDelayed(this, FADE_DELAY_MS);
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;
434
489
  }
435
- }
436
- };
437
- handler.post(fadeRunnable);
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
+ );
438
543
  }
439
544
 
440
545
  @Override
441
- public void stopWithFade() throws Exception {
546
+ public void stopWithFade(double fadeOutDurationMs, boolean toPause) throws Exception {
442
547
  owner
443
548
  .getActivity()
444
549
  .runOnUiThread(() -> {
445
- if (player.isPlaying()) {
446
- fadeOut();
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
+ }
447
558
  }
448
559
  });
449
560
  }
450
561
 
451
- private void fadeOut() {
452
- final Handler handler = new Handler(Looper.getMainLooper());
453
- final Runnable fadeRunnable = new Runnable() {
454
- float currentVolume = player.getVolume();
455
-
456
- @Override
457
- public void run() {
458
- if (currentVolume > FADE_STEP) {
459
- currentVolume -= FADE_STEP;
460
- player.setVolume(currentVolume);
461
- Log.d(TAG, "Fading out: volume = " + currentVolume);
462
- handler.postDelayed(this, FADE_DELAY_MS);
463
- } else {
464
- player.setVolume(0);
465
- // Stop and reset player
466
- player.stop();
467
- player.clearMediaItems();
468
- isPrepared = false;
562
+ @Override
563
+ public void stopWithFade() throws Exception {
564
+ stopWithFade(DEFAULT_FADE_DURATION_MS, false);
565
+ }
469
566
 
470
- // Create new media source
471
- DefaultHttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory()
472
- .setAllowCrossProtocolRedirects(true)
473
- .setConnectTimeoutMs(15000)
474
- .setReadTimeoutMs(15000)
475
- .setUserAgent("ExoPlayer");
567
+ private void fadeOut(double fadeOutDurationMs, boolean toPause) {
568
+ cancelFade();
569
+ fadeState = FadeState.FADE_OUT;
476
570
 
477
- // Add custom headers if provided
478
- if (headers != null && !headers.isEmpty()) {
479
- httpDataSourceFactory.setDefaultRequestProperties(headers);
480
- }
571
+ if (player == null) return;
481
572
 
482
- HlsMediaSource mediaSource = new HlsMediaSource.Factory(httpDataSourceFactory)
483
- .setAllowChunklessPreparation(true)
484
- .setTimestampAdjusterInitializationTimeoutMs(LIVE_OFFSET_MS)
485
- .createMediaSource(MediaItem.fromUri(uri));
573
+ final int steps = Math.max(1, (int) (fadeOutDurationMs / FADE_DELAY_MS));
574
+ final float initialVolume = player.getVolume();
575
+ final float fadeStep = initialVolume / steps;
486
576
 
487
- // Set new media source and prepare
488
- player.setMediaSource(mediaSource);
489
- player.prepare();
577
+ fadeTask = fadeExecutor.scheduleWithFixedDelay(
578
+ new Runnable() {
579
+ float currentVolume = initialVolume;
490
580
 
491
- // Add listener for preparation completion
492
- player.addListener(
493
- new Player.Listener() {
494
- @Override
495
- public void onPlaybackStateChanged(int state) {
496
- Log.d(TAG, "Fade-stop state changed to: " + getStateString(state));
497
- if (state == Player.STATE_READY) {
498
- isPrepared = true;
499
- player.removeListener(this);
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
+ }
500
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);
501
612
  }
502
- }
503
- );
613
+ });
614
+ currentVolume = nextVolume;
504
615
  }
505
- }
506
- };
507
- handler.post(fadeRunnable);
616
+ },
617
+ 0,
618
+ FADE_DELAY_MS,
619
+ java.util.concurrent.TimeUnit.MILLISECONDS
620
+ );
508
621
  }
509
622
 
510
623
  @Override
@@ -512,8 +625,84 @@ public class StreamAudioAsset extends AudioAsset {
512
625
  owner
513
626
  .getActivity()
514
627
  .runOnUiThread(() -> {
515
- Log.d(TAG, "Setting playback rate to: " + rate);
628
+ logger.debug("Setting playback rate to: " + rate);
516
629
  player.setPlaybackParameters(new PlaybackParameters(rate));
517
630
  });
518
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
+ }
519
708
  }