@capgo/native-audio 7.1.8 → 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,25 +1,62 @@
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] = []
12
+ var duration: TimeInterval = 0
13
+ var asset: AVURLAsset?
7
14
 
8
15
  override init(owner: NativeAudio, withAssetId assetId: String, withPath path: String!, withChannels channels: Int!, withVolume volume: Float!, withFadeDelay delay: Float!) {
9
- super.init(owner: owner, withAssetId: assetId, withPath: path, withChannels: channels, withVolume: volume, withFadeDelay: delay)
16
+ super.init(owner: owner, withAssetId: assetId, withPath: path, withChannels: channels ?? 1, withVolume: volume ?? 1.0, withFadeDelay: delay ?? 0.0)
10
17
 
11
- if let url = URL(string: path) {
12
- for _ in 0..<channels {
13
- let playerItem = AVPlayerItem(url: url)
18
+ owner.executeOnAudioQueue { [self] in
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)
14
32
  let player = AVPlayer(playerItem: playerItem)
15
- player.volume = volume
33
+ // Apply volume constraints consistent with AudioAsset
34
+ player.volume = self.initialVolume
35
+ player.rate = 1.0
16
36
  self.playerItems.append(playerItem)
17
37
  self.players.append(player)
18
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
45
+ }
46
+ }
47
+ }
48
+ self.playerObservers.append(durationObserver)
49
+
19
50
  // Add observer for playback finished
20
- let observer = player.observe(\.timeControlStatus) { [weak self] player, _ in
21
- if player.timeControlStatus == .paused && player.currentItem?.currentTime() == player.currentItem?.duration {
22
- self?.playerDidFinishPlaying(player: player)
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)
23
60
  }
24
61
  }
25
62
  self.playerObservers.append(observer)
@@ -27,92 +64,350 @@ public class RemoteAudioAsset: AudioAsset {
27
64
  }
28
65
  }
29
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
+
30
88
  func playerDidFinishPlaying(player: AVPlayer) {
31
- self.owner.notifyListeners("complete", data: [
32
- "assetId": self.assetId
33
- ])
89
+ owner?.executeOnAudioQueue { [self] in
90
+ self.owner?.notifyListeners("complete", data: [
91
+ "assetId": self.assetId
92
+ ])
93
+ }
34
94
  }
35
95
 
