@capgo/native-audio 8.2.12 → 8.2.13
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 +147 -34
- package/android/src/main/java/ee/forgr/audio/AudioAsset.java +352 -74
- package/android/src/main/java/ee/forgr/audio/AudioDispatcher.java +24 -3
- package/android/src/main/java/ee/forgr/audio/Constant.java +9 -1
- package/android/src/main/java/ee/forgr/audio/Logger.java +55 -0
- package/android/src/main/java/ee/forgr/audio/NativeAudio.java +336 -57
- package/android/src/main/java/ee/forgr/audio/RemoteAudioAsset.java +307 -98
- package/android/src/main/java/ee/forgr/audio/StreamAudioAsset.java +285 -96
- package/dist/docs.json +307 -41
- package/dist/esm/definitions.d.ts +116 -38
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +52 -41
- package/dist/esm/web.js +386 -41
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +386 -41
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +386 -41
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/NativeAudioPlugin/AudioAsset+Fade.swift +104 -0
- package/ios/Sources/NativeAudioPlugin/AudioAsset.swift +168 -324
- package/ios/Sources/NativeAudioPlugin/Constant.swift +17 -4
- package/ios/Sources/NativeAudioPlugin/Logger.swift +43 -0
- package/ios/Sources/NativeAudioPlugin/Plugin.swift +176 -87
- package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset+Fade.swift +110 -0
- package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset.swift +117 -273
- package/ios/Tests/NativeAudioPluginTests/PluginTests.swift +47 -72
- package/package.json +1 -1
|
@@ -8,11 +8,19 @@
|
|
|
8
8
|
|
|
9
9
|
public class Constant {
|
|
10
10
|
// Parameter keys
|
|
11
|
-
public static let FadeKey = "fade"
|
|
11
|
+
public static let FadeKey = "fade" // legacy global fade toggle
|
|
12
|
+
public static let FadeIn = "fadeIn"
|
|
13
|
+
public static let FadeOut = "fadeOut"
|
|
14
|
+
public static let FadeInDuration = "fadeInDuration"
|
|
15
|
+
public static let FadeOutDuration = "fadeOutDuration"
|
|
16
|
+
public static let FadeOutStartTime = "fadeOutStartTime"
|
|
17
|
+
public static let FadeDuration = "duration"
|
|
12
18
|
public static let FocusAudio = "focus"
|
|
13
|
-
public static let
|
|
14
|
-
public static let
|
|
19
|
+
public static let AssetPath = "assetPath"
|
|
20
|
+
public static let AssetId = "assetId"
|
|
15
21
|
public static let Volume = "volume"
|
|
22
|
+
public static let Time = "time"
|
|
23
|
+
public static let Delay = "delay"
|
|
16
24
|
public static let Rate = "rate"
|
|
17
25
|
public static let Loop = "loop"
|
|
18
26
|
public static let Background = "background"
|
|
@@ -24,7 +32,7 @@ public class Constant {
|
|
|
24
32
|
public static let DefaultVolume: Float = 1.0
|
|
25
33
|
public static let DefaultRate: Float = 1.0
|
|
26
34
|
public static let DefaultChannels: Int = 1
|
|
27
|
-
public static let
|
|
35
|
+
public static let DefaultFadeDuration: Float = 1.0
|
|
28
36
|
public static let MinRate: Float = 0.25
|
|
29
37
|
public static let MaxRate: Float = 4.0
|
|
30
38
|
public static let MinVolume: Float = 0.0
|
|
@@ -36,4 +44,9 @@ public class Constant {
|
|
|
36
44
|
public static let ErrorAssetPath = "Asset Path is missing"
|
|
37
45
|
public static let ErrorAssetNotFound = "Asset is not loaded"
|
|
38
46
|
public static let ErrorAssetAlreadyLoaded = "Asset is already loaded"
|
|
47
|
+
|
|
48
|
+
// Backward compatibility aliases
|
|
49
|
+
public static let AssetPathKey = AssetPath
|
|
50
|
+
public static let AssetIdKey = AssetId
|
|
51
|
+
public static let DefaultFadeDelay = DefaultFadeDuration
|
|
39
52
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import os.log
|
|
3
|
+
|
|
4
|
+
public class Logger {
|
|
5
|
+
private var osLogger: OSLog?
|
|
6
|
+
|
|
7
|
+
public static var debugModeEnabled = false
|
|
8
|
+
|
|
9
|
+
public init(logTag: String) {
|
|
10
|
+
osLogger = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "com.capgo.native-audio", category: logTag)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
func error(_ message: String, _ args: CVarArg...) {
|
|
14
|
+
osLog(message, args, level: .error)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func warning(_ message: String, _ args: CVarArg...) {
|
|
18
|
+
osLog(message, args, level: .fault)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func info(_ message: String, _ args: CVarArg...) {
|
|
22
|
+
osLog(message, args, level: .info)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func debug(_ message: String, _ args: CVarArg...) {
|
|
26
|
+
osLog(message, args, level: .debug)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func verbose(_ message: String, _ args: CVarArg...) {
|
|
30
|
+
osLog(message, args, level: .default)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private func osLog(_ message: String, _ args: [CVarArg], level: OSLogType = .default) {
|
|
34
|
+
if !Logger.debugModeEnabled {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
let formatted = String(format: message, arguments: args)
|
|
38
|
+
guard let osLogger else {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
os_log("%{public}@", log: osLogger, type: level, formatted)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -8,15 +8,15 @@ enum MyError: Error {
|
|
|
8
8
|
case runtimeError(String)
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
/// here: https://capacitor.ionicframework.com/docs/plugins/ios
|
|
13
|
-
// swiftlint:disable type_body_length file_length
|
|
11
|
+
// swiftlint:disable file_length
|
|
14
12
|
@objc(NativeAudio)
|
|
13
|
+
// swiftlint:disable:next type_body_length
|
|
15
14
|
public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
16
|
-
private let pluginVersion: String = "8.2.
|
|
15
|
+
private let pluginVersion: String = "8.2.13"
|
|
17
16
|
public let identifier = "NativeAudio"
|
|
18
17
|
public let jsName = "NativeAudio"
|
|
19
18
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
19
|
+
CAPPluginMethod(name: "setDebugMode", returnType: CAPPluginReturnPromise),
|
|
20
20
|
CAPPluginMethod(name: "configure", returnType: CAPPluginReturnPromise),
|
|
21
21
|
CAPPluginMethod(name: "preload", returnType: CAPPluginReturnPromise),
|
|
22
22
|
CAPPluginMethod(name: "playOnce", returnType: CAPPluginReturnPromise),
|
|
@@ -37,6 +37,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
37
37
|
CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise),
|
|
38
38
|
CAPPluginMethod(name: "deinitPlugin", returnType: CAPPluginReturnPromise)
|
|
39
39
|
]
|
|
40
|
+
private var logger = Logger(logTag: "NativeAudio")
|
|
40
41
|
internal let audioQueue = DispatchQueue(label: "ee.forgr.audio.queue", qos: .userInitiated, attributes: .concurrent)
|
|
41
42
|
/// A dictionary that stores audio asset objects by their asset IDs.
|
|
42
43
|
///
|
|
@@ -48,7 +49,6 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
private let queueKey = DispatchSpecificKey<Bool>()
|
|
51
|
-
var fadeMusic = false
|
|
52
52
|
var session = AVAudioSession.sharedInstance()
|
|
53
53
|
|
|
54
54
|
// Track if audio session has been initialized
|
|
@@ -73,6 +73,10 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
73
73
|
/// - Important: Must only be accessed within `audioQueue.sync` blocks.
|
|
74
74
|
internal var playOnceAssets: Set<String> = []
|
|
75
75
|
|
|
76
|
+
private var pendingPlayTasks: [String: DispatchWorkItem] = [:]
|
|
77
|
+
private var audioAssetData: [String: [String: Any]] = [:]
|
|
78
|
+
var isRunningTests = false
|
|
79
|
+
|
|
76
80
|
/// Initialize plugin state and audio-related handlers, and register background behavior for session management.
|
|
77
81
|
///
|
|
78
82
|
/// Performs initial plugin setup after the plugin is loaded.
|
|
@@ -82,8 +86,6 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
82
86
|
super.load()
|
|
83
87
|
audioQueue.setSpecific(key: queueKey, value: true)
|
|
84
88
|
|
|
85
|
-
self.fadeMusic = false
|
|
86
|
-
|
|
87
89
|
// Don't setup audio session on load - defer until first use
|
|
88
90
|
// setupAudioSession()
|
|
89
91
|
setupInterruptionHandling()
|
|
@@ -251,6 +253,15 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
251
253
|
}
|
|
252
254
|
// swiftlint:enable function_body_length
|
|
253
255
|
|
|
256
|
+
@objc func setDebugMode(_ call: CAPPluginCall) {
|
|
257
|
+
let debug = call.getBool("enabled") ?? false
|
|
258
|
+
Logger.debugModeEnabled = debug
|
|
259
|
+
if debug {
|
|
260
|
+
logger.info("Debug mode enabled")
|
|
261
|
+
}
|
|
262
|
+
call.resolve()
|
|
263
|
+
}
|
|
264
|
+
|
|
254
265
|
@objc func configure(_ call: CAPPluginCall) {
|
|
255
266
|
// Save original category on first configure call
|
|
256
267
|
if !audioSessionInitialized {
|
|
@@ -259,15 +270,13 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
259
270
|
audioSessionInitialized = true
|
|
260
271
|
}
|
|
261
272
|
|
|
262
|
-
if let fade = call.getBool(Constant.FadeKey) {
|
|
263
|
-
self.fadeMusic = fade
|
|
264
|
-
}
|
|
265
|
-
|
|
266
273
|
let focus = call.getBool(Constant.FocusAudio) ?? false
|
|
267
274
|
let background = call.getBool(Constant.Background) ?? false
|
|
268
275
|
let ignoreSilent = call.getBool(Constant.IgnoreSilent) ?? true
|
|
269
276
|
self.showNotification = call.getBool(Constant.ShowNotification) ?? false
|
|
270
277
|
|
|
278
|
+
logger.info("Configuring audio session with focus=%@ background=%@ ignoreSilent=%@", "\(focus)", "\(background)", "\(ignoreSilent)")
|
|
279
|
+
|
|
271
280
|
// Use a single audio session configuration block for better atomicity
|
|
272
281
|
do {
|
|
273
282
|
// Set category first
|
|
@@ -299,7 +308,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
299
308
|
}
|
|
300
309
|
|
|
301
310
|
} catch {
|
|
302
|
-
|
|
311
|
+
logger.error("Failed to configure audio session: %@", error.localizedDescription)
|
|
303
312
|
}
|
|
304
313
|
|
|
305
314
|
call.resolve()
|
|
@@ -333,23 +342,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
333
342
|
preloadAsset(call, isComplex: true)
|
|
334
343
|
}
|
|
335
344
|
|
|
336
|
-
|
|
337
|
-
///
|
|
338
|
-
/// This is a convenience method that combines preload, play, and unload into a single call.
|
|
339
|
-
/// The audio asset is automatically cleaned up after playback completes or if an error occurs.
|
|
340
|
-
/// Preloads and optionally plays a one-shot audio asset, then removes it from internal storage after completion.
|
|
341
|
-
///
|
|
342
|
-
/// The method generates a unique temporary asset identifier, loads the asset from a local file, a public bundle resource, or a remote URL (with optional headers), and tracks it as a transient "play-once" asset. If `autoPlay` is true the asset will begin playback immediately and the plugin's audio session will be activated. When playback completes (or when the asset is unloaded), the asset and any associated Now Playing metadata are removed. If `deleteAfterPlay` is true and the source was a local file URL, the file is deleted from disk if it passes safe-sandbox checks.
|
|
343
|
-
///
|
|
344
|
-
/// - Parameter call: The Capacitor plugin call containing:
|
|
345
|
-
/// - `assetPath`: Path to the audio file (required)
|
|
346
|
-
/// - `volume`: Playback volume 0.1-1.0 (default: 1.0)
|
|
347
|
-
/// - `isUrl`: Whether assetPath is a URL (default: false)
|
|
348
|
-
/// - `autoPlay`: Start playback immediately (default: true)
|
|
349
|
-
/// - `deleteAfterPlay`: Delete file after playback (default: false)
|
|
350
|
-
/// - `notificationMetadata`: Metadata for Now Playing info (optional)
|
|
351
|
-
///
|
|
352
|
-
/// The call is resolved with `["assetId": "<generated id>"]` on success or rejected with an error message on failure.
|
|
345
|
+
// swiftlint:disable:next cyclomatic_complexity function_body_length
|
|
353
346
|
@objc func playOnce(_ call: CAPPluginCall) {
|
|
354
347
|
// Generate unique temporary asset ID
|
|
355
348
|
let assetId = "playOnce_\(Int(Date().timeIntervalSince1970 * 1000))_\(UUID().uuidString.prefix(8))"
|
|
@@ -488,8 +481,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
488
481
|
withAssetId: assetId,
|
|
489
482
|
withPath: basePath,
|
|
490
483
|
withChannels: 1,
|
|
491
|
-
withVolume: volume
|
|
492
|
-
withFadeDelay: 0.5
|
|
484
|
+
withVolume: volume
|
|
493
485
|
)
|
|
494
486
|
self.audioList[assetId] = audioAsset
|
|
495
487
|
} else {
|
|
@@ -514,7 +506,6 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
514
506
|
withPath: assetPath,
|
|
515
507
|
withChannels: 1,
|
|
516
508
|
withVolume: volume,
|
|
517
|
-
withFadeDelay: 0.5,
|
|
518
509
|
withHeaders: headers
|
|
519
510
|
)
|
|
520
511
|
self.audioList[assetId] = remoteAudioAsset
|
|
@@ -537,8 +528,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
537
528
|
withAssetId: assetId,
|
|
538
529
|
withPath: basePath,
|
|
539
530
|
withChannels: 1,
|
|
540
|
-
withVolume: volume
|
|
541
|
-
withFadeDelay: 0.5
|
|
531
|
+
withVolume: volume
|
|
542
532
|
)
|
|
543
533
|
self.audioList[assetId] = audioAsset
|
|
544
534
|
} else {
|
|
@@ -557,8 +547,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
557
547
|
withAssetId: assetId,
|
|
558
548
|
withPath: basePath,
|
|
559
549
|
withChannels: 1,
|
|
560
|
-
withVolume: volume
|
|
561
|
-
withFadeDelay: 0.5
|
|
550
|
+
withVolume: volume
|
|
562
551
|
)
|
|
563
552
|
self.audioList[assetId] = audioAsset
|
|
564
553
|
} else {
|
|
@@ -584,7 +573,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
584
573
|
// Auto-play if requested
|
|
585
574
|
if autoPlay {
|
|
586
575
|
self.activateSession()
|
|
587
|
-
asset.play(time: 0,
|
|
576
|
+
asset.play(time: 0, volume: nil)
|
|
588
577
|
|
|
589
578
|
// Update notification center if enabled
|
|
590
579
|
if self.showNotification {
|
|
@@ -658,10 +647,17 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
658
647
|
}
|
|
659
648
|
}
|
|
660
649
|
|
|
650
|
+
// swiftlint:disable:next function_body_length
|
|
661
651
|
@objc func play(_ call: CAPPluginCall) {
|
|
662
652
|
let audioId = call.getString(Constant.AssetIdKey) ?? ""
|
|
663
|
-
let time = max(call.getDouble(
|
|
664
|
-
let delay = max(call.getDouble(
|
|
653
|
+
let time = max(call.getDouble(Constant.Time) ?? 0, 0)
|
|
654
|
+
let delay = max(call.getDouble(Constant.Delay) ?? 0, 0)
|
|
655
|
+
let volume = call.getFloat(Constant.Volume)
|
|
656
|
+
let fadeIn = call.getBool(Constant.FadeIn) ?? false
|
|
657
|
+
let fadeOut = call.getBool(Constant.FadeOut) ?? false
|
|
658
|
+
let fadeInDuration = call.getDouble(Constant.FadeInDuration) ?? Double(Constant.DefaultFadeDuration)
|
|
659
|
+
let fadeOutDuration = call.getDouble(Constant.FadeOutDuration) ?? Double(Constant.DefaultFadeDuration)
|
|
660
|
+
let fadeOutStartTime = call.getDouble(Constant.FadeOutStartTime) ?? 0.0
|
|
665
661
|
|
|
666
662
|
// Ensure audio session is initialized before first play
|
|
667
663
|
if !audioSessionInitialized {
|
|
@@ -682,20 +678,44 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
682
678
|
|
|
683
679
|
if let audioAsset = asset as? AudioAsset {
|
|
684
680
|
self.activateSession()
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
681
|
+
cancelPendingPlay(for: audioId)
|
|
682
|
+
clearAudioAssetData(for: audioId)
|
|
683
|
+
|
|
684
|
+
let playBlock = { [weak self] in
|
|
685
|
+
guard let self else { return }
|
|
686
|
+
self.executeOnAudioQueue {
|
|
687
|
+
if fadeIn {
|
|
688
|
+
audioAsset.playWithFade(time: time, volume: volume, fadeInDuration: fadeInDuration)
|
|
689
|
+
} else {
|
|
690
|
+
audioAsset.play(time: time, volume: volume)
|
|
691
|
+
}
|
|
692
|
+
self.pendingPlayTasks[audioId] = nil
|
|
693
|
+
|
|
694
|
+
if fadeOut {
|
|
695
|
+
self.handleFadeOut(
|
|
696
|
+
for: audioAsset,
|
|
697
|
+
audioId: audioId,
|
|
698
|
+
fadeOutDuration: fadeOutDuration,
|
|
699
|
+
fadeOutStartTime: fadeOutStartTime
|
|
700
|
+
)
|
|
701
|
+
}
|
|
690
702
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
703
|
+
if self.showNotification {
|
|
704
|
+
self.currentlyPlayingAssetId = audioId
|
|
705
|
+
self.updateNowPlayingInfo(audioId: audioId, audioAsset: audioAsset)
|
|
706
|
+
self.updatePlaybackState(isPlaying: true)
|
|
707
|
+
}
|
|
708
|
+
call.resolve()
|
|
709
|
+
}
|
|
696
710
|
}
|
|
697
711
|
|
|
698
|
-
|
|
712
|
+
if delay > 0 {
|
|
713
|
+
let workItem = DispatchWorkItem(block: playBlock)
|
|
714
|
+
pendingPlayTasks[audioId] = workItem
|
|
715
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
|
|
716
|
+
} else {
|
|
717
|
+
playBlock()
|
|
718
|
+
}
|
|
699
719
|
} else if let audioNumber = asset as? NSNumber {
|
|
700
720
|
self.activateSession()
|
|
701
721
|
AudioServicesPlaySystemSound(SystemSoundID(audioNumber.intValue))
|
|
@@ -722,7 +742,9 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
722
742
|
return
|
|
723
743
|
}
|
|
724
744
|
|
|
725
|
-
|
|
745
|
+
cancelPendingPlay(for: audioAsset.assetId)
|
|
746
|
+
clearAudioAssetData(for: audioAsset.assetId)
|
|
747
|
+
let time = max(call.getDouble(Constant.Time) ?? 0, 0)
|
|
726
748
|
audioAsset.setCurrentTime(time: time)
|
|
727
749
|
call.resolve()
|
|
728
750
|
}
|
|
@@ -761,7 +783,27 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
761
783
|
return
|
|
762
784
|
}
|
|
763
785
|
self.activateSession()
|
|
764
|
-
|
|
786
|
+
let fadeIn = call.getBool(Constant.FadeIn) ?? false
|
|
787
|
+
let fadeInDuration = call.getDouble(Constant.FadeInDuration) ?? Double(Constant.DefaultFadeDuration)
|
|
788
|
+
var restoredVolume: Float?
|
|
789
|
+
if let data = audioAssetData[audioAsset.assetId], let volume = data["volumeBeforePause"] as? Float {
|
|
790
|
+
restoredVolume = volume
|
|
791
|
+
}
|
|
792
|
+
if fadeIn {
|
|
793
|
+
let targetVolume = restoredVolume ?? (audioAsset.channels.first?.volume ?? audioAsset.initialVolume)
|
|
794
|
+
audioAsset.setVolume(volume: 0, fadeDuration: 0)
|
|
795
|
+
audioAsset.resume()
|
|
796
|
+
audioAsset.setVolume(volume: NSNumber(value: targetVolume), fadeDuration: fadeInDuration)
|
|
797
|
+
} else {
|
|
798
|
+
if let volume = restoredVolume {
|
|
799
|
+
audioAsset.setVolume(volume: NSNumber(value: volume), fadeDuration: 0)
|
|
800
|
+
}
|
|
801
|
+
audioAsset.resume()
|
|
802
|
+
}
|
|
803
|
+
if var data = audioAssetData[audioAsset.assetId] {
|
|
804
|
+
data.removeValue(forKey: "volumeBeforePause")
|
|
805
|
+
audioAssetData[audioAsset.assetId] = data
|
|
806
|
+
}
|
|
765
807
|
|
|
766
808
|
// Update notification when resumed
|
|
767
809
|
if self.showNotification {
|
|
@@ -778,8 +820,19 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
778
820
|
call.reject("Failed to get audio asset")
|
|
779
821
|
return
|
|
780
822
|
}
|
|
781
|
-
|
|
782
|
-
|
|
823
|
+
cancelPendingPlay(for: audioAsset.assetId)
|
|
824
|
+
let fadeOut = call.getBool(Constant.FadeOut) ?? false
|
|
825
|
+
let fadeOutDuration = call.getDouble(Constant.FadeOutDuration) ?? Double(Constant.DefaultFadeDuration)
|
|
826
|
+
let currentVolume = audioAsset.channels.first?.volume ?? audioAsset.initialVolume
|
|
827
|
+
var data = audioAssetData[audioAsset.assetId] ?? [:]
|
|
828
|
+
data["volumeBeforePause"] = currentVolume
|
|
829
|
+
audioAssetData[audioAsset.assetId] = data
|
|
830
|
+
|
|
831
|
+
if fadeOut {
|
|
832
|
+
audioAsset.stopWithFade(fadeOutDuration: fadeOutDuration, toPause: true)
|
|
833
|
+
} else {
|
|
834
|
+
audioAsset.pause()
|
|
835
|
+
}
|
|
783
836
|
|
|
784
837
|
// Update notification when paused
|
|
785
838
|
if self.showNotification {
|
|
@@ -796,6 +849,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
796
849
|
/// The `assetId` is read from the call using `Constant.AssetIdKey`. If the asset is currently playing it will be stopped; if `showNotification` is enabled the Now Playing info is cleared and `currentlyPlayingAssetId` is reset. 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.
|
|
797
850
|
@objc func stop(_ call: CAPPluginCall) {
|
|
798
851
|
let audioId = call.getString(Constant.AssetIdKey) ?? ""
|
|
852
|
+
let fadeOut = call.getBool(Constant.FadeOut) ?? false
|
|
853
|
+
let fadeOutDuration = call.getDouble(Constant.FadeOutDuration) ?? Double(Constant.DefaultFadeDuration)
|
|
799
854
|
|
|
800
855
|
audioQueue.sync {
|
|
801
856
|
guard !self.audioList.isEmpty else {
|
|
@@ -804,7 +859,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
804
859
|
}
|
|
805
860
|
|
|
806
861
|
do {
|
|
807
|
-
try self.stopAudio(audioId: audioId)
|
|
862
|
+
try self.stopAudio(audioId: audioId, fadeOut: fadeOut, fadeOutDuration: fadeOutDuration)
|
|
808
863
|
|
|
809
864
|
// Clear notification when stopped
|
|
810
865
|
if self.showNotification {
|
|
@@ -887,7 +942,8 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
887
942
|
}
|
|
888
943
|
|
|
889
944
|
let volume = min(max(call.getFloat(Constant.Volume) ?? Constant.DefaultVolume, Constant.MinVolume), Constant.MaxVolume)
|
|
890
|
-
|
|
945
|
+
let durationSecs = call.getDouble(Constant.FadeDuration) ?? 0.0
|
|
946
|
+
audioAsset.setVolume(volume: volume as NSNumber, fadeDuration: durationSecs)
|
|
891
947
|
call.resolve()
|
|
892
948
|
}
|
|
893
949
|
}
|
|
@@ -925,29 +981,12 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
925
981
|
}
|
|
926
982
|
}
|
|
927
983
|
|
|
928
|
-
|
|
929
|
-
///
|
|
930
|
-
/// Accepts a CAPPluginCall containing asset information, validates inputs, stores optional now‑playing metadata, and creates either a lightweight system sound (for non-complex assets) or a full AudioAsset/RemoteAudioAsset (for complex assets). Supports local file paths, file URLs, public bundle resources, and remote URLs (with optional headers).
|
|
931
|
-
/// - Parameters:
|
|
932
|
-
/// - call: CAPPluginCall containing required keys:
|
|
933
|
-
/// - "assetId" (String): unique identifier for the asset.
|
|
934
|
-
/// - "assetPath" (String): local path, file URL, public bundle resource, or remote URL.
|
|
935
|
-
/// - "isUrl" (Bool, optional): treat the provided path as a raw URL when false/omitted for non-complex loads; ignored for complex loads.
|
|
936
|
-
/// - For complex loads:
|
|
937
|
-
/// - "volume" (Float, optional): initial volume (clamped to valid range).
|
|
938
|
-
/// - "channels" (Int, optional): number of audio channels.
|
|
939
|
-
/// - "delay" (Float, optional): fade delay.
|
|
940
|
-
/// - For remote URLs:
|
|
941
|
-
/// - "headers" (Object, optional): HTTP headers to use when loading the remote asset.
|
|
942
|
-
/// - "notificationMetadata" (Object, optional): now‑playing metadata with keys "title", "artist", "album", and "artworkUrl".
|
|
943
|
-
/// - isComplex: If true, creates a full-featured AudioAsset/RemoteAudioAsset; if false, creates a lightweight system sound identifier.
|
|
944
|
-
/// - Behavior: Resolves the provided call on successful preload; rejects the call with an error message if validation fails or the asset cannot be created.
|
|
984
|
+
// swiftlint:disable:next cyclomatic_complexity function_body_length
|
|
945
985
|
@objc private func preloadAsset(_ call: CAPPluginCall, isComplex complex: Bool) {
|
|
946
986
|
// Common default values to ensure consistency
|
|
947
987
|
let audioId = call.getString(Constant.AssetIdKey) ?? ""
|
|
948
988
|
let channels: Int?
|
|
949
989
|
let volume: Float?
|
|
950
|
-
let delay: Float?
|
|
951
990
|
var isLocalUrl: Bool = call.getBool("isUrl") ?? false
|
|
952
991
|
|
|
953
992
|
if audioId == "" {
|
|
@@ -987,11 +1026,9 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
987
1026
|
if complex {
|
|
988
1027
|
volume = min(max(call.getFloat("volume") ?? Constant.DefaultVolume, Constant.MinVolume), Constant.MaxVolume)
|
|
989
1028
|
channels = max(call.getInt("channels") ?? Constant.DefaultChannels, 1)
|
|
990
|
-
delay = max(call.getFloat("delay") ?? Constant.DefaultFadeDelay, 0.0)
|
|
991
1029
|
} else {
|
|
992
1030
|
channels = Constant.DefaultChannels
|
|
993
1031
|
volume = Constant.DefaultVolume
|
|
994
|
-
delay = Constant.DefaultFadeDelay
|
|
995
1032
|
isLocalUrl = false
|
|
996
1033
|
}
|
|
997
1034
|
|
|
@@ -1017,7 +1054,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
1017
1054
|
let audioAsset = AudioAsset(
|
|
1018
1055
|
owner: self,
|
|
1019
1056
|
withAssetId: audioId, withPath: basePath, withChannels: channels,
|
|
1020
|
-
withVolume: volume
|
|
1057
|
+
withVolume: volume)
|
|
1021
1058
|
self.audioList[audioId] = audioAsset
|
|
1022
1059
|
call.resolve()
|
|
1023
1060
|
return
|
|
@@ -1034,7 +1071,14 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
1034
1071
|
}
|
|
1035
1072
|
}
|
|
1036
1073
|
}
|
|
1037
|
-
let remoteAudioAsset = RemoteAudioAsset(
|
|
1074
|
+
let remoteAudioAsset = RemoteAudioAsset(
|
|
1075
|
+
owner: self,
|
|
1076
|
+
withAssetId: audioId,
|
|
1077
|
+
withPath: assetPath,
|
|
1078
|
+
withChannels: channels,
|
|
1079
|
+
withVolume: volume,
|
|
1080
|
+
withHeaders: headers
|
|
1081
|
+
)
|
|
1038
1082
|
self.audioList[audioId] = remoteAudioAsset
|
|
1039
1083
|
call.resolve()
|
|
1040
1084
|
return
|
|
@@ -1070,7 +1114,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
1070
1114
|
let audioAsset = AudioAsset(
|
|
1071
1115
|
owner: self,
|
|
1072
1116
|
withAssetId: audioId, withPath: basePath, withChannels: channels,
|
|
1073
|
-
withVolume: volume
|
|
1117
|
+
withVolume: volume)
|
|
1074
1118
|
self.audioList[audioId] = audioAsset
|
|
1075
1119
|
}
|
|
1076
1120
|
} else {
|
|
@@ -1093,16 +1137,14 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
1093
1137
|
let audioAsset = AudioAsset(
|
|
1094
1138
|
owner: self,
|
|
1095
1139
|
withAssetId: audioId, withPath: assetPath, withChannels: channels,
|
|
1096
|
-
withVolume: volume
|
|
1140
|
+
withVolume: volume)
|
|
1097
1141
|
self.audioList[audioId] = audioAsset
|
|
1098
1142
|
}
|
|
1099
1143
|
}
|
|
1100
1144
|
call.resolve()
|
|
1101
1145
|
}
|
|
1102
1146
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
private func stopAudio(audioId: String) throws {
|
|
1147
|
+
private func stopAudio(audioId: String, fadeOut: Bool, fadeOutDuration: Double) throws {
|
|
1106
1148
|
var asset: AudioAsset?
|
|
1107
1149
|
|
|
1108
1150
|
audioQueue.sync {
|
|
@@ -1113,19 +1155,57 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
1113
1155
|
throw MyError.runtimeError(Constant.ErrorAssetNotFound)
|
|
1114
1156
|
}
|
|
1115
1157
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1158
|
+
clearAudioAssetData(for: audioId)
|
|
1159
|
+
|
|
1160
|
+
if fadeOut {
|
|
1161
|
+
audioAsset.stopWithFade(fadeOutDuration: fadeOutDuration)
|
|
1118
1162
|
} else {
|
|
1119
1163
|
audioAsset.stop()
|
|
1120
1164
|
}
|
|
1121
1165
|
}
|
|
1122
1166
|
|
|
1167
|
+
private func clearAudioAssetData(for audioId: String) {
|
|
1168
|
+
audioAssetData[audioId] = nil
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
private func cancelPendingPlay(for audioId: String) {
|
|
1172
|
+
if let task = pendingPlayTasks[audioId] {
|
|
1173
|
+
task.cancel()
|
|
1174
|
+
pendingPlayTasks[audioId] = nil
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
private func handleFadeOut(for asset: AudioAsset, audioId: String, fadeOutDuration: TimeInterval, fadeOutStartTime: TimeInterval) {
|
|
1179
|
+
let duration = asset.getDuration()
|
|
1180
|
+
if duration <= 0 || !duration.isFinite {
|
|
1181
|
+
logger.warning("Audio asset has no finite duration, skipping fadeOut for %@", audioId)
|
|
1182
|
+
return
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
var startTime = max(duration - fadeOutDuration, 0)
|
|
1186
|
+
if fadeOutStartTime > 0 {
|
|
1187
|
+
startTime = fadeOutStartTime
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
audioAssetData[audioId] = [
|
|
1191
|
+
"fadeOut": true,
|
|
1192
|
+
"fadeOutStartTime": startTime,
|
|
1193
|
+
"fadeOutDuration": fadeOutDuration
|
|
1194
|
+
]
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1123
1197
|
internal func executeOnAudioQueue(_ block: @escaping () -> Void) {
|
|
1124
1198
|
if DispatchQueue.getSpecific(key: queueKey) != nil {
|
|
1125
1199
|
block() // Already on queue
|
|
1126
1200
|
} else {
|
|
1127
|
-
|
|
1128
|
-
|
|
1201
|
+
if isRunningTests {
|
|
1202
|
+
audioQueue.async {
|
|
1203
|
+
block()
|
|
1204
|
+
}
|
|
1205
|
+
} else {
|
|
1206
|
+
audioQueue.sync(flags: .barrier) {
|
|
1207
|
+
block()
|
|
1208
|
+
}
|
|
1129
1209
|
}
|
|
1130
1210
|
}
|
|
1131
1211
|
}
|
|
@@ -1139,6 +1219,15 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
|
|
|
1139
1219
|
"currentTime": currentTime,
|
|
1140
1220
|
"assetId": asset.assetId
|
|
1141
1221
|
])
|
|
1222
|
+
|
|
1223
|
+
if let fadeData = audioAssetData[asset.assetId],
|
|
1224
|
+
let fadeOut = fadeData["fadeOut"] as? Bool, fadeOut,
|
|
1225
|
+
let fadeOutStartTime = fadeData["fadeOutStartTime"] as? Double,
|
|
1226
|
+
let fadeOutDuration = fadeData["fadeOutDuration"] as? Double,
|
|
1227
|
+
currentTime >= fadeOutStartTime {
|
|
1228
|
+
asset.stopWithFade(fadeOutDuration: fadeOutDuration)
|
|
1229
|
+
audioAssetData[asset.assetId] = nil
|
|
1230
|
+
}
|
|
1142
1231
|
}
|
|
1143
1232
|
}
|
|
1144
1233
|
|