@capgo/native-audio 7.3.0 → 7.3.9

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.
@@ -1,9 +1,14 @@
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
7
  public class RemoteAudioAsset: AudioAsset {
4
8
  var playerItems: [AVPlayerItem] = []
5
9
  var players: [AVPlayer] = []
6
10
  var playerObservers: [NSKeyValueObservation] = []
11
+ var notificationObservers: [NSObjectProtocol] = []
7
12
  var duration: TimeInterval = 0
8
13
  var asset: AVURLAsset?
9
14
 
@@ -11,73 +16,117 @@ public class RemoteAudioAsset: AudioAsset {
11
16
  super.init(owner: owner, withAssetId: assetId, withPath: path, withChannels: channels ?? 1, withVolume: volume ?? 1.0, withFadeDelay: delay ?? 0.0)
12
17
 
13
18
  owner.executeOnAudioQueue { [self] in
14
- if let url = URL(string: path) {
15
- let asset = AVURLAsset(url: url, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true])
16
- self.asset = asset
17
-
18
- for _ in 0..<(channels ?? 1) {
19
- let playerItem = AVPlayerItem(asset: asset)
20
- let player = AVPlayer(playerItem: playerItem)
21
- player.volume = volume ?? 1.0
22
- player.rate = 1.0
23
- self.playerItems.append(playerItem)
24
- self.players.append(player)
25
-
26
- // Add observer for duration
27
- let durationObserver = playerItem.observe(\.status) { [weak self] item, _ in
28
- self?.owner.executeOnAudioQueue { [self] in
29
- if item.status == .readyToPlay {
30
- self?.duration = item.duration.seconds
31
- }
19
+ guard let url = URL(string: path ?? "") else {
20
+ print("Invalid URL: \(String(describing: path))")
21
+ return
22
+ }
23
+
24
+ let asset = AVURLAsset(url: url, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true])
25
+ self.asset = asset
26
+
27
+ // Limit channels to a reasonable maximum to prevent resource issues
28
+ let channelCount = min(max(channels ?? Constant.DefaultChannels, 1), Constant.MaxChannels)
29
+
30
+ for _ in 0..<channelCount {
31
+ let playerItem = AVPlayerItem(asset: asset)
32
+ let player = AVPlayer(playerItem: playerItem)
33
+ // Apply volume constraints consistent with AudioAsset
34
+ player.volume = self.initialVolume
35
+ player.rate = 1.0
36
+ self.playerItems.append(playerItem)
37
+ self.players.append(player)
38
+
39
+ // Add observer for duration
40
+ let durationObserver = playerItem.observe(\.status) { [weak self] item, _ in
41
+ guard let strongSelf = self else { return }
42
+ strongSelf.owner?.executeOnAudioQueue {
43
+ if item.status == .readyToPlay {
44
+ strongSelf.duration = item.duration.seconds
32
45
  }
33
46
  }
34
- self.playerObservers.append(durationObserver)
35
-
36
- // Add observer for playback finished
37
- let observer = player.observe(\.timeControlStatus) { [weak self] player, _ in
38
- self?.owner.executeOnAudioQueue { [self] in
39
- if player.timeControlStatus == .paused && player.currentItem?.currentTime() == player.currentItem?.duration {
40
- self?.playerDidFinishPlaying(player: player)
41
- }
42
- }
47
+ }
48
+ self.playerObservers.append(durationObserver)
49
+
50
+ // Add observer for playback finished
51
+ let observer = player.observe(\.timeControlStatus) { [weak self, weak player] observedPlayer, _ in
52
+ guard let strongSelf = self,
53
+ let strongPlayer = player,
54
+ strongPlayer === observedPlayer else { return }
55
+
56
+ if strongPlayer.timeControlStatus == .paused &&
57
+ (strongPlayer.currentItem?.currentTime() == strongPlayer.currentItem?.duration ||
58
+ strongPlayer.currentItem?.duration == .zero) {
59
+ strongSelf.playerDidFinishPlaying(player: strongPlayer)
43
60
  }
44
- self.playerObservers.append(observer)
45
61
  }
62
+ self.playerObservers.append(observer)
46
63
  }
47
64
  }
48
65
  }
49
66
 
67
+ deinit {
68
+ // Clean up observers
69
+ for observer in playerObservers {
70
+ observer.invalidate()
71
+ }
72
+
73
+ for observer in notificationObservers {
74
+ NotificationCenter.default.removeObserver(observer)
75
+ }
76
+
77
+ // Clean up players
78
+ for player in players {
79
+ player.pause()
80
+ }
81
+
82
+ playerItems = []
83
+ players = []
84
+ playerObservers = []
85
+ notificationObservers = []
86
+ }
87
+
50
88
  func playerDidFinishPlaying(player: AVPlayer) {
51
- owner.executeOnAudioQueue { [self] in
52
- self.owner.notifyListeners("complete", data: [
89
+ owner?.executeOnAudioQueue { [self] in
90
+ self.owner?.notifyListeners("complete", data: [
53
91
  "assetId": self.assetId
54
92
  ])
55
93
  }
56
94
  }
57
95
 
58
96
  override func play(time: TimeInterval, delay: TimeInterval) {
59
- owner.executeOnAudioQueue { [self] in
97
+ owner?.executeOnAudioQueue { [self] in
60
98
  guard !players.isEmpty else { return }
99
+
100
+ // Reset play index if it's out of bounds
101
+ if playIndex >= players.count {
102
+ playIndex = 0
103
+ }
104
+
61
105
  let player = players[playIndex]
62
- if delay > 0 {
106
+
107
+ // Ensure non-negative values for time and delay
108
+ let validTime = max(time, 0)
109
+ let validDelay = max(delay, 0)
110
+
111
+ if validDelay > 0 {
63
112
  // Convert delay to CMTime and add to current time
64
113
  let currentTime = player.currentTime()
65
- let delayTime = CMTimeMakeWithSeconds(delay, preferredTimescale: currentTime.timescale)
114
+ let delayTime = CMTimeMakeWithSeconds(validDelay, preferredTimescale: currentTime.timescale)
66
115
  let timeToPlay = CMTimeAdd(currentTime, delayTime)
67
116
  player.seek(to: timeToPlay)
68
117
  } else {
69
- player.seek(to: CMTimeMakeWithSeconds(time, preferredTimescale: 1))
118
+ player.seek(to: CMTimeMakeWithSeconds(validTime, preferredTimescale: 1))
70
119
  }
71
120
  player.play()
72
121
  playIndex = (playIndex + 1) % players.count
73
- NSLog("RemoteAudioAsset: About to start timer updates")
74
122
  startCurrentTimeUpdates()
75
123
  }
76
124
  }
77
125
 
78
126
  override func pause() {
79
- owner.executeOnAudioQueue { [self] in
80
- guard !players.isEmpty else { return }
127
+ owner?.executeOnAudioQueue { [self] in
128
+ guard !players.isEmpty && playIndex < players.count else { return }
129
+
81
130
  let player = players[playIndex]
82
131
  player.pause()
83
132
  stopCurrentTimeUpdates()
@@ -85,18 +134,35 @@ public class RemoteAudioAsset: AudioAsset {
85
134
  }
86
135
 
87
136
  override func resume() {
88
- owner.executeOnAudioQueue { [self] in
89
- guard !players.isEmpty else { return }
137
+ owner?.executeOnAudioQueue { [self] in
138
+ guard !players.isEmpty && playIndex < players.count else { return }
139
+
90
140
  let player = players[playIndex]
91
141
  player.play()
92
- NSLog("RemoteAudioAsset Resume: About to start timer updates")
93
- startCurrentTimeUpdates() // Add timer start
142
+
143
+ // Add notification observer for when playback stops
144
+ cleanupNotificationObservers()
145
+
146
+ let observer = NotificationCenter.default.addObserver(
147
+ forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
148
+ object: player.currentItem,
149
+ queue: nil) { [weak self] notification in
150
+ guard let strongSelf = self else { return }
151
+
152
+ if let currentItem = notification.object as? AVPlayerItem,
153
+ currentItem == strongSelf.playerItems[strongSelf.playIndex] {
154
+ strongSelf.playerDidFinishPlaying(player: strongSelf.players[strongSelf.playIndex])
155
+ }
156
+ }
157
+ notificationObservers.append(observer)
158
+ startCurrentTimeUpdates()
94
159
  }
95
160
  }
96
161
 
97
162
  override func stop() {
98
- owner.executeOnAudioQueue { [self] in
99
- stopCurrentTimeUpdates() // Stop timer first
163
+ owner?.executeOnAudioQueue { [self] in
164
+ stopCurrentTimeUpdates()
165
+
100
166
  for player in players {
101
167
  // First pause
102
168
  player.pause()
@@ -112,26 +178,47 @@ public class RemoteAudioAsset: AudioAsset {
112
178
  }
113
179
 
114
180
  override func loop() {
115
- owner.executeOnAudioQueue { [self] in
116
- for player in players {
181
+ owner?.executeOnAudioQueue { [self] in
182
+ cleanupNotificationObservers()
183
+
184
+ for (index, player) in players.enumerated() {
117
185
  player.actionAtItemEnd = .none
118
- NotificationCenter.default.removeObserver(self,
119
- name: .AVPlayerItemDidPlayToEndTime,
120
- object: player.currentItem)
121
- NotificationCenter.default.addObserver(self,
122
- selector: #selector(playerItemDidReachEnd(notification:)),
123
- name: .AVPlayerItemDidPlayToEndTime,
124
- object: player.currentItem)
125
- player.seek(to: .zero)
126
- player.play()
186
+
187
+ guard let playerItem = player.currentItem else { continue }
188
+
189
+ let observer = NotificationCenter.default.addObserver(
190
+ forName: .AVPlayerItemDidPlayToEndTime,
191
+ object: playerItem,
192
+ queue: nil) { [weak player] notification in
193
+ guard let strongPlayer = player,
194
+ let item = notification.object as? AVPlayerItem,
195
+ strongPlayer.currentItem === item else { return }
196
+
197
+ strongPlayer.seek(to: .zero)
198
+ strongPlayer.play()
199
+ }
200
+
201
+ notificationObservers.append(observer)
202
+
203
+ if index == playIndex {
204
+ player.seek(to: .zero)
205
+ player.play()
206
+ }
127
207
  }
128
- NSLog("RemoteAudioAsset Loop: About to start timer updates")
129
- startCurrentTimeUpdates() // Add timer start
208
+
209
+ startCurrentTimeUpdates()
210
+ }
211
+ }
212
+
213
+ private func cleanupNotificationObservers() {
214
+ for observer in notificationObservers {
215
+ NotificationCenter.default.removeObserver(observer)
130
216
  }
217
+ notificationObservers = []
131
218
  }
132
219
 
133
220
  @objc func playerItemDidReachEnd(notification: Notification) {
134
- owner.executeOnAudioQueue { [self] in
221
+ owner?.executeOnAudioQueue { [self] in
135
222
  if let playerItem = notification.object as? AVPlayerItem,
136
223
  let player = players.first(where: { $0.currentItem == playerItem }) {
137
224
  player.seek(to: .zero)
@@ -141,10 +228,12 @@ public class RemoteAudioAsset: AudioAsset {
141
228
  }
142
229
 
143
230
  override func unload() {
144
- owner.executeOnAudioQueue { [self] in
231
+ owner?.executeOnAudioQueue { [self] in
145
232
  stopCurrentTimeUpdates()
146
233
  stop()
147
- NotificationCenter.default.removeObserver(self)
234
+
235
+ cleanupNotificationObservers()
236
+
148
237
  // Remove KVO observers
149
238
  for observer in playerObservers {
150
239
  observer.invalidate()
@@ -156,25 +245,29 @@ public class RemoteAudioAsset: AudioAsset {
156
245
  }
157
246
 
158
247
  override func setVolume(volume: NSNumber!) {
159
- owner.executeOnAudioQueue { [self] in
248
+ owner?.executeOnAudioQueue { [self] in
249
+ // Ensure volume is in valid range (0.0-1.0)
250
+ let validVolume = min(max(volume.floatValue, Constant.MinVolume), Constant.MaxVolume)
160
251
  for player in players {
161
- player.volume = volume.floatValue
252
+ player.volume = validVolume
162
253
  }
163
254
  }
164
255
  }
165
256
 
166
257
  override func setRate(rate: NSNumber!) {
167
- owner.executeOnAudioQueue { [self] in
258
+ owner?.executeOnAudioQueue { [self] in
259
+ // Ensure rate is in valid range
260
+ let validRate = min(max(rate.floatValue, Constant.MinRate), Constant.MaxRate)
168
261
  for player in players {
169
- player.rate = rate.floatValue
262
+ player.rate = validRate
170
263
  }
171
264
  }
172
265
  }
173
266
 
174
267
  override func isPlaying() -> Bool {
175
268
  var result = false
176
- owner.executeOnAudioQueue { [self] in
177
- guard !players.isEmpty else {
269
+ owner?.executeOnAudioQueue { [self] in
270
+ guard !players.isEmpty && playIndex < players.count else {
178
271
  result = false
179
272
  return
180
273
  }
@@ -186,8 +279,8 @@ public class RemoteAudioAsset: AudioAsset {
186
279
 
187
280
  override func getCurrentTime() -> TimeInterval {
188
281
  var result: TimeInterval = 0
189
- owner.executeOnAudioQueue { [self] in
190
- guard !players.isEmpty else {
282
+ owner?.executeOnAudioQueue { [self] in
283
+ guard !players.isEmpty && playIndex < players.count else {
191
284
  result = 0
192
285
  return
193
286
  }
@@ -199,8 +292,8 @@ public class RemoteAudioAsset: AudioAsset {
199
292
 
200
293
  override func getDuration() -> TimeInterval {
201
294
  var result: TimeInterval = 0
202
- owner.executeOnAudioQueue { [self] in
203
- guard !players.isEmpty else {
295
+ owner?.executeOnAudioQueue { [self] in
296
+ guard !players.isEmpty && playIndex < players.count else {
204
297
  result = 0
205
298
  return
206
299
  }
@@ -215,42 +308,52 @@ public class RemoteAudioAsset: AudioAsset {
215
308
  }
216
309
 
217
310
  override func playWithFade(time: TimeInterval) {
218
- owner.executeOnAudioQueue { [self] in
219
- guard !players.isEmpty else { return }
311
+ owner?.executeOnAudioQueue { [self] in
312
+ guard !players.isEmpty && playIndex < players.count else { return }
313
+
220
314
  let player = players[playIndex]
221
315
 
222
316
  if player.timeControlStatus != .playing {
223
317
  player.seek(to: CMTimeMakeWithSeconds(time, preferredTimescale: 1))
224
- player.volume = initialVolume
318
+ player.volume = 0 // Start with volume at 0
225
319
  player.play()
226
320
  playIndex = (playIndex + 1) % players.count
227
- NSLog("RemoteAudioAsset PlayWithFade: About to start timer updates")
228
321
  startCurrentTimeUpdates()
322
+
323
+ // Start fade-in using the parent class method
324
+ startVolumeRamp(from: 0, to: initialVolume, player: player)
229
325
  } else {
230
326
  if player.volume < initialVolume {
231
- player.volume += self.FADESTEP
327
+ // Continue fade-in if already in progress
328
+ startVolumeRamp(from: player.volume, to: initialVolume, player: player)
232
329
  }
233
330
  }
234
331
  }
235
332
  }
236
333
 
237
334
  override func stopWithFade() {
238
- owner.executeOnAudioQueue { [self] in
239
- guard !players.isEmpty else { return }
335
+ owner?.executeOnAudioQueue { [self] in
336
+ guard !players.isEmpty && playIndex < players.count else {
337
+ stop()
338
+ return
339
+ }
340
+
240
341
  let player = players[playIndex]
241
342
 
242
343
  if player.timeControlStatus == .playing {
243
- if player.volume > self.FADESTEP {
244
- player.volume -= self.FADESTEP
245
- // Schedule next fade step
246
- DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(self.FADEDELAY * 1000))) { [weak self] in
247
- self?.stopWithFade()
344
+ // Use parent class fade method
345
+ startVolumeRamp(from: player.volume, to: 0, player: player)
346
+
347
+ // Schedule the stop when fade is complete
348
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(self.FADEDELAY * 1000))) { [weak self, weak player] in
349
+ guard let strongSelf = self, let strongPlayer = player else { return }
350
+
351
+ if strongPlayer.volume < strongSelf.FADESTEP {
352
+ strongSelf.stop()
248
353
  }
249
- } else {
250
- // Volume is near 0, actually stop
251
- player.volume = 0
252
- self.stop()
253
354
  }
355
+ } else {
356
+ stop()
254
357
  }
255
358
  }
256
359
  }
@@ -261,7 +364,9 @@ public class RemoteAudioAsset: AudioAsset {
261
364
  if let cachePath = urls.first {
262
365
  do {
263
366
  let fileURLs = try FileManager.default.contentsOfDirectory(at: cachePath, includingPropertiesForKeys: nil)
264
- for fileURL in fileURLs where fileURL.pathExtension == "mp3" || fileURL.pathExtension == "wav" {
367
+ // Clear all audio file types
368
+ let audioExtensions = ["mp3", "wav", "aac", "m4a", "ogg", "mp4", "caf", "aiff"]
369
+ for fileURL in fileURLs where audioExtensions.contains(fileURL.pathExtension.lowercased()) {
265
370
  try FileManager.default.removeItem(at: fileURL)
266
371
  }
267
372
  } catch {
@@ -270,4 +375,39 @@ public class RemoteAudioAsset: AudioAsset {
270
375
  }
271
376
  }
272
377
  }
378
+
379
+ // Add helper method for parent class
380
+ private func startVolumeRamp(from startVolume: Float, to endVolume: Float, player: AVPlayer) {
381
+ player.volume = startVolume
382
+
383
+ // Calculate steps
384
+ let steps = abs(endVolume - startVolume) / FADESTEP
385
+ guard steps > 0 else { return }
386
+
387
+ let timeInterval = FADEDELAY / steps
388
+ var currentStep = 0
389
+ let totalSteps = Int(ceil(steps))
390
+
391
+ stopFadeTimer()
392
+
393
+ fadeTimer = Timer.scheduledTimer(withTimeInterval: TimeInterval(timeInterval), repeats: true) { [weak player] timer in
394
+ guard let strongPlayer = player else {
395
+ timer.invalidate()
396
+ return
397
+ }
398
+
399
+ currentStep += 1
400
+ let progress = Float(currentStep) / Float(totalSteps)
401
+ let newVolume = startVolume + progress * (endVolume - startVolume)
402
+
403
+ strongPlayer.volume = newVolume
404
+
405
+ if currentStep >= totalSteps {
406
+ strongPlayer.volume = endVolume
407
+ timer.invalidate()
408
+ self.fadeTimer = nil
409
+ }
410
+ }
411
+ RunLoop.current.add(fadeTimer!, forMode: .common)
412
+ }
273
413
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/native-audio",
3
- "version": "7.3.0",
3
+ "version": "7.3.9",
4
4
  "description": "A native plugin for native audio engine",
5
5
  "license": "MIT",
6
6
  "main": "dist/plugin.cjs.js",
@@ -17,10 +17,10 @@
17
17
  "author": "Martin Donadieu",
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": "git+https://github.com/Cap-go/native-audio.git"
20
+ "url": "git+https://github.com/Cap-go/capacitor-native-audio.git"
21
21
  },
22
22
  "bugs": {
23
- "url": "https://github.com/Cap-go/native-audio/issues"
23
+ "url": "https://github.com/Cap-go/capacitor-native-audio/native-audio/issues"
24
24
  },
25
25
  "keywords": [
26
26
  "capacitor",
@@ -35,6 +35,8 @@
35
35
  "verify:ios": "cd ios && pod install && xcodebuild -workspace Plugin.xcworkspace -scheme Plugin && cd ..",
36
36
  "verify:android": "cd android && ./gradlew clean build test && cd ..",
37
37
  "verify:web": "npm run build",
38
+ "test": "npm run test:ios",
39
+ "test:ios": "cd ios && xcodebuild test -workspace Plugin.xcworkspace -scheme Plugin -destination 'platform=iOS Simulator,name=iPhone 16' && cd ..",
38
40
  "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
39
41
  "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --autocorrect --format",
40
42
  "eslint": "eslint . --ext .ts",