@capgo/capacitor-native-audio 8.4.3

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.
Files changed (46) hide show
  1. package/CapgoCapacitorNativeAudio.podspec +16 -0
  2. package/LICENSE +373 -0
  3. package/Package.swift +31 -0
  4. package/README.md +1229 -0
  5. package/android/build.gradle +89 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/java/ee/forgr/audio/AudioAsset.java +611 -0
  8. package/android/src/main/java/ee/forgr/audio/AudioCompletionListener.java +5 -0
  9. package/android/src/main/java/ee/forgr/audio/AudioDispatcher.java +208 -0
  10. package/android/src/main/java/ee/forgr/audio/Constant.java +36 -0
  11. package/android/src/main/java/ee/forgr/audio/HlsAvailabilityChecker.java +84 -0
  12. package/android/src/main/java/ee/forgr/audio/Logger.java +55 -0
  13. package/android/src/main/java/ee/forgr/audio/NativeAudio.java +2022 -0
  14. package/android/src/main/java/ee/forgr/audio/RemoteAudioAsset.java +886 -0
  15. package/android/src/main/java/ee/forgr/audio/StreamAudioAsset.java +708 -0
  16. package/android/src/main/res/values/colors.xml +3 -0
  17. package/android/src/main/res/values/strings.xml +3 -0
  18. package/android/src/main/res/values/styles.xml +3 -0
  19. package/dist/docs.json +1470 -0
  20. package/dist/esm/audio-asset.d.ts +4 -0
  21. package/dist/esm/audio-asset.js +6 -0
  22. package/dist/esm/audio-asset.js.map +1 -0
  23. package/dist/esm/definitions.d.ts +597 -0
  24. package/dist/esm/definitions.js +2 -0
  25. package/dist/esm/definitions.js.map +1 -0
  26. package/dist/esm/index.d.ts +4 -0
  27. package/dist/esm/index.js +7 -0
  28. package/dist/esm/index.js.map +1 -0
  29. package/dist/esm/web.d.ts +82 -0
  30. package/dist/esm/web.js +553 -0
  31. package/dist/esm/web.js.map +1 -0
  32. package/dist/plugin.cjs.js +571 -0
  33. package/dist/plugin.cjs.js.map +1 -0
  34. package/dist/plugin.js +574 -0
  35. package/dist/plugin.js.map +1 -0
  36. package/ios/Sources/NativeAudioPlugin/AudioAsset+Fade.swift +157 -0
  37. package/ios/Sources/NativeAudioPlugin/AudioAsset.swift +403 -0
  38. package/ios/Sources/NativeAudioPlugin/Constant.swift +52 -0
  39. package/ios/Sources/NativeAudioPlugin/Logger.swift +43 -0
  40. package/ios/Sources/NativeAudioPlugin/Plugin.swift +1786 -0
  41. package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset+Fade.swift +152 -0
  42. package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset.swift +405 -0
  43. package/ios/Tests/NativeAudioPluginTests/PluginTests.swift +648 -0
  44. package/ios/Tests/README.md +39 -0
  45. package/package.json +101 -0
  46. package/scripts/configure-dependencies.js +251 -0
