@capgo/native-audio 8.3.6 → 8.3.8
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 +10 -0
- package/android/src/main/java/ee/forgr/audio/NativeAudio.java +119 -5
- package/ios/Sources/NativeAudioPlugin/AudioAsset.swift +6 -2
- package/ios/Sources/NativeAudioPlugin/Plugin.swift +3 -3
- package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset.swift +16 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -246,6 +246,16 @@ The media control buttons automatically handle:
|
|
|
246
246
|
- **Play** - Resumes paused audio
|
|
247
247
|
- **Pause** - Pauses playing audio
|
|
248
248
|
- **Stop** - Stops audio and clears the notification
|
|
249
|
+
- **Rewind 15s** (Android only) - Skips backward 15 seconds
|
|
250
|
+
- **Forward 15s** (Android only) - Skips forward 15 seconds
|
|
251
|
+
|
|
252
|
+
**Android Notification Controls:**
|
|
253
|
+
On Android, the notification displays three action buttons in this order:
|
|
254
|
+
1. ⏪ **Rewind 15s** - Skip backward 15 seconds
|
|
255
|
+
2. ▶️/⏸️ **Play/Pause** - Toggle playback (icon updates automatically)
|
|
256
|
+
3. ⏩ **Forward 15s** - Skip forward 15 seconds
|
|
257
|
+
|
|
258
|
+
The skip forward/backward buttons are automatically available when `showNotification: true` is configured. No additional setup is required.
|
|
249
259
|
|
|
250
260
|
**Notes:**
|
|
251
261
|
- All metadata fields are optional
|
|
@@ -720,6 +720,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
720
720
|
// Update notification when paused
|
|
721
721
|
if (showNotification) {
|
|
722
722
|
updatePlaybackState(PlaybackStateCompat.STATE_PAUSED);
|
|
723
|
+
updateNotification(audioId);
|
|
723
724
|
}
|
|
724
725
|
|
|
725
726
|
call.resolve();
|
|
@@ -764,6 +765,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
764
765
|
// Update notification when resumed
|
|
765
766
|
if (showNotification) {
|
|
766
767
|
updatePlaybackState(PlaybackStateCompat.STATE_PLAYING);
|
|
768
|
+
updateNotification(audioId);
|
|
767
769
|
}
|
|
768
770
|
|
|
769
771
|
call.resolve();
|
|
@@ -1492,7 +1494,12 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
1492
1494
|
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
|
|
1493
1495
|
|
|
1494
1496
|
PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder().setActions(
|
|
1495
|
-
PlaybackStateCompat.ACTION_PLAY |
|
|
1497
|
+
PlaybackStateCompat.ACTION_PLAY |
|
|
1498
|
+
PlaybackStateCompat.ACTION_PAUSE |
|
|
1499
|
+
PlaybackStateCompat.ACTION_STOP |
|
|
1500
|
+
PlaybackStateCompat.ACTION_REWIND |
|
|
1501
|
+
PlaybackStateCompat.ACTION_FAST_FORWARD |
|
|
1502
|
+
PlaybackStateCompat.ACTION_SEEK_TO
|
|
1496
1503
|
);
|
|
1497
1504
|
mediaSession.setPlaybackState(stateBuilder.build());
|
|
1498
1505
|
|
|
@@ -1507,6 +1514,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
1507
1514
|
if (asset != null && !asset.isPlaying()) {
|
|
1508
1515
|
asset.resume();
|
|
1509
1516
|
updatePlaybackState(PlaybackStateCompat.STATE_PLAYING);
|
|
1517
|
+
updateNotification(currentlyPlayingAssetId);
|
|
1510
1518
|
}
|
|
1511
1519
|
} catch (Exception e) {
|
|
1512
1520
|
Log.e(TAG, "Error resuming audio from media session", e);
|
|
@@ -1522,6 +1530,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
1522
1530
|
if (asset != null) {
|
|
1523
1531
|
asset.pause();
|
|
1524
1532
|
updatePlaybackState(PlaybackStateCompat.STATE_PAUSED);
|
|
1533
|
+
updateNotification(currentlyPlayingAssetId);
|
|
1525
1534
|
}
|
|
1526
1535
|
} catch (Exception e) {
|
|
1527
1536
|
Log.e(TAG, "Error pausing audio from media session", e);
|
|
@@ -1541,6 +1550,60 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
1541
1550
|
}
|
|
1542
1551
|
}
|
|
1543
1552
|
}
|
|
1553
|
+
|
|
1554
|
+
@Override
|
|
1555
|
+
public void onRewind() {
|
|
1556
|
+
if (currentlyPlayingAssetId != null && audioAssetList.containsKey(currentlyPlayingAssetId)) {
|
|
1557
|
+
AudioAsset asset = audioAssetList.get(currentlyPlayingAssetId);
|
|
1558
|
+
try {
|
|
1559
|
+
if (asset != null) {
|
|
1560
|
+
// Skip backward 15 seconds
|
|
1561
|
+
double currentPosition = asset.getCurrentPosition();
|
|
1562
|
+
double newPosition = Math.max(0, currentPosition - 15.0);
|
|
1563
|
+
asset.setCurrentPosition(newPosition);
|
|
1564
|
+
Log.d(TAG, "Rewind 15s: " + currentPosition + " -> " + newPosition);
|
|
1565
|
+
}
|
|
1566
|
+
} catch (Exception e) {
|
|
1567
|
+
Log.e(TAG, "Error rewinding audio from media session", e);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
@Override
|
|
1573
|
+
public void onFastForward() {
|
|
1574
|
+
if (currentlyPlayingAssetId != null && audioAssetList.containsKey(currentlyPlayingAssetId)) {
|
|
1575
|
+
AudioAsset asset = audioAssetList.get(currentlyPlayingAssetId);
|
|
1576
|
+
try {
|
|
1577
|
+
if (asset != null) {
|
|
1578
|
+
// Skip forward 15 seconds
|
|
1579
|
+
double currentPosition = asset.getCurrentPosition();
|
|
1580
|
+
double duration = asset.getDuration();
|
|
1581
|
+
double newPosition = Math.min(duration, currentPosition + 15.0);
|
|
1582
|
+
asset.setCurrentPosition(newPosition);
|
|
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
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
@Override
|
|
1592
|
+
public void onSeekTo(long pos) {
|
|
1593
|
+
if (currentlyPlayingAssetId != null && audioAssetList.containsKey(currentlyPlayingAssetId)) {
|
|
1594
|
+
AudioAsset asset = audioAssetList.get(currentlyPlayingAssetId);
|
|
1595
|
+
try {
|
|
1596
|
+
if (asset != null) {
|
|
1597
|
+
// Convert milliseconds to seconds
|
|
1598
|
+
double positionInSeconds = pos / 1000.0;
|
|
1599
|
+
asset.setCurrentPosition(positionInSeconds);
|
|
1600
|
+
Log.d(TAG, "Seek to: " + positionInSeconds);
|
|
1601
|
+
}
|
|
1602
|
+
} catch (Exception e) {
|
|
1603
|
+
Log.e(TAG, "Error seeking audio from media session", e);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1544
1607
|
}
|
|
1545
1608
|
);
|
|
1546
1609
|
|
|
@@ -1591,18 +1654,62 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
1591
1654
|
}
|
|
1592
1655
|
|
|
1593
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
|
+
|
|
1670
|
+
// Build notification with proper action order: Rewind, Play/Pause, Fast Forward
|
|
1671
|
+
// Use MediaButtonReceiver to properly wire actions to MediaSession callbacks
|
|
1594
1672
|
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getContext(), CHANNEL_ID)
|
|
1595
1673
|
.setSmallIcon(android.R.drawable.ic_media_play)
|
|
1596
1674
|
.setContentTitle(title)
|
|
1597
1675
|
.setContentText(artist)
|
|
1676
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
1677
|
+
// Add actions BEFORE setStyle() for proper wiring
|
|
1678
|
+
.addAction(
|
|
1679
|
+
new NotificationCompat.Action.Builder(
|
|
1680
|
+
android.R.drawable.ic_media_rew,
|
|
1681
|
+
"Rewind 15 seconds",
|
|
1682
|
+
androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
|
|
1683
|
+
getContext(),
|
|
1684
|
+
PlaybackStateCompat.ACTION_REWIND
|
|
1685
|
+
)
|
|
1686
|
+
).build()
|
|
1687
|
+
)
|
|
1688
|
+
.addAction(
|
|
1689
|
+
new NotificationCompat.Action.Builder(
|
|
1690
|
+
isPlaying ? android.R.drawable.ic_media_pause : android.R.drawable.ic_media_play,
|
|
1691
|
+
isPlaying ? "Pause" : "Play",
|
|
1692
|
+
androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
|
|
1693
|
+
getContext(),
|
|
1694
|
+
isPlaying ? PlaybackStateCompat.ACTION_PAUSE : PlaybackStateCompat.ACTION_PLAY
|
|
1695
|
+
)
|
|
1696
|
+
).build()
|
|
1697
|
+
)
|
|
1698
|
+
.addAction(
|
|
1699
|
+
new NotificationCompat.Action.Builder(
|
|
1700
|
+
android.R.drawable.ic_media_ff,
|
|
1701
|
+
"Fast forward 15 seconds",
|
|
1702
|
+
androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
|
|
1703
|
+
getContext(),
|
|
1704
|
+
PlaybackStateCompat.ACTION_FAST_FORWARD
|
|
1705
|
+
)
|
|
1706
|
+
).build()
|
|
1707
|
+
)
|
|
1598
1708
|
.setStyle(
|
|
1599
1709
|
new androidx.media.app.NotificationCompat.MediaStyle()
|
|
1600
1710
|
.setMediaSession(mediaSession.getSessionToken())
|
|
1601
1711
|
.setShowActionsInCompactView(0, 1, 2)
|
|
1602
1712
|
)
|
|
1603
|
-
.addAction(android.R.drawable.ic_media_previous, "Previous", null)
|
|
1604
|
-
.addAction(android.R.drawable.ic_media_pause, "Pause", null)
|
|
1605
|
-
.addAction(android.R.drawable.ic_media_next, "Next", null)
|
|
1606
1713
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
1607
1714
|
.setOnlyAlertOnce(true);
|
|
1608
1715
|
|
|
@@ -1624,7 +1731,14 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
|
|
|
1624
1731
|
|
|
1625
1732
|
PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder()
|
|
1626
1733
|
.setState(state, 0, state == PlaybackStateCompat.STATE_PLAYING ? 1.0f : 0.0f)
|
|
1627
|
-
.setActions(
|
|
1734
|
+
.setActions(
|
|
1735
|
+
PlaybackStateCompat.ACTION_PLAY |
|
|
1736
|
+
PlaybackStateCompat.ACTION_PAUSE |
|
|
1737
|
+
PlaybackStateCompat.ACTION_STOP |
|
|
1738
|
+
PlaybackStateCompat.ACTION_REWIND |
|
|
1739
|
+
PlaybackStateCompat.ACTION_FAST_FORWARD |
|
|
1740
|
+
PlaybackStateCompat.ACTION_SEEK_TO
|
|
1741
|
+
);
|
|
1628
1742
|
mediaSession.setPlaybackState(stateBuilder.build());
|
|
1629
1743
|
}
|
|
1630
1744
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import AVFoundation
|
|
1
|
+
@preconcurrency import AVFoundation
|
|
2
2
|
|
|
3
3
|
// swiftlint:disable:next type_body_length
|
|
4
4
|
public class AudioAsset: NSObject, AVAudioPlayerDelegate {
|
|
@@ -318,6 +318,10 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
|
|
|
318
318
|
return result
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
internal func shouldStopCurrentTimeUpdatesWhenNotPlaying() -> Bool {
|
|
322
|
+
true
|
|
323
|
+
}
|
|
324
|
+
|
|
321
325
|
public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
|
|
322
326
|
owner?.executeOnAudioQueue { [weak self] in
|
|
323
327
|
guard let self else { return }
|
|
@@ -345,7 +349,7 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
|
|
|
345
349
|
}
|
|
346
350
|
if self.isPlaying() {
|
|
347
351
|
owner.notifyCurrentTime(self)
|
|
348
|
-
} else {
|
|
352
|
+
} else if self.shouldStopCurrentTimeUpdatesWhenNotPlaying() {
|
|
349
353
|
self.stopCurrentTimeUpdates()
|
|
350
354
|
}
|
|
351
355
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import AVFoundation
|
|
1
|
+
@preconcurrency import AVFoundation
|
|
2
2
|
import Capacitor
|
|
3
3
|
import CoreAudio
|
|
4
4
|
import Foundation
|
|
5
|
-
import MediaPlayer
|
|
5
|
+
@preconcurrency import MediaPlayer
|
|
6
6
|
|
|
7
7
|
enum MyError: Error {
|
|
8
8
|
case runtimeError(String)
|
|
@@ -12,7 +12,7 @@ enum MyError: Error {
|
|
|
12
12
|
@objc(NativeAudio)
|
|
13
13
|
// swiftlint:disable:next type_body_length
|
|
14
14
|
public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
15
|
-
private let pluginVersion: String = "8.3.
|
|
15
|
+
private let pluginVersion: String = "8.3.8"
|
|
16
16
|
public let identifier = "NativeAudio"
|
|
17
17
|
public let jsName = "NativeAudio"
|
|
18
18
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import AVFoundation
|
|
1
|
+
@preconcurrency import AVFoundation
|
|
2
2
|
|
|
3
3
|
// swiftlint:disable:next type_body_length
|
|
4
4
|
public class RemoteAudioAsset: AudioAsset {
|
|
@@ -265,6 +265,21 @@ public class RemoteAudioAsset: AudioAsset {
|
|
|
265
265
|
return result
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
override func shouldStopCurrentTimeUpdatesWhenNotPlaying() -> Bool {
|
|
269
|
+
var shouldStop = true
|
|
270
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
271
|
+
guard let self else { return }
|
|
272
|
+
guard !players.isEmpty && playIndex < players.count else {
|
|
273
|
+
shouldStop = true
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let status = players[playIndex].timeControlStatus
|
|
278
|
+
shouldStop = status != .waitingToPlayAtSpecifiedRate
|
|
279
|
+
}
|
|
280
|
+
return shouldStop
|
|
281
|
+
}
|
|
282
|
+
|
|
268
283
|
override func getCurrentTime() -> TimeInterval {
|
|
269
284
|
var result: TimeInterval = 0
|
|
270
285
|
owner?.executeOnAudioQueue { [weak self] in
|