36
96
  override func play(time: TimeInterval, delay: TimeInterval) {
37
- guard !players.isEmpty else { return }
38
- let player = players[playIndex]
39
- if delay > 0 {
40
- let timeToPlay = CMTimeAdd(CMTimeMakeWithSeconds(player.currentTime().seconds, preferredTimescale: 1), CMTimeMakeWithSeconds(delay, preferredTimescale: 1))
41
- player.seek(to: timeToPlay)
42
- } else {
43
- player.seek(to: CMTimeMakeWithSeconds(time, preferredTimescale: 1))
97
+ owner?.executeOnAudioQueue { [self] in
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
+
105
+ let player = players[playIndex]
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 {
112
+ // Convert delay to CMTime and add to current time
113
+ let currentTime = player.currentTime()
114
+ let delayTime = CMTimeMakeWithSeconds(validDelay, preferredTimescale: currentTime.timescale)
115
+ let timeToPlay = CMTimeAdd(currentTime, delayTime)
116
+ player.seek(to: timeToPlay)
117
+ } else {
118
+ player.seek(to: CMTimeMakeWithSeconds(validTime, preferredTimescale: 1))
119
+ }
120
+ player.play()
121
+ playIndex = (playIndex + 1) % players.count
122
+ startCurrentTimeUpdates()
44
123
  }
45
- player.play()
46
- playIndex = (playIndex + 1) % players.count
47
124
  }
48
125
 
49
126
  override func pause() {
50
- guard !players.isEmpty else { return }
51
- let player = players[playIndex]
52
- player.pause()
127
+ owner?.executeOnAudioQueue { [self] in
128
+ guard !players.isEmpty && playIndex < players.count else { return }
129
+
130
+ let player = players[playIndex]
131
+ player.pause()
132
+ stopCurrentTimeUpdates()
133
+ }
53
134
  }
54
135
 
55
136
  override func resume() {
56
- guard !players.isEmpty else { return }
57
- let player = players[playIndex]
58
- player.play()
137
+ owner?.executeOnAudioQueue { [self] in
138
+ guard !players.isEmpty && playIndex < players.count else { return }
139
+
140
+ let player = players[playIndex]
141
+ player.play()
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()
159
+ }
59
160
  }
60
161
 
61
162
  override func stop() {
62
- for player in players {
63
- player.pause()
64
- player.seek(to: CMTime.zero)
163
+ owner?.executeOnAudioQueue { [self] in
164
+ stopCurrentTimeUpdates()
165
+
166
+ for player in players {
167
+ // First pause
168
+ player.pause()
169
+ // Then reset to beginning
170
+ player.seek(to: .zero, completionHandler: { _ in
171
+ // Reset any loop settings
172
+ player.actionAtItemEnd = .pause
173
+ })
174
+ }
175
+ // Reset playback state
176
+ playIndex = 0
65
177
  }
66
178
  }
67
179
 
68
180
  override func loop() {
69
- for player in players {
70
- player.actionAtItemEnd = .none
71
- NotificationCenter.default.addObserver(self,
72
- selector: #selector(playerItemDidReachEnd(notification:)),
73
- name: .AVPlayerItemDidPlayToEndTime,
74
- object: player.currentItem)
75
- player.play()
181
+ owner?.executeOnAudioQueue { [self] in
182
+ cleanupNotificationObservers()
183
+
184
+ for (index, player) in players.enumerated() {
185
+ player.actionAtItemEnd = .none
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
+ }
207
+ }
208
+
209
+ startCurrentTimeUpdates()
210
+ }
211
+ }
212
+
213
+ private func cleanupNotificationObservers() {
214
+ for observer in notificationObservers {
215
+ NotificationCenter.default.removeObserver(observer)
76
216
  }
217
+ notificationObservers = []
77
218
  }
78
219
 
79
220
  @objc func playerItemDidReachEnd(notification: Notification) {
80
- guard let player = notification.object as? AVPlayer else { return }
81
- player.seek(to: CMTime.zero)
82
- player.play()
221
+ owner?.executeOnAudioQueue { [self] in
222
+ if let playerItem = notification.object as? AVPlayerItem,
223
+ let player = players.first(where: { $0.currentItem == playerItem }) {
224
+ player.seek(to: .zero)
225
+ player.play()
226
+ }
227
+ }
83
228
  }
84
229
 
85
230
  override func unload() {
86
- stop()
87
- NotificationCenter.default.removeObserver(self)
88
- // Remove KVO observers
89
- for observer in playerObservers {
90
- observer.invalidate()
231
+ owner?.executeOnAudioQueue { [self] in
232
+ stopCurrentTimeUpdates()
233
+ stop()
234
+
235
+ cleanupNotificationObservers()
236
+
237
+ // Remove KVO observers
238
+ for observer in playerObservers {
239
+ observer.invalidate()
240
+ }
241
+ playerObservers = []
242
+ players = []
243
+ playerItems = []
91
244
  }
92
- playerObservers = []
93
- players = []
94
- playerItems = []
95
245
  }
96
246
 
97
247
  override func setVolume(volume: NSNumber!) {
98
- for player in players {
99
- player.volume = volume.floatValue
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)
251
+ for player in players {
252
+ player.volume = validVolume
253
+ }
100
254
  }
101
255
  }
102
256
 
103
257
  override func setRate(rate: NSNumber!) {
104
- for player in players {
105
- player.rate = rate.floatValue
258
+ owner?.executeOnAudioQueue { [self] in
259
+ // Ensure rate is in valid range
260
+ let validRate = min(max(rate.floatValue, Constant.MinRate), Constant.MaxRate)
261
+ for player in players {
262
+ player.rate = validRate
263
+ }
106
264
  }
107
265
  }
108
266
 
109
267
  override func isPlaying() -> Bool {
110
- return players.contains { $0.timeControlStatus == .playing }
268
+ var result = false
269
+ owner?.executeOnAudioQueue { [self] in
270
+ guard !players.isEmpty && playIndex < players.count else {
271
+ result = false
272
+ return
273
+ }
274
+ let player = players[playIndex]
275
+ result = player.timeControlStatus == .playing
276
+ }
277
+ return result
111
278
  }
112
279
 
113
280
  override func getCurrentTime() -> TimeInterval {
114
- guard !players.isEmpty else { return 0 }
115
- let player = players[playIndex]
116
- return player.currentTime().seconds
281
+ var result: TimeInterval = 0
282
+ owner?.executeOnAudioQueue { [self] in
283
+ guard !players.isEmpty && playIndex < players.count else {
284
+ result = 0
285
+ return
286
+ }
287
+ let player = players[playIndex]
288
+ result = player.currentTime().seconds
289
+ }
290
+ return result
291
+ }
292
+
293
+ override func getDuration() -> TimeInterval {
294
+ var result: TimeInterval = 0
295
+ owner?.executeOnAudioQueue { [self] in
296
+ guard !players.isEmpty && playIndex < players.count else {
297
+ result = 0
298
+ return
299
+ }
300
+ let player = players[playIndex]
301
+ if player.currentItem?.duration == CMTime.indefinite {
302
+ result = 0
303
+ return
304
+ }
305
+ result = player.currentItem?.duration.seconds ?? 0
306
+ }
307
+ return result
308
+ }
309
+
310
+ override func playWithFade(time: TimeInterval) {
311
+ owner?.executeOnAudioQueue { [self] in
312
+ guard !players.isEmpty && playIndex < players.count else { return }
313
+
314
+ let player = players[playIndex]
315
+
316
+ if player.timeControlStatus != .playing {
317
+ player.seek(to: CMTimeMakeWithSeconds(time, preferredTimescale: 1))
318
+ player.volume = 0 // Start with volume at 0
319
+ player.play()
320
+ playIndex = (playIndex + 1) % players.count
321
+ startCurrentTimeUpdates()
322
+
323
+ // Start fade-in using the parent class method
324
+ startVolumeRamp(from: 0, to: initialVolume, player: player)
325
+ } else {
326
+ if player.volume < initialVolume {
327
+ // Continue fade-in if already in progress
328
+ startVolumeRamp(from: player.volume, to: initialVolume, player: player)
329
+ }
330
+ }
331
+ }
332
+ }
333
+
334
+ override func stopWithFade() {
335
+ owner?.executeOnAudioQueue { [self] in
336
+ guard !players.isEmpty && playIndex < players.count else {
337
+ stop()
338
+ return
339
+ }
340
+
341
+ let player = players[playIndex]
342
+
343
+ if player.timeControlStatus == .playing {
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()
353
+ }
354
+ }
355
+ } else {
356
+ stop()
357
+ }
358
+ }
359
+ }
360
+
361
+ static func clearCache() {
362
+ DispatchQueue.global(qos: .background).sync {
363
+ let urls = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
364
+ if let cachePath = urls.first {
365
+ do {
366
+ let fileURLs = try FileManager.default.contentsOfDirectory(at: cachePath, includingPropertiesForKeys: nil)
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()) {
370
+ try FileManager.default.removeItem(at: fileURL)
371
+ }
372
+ } catch {
373
+ print("Error clearing audio cache: \(error)")
374
+ }
375
+ }
376
+ }
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)
117
412
  }
