@capgo/native-audio 8.4.0 → 8.4.2
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 +63 -13
- package/android/build.gradle +2 -2
- package/android/src/main/java/ee/forgr/audio/NativeAudio.java +310 -89
- package/dist/docs.json +114 -0
- package/dist/esm/definitions.d.ts +36 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/NativeAudioPlugin/Plugin.swift +109 -15
- package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset.swift +6 -3
- package/package.json +14 -8
|
@@ -27,7 +27,9 @@ import static ee.forgr.audio.Constant.VOLUME;
|
|
|
27
27
|
import android.Manifest;
|
|
28
28
|
import android.app.NotificationChannel;
|
|
29
29
|
import android.app.NotificationManager;
|
|
30
|
+
import android.app.PendingIntent;
|
|
30
31
|
import android.content.Context;
|
|
32
|
+
import android.content.Intent;
|
|
31
33
|
import android.content.res.AssetFileDescriptor;
|
|
32
34
|
import android.content.res.AssetManager;
|
|
33
35
|
import android.graphics.Bitmap;
|
|
@@ -38,6 +40,7 @@ import android.os.Build;
|
|
|
38
40
|
import android.os.Handler;
|
|
39
41
|
import android.os.Looper;
|
|
40
42
|
import android.os.ParcelFileDescriptor;
|
|
43
|
+
import android.os.SystemClock;
|
|
41
44
|
import android.support.v4.media.MediaMetadataCompat;
|
|
42
45
|
import android.support.v4.media.session.MediaSessionCompat;
|
|
43
46
|
import android.support.v4.media.session.PlaybackStateCompat;
|
|
@@ -93,6 +96,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
93
96
|
private final Map<String, Handler> pendingPlayHandlers = new ConcurrentHashMap<>();
|
|
94
97
|
private final Map<String, Runnable> pendingPlayRunnables = new ConcurrentHashMap<>();
|
|
95
98
|
private final Map<String, JSObject> audioData = new ConcurrentHashMap<>();
|
|
99
|
+
private final Map<String, Integer> playbackStateByAssetId = new ConcurrentHashMap<>();
|
|
96
100
|
|
|
97
101
|
// Notification center support
|
|
98
102
|
private boolean showNotification = false;
|
|
@@ -102,6 +106,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
102
106
|
private static final int NOTIFICATION_ID = 1001;
|
|
103
107
|
private static final String CHANNEL_ID = "native_audio_channel";
|
|
104
108
|
private static final int MAX_NOTIFICATION_ARTWORK_SIZE = 512;
|
|
109
|
+
private static final double NOTIFICATION_SKIP_SECONDS = 15.0;
|
|
105
110
|
|
|
106
111
|
// Track playOnce assets for automatic cleanup
|
|
107
112
|
private Set<String> playOnceAssets = new HashSet<>();
|
|
@@ -135,23 +140,34 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
135
140
|
for (AudioAsset audio : audioAssetList.values()) {
|
|
136
141
|
if (audio.isPlaying()) {
|
|
137
142
|
audio.pause();
|
|
143
|
+
updateTrackedPlaybackState(audio.getAssetId(), PlaybackStateCompat.STATE_PAUSED);
|
|
138
144
|
resumeList.add(audio);
|
|
139
145
|
}
|
|
140
146
|
}
|
|
147
|
+
syncCurrentPlaybackState("audioFocusLossTransient");
|
|
141
148
|
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
|
|
142
149
|
// Resume playback
|
|
143
150
|
if (resumeList != null) {
|
|
144
151
|
while (!resumeList.isEmpty()) {
|
|
145
152
|
AudioAsset audio = resumeList.remove(0);
|
|
146
153
|
audio.resume();
|
|
154
|
+
updateTrackedPlaybackState(audio.getAssetId(), PlaybackStateCompat.STATE_PLAYING);
|
|
147
155
|
}
|
|
148
156
|
}
|
|
157
|
+
syncCurrentPlaybackState("audioFocusGain");
|
|
149
158
|
} else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
|
|
150
159
|
// Stop playback - permanent loss
|
|
160
|
+
String stoppedAssetId = currentlyPlayingAssetId;
|
|
151
161
|
for (AudioAsset audio : audioAssetList.values()) {
|
|
152
162
|
audio.stop();
|
|
163
|
+
updateTrackedPlaybackState(audio.getAssetId(), PlaybackStateCompat.STATE_STOPPED);
|
|
153
164
|
}
|
|
154
165
|
audioManager.abandonAudioFocus(this);
|
|
166
|
+
if (isStringValid(stoppedAssetId)) {
|
|
167
|
+
clearNotification();
|
|
168
|
+
currentlyPlayingAssetId = null;
|
|
169
|
+
notifyPlaybackState(stoppedAssetId, "audioFocusLoss");
|
|
170
|
+
}
|
|
155
171
|
}
|
|
156
172
|
} catch (Exception ex) {
|
|
157
173
|
Log.e(TAG, "Error handling audio focus change", ex);
|
|
@@ -177,11 +193,13 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
177
193
|
boolean wasPlaying = audio.pause();
|
|
178
194
|
|
|
179
195
|
if (wasPlaying) {
|
|
196
|
+
updateTrackedPlaybackState(entry.getKey(), PlaybackStateCompat.STATE_PAUSED);
|
|
180
197
|
resumeList.add(audio);
|
|
181
198
|
}
|
|
182
199
|
}
|
|
183
200
|
}
|
|
184
201
|
}
|
|
202
|
+
syncCurrentPlaybackState("appPause");
|
|
185
203
|
} catch (Exception ex) {
|
|
186
204
|
Log.d(TAG, "Exception caught while listening for handleOnPause: " + ex.getLocalizedMessage());
|
|
187
205
|
}
|
|
@@ -204,9 +222,11 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
204
222
|
|
|
205
223
|
if (audio != null) {
|
|
206
224
|
audio.resume();
|
|
225
|
+
updateTrackedPlaybackState(audio.getAssetId(), PlaybackStateCompat.STATE_PLAYING);
|
|
207
226
|
}
|
|
208
227
|
}
|
|
209
228
|
}
|
|
229
|
+
syncCurrentPlaybackState("appResume");
|
|
210
230
|
} catch (Exception ex) {
|
|
211
231
|
Log.d(TAG, "Exception caught while listening for handleOnResume: " + ex.getLocalizedMessage());
|
|
212
232
|
}
|
|
@@ -424,6 +444,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
424
444
|
assetToUnload.unload();
|
|
425
445
|
NativeAudio.this.audioAssetList.remove(assetId);
|
|
426
446
|
}
|
|
447
|
+
NativeAudio.this.clearTrackedPlaybackState(assetId);
|
|
427
448
|
|
|
428
449
|
// Remove from tracking sets
|
|
429
450
|
NativeAudio.this.playOnceAssets.remove(assetId);
|
|
@@ -457,12 +478,14 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
457
478
|
// Auto-play if requested
|
|
458
479
|
if (autoPlay) {
|
|
459
480
|
asset.play(0.0);
|
|
481
|
+
updateTrackedPlaybackState(assetId, PlaybackStateCompat.STATE_PLAYING);
|
|
460
482
|
|
|
461
483
|
// Update notification if enabled
|
|
462
484
|
if (showNotification) {
|
|
463
485
|
currentlyPlayingAssetId = assetId;
|
|
464
486
|
updateNotification(assetId);
|
|
465
487
|
}
|
|
488
|
+
NativeAudio.this.notifyPlaybackState(assetId, "playOnce");
|
|
466
489
|
}
|
|
467
490
|
|
|
468
491
|
// Return the generated assetId
|
|
@@ -478,6 +501,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
478
501
|
failedAsset.unload();
|
|
479
502
|
NativeAudio.this.audioAssetList.remove(assetId);
|
|
480
503
|
}
|
|
504
|
+
NativeAudio.this.clearTrackedPlaybackState(assetId);
|
|
481
505
|
call.reject("Failed to load asset for playOnce: " + ex.getMessage());
|
|
482
506
|
}
|
|
483
507
|
} catch (Exception ex) {
|
|
@@ -602,10 +626,13 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
602
626
|
handleFadeOut(asset, audioId, fadeOutDurationMs, fadeOutStartTimeSecs);
|
|
603
627
|
}
|
|
604
628
|
|
|
629
|
+
updateTrackedPlaybackState(audioId, PlaybackStateCompat.STATE_PLAYING);
|
|
630
|
+
|
|
605
631
|
if (showNotification) {
|
|
606
632
|
currentlyPlayingAssetId = audioId;
|
|
607
633
|
updateNotification(audioId);
|
|
608
634
|
}
|
|
635
|
+
notifyPlaybackState(audioId, "play");
|
|
609
636
|
|
|
610
637
|
call.resolve();
|
|
611
638
|
} catch (Exception ex) {
|
|
@@ -717,12 +744,16 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
717
744
|
resumeList.add(asset);
|
|
718
745
|
}
|
|
719
746
|
|
|
747
|
+
updateTrackedPlaybackState(audioId, PlaybackStateCompat.STATE_PAUSED);
|
|
748
|
+
|
|
720
749
|
// Update notification when paused
|
|
721
750
|
if (showNotification) {
|
|
722
751
|
updatePlaybackState(PlaybackStateCompat.STATE_PAUSED);
|
|
723
752
|
updateNotification(audioId);
|
|
724
753
|
}
|
|
725
754
|
|
|
755
|
+
notifyPlaybackState(audioId, "pause");
|
|
756
|
+
|
|
726
757
|
call.resolve();
|
|
727
758
|
} else {
|
|
728
759
|
call.reject(ERROR_ASSET_NOT_LOADED + " - " + audioId);
|
|
@@ -761,13 +792,17 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
761
792
|
data.remove("volumeBeforePause");
|
|
762
793
|
setAudioAssetData(audioId, data);
|
|
763
794
|
resumeList.add(asset);
|
|
795
|
+
updateTrackedPlaybackState(audioId, PlaybackStateCompat.STATE_PLAYING);
|
|
764
796
|
|
|
765
797
|
// Update notification when resumed
|
|
766
798
|
if (showNotification) {
|
|
799
|
+
currentlyPlayingAssetId = audioId;
|
|
767
800
|
updatePlaybackState(PlaybackStateCompat.STATE_PLAYING);
|
|
768
801
|
updateNotification(audioId);
|
|
769
802
|
}
|
|
770
803
|
|
|
804
|
+
notifyPlaybackState(audioId, "resume");
|
|
805
|
+
|
|
771
806
|
call.resolve();
|
|
772
807
|
} else {
|
|
773
808
|
call.reject(ERROR_ASSET_NOT_LOADED + " - " + audioId);
|
|
@@ -800,6 +835,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
800
835
|
clearFadeOutToStopTimer(audioId);
|
|
801
836
|
stopAudio(audioId, fadeOut, fadeOutDurationMs);
|
|
802
837
|
audioData.remove(audioId);
|
|
838
|
+
updateTrackedPlaybackState(audioId, PlaybackStateCompat.STATE_STOPPED);
|
|
803
839
|
|
|
804
840
|
// Clear notification when stopped
|
|
805
841
|
if (showNotification) {
|
|
@@ -807,6 +843,8 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
807
843
|
currentlyPlayingAssetId = null;
|
|
808
844
|
}
|
|
809
845
|
|
|
846
|
+
notifyPlaybackState(audioId, "stop");
|
|
847
|
+
|
|
810
848
|
call.resolve();
|
|
811
849
|
} catch (Exception ex) {
|
|
812
850
|
call.reject(ex.getMessage());
|
|
@@ -833,6 +871,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
833
871
|
clearFadeOutToStopTimer(audioId);
|
|
834
872
|
asset.unload();
|
|
835
873
|
audioAssetList.remove(audioId);
|
|
874
|
+
clearTrackedPlaybackState(audioId);
|
|
836
875
|
call.resolve();
|
|
837
876
|
} else {
|
|
838
877
|
call.reject(ERROR_AUDIO_ASSET_MISSING + " - " + audioId);
|
|
@@ -989,6 +1028,14 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
989
1028
|
JSObject ret = new JSObject();
|
|
990
1029
|
ret.put("assetId", assetId);
|
|
991
1030
|
notifyListeners("complete", ret);
|
|
1031
|
+
|
|
1032
|
+
updateTrackedPlaybackState(assetId, PlaybackStateCompat.STATE_STOPPED);
|
|
1033
|
+
|
|
1034
|
+
if (assetId != null && assetId.equals(currentlyPlayingAssetId)) {
|
|
1035
|
+
clearNotification();
|
|
1036
|
+
currentlyPlayingAssetId = null;
|
|
1037
|
+
}
|
|
1038
|
+
notifyPlaybackState(assetId, "complete");
|
|
992
1039
|
}
|
|
993
1040
|
|
|
994
1041
|
/**
|
|
@@ -1240,12 +1287,16 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
1240
1287
|
asset.play(time);
|
|
1241
1288
|
}
|
|
1242
1289
|
|
|
1290
|
+
updateTrackedPlaybackState(audioId, PlaybackStateCompat.STATE_PLAYING);
|
|
1291
|
+
|
|
1243
1292
|
// Update notification if enabled
|
|
1244
1293
|
if (showNotification) {
|
|
1245
1294
|
currentlyPlayingAssetId = audioId;
|
|
1246
1295
|
updateNotification(audioId);
|
|
1247
1296
|
}
|
|
1248
1297
|
|
|
1298
|
+
notifyPlaybackState(audioId, LOOP.equals(action) ? "loop" : "play");
|
|
1299
|
+
|
|
1249
1300
|
call.resolve();
|
|
1250
1301
|
} else {
|
|
1251
1302
|
call.reject("Asset is null: " + audioId);
|
|
@@ -1486,6 +1537,14 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
1486
1537
|
|
|
1487
1538
|
// Notification and MediaSession methods
|
|
1488
1539
|
|
|
1540
|
+
static double clampSeekPositionSeconds(double currentPosition, double duration, double deltaSeconds) {
|
|
1541
|
+
double targetPosition = currentPosition + deltaSeconds;
|
|
1542
|
+
if (duration > 0) {
|
|
1543
|
+
return Math.max(0, Math.min(duration, targetPosition));
|
|
1544
|
+
}
|
|
1545
|
+
return Math.max(0, targetPosition);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1489
1548
|
private void setupMediaSession() {
|
|
1490
1549
|
if (mediaSession != null) return;
|
|
1491
1550
|
|
|
@@ -1508,101 +1567,81 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
1508
1567
|
new MediaSessionCompat.Callback() {
|
|
1509
1568
|
@Override
|
|
1510
1569
|
public void onPlay() {
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
if (asset != null && !asset.isPlaying()) {
|
|
1515
|
-
asset.resume();
|
|
1516
|
-
updatePlaybackState(PlaybackStateCompat.STATE_PLAYING);
|
|
1517
|
-
updateNotification(currentlyPlayingAssetId);
|
|
1518
|
-
}
|
|
1519
|
-
} catch (Exception e) {
|
|
1520
|
-
Log.e(TAG, "Error resuming audio from media session", e);
|
|
1570
|
+
handleCurrentMediaAction("resuming", (audioId, asset) -> {
|
|
1571
|
+
if (!asset.isPlaying()) {
|
|
1572
|
+
asset.resume();
|
|
1521
1573
|
}
|
|
1522
|
-
|
|
1574
|
+
updateTrackedPlaybackState(audioId, PlaybackStateCompat.STATE_PLAYING);
|
|
1575
|
+
updateNotification(audioId);
|
|
1576
|
+
notifyPlaybackState(audioId, "remotePlay");
|
|
1577
|
+
});
|
|
1523
1578
|
}
|
|
1524
1579
|
|
|
1525
1580
|
@Override
|
|
1526
1581
|
public void onPause() {
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
updateNotification(currentlyPlayingAssetId);
|
|
1534
|
-
}
|
|
1535
|
-
} catch (Exception e) {
|
|
1536
|
-
Log.e(TAG, "Error pausing audio from media session", e);
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1582
|
+
handleCurrentMediaAction("pausing", (audioId, asset) -> {
|
|
1583
|
+
asset.pause();
|
|
1584
|
+
updateTrackedPlaybackState(audioId, PlaybackStateCompat.STATE_PAUSED);
|
|
1585
|
+
updateNotification(audioId);
|
|
1586
|
+
notifyPlaybackState(audioId, "remotePause");
|
|
1587
|
+
});
|
|
1539
1588
|
}
|
|
1540
1589
|
|
|
1541
1590
|
@Override
|
|
1542
1591
|
public void onStop() {
|
|
1543
|
-
|
|
1592
|
+
String audioId = currentlyPlayingAssetId;
|
|
1593
|
+
runOnMainThread(() -> {
|
|
1594
|
+
if (!isStringValid(audioId)) {
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1544
1597
|
try {
|
|
1545
|
-
stopAudio(
|
|
1598
|
+
stopAudio(audioId, false, 0);
|
|
1599
|
+
updateTrackedPlaybackState(audioId, PlaybackStateCompat.STATE_STOPPED);
|
|
1546
1600
|
clearNotification();
|
|
1547
1601
|
currentlyPlayingAssetId = null;
|
|
1602
|
+
notifyPlaybackState(audioId, "remoteStop");
|
|
1548
1603
|
} catch (Exception e) {
|
|
1549
1604
|
Log.e(TAG, "Error stopping audio from media session", e);
|
|
1550
1605
|
}
|
|
1551
|
-
}
|
|
1606
|
+
});
|
|
1552
1607
|
}
|
|
1553
1608
|
|
|
1554
1609
|
@Override
|
|
1555
1610
|
public void onRewind() {
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
}
|
|
1566
|
-
} catch (Exception e) {
|
|
1567
|
-
Log.e(TAG, "Error rewinding audio from media session", e);
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1611
|
+
handleCurrentMediaAction("rewinding", (audioId, asset) -> {
|
|
1612
|
+
double currentPosition = asset.getCurrentPosition();
|
|
1613
|
+
double duration = asset.getDuration();
|
|
1614
|
+
double newPosition = clampSeekPositionSeconds(currentPosition, duration, -NOTIFICATION_SKIP_SECONDS);
|
|
1615
|
+
asset.setCurrentPosition(newPosition);
|
|
1616
|
+
updatePlaybackState(resolvePlaybackState(audioId, asset), asset);
|
|
1617
|
+
notifyPlaybackState(audioId, "remoteRewind");
|
|
1618
|
+
Log.d(TAG, "Rewind 15s: " + currentPosition + " -> " + newPosition);
|
|
1619
|
+
});
|
|
1570
1620
|
}
|
|
1571
1621
|
|
|
1572
1622
|
@Override
|
|
1573
1623
|
public void onFastForward() {
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
Log.d(TAG, "Fast forward 15s: " + currentPosition + " -> " + newPosition);
|
|
1584
|
-
}
|
|
1585
|
-
} catch (Exception e) {
|
|
1586
|
-
Log.e(TAG, "Error fast forwarding audio from media session", e);
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1624
|
+
handleCurrentMediaAction("fast forwarding", (audioId, asset) -> {
|
|
1625
|
+
double currentPosition = asset.getCurrentPosition();
|
|
1626
|
+
double duration = asset.getDuration();
|
|
1627
|
+
double newPosition = clampSeekPositionSeconds(currentPosition, duration, NOTIFICATION_SKIP_SECONDS);
|
|
1628
|
+
asset.setCurrentPosition(newPosition);
|
|
1629
|
+
updatePlaybackState(resolvePlaybackState(audioId, asset), asset);
|
|
1630
|
+
notifyPlaybackState(audioId, "remoteFastForward");
|
|
1631
|
+
Log.d(TAG, "Fast forward 15s: " + currentPosition + " -> " + newPosition);
|
|
1632
|
+
});
|
|
1589
1633
|
}
|
|
1590
1634
|
|
|
1591
1635
|
@Override
|
|
1592
1636
|
public void onSeekTo(long pos) {
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
}
|
|
1602
|
-
} catch (Exception e) {
|
|
1603
|
-
Log.e(TAG, "Error seeking audio from media session", e);
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1637
|
+
handleCurrentMediaAction("seeking", (audioId, asset) -> {
|
|
1638
|
+
double duration = asset.getDuration();
|
|
1639
|
+
double positionInSeconds = clampSeekPositionSeconds(0, duration, pos / 1000.0);
|
|
1640
|
+
asset.setCurrentPosition(positionInSeconds);
|
|
1641
|
+
updatePlaybackState(resolvePlaybackState(audioId, asset), asset);
|
|
1642
|
+
notifyPlaybackState(audioId, "remoteSeek");
|
|
1643
|
+
Log.d(TAG, "Seek to: " + positionInSeconds);
|
|
1644
|
+
});
|
|
1606
1645
|
}
|
|
1607
1646
|
}
|
|
1608
1647
|
);
|
|
@@ -1622,7 +1661,10 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
1622
1661
|
}
|
|
1623
1662
|
|
|
1624
1663
|
private void updateNotification(String audioId) {
|
|
1625
|
-
if (mediaSession == null) return;
|
|
1664
|
+
if (mediaSession == null || !isStringValid(audioId)) return;
|
|
1665
|
+
|
|
1666
|
+
AudioAsset asset = audioAssetList.get(audioId);
|
|
1667
|
+
int playbackState = resolvePlaybackState(audioId, asset);
|
|
1626
1668
|
|
|
1627
1669
|
Map<String, String> metadata = notificationMetadataMap.get(audioId);
|
|
1628
1670
|
String title = metadata != null && metadata.containsKey("title") ? metadata.get("title") : "Playing";
|
|
@@ -1635,38 +1677,37 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
1635
1677
|
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title);
|
|
1636
1678
|
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist);
|
|
1637
1679
|
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album);
|
|
1680
|
+
long durationMs = getPlaybackDurationMs(asset);
|
|
1681
|
+
if (durationMs > 0) {
|
|
1682
|
+
metadataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMs);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
updatePlaybackState(playbackState, asset);
|
|
1638
1686
|
|
|
1639
1687
|
// Load artwork if provided
|
|
1640
1688
|
if (artworkUrl != null) {
|
|
1689
|
+
String targetAudioId = audioId;
|
|
1641
1690
|
loadArtwork(artworkUrl, (bitmap) -> {
|
|
1691
|
+
if (mediaSession == null || !targetAudioId.equals(currentlyPlayingAssetId)) {
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
AudioAsset currentAsset = audioAssetList.get(targetAudioId);
|
|
1696
|
+
int currentPlaybackState = resolvePlaybackState(targetAudioId, currentAsset);
|
|
1642
1697
|
if (bitmap != null) {
|
|
1643
1698
|
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap);
|
|
1699
|
+
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, bitmap);
|
|
1644
1700
|
}
|
|
1645
1701
|
mediaSession.setMetadata(metadataBuilder.build());
|
|
1646
|
-
showNotification(title, artist);
|
|
1702
|
+
showNotification(title, artist, bitmap, currentPlaybackState == PlaybackStateCompat.STATE_PLAYING);
|
|
1647
1703
|
});
|
|
1648
1704
|
} else {
|
|
1649
1705
|
mediaSession.setMetadata(metadataBuilder.build());
|
|
1650
|
-
showNotification(title, artist);
|
|
1706
|
+
showNotification(title, artist, null, playbackState == PlaybackStateCompat.STATE_PLAYING);
|
|
1651
1707
|
}
|
|
1652
|
-
|
|
1653
|
-
updatePlaybackState(PlaybackStateCompat.STATE_PLAYING);
|
|
1654
1708
|
}
|
|
1655
1709
|
|
|
1656
|
-
private void showNotification(String title, String artist) {
|
|
1657
|
-
// Determine if currently playing
|
|
1658
|
-
boolean isPlaying = false;
|
|
1659
|
-
if (currentlyPlayingAssetId != null && audioAssetList.containsKey(currentlyPlayingAssetId)) {
|
|
1660
|
-
AudioAsset asset = audioAssetList.get(currentlyPlayingAssetId);
|
|
1661
|
-
if (asset != null) {
|
|
1662
|
-
try {
|
|
1663
|
-
isPlaying = asset.isPlaying();
|
|
1664
|
-
} catch (Exception e) {
|
|
1665
|
-
Log.e(TAG, "Error checking playback state", e);
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1710
|
+
private void showNotification(String title, String artist, Bitmap artwork, boolean isPlaying) {
|
|
1670
1711
|
// Build notification with proper action order: Rewind, Play/Pause, Fast Forward
|
|
1671
1712
|
// Use MediaButtonReceiver to properly wire actions to MediaSession callbacks
|
|
1672
1713
|
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getContext(), CHANNEL_ID)
|
|
@@ -1674,6 +1715,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
1674
1715
|
.setContentTitle(title)
|
|
1675
1716
|
.setContentText(artist)
|
|
1676
1717
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
1718
|
+
.setOngoing(isPlaying)
|
|
1677
1719
|
// Add actions BEFORE setStyle() for proper wiring
|
|
1678
1720
|
.addAction(
|
|
1679
1721
|
new NotificationCompat.Action.Builder(
|
|
@@ -1713,6 +1755,15 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
1713
1755
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
1714
1756
|
.setOnlyAlertOnce(true);
|
|
1715
1757
|
|
|
1758
|
+
if (artwork != null) {
|
|
1759
|
+
notificationBuilder.setLargeIcon(artwork);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
PendingIntent contentIntent = getNotificationContentIntent();
|
|
1763
|
+
if (contentIntent != null) {
|
|
1764
|
+
notificationBuilder.setContentIntent(contentIntent);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1716
1767
|
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getContext());
|
|
1717
1768
|
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
|
1718
1769
|
}
|
|
@@ -1722,15 +1773,20 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
1722
1773
|
notificationManager.cancel(NOTIFICATION_ID);
|
|
1723
1774
|
|
|
1724
1775
|
if (mediaSession != null) {
|
|
1725
|
-
updatePlaybackState(PlaybackStateCompat.STATE_STOPPED);
|
|
1776
|
+
updatePlaybackState(PlaybackStateCompat.STATE_STOPPED, null);
|
|
1726
1777
|
}
|
|
1727
1778
|
}
|
|
1728
1779
|
|
|
1729
1780
|
private void updatePlaybackState(int state) {
|
|
1781
|
+
updatePlaybackState(state, getCurrentPlaybackAsset());
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
private void updatePlaybackState(int state, AudioAsset asset) {
|
|
1730
1785
|
if (mediaSession == null) return;
|
|
1731
1786
|
|
|
1787
|
+
long positionMs = getPlaybackPositionMs(asset);
|
|
1732
1788
|
PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder()
|
|
1733
|
-
.setState(state,
|
|
1789
|
+
.setState(state, positionMs, state == PlaybackStateCompat.STATE_PLAYING ? 1.0f : 0.0f, SystemClock.elapsedRealtime())
|
|
1734
1790
|
.setActions(
|
|
1735
1791
|
PlaybackStateCompat.ACTION_PLAY |
|
|
1736
1792
|
PlaybackStateCompat.ACTION_PAUSE |
|
|
@@ -1742,6 +1798,167 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
1742
1798
|
mediaSession.setPlaybackState(stateBuilder.build());
|
|
1743
1799
|
}
|
|
1744
1800
|
|
|
1801
|
+
private void syncCurrentPlaybackState(String reason) {
|
|
1802
|
+
if (!isStringValid(currentlyPlayingAssetId)) {
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
updateTrackedPlaybackState(currentlyPlayingAssetId, resolvePlaybackState(currentlyPlayingAssetId, getCurrentPlaybackAsset()));
|
|
1806
|
+
if (showNotification) {
|
|
1807
|
+
updateNotification(currentlyPlayingAssetId);
|
|
1808
|
+
}
|
|
1809
|
+
notifyPlaybackState(currentlyPlayingAssetId, reason);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
private void handleCurrentMediaAction(String verb, MediaSessionAction action) {
|
|
1813
|
+
String audioId = currentlyPlayingAssetId;
|
|
1814
|
+
runOnMainThread(() -> {
|
|
1815
|
+
if (!isStringValid(audioId) || !audioAssetList.containsKey(audioId)) {
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
AudioAsset asset = audioAssetList.get(audioId);
|
|
1820
|
+
if (asset == null) {
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
try {
|
|
1825
|
+
action.run(audioId, asset);
|
|
1826
|
+
} catch (Exception e) {
|
|
1827
|
+
Log.e(TAG, "Error " + verb + " audio from media session", e);
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
private void runOnMainThread(Runnable action) {
|
|
1833
|
+
if (getActivity() != null) {
|
|
1834
|
+
getActivity().runOnUiThread(action);
|
|
1835
|
+
} else {
|
|
1836
|
+
new Handler(Looper.getMainLooper()).post(action);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
private AudioAsset getCurrentPlaybackAsset() {
|
|
1841
|
+
if (!isStringValid(currentlyPlayingAssetId)) {
|
|
1842
|
+
return null;
|
|
1843
|
+
}
|
|
1844
|
+
return audioAssetList.get(currentlyPlayingAssetId);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
private int resolvePlaybackState(String audioId, AudioAsset asset) {
|
|
1848
|
+
if (!isStringValid(audioId)) {
|
|
1849
|
+
return PlaybackStateCompat.STATE_STOPPED;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
if (asset != null) {
|
|
1853
|
+
try {
|
|
1854
|
+
if (asset.isPlaying()) {
|
|
1855
|
+
return PlaybackStateCompat.STATE_PLAYING;
|
|
1856
|
+
}
|
|
1857
|
+
} catch (Exception e) {
|
|
1858
|
+
Log.e(TAG, "Error resolving playback state", e);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
Integer trackedState = playbackStateByAssetId.get(audioId);
|
|
1863
|
+
if (trackedState != null) {
|
|
1864
|
+
return trackedState;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
if (audioId.equals(currentlyPlayingAssetId)) {
|
|
1868
|
+
return PlaybackStateCompat.STATE_PAUSED;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
return PlaybackStateCompat.STATE_STOPPED;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
private long getPlaybackPositionMs(AudioAsset asset) {
|
|
1875
|
+
if (asset == null) {
|
|
1876
|
+
return 0;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
try {
|
|
1880
|
+
return Math.max(0, Math.round(asset.getCurrentPosition() * 1000));
|
|
1881
|
+
} catch (Exception e) {
|
|
1882
|
+
Log.e(TAG, "Error reading playback position", e);
|
|
1883
|
+
return 0;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
private long getPlaybackDurationMs(AudioAsset asset) {
|
|
1888
|
+
if (asset == null) {
|
|
1889
|
+
return 0;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
try {
|
|
1893
|
+
return Math.max(0, Math.round(asset.getDuration() * 1000));
|
|
1894
|
+
} catch (Exception e) {
|
|
1895
|
+
Log.e(TAG, "Error reading playback duration", e);
|
|
1896
|
+
return 0;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
private void notifyPlaybackState(String audioId, String reason) {
|
|
1901
|
+
if (!isStringValid(audioId)) {
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
AudioAsset asset = audioAssetList.get(audioId);
|
|
1906
|
+
int playbackState = resolvePlaybackState(audioId, asset);
|
|
1907
|
+
|
|
1908
|
+
JSObject ret = new JSObject();
|
|
1909
|
+
ret.put("assetId", audioId);
|
|
1910
|
+
ret.put("state", playbackStateToString(playbackState));
|
|
1911
|
+
ret.put("reason", reason);
|
|
1912
|
+
ret.put("isPlaying", playbackState == PlaybackStateCompat.STATE_PLAYING);
|
|
1913
|
+
|
|
1914
|
+
if (asset != null) {
|
|
1915
|
+
ret.put("currentTime", getPlaybackPositionMs(asset) / 1000.0);
|
|
1916
|
+
ret.put("duration", getPlaybackDurationMs(asset) / 1000.0);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
notifyListeners("playbackState", ret);
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
private void updateTrackedPlaybackState(String audioId, int playbackState) {
|
|
1923
|
+
if (!isStringValid(audioId)) {
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
playbackStateByAssetId.put(audioId, playbackState);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
private void clearTrackedPlaybackState(String audioId) {
|
|
1930
|
+
if (!isStringValid(audioId)) {
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
playbackStateByAssetId.remove(audioId);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
private String playbackStateToString(int playbackState) {
|
|
1937
|
+
if (playbackState == PlaybackStateCompat.STATE_PLAYING) {
|
|
1938
|
+
return "playing";
|
|
1939
|
+
}
|
|
1940
|
+
if (playbackState == PlaybackStateCompat.STATE_PAUSED) {
|
|
1941
|
+
return "paused";
|
|
1942
|
+
}
|
|
1943
|
+
return "stopped";
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
private PendingIntent getNotificationContentIntent() {
|
|
1947
|
+
Intent launchIntent = getContext().getPackageManager().getLaunchIntentForPackage(getContext().getPackageName());
|
|
1948
|
+
if (launchIntent == null) {
|
|
1949
|
+
return null;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
launchIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
|
1953
|
+
|
|
1954
|
+
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
|
1955
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
1956
|
+
flags |= PendingIntent.FLAG_IMMUTABLE;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
return PendingIntent.getActivity(getContext(), NOTIFICATION_ID, launchIntent, flags);
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1745
1962
|
private void loadArtwork(String urlString, ArtworkCallback callback) {
|
|
1746
1963
|
new Thread(() -> {
|
|
1747
1964
|
try {
|
|
@@ -1797,4 +2014,8 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
1797
2014
|
interface ArtworkCallback {
|
|
1798
2015
|
void onArtworkLoaded(Bitmap bitmap);
|
|
1799
2016
|
}
|
|
2017
|
+
|
|
2018
|
+
private interface MediaSessionAction {
|
|
2019
|
+
void run(String audioId, AudioAsset asset) throws Exception;
|
|
2020
|
+
}
|
|
1800
2021
|
}
|