@capgo/native-audio 8.3.16 → 8.3.18
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/android/src/main/java/ee/forgr/audio/AudioAsset.java +2 -0
- package/android/src/main/java/ee/forgr/audio/RemoteAudioAsset.java +22 -2
- package/ios/Sources/NativeAudioPlugin/AudioAsset+Fade.swift +16 -7
- package/ios/Sources/NativeAudioPlugin/AudioAsset.swift +9 -3
- package/ios/Sources/NativeAudioPlugin/Plugin.swift +49 -6
- package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset+Fade.swift +4 -2
- package/package.json +1 -1
|
@@ -612,8 +612,16 @@ public class RemoteAudioAsset extends AudioAsset {
|
|
|
612
612
|
);
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
+
@Override
|
|
616
|
+
public void stopWithFade(double fadeOutDurationMs, boolean toPause) throws Exception {
|
|
617
|
+
stopWithFade((float) fadeOutDurationMs, toPause);
|
|
618
|
+
}
|
|
619
|
+
|
|
615
620
|
public void stopWithFade(float fadeOutDurationMs, boolean asPause) throws Exception {
|
|
616
621
|
if (players.isEmpty()) {
|
|
622
|
+
if (!asPause) {
|
|
623
|
+
stop();
|
|
624
|
+
}
|
|
617
625
|
return;
|
|
618
626
|
}
|
|
619
627
|
|
|
@@ -623,6 +631,12 @@ public class RemoteAudioAsset extends AudioAsset {
|
|
|
623
631
|
.runOnUiThread(() -> {
|
|
624
632
|
if (player != null && player.isPlaying()) {
|
|
625
633
|
fadeOut(player, fadeOutDurationMs, asPause);
|
|
634
|
+
} else if (!asPause) {
|
|
635
|
+
try {
|
|
636
|
+
stop();
|
|
637
|
+
} catch (Exception e) {
|
|
638
|
+
logger.error("Error stopping remote audio after failed fade", e);
|
|
639
|
+
}
|
|
626
640
|
}
|
|
627
641
|
});
|
|
628
642
|
}
|
|
@@ -661,13 +675,19 @@ public class RemoteAudioAsset extends AudioAsset {
|
|
|
661
675
|
owner
|
|
662
676
|
.getActivity()
|
|
663
677
|
.runOnUiThread(() -> {
|
|
664
|
-
|
|
678
|
+
// Stop/pause unconditionally: the fade brought volume to zero so the
|
|
679
|
+
// player must be stopped regardless of its current isPlaying() state
|
|
680
|
+
// (e.g. ExoPlayer may have auto-stopped at volume 0 on some devices).
|
|
681
|
+
if (player != null) {
|
|
665
682
|
if (asPause) {
|
|
666
683
|
player.pause();
|
|
667
684
|
logger.verbose("Faded out to pause at time " + getCurrentPosition());
|
|
668
685
|
} else {
|
|
669
686
|
player.setVolume(0);
|
|
670
687
|
player.stop();
|
|
688
|
+
dispatchComplete();
|
|
689
|
+
initializePlayer(player);
|
|
690
|
+
isPrepared = false;
|
|
671
691
|
logger.verbose("Faded out to stop at time " + getCurrentPosition());
|
|
672
692
|
}
|
|
673
693
|
}
|
|
@@ -685,7 +705,7 @@ public class RemoteAudioAsset extends AudioAsset {
|
|
|
685
705
|
owner
|
|
686
706
|
.getActivity()
|
|
687
707
|
.runOnUiThread(() -> {
|
|
688
|
-
if (player != null
|
|
708
|
+
if (player != null) {
|
|
689
709
|
player.setVolume(thisTargetVolume);
|
|
690
710
|
}
|
|
691
711
|
});
|
|
@@ -34,14 +34,17 @@ extension AudioAsset {
|
|
|
34
34
|
scheduleLocalFadeOutPauseOnMain(audio: audio, beforePause: beforePause)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
fileprivate func scheduleLocalFadeOutStopOnMain(audio: AVAudioPlayer) {
|
|
37
|
+
fileprivate func scheduleLocalFadeOutStopOnMain(audio: AVAudioPlayer, beforeStop: ((TimeInterval, TimeInterval) -> Void)?) {
|
|
38
38
|
DispatchQueue.main.async { [weak self] in
|
|
39
39
|
guard let self else { return }
|
|
40
|
-
self.performLocalFadeOutStopOnMain(audio: audio)
|
|
40
|
+
self.performLocalFadeOutStopOnMain(audio: audio, beforeStop: beforeStop)
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
fileprivate func performLocalFadeOutStopOnMain(audio: AVAudioPlayer) {
|
|
44
|
+
fileprivate func performLocalFadeOutStopOnMain(audio: AVAudioPlayer, beforeStop: ((TimeInterval, TimeInterval) -> Void)?) {
|
|
45
|
+
let elapsed = audio.currentTime
|
|
46
|
+
let duration = audio.duration.isFinite ? audio.duration : 0
|
|
47
|
+
beforeStop?(elapsed, duration)
|
|
45
48
|
audio.stop()
|
|
46
49
|
dispatchComplete()
|
|
47
50
|
}
|
|
@@ -71,21 +74,25 @@ extension AudioAsset {
|
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
/// - Parameter beforeStop: Called on the main queue immediately before `stop()` when `toPause` is false,
|
|
78
|
+
/// so the plugin can refresh Now Playing (rate 0) at the final elapsed time.
|
|
74
79
|
/// - Parameter beforePause: Called on the main queue immediately before `pause()` when `toPause` is true,
|
|
75
80
|
/// so the plugin can persist `timeBeforePause` and update Now Playing at the actual stop position.
|
|
81
|
+
/// Kept last so call sites can use a trailing closure for fade-out-to-pause.
|
|
76
82
|
func fadeOut(
|
|
77
83
|
audio: AVAudioPlayer,
|
|
78
84
|
fadeOutDuration: TimeInterval,
|
|
79
85
|
toPause: Bool = false,
|
|
86
|
+
beforeStop: ((TimeInterval, TimeInterval) -> Void)? = nil,
|
|
80
87
|
beforePause: ((TimeInterval, TimeInterval) -> Void)? = nil
|
|
81
88
|
) {
|
|
82
89
|
cancelFade()
|
|
83
|
-
let steps = Int(fadeOutDuration / TimeInterval(fadeDelaySecs))
|
|
90
|
+
let steps = max(0, Int(fadeOutDuration / TimeInterval(fadeDelaySecs)))
|
|
84
91
|
guard steps > 0 else {
|
|
85
92
|
if toPause {
|
|
86
93
|
scheduleLocalFadeOutPauseOnMain(audio: audio, beforePause: beforePause)
|
|
87
94
|
} else {
|
|
88
|
-
scheduleLocalFadeOutStopOnMain(audio: audio)
|
|
95
|
+
scheduleLocalFadeOutStopOnMain(audio: audio, beforeStop: beforeStop)
|
|
89
96
|
}
|
|
90
97
|
return
|
|
91
98
|
}
|
|
@@ -96,19 +103,21 @@ extension AudioAsset {
|
|
|
96
103
|
task = DispatchWorkItem { [weak self] in
|
|
97
104
|
guard let self else { return }
|
|
98
105
|
for _ in 0..<steps {
|
|
99
|
-
guard let task, !task.isCancelled
|
|
106
|
+
guard let task, !task.isCancelled else { return }
|
|
107
|
+
guard self.isPlaying(), audio.isPlaying else { break }
|
|
100
108
|
currentVolume -= fadeStep
|
|
101
109
|
DispatchQueue.main.async {
|
|
102
110
|
audio.volume = max(currentVolume, 0)
|
|
103
111
|
}
|
|
104
112
|
Thread.sleep(forTimeInterval: TimeInterval(self.fadeDelaySecs))
|
|
105
113
|
}
|
|
114
|
+
guard let task, !task.isCancelled else { return }
|
|
106
115
|
DispatchQueue.main.async { [weak self] in
|
|
107
116
|
guard let self else { return }
|
|
108
117
|
if toPause {
|
|
109
118
|
self.performLocalFadeOutPauseOnMain(audio: audio, beforePause: beforePause)
|
|
110
119
|
} else {
|
|
111
|
-
self.performLocalFadeOutStopOnMain(audio: audio)
|
|
120
|
+
self.performLocalFadeOutStopOnMain(audio: audio, beforeStop: beforeStop)
|
|
112
121
|
}
|
|
113
122
|
}
|
|
114
123
|
}
|
|
@@ -261,10 +261,10 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
|
|
|
261
261
|
if player.isPlaying {
|
|
262
262
|
if toPause {
|
|
263
263
|
if player.volume > 0 {
|
|
264
|
-
fadeOut(audio: player, fadeOutDuration: fadeOutDuration, toPause: true
|
|
264
|
+
fadeOut(audio: player, fadeOutDuration: fadeOutDuration, toPause: true, beforePause: { [weak self] elapsed, duration in
|
|
265
265
|
guard let self, let owner = self.owner else { return }
|
|
266
266
|
owner.recordPausePositionAfterFade(assetId: self.assetId, elapsedTime: elapsed, duration: duration)
|
|
267
|
-
}
|
|
267
|
+
})
|
|
268
268
|
} else {
|
|
269
269
|
cancelFade()
|
|
270
270
|
schedulePauseWithPositionRecording(audio: player) { [weak self] elapsed, duration in
|
|
@@ -273,9 +273,15 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
|
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
} else if player.volume > 0 {
|
|
276
|
-
fadeOut(audio: player, fadeOutDuration: fadeOutDuration, toPause: false
|
|
276
|
+
fadeOut(audio: player, fadeOutDuration: fadeOutDuration, toPause: false, beforeStop: { [weak self] elapsed, duration in
|
|
277
|
+
guard let self, let owner = self.owner else { return }
|
|
278
|
+
owner.recordStoppedPlaybackStateAfterFade(assetId: self.assetId, elapsedTime: elapsed, duration: duration)
|
|
279
|
+
})
|
|
277
280
|
} else {
|
|
281
|
+
let elapsed = player.currentTime
|
|
282
|
+
let duration = player.duration.isFinite ? player.duration : 0
|
|
278
283
|
stop()
|
|
284
|
+
owner?.recordStoppedPlaybackStateAfterFade(assetId: assetId, elapsedTime: elapsed, duration: duration)
|
|
279
285
|
}
|
|
280
286
|
} else if !toPause {
|
|
281
287
|
stop()
|
|
@@ -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.18"
|
|
16
16
|
public let identifier = "NativeAudio"
|
|
17
17
|
public let jsName = "NativeAudio"
|
|
18
18
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -266,8 +266,20 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
266
266
|
return
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
// Sample before `stop()` — `AudioAsset.stop()` resets every channel's `currentTime` to 0.
|
|
270
|
+
let elapsedTime = asset.getCurrentTime()
|
|
271
|
+
let duration = asset.getDuration()
|
|
269
272
|
asset.stop()
|
|
270
|
-
|
|
273
|
+
// Keep `currentlyPlayingAssetId` and Now Playing metadata so the lock screen card
|
|
274
|
+
// stays until `unload()` (or natural completion / another `play()` replaces it).
|
|
275
|
+
if self.showNotification,
|
|
276
|
+
self.currentlyPlayingAssetId == assetId {
|
|
277
|
+
self.updatePlaybackState(
|
|
278
|
+
isPlaying: false,
|
|
279
|
+
elapsedTime: elapsedTime,
|
|
280
|
+
duration: duration
|
|
281
|
+
)
|
|
282
|
+
}
|
|
271
283
|
}
|
|
272
284
|
return .success
|
|
273
285
|
}
|
|
@@ -974,7 +986,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
974
986
|
|
|
975
987
|
/// Stops playback of the audio asset identified by `assetId` from the plugin call and performs related cleanup.
|
|
976
988
|
///
|
|
977
|
-
/// The `assetId` is read from the call using `Constant.AssetIdKey`. If the asset is currently playing it will be stopped
|
|
989
|
+
/// The `assetId` is read from the call using `Constant.AssetIdKey`. If the asset is currently playing it will be stopped. When `showNotification` is enabled and this asset owns Now Playing, playback state is updated to stopped but the Now Playing card is left in place until `unload()` or natural completion. If the asset was created by `playOnce`, it is removed from `playOnceAssets` and its notification metadata is removed. The audio session is ended if appropriate. The call is resolved on success or rejected with an error message on failure.
|
|
978
990
|
@objc func stop(_ call: CAPPluginCall) {
|
|
979
991
|
let audioId = call.getString(Constant.AssetIdKey) ?? ""
|
|
980
992
|
let fadeOut = call.getBool(Constant.FadeOut) ?? false
|
|
@@ -989,11 +1001,32 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
989
1001
|
}
|
|
990
1002
|
|
|
991
1003
|
do {
|
|
1004
|
+
// Sample before `stopAudio` — non-fade `AudioAsset.stop()` resets `currentTime` to 0.
|
|
1005
|
+
var preStopNowPlayingElapsed: TimeInterval?
|
|
1006
|
+
var preStopNowPlayingDuration: TimeInterval?
|
|
1007
|
+
if !fadeOut,
|
|
1008
|
+
self.showNotification,
|
|
1009
|
+
self.currentlyPlayingAssetId == audioId,
|
|
1010
|
+
let preStopAsset = self.audioList[audioId] as? AudioAsset {
|
|
1011
|
+
preStopNowPlayingElapsed = preStopAsset.getCurrentTime()
|
|
1012
|
+
preStopNowPlayingDuration = preStopAsset.getDuration()
|
|
1013
|
+
}
|
|
1014
|
+
|
|
992
1015
|
try self.stopAudio(audioId: audioId, fadeOut: fadeOut, fadeOutDuration: fadeOutDuration)
|
|
993
1016
|
|
|
994
|
-
//
|
|
995
|
-
|
|
996
|
-
|
|
1017
|
+
// Keep `currentlyPlayingAssetId` so lock screen / Control Center stays tied to this asset
|
|
1018
|
+
// until `unload()` clears it; refresh Now Playing to a stopped state (rate 0).
|
|
1019
|
+
// Skip when fading out to stop: `recordStoppedPlaybackStateAfterFade` runs when the fade finishes
|
|
1020
|
+
// (and for zero-volume immediate stop inside `stopWithFade`).
|
|
1021
|
+
if let elapsed = preStopNowPlayingElapsed,
|
|
1022
|
+
let duration = preStopNowPlayingDuration,
|
|
1023
|
+
self.showNotification,
|
|
1024
|
+
self.currentlyPlayingAssetId == audioId {
|
|
1025
|
+
self.updatePlaybackState(
|
|
1026
|
+
isPlaying: false,
|
|
1027
|
+
elapsedTime: elapsed,
|
|
1028
|
+
duration: duration
|
|
1029
|
+
)
|
|
997
1030
|
}
|
|
998
1031
|
|
|
999
1032
|
// Clean up playOnce tracking if this was a playOnce asset
|
|
@@ -1517,6 +1550,16 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
1517
1550
|
}
|
|
1518
1551
|
}
|
|
1519
1552
|
|
|
1553
|
+
/// Refreshes Now Playing to a stopped state after fade-out-to-stop completes (or zero-volume stop-with-fade).
|
|
1554
|
+
internal func recordStoppedPlaybackStateAfterFade(assetId: String, elapsedTime: TimeInterval, duration: TimeInterval) {
|
|
1555
|
+
audioQueue.async { [weak self] in
|
|
1556
|
+
guard let self else { return }
|
|
1557
|
+
if self.showNotification && self.currentlyPlayingAssetId == assetId {
|
|
1558
|
+
self.updatePlaybackState(isPlaying: false, elapsedTime: elapsedTime, duration: duration)
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1520
1563
|
private func updatePlaybackState(isPlaying: Bool, elapsedTime: TimeInterval? = nil, duration: TimeInterval? = nil) {
|
|
1521
1564
|
DispatchQueue.main.async {
|
|
1522
1565
|
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
|
|
@@ -62,7 +62,7 @@ extension RemoteAudioAsset {
|
|
|
62
62
|
beforePause: ((TimeInterval, TimeInterval) -> Void)? = nil
|
|
63
63
|
) {
|
|
64
64
|
cancelFade()
|
|
65
|
-
let steps = Int(fadeOutDuration / TimeInterval(fadeDelaySecs))
|
|
65
|
+
let steps = max(0, Int(fadeOutDuration / TimeInterval(fadeDelaySecs)))
|
|
66
66
|
guard steps > 0 else {
|
|
67
67
|
if toPause {
|
|
68
68
|
scheduleRemoteFadeOutPauseOnMain(player: player, beforePause: beforePause)
|
|
@@ -81,13 +81,15 @@ extension RemoteAudioAsset {
|
|
|
81
81
|
task = DispatchWorkItem { [weak self] in
|
|
82
82
|
guard let self else { return }
|
|
83
83
|
for _ in 0..<steps {
|
|
84
|
-
guard let task, !task.isCancelled
|
|
84
|
+
guard let task, !task.isCancelled else { return }
|
|
85
|
+
guard self.isPlaying(), player.timeControlStatus == .playing else { break }
|
|
85
86
|
currentVolume -= fadeStep
|
|
86
87
|
DispatchQueue.main.async {
|
|
87
88
|
player.volume = max(currentVolume, 0)
|
|
88
89
|
}
|
|
89
90
|
Thread.sleep(forTimeInterval: TimeInterval(self.fadeDelaySecs))
|
|
90
91
|
}
|
|
92
|
+
guard let task, !task.isCancelled else { return }
|
|
91
93
|
DispatchQueue.main.async { [weak self] in
|
|
92
94
|
guard let self else { return }
|
|
93
95
|
if toPause {
|