@capgo/native-audio 7.1.7 → 7.3.0

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.
@@ -20,70 +20,88 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
20
20
  let FADESTEP: Float = 0.05
21
21
  let FADEDELAY: Float = 0.08
22
22
 
23
+ private var currentTimeTimer: Timer?
24
+
23
25
  init(owner: NativeAudio, withAssetId assetId: String, withPath path: String!, withChannels channels: Int!, withVolume volume: Float!, withFadeDelay delay: Float!) {
24
26
 
25
27
  self.owner = owner
26
28
  self.assetId = assetId
27
29
  self.channels = []
30
+ self.initialVolume = volume ?? 1.0
28
31
 
29
32
  super.init()
30
33
 
31
34
  let pathUrl: URL = URL(string: path.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)!
32
- for _ in 0..<channels {
33
- do {
34
- let player: AVAudioPlayer! = try AVAudioPlayer(contentsOf: pathUrl)
35
- player.delegate = owner
36
-
37
- if player != nil {
38
- player.enableRate = true
39
- player.volume = volume
40
- player.prepareToPlay()
41
- self.channels.append(player)
42
- if channels == 1 {
43
- player.delegate = self
35
+
36
+ owner.executeOnAudioQueue { [self] in
37
+ for _ in 0..<channels {
38
+ do {
39
+ let player: AVAudioPlayer! = try AVAudioPlayer(contentsOf: pathUrl)
40
+ player.delegate = self
41
+ if player != nil {
42
+ player.enableRate = true
43
+ player.volume = volume
44
+ player.rate = 1.0
45
+ player.prepareToPlay()
46
+ self.channels.append(player)
44
47
  }
48
+ } catch let error as NSError {
49
+ print(error.debugDescription)
50
+ print("Error loading \(String(describing: path))")
45
51
  }
46
- } catch let error as NSError {
47
- print(error.debugDescription)
48
- print("Error loading \(String(describing: path))")
49
52
  }
50
53
  }
51
54
  }
52
55
 
53
56
  func getCurrentTime() -> TimeInterval {
54
- if channels.count != 1 {
55
- return 0
57
+ var result: TimeInterval = 0
58
+ owner.executeOnAudioQueue { [self] in
59
+ if channels.count != 1 {
60
+ result = 0
61
+ return
62
+ }
63
+ let player: AVAudioPlayer = channels[playIndex]
64
+ result = player.currentTime
56
65
  }
57
- let player: AVAudioPlayer = channels[playIndex]
58
-
59
- return player.currentTime
66
+ return result
60
67
  }
61
68
 
62
69
  func setCurrentTime(time: TimeInterval) {
63
- if channels.count != 1 {
64
- return
70
+ owner.executeOnAudioQueue { [self] in
71
+ if channels.count != 1 {
72
+ return
73
+ }
74
+ let player: AVAudioPlayer = channels[playIndex]
75
+ player.currentTime = time
65
76
  }
66
-
67
- let player: AVAudioPlayer = channels[playIndex]
68
-
69
- player.currentTime = time
70
77
  }
71
78
 
72
79
  func getDuration() -> TimeInterval {
73
- if channels.count != 1 {
74
- return 0
80
+ var result: TimeInterval = 0
81
+ owner.executeOnAudioQueue { [self] in
82
+ if channels.count != 1 {
83
+ result = 0
84
+ return
85
+ }
86
+ let player: AVAudioPlayer = channels[playIndex]
87
+ result = player.duration
75
88
  }
76
-
77
- let player: AVAudioPlayer = channels[playIndex]
78
-
79
- return player.duration
89
+ return result
80
90
  }
81
91
 
82
92
  func play(time: TimeInterval, delay: TimeInterval) {
83
- let player: AVAudioPlayer
93
+ owner.executeOnAudioQueue { [self] in
94
+ guard !channels.isEmpty else {
95
+ NSLog("No channels available")
96
+ return
97
+ }
98
+ guard playIndex < channels.count else {
99
+ NSLog("PlayIndex out of bounds")
100
+ playIndex = 0
101
+ return
102
+ }
84
103
 
85
- if channels.count > 0 {
86
- player = channels[playIndex]
104
+ let player = channels[playIndex]
87
105
  player.currentTime = time
88
106
  player.numberOfLoops = 0
89
107
  if delay > 0 {
@@ -93,100 +111,137 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
93
111
  }
94
112
  playIndex += 1
95
113
  playIndex = playIndex % channels.count
114
+ NSLog("About to start timer updates")
115
+ startCurrentTimeUpdates()
96
116
  }
97
117
  }
98
118
 
99
119
  func playWithFade(time: TimeInterval) {
100
- let player: AVAudioPlayer = channels[playIndex]
101
- player.currentTime = time
120
+ owner.executeOnAudioQueue { [self] in
121
+ guard !channels.isEmpty else {
122
+ NSLog("No channels available")
123
+ return
124
+ }
125
+ guard playIndex < channels.count else {
126
+ NSLog("PlayIndex out of bounds")
127
+ playIndex = 0
128
+ return
129
+ }
102
130
 
103
- if !player.isPlaying {
104
- player.numberOfLoops = 0
105
- player.volume = 0
106
- player.play()
107
- playIndex += 1
108
- playIndex = playIndex % channels.count
109
- } else {
110
- if player.volume < initialVolume {
111
- player.volume += self.FADESTEP
131
+ let player: AVAudioPlayer = channels[playIndex]
132
+ player.currentTime = time
133
+
134
+ if !player.isPlaying {
135
+ player.numberOfLoops = 0
136
+ player.volume = initialVolume
137
+ player.play()
138
+ playIndex += 1
139
+ playIndex = playIndex % channels.count
140
+ NSLog("PlayWithFade: About to start timer updates")
141
+ startCurrentTimeUpdates()
142
+ } else {
143
+ if player.volume < initialVolume {
144
+ player.volume += self.FADESTEP
145
+ }
112
146
  }
113
147
  }
114
-
115
148
  }
116
149
 
117
150
  func pause() {
118
- let player: AVAudioPlayer = channels[playIndex]
119
- player.pause()
151
+ owner.executeOnAudioQueue { [self] in
152
+ stopCurrentTimeUpdates()
153
+ let player: AVAudioPlayer = channels[playIndex]
154
+ player.pause()
155
+ }
120
156
  }
121
157
 
122
158
  func resume() {
123
- let player: AVAudioPlayer = channels[playIndex]
124
-
125
- let timeOffset = player.deviceCurrentTime + 0.01
126
- player.play(atTime: timeOffset)
159
+ owner.executeOnAudioQueue { [self] in
160
+ let player: AVAudioPlayer = channels[playIndex]
161
+ let timeOffset = player.deviceCurrentTime + 0.01
162
+ player.play(atTime: timeOffset)
163
+ NSLog("Resume: About to start timer updates")
164
+ startCurrentTimeUpdates()
165
+ }
127
166
  }
128
167
 
129
168
  func stop() {
130
- for player in channels {
131
- player.stop()
169
+ owner.executeOnAudioQueue { [self] in
170
+ stopCurrentTimeUpdates()
171
+ for player in channels {
172
+ if player.isPlaying {
173
+ player.stop()
174
+ }
175
+ player.currentTime = 0
176
+ player.numberOfLoops = 0
177
+ }
178
+ playIndex = 0
132
179
  }
133
180
  }
134
181
 
135
182
  func stopWithFade() {
136
- let player: AVAudioPlayer = channels[playIndex]
137
-
138
- if !player.isPlaying {
139
- player.currentTime = 0.0
140
- player.numberOfLoops = 0
141
- player.volume = 0
142
- player.play()
143
- playIndex += 1
144
- playIndex = playIndex % channels.count
145
- } else {
146
- if player.volume < initialVolume {
147
- player.volume += self.FADESTEP
183
+ owner.executeOnAudioQueue { [self] in
184
+ let player: AVAudioPlayer = channels[playIndex]
185
+ if player.isPlaying {
186
+ if player.volume > self.FADESTEP {
187
+ player.volume -= self.FADESTEP
188
+ // Schedule next fade step
189
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(self.FADEDELAY * 1000))) { [weak self] in
190
+ self?.stopWithFade()
191
+ }
192
+ } else {
193
+ // Volume is near 0, actually stop
194
+ player.volume = 0
195
+ self.stop()
196
+ }
148
197
  }
149
198
  }
150
199
  }
151
200
 
152
201
  func loop() {
153
- self.stop()
154
-
155
- let player: AVAudioPlayer = channels[playIndex]
156
- player.numberOfLoops = -1
157
- player.play()
158
- playIndex += 1
159
- playIndex = playIndex % channels.count
202
+ owner.executeOnAudioQueue { [self] in
203
+ self.stop()
204
+ let player: AVAudioPlayer = channels[playIndex]
205
+ player.delegate = self
206
+ player.numberOfLoops = -1
207
+ player.play()
208
+ playIndex += 1
209
+ playIndex = playIndex % channels.count
210
+ NSLog("Loop: About to start timer updates")
211
+ startCurrentTimeUpdates()
212
+ }
160
213
  }
161
214
 
162
215
  func unload() {
163
- self.stop()
164
-
165
- // for i in 0..<channels.count {
166
- // var player: AVAudioPlayer! = channels.object(at: i) as? AVAudioPlayer
167
- //
168
- // player = nil
169
- // }
170
- channels = []
216
+ owner.executeOnAudioQueue { [self] in
217
+ self.stop()
218
+ channels = []
219
+ }
171
220
  }
172
221
 
173
222
  func setVolume(volume: NSNumber!) {
174
- for player in channels {
175
- player.volume = volume.floatValue
223
+ owner.executeOnAudioQueue { [self] in
224
+ for player in channels {
225
+ player.volume = volume.floatValue
226
+ }
176
227
  }
177
228
  }
178
229
 
179
230
  func setRate(rate: NSNumber!) {
180
- for player in channels {
181
- player.rate = rate.floatValue
231
+ owner.executeOnAudioQueue { [self] in
232
+ for player in channels {
233
+ player.rate = rate.floatValue
234
+ }
182
235
  }
183
236
  }
184
237
 
185
238
  public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
186
- NSLog("playerDidFinish")
187
- self.owner.notifyListeners("complete", data: [
188
- "assetId": self.assetId
189
- ])
239
+ owner.executeOnAudioQueue { [self] in
240
+ NSLog("playerDidFinish")
241
+ self.owner.notifyListeners("complete", data: [
242
+ "assetId": self.assetId
243
+ ])
244
+ }
190
245
  }
191
246
 
192
247
  func playerDecodeError(player: AVAudioPlayer!, error: NSError!) {
@@ -194,11 +249,62 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
194
249
  }
195
250
 
196
251
  func isPlaying() -> Bool {
197
- if channels.count != 1 {
198
- return false
252
+ var result: Bool = false
253
+ owner.executeOnAudioQueue { [self] in
254
+ if channels.count != 1 {
255
+ result = false
256
+ return
257
+ }
258
+ let player: AVAudioPlayer = channels[playIndex]
259
+ result = player.isPlaying
199
260
  }
261
+ return result
262
+ }
263
+
264
+ internal func startCurrentTimeUpdates() {
265
+ NSLog("Starting timer updates")
266
+ DispatchQueue.main.async { [weak self] in
267
+ guard let self = self else {
268
+ NSLog("Self is nil in timer start")
269
+ return
270
+ }
271
+
272
+ self.currentTimeTimer?.invalidate()
273
+ self.currentTimeTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
274
+ guard let self = self else {
275
+ NSLog("Self is nil in timer callback")
276
+ return
277
+ }
278
+
279
+ var shouldNotify = false
280
+ var currentTime: TimeInterval = 0
200
281
 
201
- let player: AVAudioPlayer = channels[playIndex]
202
- return player.isPlaying
282
+ self.owner.executeOnAudioQueue {
283
+ shouldNotify = self.isPlaying()
284
+ NSLog("isPlaying returned: \(shouldNotify)")
285
+ if shouldNotify {
286
+ currentTime = self.getCurrentTime()
287
+ NSLog("getCurrentTime returned: \(currentTime)")
288
+ }
289
+ }
290
+
291
+ if shouldNotify {
292
+ NSLog("Calling notifyCurrentTime")
293
+ self.owner.notifyCurrentTime(self)
294
+ } else {
295
+ NSLog("Stopping timer - not playing")
296
+ self.stopCurrentTimeUpdates()
297
+ }
298
+ }
299
+ RunLoop.current.add(self.currentTimeTimer!, forMode: .common)
300
+ NSLog("Timer added to RunLoop")
301
+ }
302
+ }
303
+
304
+ internal func stopCurrentTimeUpdates() {
305
+ DispatchQueue.main.async { [weak self] in
306
+ self?.currentTimeTimer?.invalidate()
307
+ self?.currentTimeTimer = nil
308
+ }
203
309
  }
204
310
  }
@@ -10,7 +10,7 @@ enum MyError: Error {
10
10
  /// Please read the Capacitor iOS Plugin Development Guide
11
11
  /// here: https://capacitor.ionicframework.com/docs/plugins/ios
12
12
  @objc(NativeAudio)
13
- public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate {
13
+ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate, CAPBridgedPlugin {
14
14
  public let identifier = "NativeAudio"
15
15
  public let jsName = "NativeAudio"
16
16
  public let pluginMethods: [CAPPluginMethod] = [
@@ -28,9 +28,10 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate {
28
28
  CAPPluginMethod(name: "getCurrentTime", returnType: CAPPluginReturnPromise),
29
29
  CAPPluginMethod(name: "getDuration", returnType: CAPPluginReturnPromise),
30
30
  CAPPluginMethod(name: "resume", returnType: CAPPluginReturnPromise),
31
- CAPPluginMethod(name: "setCurrentTime", returnType: CAPPluginReturnPromise)
31
+ CAPPluginMethod(name: "setCurrentTime", returnType: CAPPluginReturnPromise),
32
+ CAPPluginMethod(name: "clearCache", returnType: CAPPluginReturnPromise)
32
33
  ]
33
- private let audioQueue = DispatchQueue(label: "ee.forgr.audio.queue", qos: .userInitiated)
34
+ internal let audioQueue = DispatchQueue(label: "ee.forgr.audio.queue", qos: .userInitiated, attributes: .concurrent)
34
35
  private var audioList: [String: Any] = [:] {
35
36
  didSet {
36
37
  // Ensure audioList modifications happen on audioQueue
@@ -165,18 +166,13 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate {
165
166
  let time = call.getDouble("time") ?? 0
166
167
  let delay = call.getDouble("delay") ?? 0
167
168
 
168
- if audioId.isEmpty {
169
- call.reject(Constant.ErrorAssetId)
170
- return
171
- }
172
-
173
- audioQueue.async {
174
- guard !self.audioList.isEmpty else {
169
+ audioQueue.sync { [self] in // Changed from async to sync for consistency
170
+ guard !audioList.isEmpty else {
175
171
  call.reject("Audio list is empty")
176
172
  return
177
173
  }
178
174
 
179
- guard let asset = self.audioList[audioId] else {
175
+ guard let asset = audioList[audioId] else {
180
176
  call.reject(Constant.ErrorAssetNotFound)
181
177
  return
182
178
  }
@@ -200,29 +196,9 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate {
200
196
  }
201
197
 
202
198
  @objc private func getAudioAsset(_ call: CAPPluginCall) -> AudioAsset? {
203
- let audioId = call.getString(Constant.AssetIdKey) ?? ""
204
- if audioId.isEmpty {
205
- call.reject(Constant.ErrorAssetId)
206
- return nil
207
- }
208
-
209
199
  var asset: AudioAsset?
210
- audioQueue.sync {
211
- if self.audioList.isEmpty {
212
- call.reject("Audio list is empty")
213
- return
214
- }
215
-
216
- guard let foundAsset = self.audioList[audioId] as? AudioAsset else {
217
- call.reject(Constant.ErrorAssetNotFound + " - " + audioId)
218
- return
219
- }
220
- asset = foundAsset
221
- }
222
-
223
- if asset == nil {
224
- call.reject("Failed to get audio asset")
225
- return nil
200
+ audioQueue.sync { [self] in // Read operation
201
+ asset = self.audioList[call.getString(Constant.AssetIdKey) ?? ""] as? AudioAsset
226
202
  }
227
203
  return asset
228
204
  }
@@ -380,12 +356,20 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate {
380
356
  }
381
357
  }
382
358
 
383
- private func preloadAsset(_ call: CAPPluginCall, isComplex complex: Bool) {
359
+ @objc func clearCache(_ call: CAPPluginCall) {
360
+ audioQueue.async {
361
+ RemoteAudioAsset.clearCache()
362
+ call.resolve()
363
+ }
364
+ }
365
+
366
+ @objc private func preloadAsset(_ call: CAPPluginCall, isComplex complex: Bool) {
384
367
  let audioId = call.getString(Constant.AssetIdKey) ?? ""
385
368
  let channels: Int?
386
369
  let volume: Float?
387
370
  let delay: Float?
388
371
  var isLocalUrl: Bool = call.getBool("isUrl") ?? false
372
+
389
373
  if audioId == "" {
390
374
  call.reject(Constant.ErrorAssetId)
391
375
  return
@@ -397,23 +381,22 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate {
397
381
  channels = call.getInt("channels") ?? 1
398
382
  delay = call.getFloat("delay") ?? 1.0
399
383
  } else {
400
- channels = 0
401
- volume = 0
402
- delay = 0
384
+ channels = 1
385
+ volume = 1.0
386
+ delay = 0.0
403
387
  isLocalUrl = false
404
388
  }
405
389
 
406
- if audioList.isEmpty {
407
- audioList = [:]
408
- }
390
+ audioQueue.sync(flags: .barrier) { [self] in
391
+ if audioList.isEmpty {
392
+ audioList = [:]
393
+ }
394
+
395
+ if audioList[audioId] != nil {
396
+ call.reject(Constant.ErrorAssetAlreadyLoaded + " - " + audioId)
397
+ return
398
+ }
409
399
 
410
- let asset = audioList[audioId]
411
- let queue = DispatchQueue(label: "ee.forgr.audio.simple.queue", qos: .userInitiated)
412
- if asset != nil {
413
- call.reject(Constant.ErrorAssetAlreadyLoaded + " - " + audioId)
414
- return
415
- }
416
- queue.async {
417
400
  var basePath: String?
418
401
  if let url = URL(string: assetPath), url.scheme != nil {
419
402
  // Handle remote URL
@@ -423,9 +406,7 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate {
423
406
  return
424
407
  } else if isLocalUrl == false {
425
408
  // Handle public folder
426
- // if assetPath doesnt start with public/ add it
427
409
  assetPath = assetPath.starts(with: "public/") ? assetPath : "public/" + assetPath
428
-
429
410
  let assetPathSplit = assetPath.components(separatedBy: ".")
430
411
  basePath = Bundle.main.path(forResource: assetPathSplit[0], ofType: assetPathSplit[1])
431
412
  } else {
@@ -482,9 +463,34 @@ public class NativeAudio: CAPPlugin, AVAudioPlayerDelegate {
482
463
  }
483
464
 
484
465
  if self.fadeMusic {
485
- audioAsset.playWithFade(time: audioAsset.getCurrentTime())
466
+ audioAsset.stopWithFade()
486
467
  } else {
487
468
  audioAsset.stop()
488
469
  }
489
470
  }
471
+
472
+ internal func executeOnAudioQueue(_ block: @escaping () -> Void) {
473
+ if DispatchQueue.getSpecific(key: queueKey) != nil {
474
+ block() // Already on queue
475
+ } else {
476
+ audioQueue.sync(flags: .barrier) {
477
+ block()
478
+ }
479
+ }
480
+ }
481
+
482
+ @objc func notifyCurrentTime(_ asset: AudioAsset) {
483
+ NSLog("notifyCurrentTime called in Plugin")
484
+ audioQueue.sync {
485
+ let rawTime = asset.getCurrentTime()
486
+ // Round to nearest 100ms (0.1 seconds)
487
+ let currentTime = round(rawTime * 10) / 10
488
+ NSLog("About to notify listeners with time: \(currentTime)")
489
+ notifyListeners("currentTime", data: [
490
+ "currentTime": currentTime,
491
+ "assetId": asset.assetId
492
+ ])
493
+ NSLog("Listeners notified")
494
+ }
495
+ }
490
496
  }