@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
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import AVFoundation
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
* RemoteAudioAsset extends AudioAsset to handle remote (URL-based) audio files
|
|
5
|
-
* Provides network audio playback using AVPlayer instead of AVAudioPlayer
|
|
6
|
-
*/
|
|
3
|
+
// swiftlint:disable:next type_body_length
|
|
7
4
|
public class RemoteAudioAsset: AudioAsset {
|
|
8
5
|
var playerItems: [AVPlayerItem] = []
|
|
9
6
|
var players: [AVPlayer] = []
|
|
@@ -11,172 +8,147 @@ public class RemoteAudioAsset: AudioAsset {
|
|
|
11
8
|
var notificationObservers: [NSObjectProtocol] = []
|
|
12
9
|
var duration: TimeInterval = 0
|
|
13
10
|
var asset: AVURLAsset?
|
|
11
|
+
private var logger = Logger(logTag: "RemoteAudioAsset")
|
|
12
|
+
static let staticLogger = Logger(logTag: "RemoteAudioAsset")
|
|
14
13
|
|
|
15
|
-
init(owner: NativeAudio, withAssetId assetId: String, withPath path: String
|
|
16
|
-
super.init(owner: owner, withAssetId: assetId, withPath: path, withChannels: channels ?? 1, withVolume: volume ?? 1.0
|
|
14
|
+
init(owner: NativeAudio, withAssetId assetId: String, withPath path: String, withChannels channels: Int?, withVolume volume: Float?, withHeaders headers: [String: String]?) {
|
|
15
|
+
super.init(owner: owner, withAssetId: assetId, withPath: path, withChannels: channels ?? 1, withVolume: volume ?? 1.0)
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
guard let self
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
print("Invalid URL: \(String(describing: path))")
|
|
17
|
+
let setupBlock = { [weak self] in
|
|
18
|
+
guard let self else { return }
|
|
19
|
+
guard let url = URL(string: path) else {
|
|
20
|
+
self.logger.error("Invalid URL: %@", String(describing: path))
|
|
23
21
|
return
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
// Build AVURLAsset options with custom headers if provided
|
|
27
24
|
var options: [String: Any] = [AVURLAssetPreferPreciseDurationAndTimingKey: true]
|
|
28
|
-
if let headers
|
|
25
|
+
if let headers, !headers.isEmpty {
|
|
29
26
|
options["AVURLAssetHTTPHeaderFieldsKey"] = headers
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
let asset = AVURLAsset(url: url, options: options)
|
|
33
30
|
self.asset = asset
|
|
34
|
-
|
|
35
|
-
// Limit channels to a reasonable maximum to prevent resource issues
|
|
36
31
|
let channelCount = min(max(channels ?? Constant.DefaultChannels, 1), Constant.MaxChannels)
|
|
37
32
|
|
|
38
33
|
for _ in 0..<channelCount {
|
|
39
34
|
let playerItem = AVPlayerItem(asset: asset)
|
|
40
35
|
let player = AVPlayer(playerItem: playerItem)
|
|
41
|
-
// Apply volume constraints consistent with AudioAsset
|
|
42
36
|
player.volume = self.initialVolume
|
|
43
37
|
player.rate = 1.0
|
|
44
38
|
self.playerItems.append(playerItem)
|
|
45
39
|
self.players.append(player)
|
|
46
40
|
|
|
47
|
-
// Add observer for duration
|
|
48
41
|
let durationObserver = playerItem.observe(\.status) { [weak self] item, _ in
|
|
49
|
-
guard let
|
|
50
|
-
|
|
42
|
+
guard let self else { return }
|
|
43
|
+
self.owner?.executeOnAudioQueue {
|
|
51
44
|
if item.status == .readyToPlay {
|
|
52
|
-
|
|
45
|
+
self.duration = item.duration.seconds
|
|
53
46
|
}
|
|
54
47
|
}
|
|
55
48
|
}
|
|
56
49
|
self.playerObservers.append(durationObserver)
|
|
57
50
|
|
|
58
|
-
// Add observer for playback finished
|
|
59
51
|
let observer = player.observe(\.timeControlStatus) { [weak self, weak player] observedPlayer, _ in
|
|
60
|
-
guard let
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if strongPlayer.timeControlStatus == .paused &&
|
|
65
|
-
(strongPlayer.currentItem?.currentTime() == strongPlayer.currentItem?.duration ||
|
|
66
|
-
strongPlayer.currentItem?.duration == .zero) {
|
|
67
|
-
strongSelf.playerDidFinishPlaying(player: strongPlayer)
|
|
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)
|
|
68
56
|
}
|
|
69
57
|
}
|
|
70
58
|
self.playerObservers.append(observer)
|
|
71
59
|
}
|
|
72
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)
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
deinit {
|
|
76
|
-
// Clean up observers
|
|
77
75
|
for observer in playerObservers {
|
|
78
76
|
observer.invalidate()
|
|
79
77
|
}
|
|
80
|
-
|
|
81
|
-
for observer in notificationObservers {
|
|
82
|
-
NotificationCenter.default.removeObserver(observer)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Clean up players
|
|
78
|
+
cleanupNotificationObservers()
|
|
86
79
|
for player in players {
|
|
87
80
|
player.pause()
|
|
88
81
|
}
|
|
89
|
-
|
|
90
82
|
playerItems = []
|
|
91
83
|
players = []
|
|
92
84
|
playerObservers = []
|
|
93
|
-
|
|
85
|
+
cancelFade()
|
|
94
86
|
}
|
|
95
87
|
|
|
96
|
-
/// Notifies listeners that this asset finished playing and invokes the optional completion callback if set.
|
|
97
|
-
///
|
|
98
|
-
/// Handle a player's end-of-playback by notifying listeners and invoking the optional completion callback.
|
|
99
|
-
/// Dispatches the notification on the owner's audio queue and sends a `complete` event containing the assetId.
|
|
100
|
-
/// - Parameter player: The `AVPlayer` instance that finished playback.
|
|
101
88
|
func playerDidFinishPlaying(player: AVPlayer) {
|
|
102
89
|
owner?.executeOnAudioQueue { [weak self] in
|
|
103
|
-
guard let self
|
|
104
|
-
|
|
105
|
-
self.
|
|
106
|
-
"assetId": self.assetId
|
|
107
|
-
])
|
|
108
|
-
|
|
109
|
-
// Invoke completion callback if set
|
|
90
|
+
guard let self else { return }
|
|
91
|
+
self.owner?.notifyListeners("complete", data: ["assetId": self.assetId])
|
|
92
|
+
self.dispatchedCompleteMap[self.assetId] = true
|
|
110
93
|
self.onComplete?()
|
|
111
94
|
}
|
|
112
95
|
}
|
|
113
96
|
|
|
114
|
-
override func play(time: TimeInterval,
|
|
97
|
+
override func play(time: TimeInterval, volume: Float? = nil) {
|
|
115
98
|
owner?.executeOnAudioQueue { [weak self] in
|
|
116
|
-
guard let self
|
|
117
|
-
|
|
99
|
+
guard let self else { return }
|
|
118
100
|
guard !players.isEmpty else { return }
|
|
119
|
-
|
|
120
|
-
// Reset play index if it's out of bounds
|
|
121
101
|
if playIndex >= players.count {
|
|
122
102
|
playIndex = 0
|
|
123
103
|
}
|
|
124
|
-
|
|
104
|
+
cancelFade()
|
|
125
105
|
let player = players[playIndex]
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
let validTime = max(time, 0)
|
|
129
|
-
let validDelay = max(delay, 0)
|
|
130
|
-
|
|
131
|
-
if validDelay > 0 {
|
|
132
|
-
// Convert delay to CMTime and add to current time
|
|
133
|
-
let currentTime = player.currentTime()
|
|
134
|
-
let delayTime = CMTimeMakeWithSeconds(validDelay, preferredTimescale: currentTime.timescale)
|
|
135
|
-
let timeToPlay = CMTimeAdd(currentTime, delayTime)
|
|
136
|
-
player.seek(to: timeToPlay)
|
|
137
|
-
} else {
|
|
138
|
-
player.seek(to: CMTimeMakeWithSeconds(validTime, preferredTimescale: 1))
|
|
139
|
-
}
|
|
106
|
+
player.seek(to: CMTimeMakeWithSeconds(max(time, 0), preferredTimescale: 1))
|
|
107
|
+
player.volume = volume ?? self.initialVolume
|
|
140
108
|
player.play()
|
|
141
109
|
playIndex = (playIndex + 1) % players.count
|
|
142
110
|
startCurrentTimeUpdates()
|
|
143
111
|
}
|
|
144
112
|
}
|
|
145
113
|
|
|
114
|
+
// Backward-compatible signature
|
|
115
|
+
override func play(time: TimeInterval, delay: TimeInterval) {
|
|
116
|
+
let validDelay = max(delay, 0)
|
|
117
|
+
if validDelay > 0 {
|
|
118
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + validDelay) { [weak self] in
|
|
119
|
+
self?.play(time: time, volume: nil)
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
play(time: time, volume: nil)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
146
126
|
override func pause() {
|
|
147
127
|
owner?.executeOnAudioQueue { [weak self] in
|
|
148
|
-
guard let self
|
|
149
|
-
|
|
128
|
+
guard let self else { return }
|
|
150
129
|
guard !players.isEmpty && playIndex < players.count else { return }
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
player.pause()
|
|
130
|
+
cancelFade()
|
|
131
|
+
players[playIndex].pause()
|
|
154
132
|
stopCurrentTimeUpdates()
|
|
155
133
|
}
|
|
156
134
|
}
|
|
157
135
|
|
|
158
136
|
override func resume() {
|
|
159
137
|
owner?.executeOnAudioQueue { [weak self] in
|
|
160
|
-
guard let self
|
|
161
|
-
|
|
138
|
+
guard let self else { return }
|
|
162
139
|
guard !players.isEmpty && playIndex < players.count else { return }
|
|
163
140
|
|
|
164
141
|
let player = players[playIndex]
|
|
165
142
|
player.play()
|
|
166
|
-
|
|
167
|
-
// Add notification observer for when playback stops
|
|
168
143
|
cleanupNotificationObservers()
|
|
169
|
-
|
|
170
|
-
// Capture weak reference to self
|
|
171
144
|
let observer = NotificationCenter.default.addObserver(
|
|
172
145
|
forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
|
|
173
146
|
object: player.currentItem,
|
|
174
|
-
queue: OperationQueue.main
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if let currentItem = notification.object as? AVPlayerItem,
|
|
178
|
-
|
|
179
|
-
strongSelf.playerDidFinishPlaying(player: strongPlayer)
|
|
147
|
+
queue: OperationQueue.main
|
|
148
|
+
) { [weak self, weak player] notification in
|
|
149
|
+
guard let self, let player else { return }
|
|
150
|
+
if let currentItem = notification.object as? AVPlayerItem, player.currentItem === currentItem {
|
|
151
|
+
self.playerDidFinishPlaying(player: player)
|
|
180
152
|
}
|
|
181
153
|
}
|
|
182
154
|
notificationObservers.append(observer)
|
|
@@ -184,106 +156,65 @@ public class RemoteAudioAsset: AudioAsset {
|
|
|
184
156
|
}
|
|
185
157
|
}
|
|
186
158
|
|
|
187
|
-
/// Stops playback on all player channels and resets them to the beginning.
|
|
188
|
-
///
|
|
189
|
-
/// Stops periodic current-time updates, pauses each `AVPlayer`, seeks each player to time zero,
|
|
190
|
-
/// sets each player's `actionAtItemEnd` to `.pause`, and resets `playIndex` to `0`.
|
|
191
|
-
/// The operations are dispatched on the owner's audio queue.
|
|
192
159
|
override func stop() {
|
|
193
160
|
owner?.executeOnAudioQueue { [weak self] in
|
|
194
|
-
guard let self
|
|
195
|
-
|
|
161
|
+
guard let self else { return }
|
|
196
162
|
stopCurrentTimeUpdates()
|
|
197
|
-
|
|
163
|
+
cancelFade()
|
|
198
164
|
for player in players {
|
|
199
|
-
// First pause
|
|
200
165
|
player.pause()
|
|
201
|
-
// Then reset to beginning
|
|
202
166
|
player.seek(to: .zero, completionHandler: { _ in
|
|
203
|
-
// Reset any loop settings
|
|
204
167
|
player.actionAtItemEnd = .pause
|
|
205
168
|
})
|
|
206
169
|
}
|
|
207
|
-
// Reset playback state
|
|
208
170
|
playIndex = 0
|
|
171
|
+
self.owner?.notifyListeners("complete", data: ["assetId": self.assetId as Any])
|
|
172
|
+
self.dispatchedCompleteMap[self.assetId] = true
|
|
209
173
|
}
|
|
210
174
|
}
|
|
211
175
|
|
|
212
|
-
/// Configures all player channels to loop and starts playback for the current channel.
|
|
213
|
-
///
|
|
214
|
-
/// Configures all player channels to loop playback and starts playback on the current channel.
|
|
215
|
-
///
|
|
216
|
-
/// Cleans existing end-of-playback observers, sets each player's end action to `.none`, and registers observers that seek the finished item back to the start and resume playback when it reaches its end. Seeks and starts the player at `playIndex`, and starts periodic current-time updates. This work is performed on the owner's audio queue.
|
|
217
176
|
override func loop() {
|
|
218
177
|
owner?.executeOnAudioQueue { [weak self] in
|
|
219
|
-
guard let self
|
|
220
|
-
|
|
178
|
+
guard let self else { return }
|
|
221
179
|
cleanupNotificationObservers()
|
|
222
|
-
|
|
223
180
|
for (index, player) in players.enumerated() {
|
|
224
181
|
player.actionAtItemEnd = .none
|
|
225
|
-
|
|
226
182
|
guard let playerItem = player.currentItem else { continue }
|
|
227
|
-
|
|
228
183
|
let observer = NotificationCenter.default.addObserver(
|
|
229
184
|
forName: .AVPlayerItemDidPlayToEndTime,
|
|
230
185
|
object: playerItem,
|
|
231
|
-
queue: OperationQueue.main
|
|
232
|
-
|
|
233
|
-
|
|
186
|
+
queue: OperationQueue.main
|
|
187
|
+
) { [weak player] notification in
|
|
188
|
+
guard let player,
|
|
234
189
|
let item = notification.object as? AVPlayerItem,
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
strongPlayer.play()
|
|
190
|
+
player.currentItem === item else { return }
|
|
191
|
+
player.seek(to: .zero)
|
|
192
|
+
player.play()
|
|
239
193
|
}
|
|
240
|
-
|
|
241
194
|
notificationObservers.append(observer)
|
|
242
|
-
|
|
243
195
|
if index == playIndex {
|
|
244
196
|
player.seek(to: .zero)
|
|
245
197
|
player.play()
|
|
246
198
|
}
|
|
247
199
|
}
|
|
248
|
-
|
|
249
200
|
startCurrentTimeUpdates()
|
|
250
201
|
}
|
|
251
202
|
}
|
|
252
203
|
|
|
253
|
-
|
|
254
|
-
///
|
|
255
|
-
/// Removes all NotificationCenter observers tracked by this instance and clears the internal observer list.
|
|
256
|
-
///
|
|
257
|
-
/// This unregisters each observer previously added to NotificationCenter.default and resets `notificationObservers` to an empty array.
|
|
258
|
-
internal func cleanupNotificationObservers() {
|
|
204
|
+
public func cleanupNotificationObservers() {
|
|
259
205
|
for observer in notificationObservers {
|
|
260
206
|
NotificationCenter.default.removeObserver(observer)
|
|
261
207
|
}
|
|
262
208
|
notificationObservers = []
|
|
263
209
|
}
|
|
264
210
|
|
|
265
|
-
@objc func playerItemDidReachEnd(notification: Notification) {
|
|
266
|
-
owner?.executeOnAudioQueue { [weak self] in
|
|
267
|
-
guard let self = self else { return }
|
|
268
|
-
|
|
269
|
-
if let playerItem = notification.object as? AVPlayerItem,
|
|
270
|
-
let player = players.first(where: { $0.currentItem == playerItem }) {
|
|
271
|
-
player.seek(to: .zero)
|
|
272
|
-
player.play()
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
211
|
override func unload() {
|
|
278
212
|
owner?.executeOnAudioQueue { [weak self] in
|
|
279
|
-
guard let self
|
|
280
|
-
|
|
213
|
+
guard let self else { return }
|
|
214
|
+
cancelFade()
|
|
281
215
|
stopCurrentTimeUpdates()
|
|
282
216
|
stop()
|
|
283
|
-
|
|
284
217
|
cleanupNotificationObservers()
|
|
285
|
-
|
|
286
|
-
// Remove KVO observers
|
|
287
218
|
for observer in playerObservers {
|
|
288
219
|
observer.invalidate()
|
|
289
220
|
}
|
|
@@ -293,23 +224,28 @@ public class RemoteAudioAsset: AudioAsset {
|
|
|
293
224
|
}
|
|
294
225
|
}
|
|
295
226
|
|
|
296
|
-
override func setVolume(volume: NSNumber
|
|
227
|
+
override func setVolume(volume: NSNumber, fadeDuration: Double) {
|
|
297
228
|
owner?.executeOnAudioQueue { [weak self] in
|
|
298
|
-
guard let self
|
|
299
|
-
|
|
300
|
-
// Ensure volume is in valid range (0.0-1.0)
|
|
229
|
+
guard let self else { return }
|
|
230
|
+
cancelFade()
|
|
301
231
|
let validVolume = min(max(volume.floatValue, Constant.MinVolume), Constant.MaxVolume)
|
|
302
232
|
for player in players {
|
|
303
|
-
|
|
233
|
+
if isPlaying() && fadeDuration > 0 {
|
|
234
|
+
fadeTo(player: player, fadeOutDuration: fadeDuration, targetVolume: validVolume)
|
|
235
|
+
} else {
|
|
236
|
+
player.volume = validVolume
|
|
237
|
+
}
|
|
304
238
|
}
|
|
305
239
|
}
|
|
306
240
|
}
|
|
307
241
|
|
|
308
|
-
override func
|
|
309
|
-
|
|
310
|
-
|
|
242
|
+
override func setVolume(volume: NSNumber) {
|
|
243
|
+
setVolume(volume: volume, fadeDuration: 0)
|
|
244
|
+
}
|
|
311
245
|
|
|
312
|
-
|
|
246
|
+
override func setRate(rate: NSNumber) {
|
|
247
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
248
|
+
guard let self else { return }
|
|
313
249
|
let validRate = min(max(rate.floatValue, Constant.MinRate), Constant.MaxRate)
|
|
314
250
|
for player in players {
|
|
315
251
|
player.rate = validRate
|
|
@@ -320,14 +256,12 @@ public class RemoteAudioAsset: AudioAsset {
|
|
|
320
256
|
override func isPlaying() -> Bool {
|
|
321
257
|
var result = false
|
|
322
258
|
owner?.executeOnAudioQueue { [weak self] in
|
|
323
|
-
guard let self
|
|
324
|
-
|
|
259
|
+
guard let self else { return }
|
|
325
260
|
guard !players.isEmpty && playIndex < players.count else {
|
|
326
261
|
result = false
|
|
327
262
|
return
|
|
328
263
|
}
|
|
329
|
-
|
|
330
|
-
result = player.timeControlStatus == .playing
|
|
264
|
+
result = players[playIndex].timeControlStatus == .playing
|
|
331
265
|
}
|
|
332
266
|
return result
|
|
333
267
|
}
|
|
@@ -335,14 +269,9 @@ public class RemoteAudioAsset: AudioAsset {
|
|
|
335
269
|
override func getCurrentTime() -> TimeInterval {
|
|
336
270
|
var result: TimeInterval = 0
|
|
337
271
|
owner?.executeOnAudioQueue { [weak self] in
|
|
338
|
-
guard let self
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
result = 0
|
|
342
|
-
return
|
|
343
|
-
}
|
|
344
|
-
let player = players[playIndex]
|
|
345
|
-
result = player.currentTime().seconds
|
|
272
|
+
guard let self else { return }
|
|
273
|
+
guard !players.isEmpty && playIndex < players.count else { return }
|
|
274
|
+
result = players[playIndex].currentTime().seconds
|
|
346
275
|
}
|
|
347
276
|
return result
|
|
348
277
|
}
|
|
@@ -350,12 +279,8 @@ public class RemoteAudioAsset: AudioAsset {
|
|
|
350
279
|
override func getDuration() -> TimeInterval {
|
|
351
280
|
var result: TimeInterval = 0
|
|
352
281
|
owner?.executeOnAudioQueue { [weak self] in
|
|
353
|
-
guard let self
|
|
354
|
-
|
|
355
|
-
guard !players.isEmpty && playIndex < players.count else {
|
|
356
|
-
result = 0
|
|
357
|
-
return
|
|
358
|
-
}
|
|
282
|
+
guard let self else { return }
|
|
283
|
+
guard !players.isEmpty && playIndex < players.count else { return }
|
|
359
284
|
let player = players[playIndex]
|
|
360
285
|
if player.currentItem?.duration == CMTime.indefinite {
|
|
361
286
|
result = 0
|
|
@@ -366,129 +291,48 @@ public class RemoteAudioAsset: AudioAsset {
|
|
|
366
291
|
return result
|
|
367
292
|
}
|
|
368
293
|
|
|
369
|
-
override func playWithFade(time: TimeInterval) {
|
|
294
|
+
override func playWithFade(time: TimeInterval, volume: Float?, fadeInDuration: TimeInterval) {
|
|
370
295
|
owner?.executeOnAudioQueue { [weak self] in
|
|
371
|
-
guard let self
|
|
372
|
-
|
|
296
|
+
guard let self else { return }
|
|
373
297
|
guard !players.isEmpty && playIndex < players.count else { return }
|
|
374
|
-
|
|
375
298
|
let player = players[playIndex]
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
} else {
|
|
387
|
-
if player.volume < initialVolume {
|
|
388
|
-
// Continue fade-in if already in progress
|
|
389
|
-
startVolumeRamp(from: player.volume, to: initialVolume, player: player)
|
|
299
|
+
player.seek(to: CMTimeMakeWithSeconds(time, preferredTimescale: 1)) { [weak self] _ in
|
|
300
|
+
guard let self else { return }
|
|
301
|
+
DispatchQueue.main.async {
|
|
302
|
+
if player.timeControlStatus != .playing {
|
|
303
|
+
player.volume = 0
|
|
304
|
+
player.play()
|
|
305
|
+
self.fadeIn(player: player, fadeInDuration: fadeInDuration, targetVolume: volume ?? self.initialVolume)
|
|
306
|
+
self.playIndex = (self.playIndex + 1) % self.players.count
|
|
307
|
+
self.startCurrentTimeUpdates()
|
|
308
|
+
}
|
|
390
309
|
}
|
|
391
310
|
}
|
|
392
311
|
}
|
|
393
312
|
}
|
|
394
313
|
|
|
395
|
-
override func
|
|
396
|
-
|
|
397
|
-
|
|
314
|
+
override func playWithFade(time: TimeInterval) {
|
|
315
|
+
playWithFade(time: time, volume: nil, fadeInDuration: TimeInterval(Constant.DefaultFadeDuration))
|
|
316
|
+
}
|
|
398
317
|
|
|
318
|
+
override func stopWithFade(fadeOutDuration: TimeInterval, toPause: Bool = false) {
|
|
319
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
320
|
+
guard let self else { return }
|
|
399
321
|
guard !players.isEmpty && playIndex < players.count else {
|
|
400
|
-
stop()
|
|
322
|
+
if !toPause { stop() }
|
|
401
323
|
return
|
|
402
324
|
}
|
|
403
|
-
|
|
404
325
|
let player = players[playIndex]
|
|
405
|
-
|
|
406
326
|
if player.timeControlStatus == .playing {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
// Schedule the stop when fade is complete
|
|
411
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(self.FADEDELAY * 1000))) { [weak self, weak player] in
|
|
412
|
-
guard let strongSelf = self, let strongPlayer = player else { return }
|
|
413
|
-
|
|
414
|
-
if strongPlayer.volume < strongSelf.FADESTEP {
|
|
415
|
-
strongSelf.stop()
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
} else {
|
|
327
|
+
fadeOut(player: player, fadeOutDuration: fadeOutDuration, toPause: toPause)
|
|
328
|
+
} else if !toPause {
|
|
419
329
|
stop()
|
|
420
330
|
}
|
|
421
331
|
}
|
|
422
332
|
}
|
|
423
333
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
let urls = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
|
|
427
|
-
if let cachePath = urls.first {
|
|
428
|
-
do {
|
|
429
|
-
let fileURLs = try FileManager.default.contentsOfDirectory(at: cachePath, includingPropertiesForKeys: nil)
|
|
430
|
-
// Clear all audio file types
|
|
431
|
-
let audioExtensions = ["mp3", "wav", "aac", "m4a", "ogg", "mp4", "caf", "aiff"]
|
|
432
|
-
for fileURL in fileURLs where audioExtensions.contains(fileURL.pathExtension.lowercased()) {
|
|
433
|
-
try FileManager.default.removeItem(at: fileURL)
|
|
434
|
-
}
|
|
435
|
-
} catch {
|
|
436
|
-
print("Error clearing audio cache: \(error)")
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
334
|
+
override func stopWithFade() {
|
|
335
|
+
stopWithFade(fadeOutDuration: TimeInterval(Constant.DefaultFadeDuration), toPause: false)
|
|
440
336
|
}
|
|
441
337
|
|
|
442
|
-
/// Gradually adjusts the given player's volume from a start level to an end level over the configured fade duration.
|
|
443
|
-
///
|
|
444
|
-
/// The method stops any existing fade timer, sets the player's volume to `startVolume`, and schedules a `Timer` on the main run loop that increments the volume in steps determined by `FADESTEP` and `FADEDELAY`. When the ramp completes the player's volume is set to `endVolume` and the internal `fadeTimer` reference is cleared.
|
|
445
|
-
/// - Parameters:
|
|
446
|
-
/// - startVolume: The initial volume level to apply before beginning the ramp (typically 0.0–1.0).
|
|
447
|
-
/// - endVolume: The target volume level to reach at the end of the ramp (typically 0.0–1.0).
|
|
448
|
-
/// - player: The `AVPlayer` whose `volume` property will be adjusted.
|
|
449
|
-
private func startVolumeRamp(from startVolume: Float, to endVolume: Float, player: AVPlayer) {
|
|
450
|
-
player.volume = startVolume
|
|
451
|
-
|
|
452
|
-
// Calculate steps
|
|
453
|
-
let steps = abs(endVolume - startVolume) / FADESTEP
|
|
454
|
-
guard steps > 0 else { return }
|
|
455
|
-
|
|
456
|
-
let timeInterval = FADEDELAY / steps
|
|
457
|
-
var currentStep = 0
|
|
458
|
-
let totalSteps = Int(ceil(steps))
|
|
459
|
-
|
|
460
|
-
stopFadeTimer()
|
|
461
|
-
|
|
462
|
-
// Ensure timer creation happens on main thread
|
|
463
|
-
DispatchQueue.main.async { [weak self, weak player] in
|
|
464
|
-
guard let self = self else { return }
|
|
465
|
-
|
|
466
|
-
self.fadeTimer = Timer.scheduledTimer(withTimeInterval: TimeInterval(timeInterval), repeats: true) { [weak self, weak player] timer in
|
|
467
|
-
guard let strongPlayer = player, let strongSelf = self else {
|
|
468
|
-
timer.invalidate()
|
|
469
|
-
return
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
currentStep += 1
|
|
473
|
-
let progress = Float(currentStep) / Float(totalSteps)
|
|
474
|
-
let newVolume = startVolume + progress * (endVolume - startVolume)
|
|
475
|
-
|
|
476
|
-
strongPlayer.volume = newVolume
|
|
477
|
-
|
|
478
|
-
if currentStep >= totalSteps {
|
|
479
|
-
strongPlayer.volume = endVolume
|
|
480
|
-
timer.invalidate()
|
|
481
|
-
|
|
482
|
-
// Update timer reference on main thread
|
|
483
|
-
DispatchQueue.main.async {
|
|
484
|
-
strongSelf.fadeTimer = nil
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
if let timer = self.fadeTimer {
|
|
490
|
-
RunLoop.current.add(timer, forMode: .common)
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
338
|
}
|