118
413
  }
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@capgo/native-audio",
3
- "version": "7.1.8",
3
+ "version": "7.3.9",
4
4
  "description": "A native plugin for native audio engine",
5
- "main": "dist/plugin.js",
5
+ "license": "MIT",
6
+ "main": "dist/plugin.cjs.js",
6
7
  "module": "dist/esm/index.js",
7
8
  "types": "dist/esm/index.d.ts",
8
9
  "unpkg": "dist/plugin.js",
@@ -13,11 +14,20 @@
13
14
  "ios/Plugin/",
14
15
  "CapgoNativeAudio.podspec"
15
16
  ],
17
+ "author": "Martin Donadieu",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/Cap-go/capacitor-native-audio.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/Cap-go/capacitor-native-audio/native-audio/issues"
24
+ },
16
25
  "keywords": [
17
26
  "capacitor",
18
27
  "plugin",
19
28
  "audio",
20
29
  "media",
30
+ "capgo",
21
31
  "native"
22
32
  ],
23
33
  "scripts": {
@@ -25,10 +35,12 @@
25
35
  "verify:ios": "cd ios && pod install && xcodebuild -workspace Plugin.xcworkspace -scheme Plugin && cd ..",
26
36
  "verify:android": "cd android && ./gradlew clean build test && cd ..",
27
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 ..",
28
40
  "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
29
41
  "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --autocorrect --format",
30
42
  "eslint": "eslint . --ext .ts",
31
- "prettier": "prettier --config .prettierrc.js \"**/*.{css,html,ts,js,java}\"",
43
+ "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
32
44
  "swiftlint": "node-swiftlint",
33
45
  "docgen": "docgen --api NativeAudio --output-readme README.md --output-json dist/docs.json",
34
46
  "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
@@ -36,8 +48,6 @@
36
48
  "watch": "tsc --watch",
37
49
  "prepublishOnly": "npm run build"
38
50
  },
39
- "author": "Martin Donadieu <martin@capgo.app>",
40
- "license": "MIT",
41
51
  "devDependencies": {
42
52
  "@capacitor/android": "^7.0.0",
43
53
  "@capacitor/cli": "^7.0.0",
@@ -61,6 +71,11 @@
61
71
  "peerDependencies": {
62
72
  "@capacitor/core": ">=7.0.0"
63
73
  },
74
+ "prettier": "@ionic/prettier-config",
75
+ "swiftlint": "@ionic/swiftlint-config",
76
+ "eslintConfig": {
77
+ "extends": "@ionic/eslint-config/recommended"
78
+ },
64
79
  "capacitor": {
65
80
  "ios": {
66
81
  "src": "ios"
@@ -68,20 +83,5 @@
68
83
  "android": {
69
84
  "src": "android"
70
85
  }
71
- },
72
- "eslintConfig": {
73
- "extends": "@ionic/eslint-config/recommended"
74
- },
75
- "prettier": "@ionic/prettier-config",
76
- "swiftlint": "@ionic/swiftlint-config",
77
- "repository": {
78
- "type": "git",
79
- "url": "https://github.com/Cap-go/native-audio"
80
- },
81
- "bugs": {
82
- "url": "https://github.com/Cap-go/native-audio/issues"
83
- },
84
- "publishConfig": {
85
- "access": "public"
86
86
  }
87
87
  }