@@ -0,0 +1,152 @@
1
+ import AVFoundation
2
+
3
+ extension RemoteAudioAsset {
4
+
5
+ /// Pause after sampling elapsed/duration for Now Playing. Caller must be on the main queue.
6
+ fileprivate func performRemoteFadeOutPauseOnMain(player: AVPlayer, beforePause: ((TimeInterval, TimeInterval) -> Void)?) {
7
+ let elapsed = player.currentTime().seconds
8
+ let rawDuration = player.currentItem?.duration ?? .zero
9
+ let duration = rawDuration.isNumeric && rawDuration.isValid ? rawDuration.seconds : 0
10
+ beforePause?(elapsed, duration.isFinite ? duration : 0)
11
+ player.pause()
12
+ }
13
+
14
+ fileprivate func scheduleRemoteFadeOutPauseOnMain(player: AVPlayer, beforePause: ((TimeInterval, TimeInterval) -> Void)?) {
15
+ DispatchQueue.main.async { [weak self] in
16
+ guard let self else { return }
17
+ self.performRemoteFadeOutPauseOnMain(player: player, beforePause: beforePause)
18
+ }
19
+ }
20
+
21
+ /// Pause, seek to start, then emit `complete` on the main queue after the seek finishes.
22
+ fileprivate func pauseSeekToStartThenDispatchComplete(on player: AVPlayer) {
23
+ player.pause()
24
+ player.seek(to: .zero) { [weak self] _ in
25
+ guard let self else { return }
26
+ DispatchQueue.main.async {
27
+ self.dispatchComplete()
28
+ }
29
+ }
30
+ }
31
+
32
+ func fadeIn(player: AVPlayer, fadeInDuration: TimeInterval, targetVolume: Float) {
33
+ cancelFade()
34
+ let steps = Int(fadeInDuration / TimeInterval(fadeDelaySecs))
35
+ guard steps > 0 else { return }
36
+ let fadeStep = targetVolume / Float(steps)
37
+ var currentVolume: Float = 0
38
+
39
+ var task: DispatchWorkItem?
40
+ task = DispatchWorkItem { [weak self] in
41
+ guard let self else { return }
42
+ for _ in 0..<steps {
43
+ guard let task, !task.isCancelled, self.isPlaying(), player.timeControlStatus == .playing else { return }
44
+ currentVolume += fadeStep
45
+ DispatchQueue.main.async {
46
+ player.volume = min(currentVolume, targetVolume)
47
+ }
48
+ Thread.sleep(forTimeInterval: TimeInterval(self.fadeDelaySecs))
49
+ }
50
+ }
51
+ fadeTask = task
52
+ if let task {
53
+ fadeQueue.async(execute: task)
54
+ }
55
+ }
56
+
57
+ /// - Parameter beforePause: Called on the main queue immediately before `pause()` when `toPause` is true.
58
+ func fadeOut(
59
+ player: AVPlayer,
60
+ fadeOutDuration: TimeInterval,
61
+ toPause: Bool = false,
62
+ beforePause: ((TimeInterval, TimeInterval) -> Void)? = nil
63
+ ) {
64
+ cancelFade()
65
+ let steps = max(0, Int(fadeOutDuration / TimeInterval(fadeDelaySecs)))
66
+ guard steps > 0 else {
67
+ if toPause {
68
+ scheduleRemoteFadeOutPauseOnMain(player: player, beforePause: beforePause)
69
+ } else {
70
+ DispatchQueue.main.async { [weak self] in
71
+ guard let self else { return }
72
+ self.pauseSeekToStartThenDispatchComplete(on: player)
73
+ }
74
+ }
75
+ return
76
+ }
77
+ let fadeStep = player.volume / Float(steps)
78
+ var currentVolume = player.volume
79
+
80
+ var task: DispatchWorkItem?
81
+ task = DispatchWorkItem { [weak self] in
82
+ guard let self else { return }
83
+ for _ in 0..<steps {
84
+ guard let task, !task.isCancelled else { return }
85
+ guard self.isPlaying(), player.timeControlStatus == .playing else { break }
86
+ currentVolume -= fadeStep
87
+ DispatchQueue.main.async {
88
+ player.volume = max(currentVolume, 0)
89
+ }
90
+ Thread.sleep(forTimeInterval: TimeInterval(self.fadeDelaySecs))
91
+ }
92
+ guard let task, !task.isCancelled else { return }
93
+ DispatchQueue.main.async { [weak self] in
94
+ guard let self else { return }
95
+ if toPause {
96
+ self.performRemoteFadeOutPauseOnMain(player: player, beforePause: beforePause)
97
+ } else {
98
+ self.pauseSeekToStartThenDispatchComplete(on: player)
99
+ }
100
+ }
101
+ }
102
+ fadeTask = task
103
+ if let task {
104
+ fadeQueue.async(execute: task)
105
+ }
106
+ }
107
+
108
+ func fadeTo(player: AVPlayer, fadeOutDuration: TimeInterval, targetVolume: Float) {
109
+ cancelFade()
110
+ let steps = Int(fadeOutDuration / TimeInterval(fadeDelaySecs))
111
+ guard steps > 0 else { return }
112
+
113
+ let minVolume = zeroVolume
114
+ var currentVolume: Float = max(player.volume, minVolume)
115
+ let safeTargetVolume: Float = max(targetVolume, minVolume)
116
+ let ratio = pow(safeTargetVolume / currentVolume, 1.0 / Float(steps))
117
+
118
+ var task: DispatchWorkItem?
119
+ task = DispatchWorkItem { [weak self] in
120
+ guard let self else { return }
121
+ for _ in 0..<steps {
122
+ guard let task, !task.isCancelled, self.isPlaying(), player.timeControlStatus == .playing else { return }
123
+ currentVolume *= ratio
124
+ DispatchQueue.main.async {
125
+ player.volume = min(max(currentVolume, minVolume), self.maxVolume)
126
+ }
127
+ Thread.sleep(forTimeInterval: TimeInterval(self.fadeDelaySecs))
128
+ }
129
+ }
130
+ fadeTask = task
131
+ if let task {
132
+ fadeQueue.async(execute: task)
133
+ }
134
+ }
135
+
136
+ static func clearCache() {
137
+ DispatchQueue.global(qos: .background).sync {
138
+ let urls = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
139
+ if let cachePath = urls.first {
140
+ do {
141
+ let fileURLs = try FileManager.default.contentsOfDirectory(at: cachePath, includingPropertiesForKeys: nil)
142
+ let audioExtensions = ["mp3", "wav", "aac", "m4a", "ogg", "mp4", "caf", "aiff"]
143
+ for fileURL in fileURLs where audioExtensions.contains(fileURL.pathExtension.lowercased()) {
144
+ try FileManager.default.removeItem(at: fileURL)
145
+ }
146
+ } catch {
147
+ staticLogger.error("Error clearing audio cache: %@", error.localizedDescription)
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
@@ -0,0 +1,405 @@
1
+ @preconcurrency import AVFoundation
2
+
3
+ // swiftlint:disable file_length
4
+ // swiftlint:disable:next type_body_length
5
+ public class RemoteAudioAsset: AudioAsset {
6
+ var playerItems: [AVPlayerItem] = []
7
+ var players: [AVPlayer] = []
8
+ var playerObservers: [NSKeyValueObservation] = []
9
+ var notificationObservers: [NSObjectProtocol] = []
10
+ var duration: TimeInterval = 0
11
+ var asset: AVURLAsset?
12
+ private var logger = Logger(logTag: "RemoteAudioAsset")
13
+ static let staticLogger = Logger(logTag: "RemoteAudioAsset")
14
+
15
+ init(owner: NativeAudio, withAssetId assetId: String, withPath path: String, withChannels channels: Int?, withVolume volume: Float?, withHeaders headers: [String: String]?) {
16
+ super.init(owner: owner, withAssetId: assetId, withPath: path, withChannels: channels ?? 1, withVolume: volume ?? 1.0)
17
+
18
+ let setupBlock = { [weak self] in
19
+ guard let self else { return }
20
+ guard let url = URL(string: path) else {
21
+ self.logger.error("Invalid URL: %@", String(describing: path))
22
+ return
23
+ }
24
+
25
+ var options: [String: Any] = [AVURLAssetPreferPreciseDurationAndTimingKey: true]
26
+ if let headers, !headers.isEmpty {
27
+ options["AVURLAssetHTTPHeaderFieldsKey"] = headers
28
+ }
29
+
30
+ let asset = AVURLAsset(url: url, options: options)
31
+ self.asset = asset
32
+ let channelCount = min(max(channels ?? Constant.DefaultChannels, 1), Constant.MaxChannels)
33
+
34
+ for _ in 0..<channelCount {
35
+ let playerItem = AVPlayerItem(asset: asset)
36
+ let player = AVPlayer(playerItem: playerItem)
37
+ player.volume = self.initialVolume
38
+ self.playerItems.append(playerItem)
39
+ self.players.append(player)
40
+
41
+ let durationObserver = playerItem.observe(\.status) { [weak self] item, _ in
42
+ guard let self else { return }
43
+ self.owner?.executeOnAudioQueue {
44
+ if item.status == .readyToPlay {
45
+ self.duration = item.duration.seconds
46
+ }
47
+ }
48
+ }
49
+ self.playerObservers.append(durationObserver)
50
+
51
+ let observer = player.observe(\.timeControlStatus) { [weak self, weak player] observedPlayer, _ in
52
+ guard let self, let player, player === observedPlayer else { return }
53
+ if player.timeControlStatus == .paused &&
54
+ (player.currentItem?.currentTime() == player.currentItem?.duration || player.currentItem?.duration == .zero) {
55
+ self.playerDidFinishPlaying(player: player)
56
+ }
57
+ }
58
+ self.playerObservers.append(observer)
59
+ }
60
+ }
61
+
62
+ if owner.isRunningTests {
63
+ setupBlock()
64
+ } else {
65
+ owner.executeOnAudioQueue(setupBlock)
66
+ }
67
+ }
68
+
69
+ // Backward-compatible initializer signature (delegates to primary init)
70
+ convenience init(owner: NativeAudio, withAssetId assetId: String, withPath path: String, withChannels channels: Int?, withVolume volume: Float?, withFadeDelay _: Float?, withHeaders headers: [String: String]?) {
71
+ self.init(owner: owner, withAssetId: assetId, withPath: path, withChannels: channels, withVolume: volume, withHeaders: headers)
72
+ }
73
+
74
+ deinit {
75
+ for observer in playerObservers {
76
+ observer.invalidate()
77
+ }
78
+ cleanupNotificationObservers()
79
+ for player in players {
80
+ player.pause()
81
+ }
82
+ playerItems = []
83
+ players = []
84
+ playerObservers = []
85
+ cancelFade()
86
+ }
87
+
88
+ func playerDidFinishPlaying(player: AVPlayer) {
89
+ owner?.executeOnAudioQueue { [weak self] in
90
+ guard let self else { return }
91
+ self.owner?.notifyListeners("complete", data: ["assetId": self.assetId])
92
+ self.dispatchedCompleteMap[self.assetId] = true
93
+ self.owner?.handlePlaybackCompletion(assetId: self.assetId, audioAsset: self)
94
+ self.onComplete?()
95
+ }
96
+ }
97
+
98
+ override func play(time: TimeInterval, volume: Float? = nil) {
99
+ owner?.executeOnAudioQueue { [weak self] in
100
+ guard let self else { return }
101
+ guard !players.isEmpty else { return }
102
+ if playIndex >= players.count {
103
+ playIndex = 0
104
+ }
105
+ cancelFade()
106
+ let player = players[playIndex]
107
+ player.seek(to: CMTimeMakeWithSeconds(max(time, 0), preferredTimescale: 1))
108
+ player.volume = volume ?? self.initialVolume
109
+ player.play()
110
+ playIndex = (playIndex + 1) % players.count
111
+ startCurrentTimeUpdates()
112
+ }
113
+ }
114
+
115
+ // Backward-compatible signature
116
+ override func play(time: TimeInterval, delay: TimeInterval) {
117
+ let validDelay = max(delay, 0)
118
+ if validDelay > 0 {
119
+ DispatchQueue.main.asyncAfter(deadline: .now() + validDelay) { [weak self] in
120
+ self?.play(time: time, volume: nil)
121
+ }
122
+ } else {
123
+ play(time: time, volume: nil)
124
+ }
125
+ }
126
+
127
+ override func pause() {
128
+ owner?.executeOnAudioQueue { [weak self] in
129
+ guard let self else { return }
130
+ guard !players.isEmpty && playIndex < players.count else { return }
131
+ cancelFade()
132
+ players[playIndex].pause()
133
+ stopCurrentTimeUpdates()
134
+ }
135
+ }
136
+
137
+ /// Timescale for seek targets; 600 is a common media default and avoids coarse rounding from timescale 1.
138
+ private static let seekPreferredTimescale: CMTimeScale = 600
139
+
140
+ override func setCurrentTime(time: TimeInterval, completion: (() -> Void)? = nil) {
141
+ guard let owner else {
142
+ completion?()
143
+ return
144
+ }
145
+ owner.executeOnAudioQueue { [weak self] in
146
+ guard let self else {
147
+ completion?()
148
+ return
149
+ }
150
+ guard !players.isEmpty && playIndex < players.count else {
151
+ completion?()
152
+ return
153
+ }
154
+ let player = players[playIndex]
155
+ let lowerBound = max(time, 0)
156
+ let validTime: TimeInterval
157
+ if let item = player.currentItem {
158
+ let itemDuration = item.duration
159
+ if itemDuration == .indefinite || !itemDuration.isValid {
160
+ validTime = lowerBound
161
+ } else {
162
+ let durationSeconds = itemDuration.seconds
163
+ if durationSeconds.isFinite && durationSeconds > 0 {
164
+ validTime = min(lowerBound, durationSeconds)
165
+ } else {
166
+ validTime = lowerBound
167
+ }
168
+ }
169
+ } else {
170
+ validTime = lowerBound
171
+ }
172
+ let target = CMTime(seconds: validTime, preferredTimescale: Self.seekPreferredTimescale)
173
+ player.seek(to: target, toleranceBefore: .zero, toleranceAfter: .zero) { finished in
174
+ guard finished else { return }
175
+ completion?()
176
+ }
177
+ }
178
+ }
179
+
180
+ override func resume() {
181
+ owner?.executeOnAudioQueue { [weak self] in
182
+ guard let self else { return }
183
+ guard !players.isEmpty && playIndex < players.count else { return }
184
+
185
+ let player = players[playIndex]
186
+ player.play()
187
+ cleanupNotificationObservers()
188
+ let observer = NotificationCenter.default.addObserver(
189
+ forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
190
+ object: player.currentItem,
191
+ queue: OperationQueue.main
192
+ ) { [weak self, weak player] notification in
193
+ guard let self, let player else { return }
194
+ if let currentItem = notification.object as? AVPlayerItem, player.currentItem === currentItem {
195
+ self.playerDidFinishPlaying(player: player)
196
+ }
197
+ }
198
+ notificationObservers.append(observer)
199
+ startCurrentTimeUpdates()
200
+ }
201
+ }
202
+
203
+ override func stop() {
204
+ owner?.executeOnAudioQueue { [weak self] in
205
+ guard let self else { return }
206
+ stopCurrentTimeUpdates()
207
+ cancelFade()
208
+ for player in players {
209
+ player.pause()
210
+ player.seek(to: .zero, completionHandler: { _ in
211
+ player.actionAtItemEnd = .pause
212
+ })
213
+ }
214
+ playIndex = 0
215
+ self.owner?.notifyListeners("complete", data: ["assetId": self.assetId as Any])
216
+ self.dispatchedCompleteMap[self.assetId] = true
217
+ }
218
+ }
219
+
220
+ override func loop() {
221
+ owner?.executeOnAudioQueue { [weak self] in
222
+ guard let self else { return }
223
+ cleanupNotificationObservers()
224
+ for (index, player) in players.enumerated() {
225
+ player.actionAtItemEnd = .none
226
+ guard let playerItem = player.currentItem else { continue }
227
+ let observer = NotificationCenter.default.addObserver(
228
+ forName: .AVPlayerItemDidPlayToEndTime,
229
+ object: playerItem,
230
+ queue: OperationQueue.main
231
+ ) { [weak player] notification in
232
+ guard let player,
233
+ let item = notification.object as? AVPlayerItem,
234
+ player.currentItem === item else { return }
235
+ player.seek(to: .zero)
236
+ player.play()
237
+ }
238
+ notificationObservers.append(observer)
239
+ if index == playIndex {
240
+ player.seek(to: .zero)
241
+ player.play()
242
+ }
243
+ }
244
+ startCurrentTimeUpdates()
245
+ }
246
+ }
247
+
248
+ public func cleanupNotificationObservers() {
249
+ for observer in notificationObservers {
250
+ NotificationCenter.default.removeObserver(observer)
251
+ }
252
+ notificationObservers = []
253
+ }
254
+
255
+ override func unload() {
256
+ owner?.executeOnAudioQueue { [weak self] in
257
+ guard let self else { return }
258
+ cancelFade()
259
+ stopCurrentTimeUpdates()
260
+ stop()
261
+ cleanupNotificationObservers()
262
+ for observer in playerObservers {
263
+ observer.invalidate()
264
+ }
265
+ playerObservers = []
266
+ players = []
267
+ playerItems = []
268
+ }
269
+ }
270
+
271
+ override func setVolume(volume: NSNumber, fadeDuration: Double) {
272
+ owner?.executeOnAudioQueue { [weak self] in
273
+ guard let self else { return }
274
+ cancelFade()
275
+ let validVolume = min(max(volume.floatValue, Constant.MinVolume), Constant.MaxVolume)
276
+ for player in players {
277
+ if isPlaying() && fadeDuration > 0 {
278
+ fadeTo(player: player, fadeOutDuration: fadeDuration, targetVolume: validVolume)
279
+ } else {
280
+ player.volume = validVolume
281
+ }
282
+ }
283
+ }
284
+ }
285
+
286
+ override func setVolume(volume: NSNumber) {
287
+ setVolume(volume: volume, fadeDuration: 0)
288
+ }
289
+
290
+ override func setRate(rate: NSNumber) {
291
+ owner?.executeOnAudioQueue { [weak self] in
292
+ guard let self else { return }
293
+ let validRate = min(max(rate.floatValue, Constant.MinRate), Constant.MaxRate)
294
+ for player in players {
295
+ player.rate = validRate
296
+ }
297
+ }
298
+ }
299
+
300
+ override func isPlaying() -> Bool {
301
+ var result = false
302
+ owner?.executeOnAudioQueue { [weak self] in
303
+ guard let self else { return }
304
+ guard !players.isEmpty && playIndex < players.count else {
305
+ result = false
306
+ return
307
+ }
308
+ result = players[playIndex].timeControlStatus == .playing
309
+ }
310
+ return result
311
+ }
312
+
313
+ override func shouldStopCurrentTimeUpdatesWhenNotPlaying() -> Bool {
314
+ var shouldStop = true
315
+ owner?.executeOnAudioQueue { [weak self] in
316
+ guard let self else { return }
317
+ guard !players.isEmpty && playIndex < players.count else {
318
+ shouldStop = true
319
+ return
320
+ }
321
+
322
+ let status = players[playIndex].timeControlStatus
323
+ shouldStop = status != .waitingToPlayAtSpecifiedRate
324
+ }
325
+ return shouldStop
326
+ }
327
+
328
+ override func getCurrentTime() -> TimeInterval {
329
+ var result: TimeInterval = 0
330
+ owner?.executeOnAudioQueue { [weak self] in
331
+ guard let self else { return }
332
+ guard !players.isEmpty && playIndex < players.count else { return }
333
+ result = players[playIndex].currentTime().seconds
334
+ }
335
+ return result
336
+ }
337
+
338
+ override func getDuration() -> TimeInterval {
339
+ var result: TimeInterval = 0
340
+ owner?.executeOnAudioQueue { [weak self] in
341
+ guard let self else { return }
342
+ guard !players.isEmpty && playIndex < players.count else { return }
343
+ let player = players[playIndex]
344
+ if player.currentItem?.duration == CMTime.indefinite {
345
+ result = 0
346
+ return
347
+ }
348
+ result = player.currentItem?.duration.seconds ?? 0
349
+ }
350
+ return result
351
+ }
352
+
353
+ override func playWithFade(time: TimeInterval, volume: Float?, fadeInDuration: TimeInterval) {
354
+ owner?.executeOnAudioQueue { [weak self] in
355
+ guard let self else { return }
356
+ guard !players.isEmpty && playIndex < players.count else { return }
357
+ let player = players[playIndex]
358
+ player.seek(to: CMTimeMakeWithSeconds(time, preferredTimescale: 1)) { [weak self] _ in
359
+ guard let self else { return }
360
+ DispatchQueue.main.async {
361
+ if player.timeControlStatus != .playing {
362
+ player.volume = 0
363
+ player.play()
364
+ self.fadeIn(player: player, fadeInDuration: fadeInDuration, targetVolume: volume ?? self.initialVolume)
365
+ self.playIndex = (self.playIndex + 1) % self.players.count
366
+ self.startCurrentTimeUpdates()
367
+ }
368
+ }
369
+ }
370
+ }
371
+ }
372
+
373
+ override func playWithFade(time: TimeInterval) {
374
+ playWithFade(time: time, volume: nil, fadeInDuration: TimeInterval(Constant.DefaultFadeDuration))
375
+ }
376
+
377
+ override func stopWithFade(fadeOutDuration: TimeInterval, toPause: Bool = false) {
378
+ owner?.executeOnAudioQueue { [weak self] in
379
+ guard let self else { return }
380
+ guard !players.isEmpty && playIndex < players.count else {
381
+ if !toPause { stop() }
382
+ return
383
+ }
384
+ let player = players[playIndex]
385
+ if player.timeControlStatus == .playing {
386
+ if toPause {
387
+ fadeOut(player: player, fadeOutDuration: fadeOutDuration, toPause: true) { [weak self] elapsed, duration in
388
+ guard let self, let owner = self.owner else { return }
389
+ owner.recordPausePositionAfterFade(assetId: self.assetId, elapsedTime: elapsed, duration: duration)
390
+ }
391
+ } else {
392
+ fadeOut(player: player, fadeOutDuration: fadeOutDuration, toPause: false)
393
+ }
394
+ } else if !toPause {
395
+ stop()
396
+ }
397
+ }
398
+ }
399
+
400
+ override func stopWithFade() {
401
+ stopWithFade(fadeOutDuration: TimeInterval(Constant.DefaultFadeDuration), toPause: false)
402
+ }
403
+
404
+ }
405
+ // swiftlint:enable file_length