@capgo/native-audio 8.2.12 → 8.2.14

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,76 +1,44 @@
1
- //
2
- // AudioAsset.swift
3
- // Plugin
4
- //
5
- // Created by priyank on 2020-05-29.
6
- // Copyright © 2022 Martin Donadieu. All rights reserved.
7
- //
8
-
9
1
  import AVFoundation
10
2
 
11
- /**
12
- * AudioAsset class handles local audio playback via AVAudioPlayer
13
- * Supports volume control, fade effects, rate changes, and looping
14
- */
3
+ // swiftlint:disable:next type_body_length
15
4
  public class AudioAsset: NSObject, AVAudioPlayerDelegate {
16
5
 
17
6
  var channels: [AVAudioPlayer] = []
18
7
  var playIndex: Int = 0
19
8
  var assetId: String = ""
20
9
  var initialVolume: Float = 1.0
21
- var fadeDelay: Float = 1.0
10
+ let zeroVolume: Float = 0.001
11
+ let maxVolume: Float = 1.0
22
12
  weak var owner: NativeAudio?
23
13
  var onComplete: (() -> Void)?
24
14
 
25
- // Constants for fade effect
26
- let FADESTEP: Float = 0.05
27
- let FADEDELAY: Float = 0.08
28
-
29
- // Maximum number of channels to prevent excessive resource usage
30
- private let maxChannels = Constant.MaxChannels
31
-
32
- // Timers - must only be accessed from main thread
15
+ let fadeDelaySecs: Float = 0.08
33
16
  private var currentTimeTimer: Timer?
34
17
  internal var fadeTimer: Timer?
18
+ var fadeTask: DispatchWorkItem?
19
+ let fadeQueue: DispatchQueue = DispatchQueue(label: "com.audioasset.fadeQueue")
20
+ var dispatchedCompleteMap: [String: Bool] = [:]
35
21
 
36
- /**
37
- * Initialize a new audio asset
38
- * - Parameters:
39
- * - owner: The plugin that owns this asset
40
- * - assetId: Unique identifier for this asset
41
- * - path: File path to the audio file
42
- * - channels: Number of simultaneous playback channels (polyphony)
43
- * - volume: Initial volume (0.0-1.0)
44
- * - delay: Fade delay in seconds
45
- */
46
- init(owner: NativeAudio, withAssetId assetId: String, withPath path: String!, withChannels channels: Int!, withVolume volume: Float!, withFadeDelay delay: Float!) {
22
+ private var logger = Logger(logTag: "AudioAsset")
47
23
 
24
+ init(owner: NativeAudio, withAssetId assetId: String, withPath path: String, withChannels channels: Int?, withVolume volume: Float?) {
48
25
  self.owner = owner
49
26
  self.assetId = assetId
50
27
  self.channels = []
51
- self.initialVolume = min(max(volume ?? Constant.DefaultVolume, Constant.MinVolume), Constant.MaxVolume) // Validate volume range
52
- self.fadeDelay = max(delay ?? Constant.DefaultFadeDelay, 0.0) // Ensure non-negative delay
28
+ self.initialVolume = min(max(volume ?? Constant.DefaultVolume, Constant.MinVolume), Constant.MaxVolume)
53
29
 
54
30
  super.init()
55
31
 
56
32
  guard let encodedPath = path.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
57
- print("Failed to encode path: \(String(describing: path))")
33
+ logger.error("Failed to encode path: %@", String(describing: path))
58
34
  return
59
35
  }
60
36
 
61
- // Try to create URL from string first, fall back to file URL if that fails
62
- let pathUrl: URL
63
- if let url = URL(string: encodedPath) {
64
- pathUrl = url
65
- } else {
66
- pathUrl = URL(fileURLWithPath: encodedPath)
67
- }
68
-
69
- // Limit channels to a reasonable maximum to prevent resource issues
70
- let channelCount = min(max(channels ?? 1, 1), maxChannels)
37
+ let pathUrl = URL(string: encodedPath) ?? URL(fileURLWithPath: encodedPath)
38
+ let channelCount = min(max(channels ?? 1, 1), Constant.MaxChannels)
71
39
 
72
- owner.executeOnAudioQueue { [weak self] in
73
- guard let self = self else { return }
40
+ let setupBlock = { [weak self] in
41
+ guard let self else { return }
74
42
  for _ in 0..<channelCount {
75
43
  do {
76
44
  let player = try AVAudioPlayer(contentsOf: pathUrl)
@@ -81,246 +49,170 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
81
49
  player.prepareToPlay()
82
50
  self.channels.append(player)
83
51
  } catch {
84
- print("Error loading audio file: \(error.localizedDescription)")
85
- print("Path: \(String(describing: path))")
52
+ self.logger.error("Error loading audio file: %@", error.localizedDescription)
86
53
  }
87
54
  }
88
55
  }
56
+
57
+ if owner.isRunningTests {
58
+ setupBlock()
59
+ } else {
60
+ owner.executeOnAudioQueue(setupBlock)
61
+ }
89
62
  }
90
63
 
91
- deinit {
92
- // Invalidate timers - must be done on main thread
93
- let currentTimer = currentTimeTimer
94
- let fadeTimerRef = fadeTimer
64
+ // Backward-compatible initializer signature
65
+ init(owner: NativeAudio, withAssetId assetId: String, withPath path: String, withChannels channels: Int?, withVolume volume: Float?, withFadeDelay _: Float?) {
66
+ self.owner = owner
67
+ self.assetId = assetId
68
+ self.channels = []
69
+ self.initialVolume = min(max(volume ?? Constant.DefaultVolume, Constant.MinVolume), Constant.MaxVolume)
70
+ super.init()
95
71
 
96
- if Thread.isMainThread {
97
- currentTimer?.invalidate()
98
- fadeTimerRef?.invalidate()
99
- } else {
100
- DispatchQueue.main.async {
101
- currentTimer?.invalidate()
102
- fadeTimerRef?.invalidate()
103
- }
72
+ guard let encodedPath = path.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
73
+ logger.error("Failed to encode path: %@", String(describing: path))
74
+ return
104
75
  }
105
76
 
106
- // Clean up any players that might still be playing
107
- for player in channels {
108
- if player.isPlaying {
109
- player.stop()
77
+ let pathUrl = URL(string: encodedPath) ?? URL(fileURLWithPath: encodedPath)
78
+ let channelCount = min(max(channels ?? 1, 1), Constant.MaxChannels)
79
+ let setupBlock = { [weak self] in
80
+ guard let self else { return }
81
+ for _ in 0..<channelCount {
82
+ do {
83
+ let player = try AVAudioPlayer(contentsOf: pathUrl)
84
+ player.delegate = self
85
+ player.enableRate = true
86
+ player.volume = self.initialVolume
87
+ player.rate = 1.0
88
+ player.prepareToPlay()
89
+ self.channels.append(player)
90
+ } catch {
91
+ self.logger.error("Error loading audio file: %@", error.localizedDescription)
92
+ }
110
93
  }
111
94
  }
95
+ if owner.isRunningTests {
96
+ setupBlock()
97
+ } else {
98
+ owner.executeOnAudioQueue(setupBlock)
99
+ }
100
+ }
101
+
102
+ deinit {
103
+ currentTimeTimer?.invalidate()
104
+ currentTimeTimer = nil
105
+ fadeTimer?.invalidate()
106
+ fadeTimer = nil
107
+ cancelFade()
108
+ for player in channels where player.isPlaying {
109
+ player.stop()
110
+ }
112
111
  channels = []
113
112
  }
114
113
 
115
- /**
116
- * Get the current playback time
117
- * - Returns: Current time in seconds
118
- */
119
114
  func getCurrentTime() -> TimeInterval {
120
115
  var result: TimeInterval = 0
121
116
  owner?.executeOnAudioQueue { [weak self] in
122
- guard let self = self else { return }
123
-
124
- if channels.isEmpty || playIndex >= channels.count {
125
- result = 0
126
- return
127
- }
128
- let player = channels[playIndex]
129
- result = player.currentTime
117
+ guard let self else { return }
118
+ guard !channels.isEmpty, playIndex < channels.count else { return }
119
+ result = channels[playIndex].currentTime
130
120
  }
131
121
  return result
132
122
  }
133
123
 
134
- /**
135
- * Set the current playback time
136
- * - Parameter time: Time in seconds
137
- */
138
124
  func setCurrentTime(time: TimeInterval) {
139
125
  owner?.executeOnAudioQueue { [weak self] in
140
- guard let self = self else { return }
141
-
142
- if channels.isEmpty || playIndex >= channels.count {
143
- return
144
- }
126
+ guard let self else { return }
127
+ guard !channels.isEmpty, playIndex < channels.count else { return }
145
128
  let player = channels[playIndex]
146
- // Ensure time is valid
147
129
  let validTime = min(max(time, 0), player.duration)
148
130
  player.currentTime = validTime
149
131
  }
150
132
  }
151
133
 
152
- /**
153
- * Get the total duration of the audio file
154
- * - Returns: Duration in seconds
155
- */
156
134
  func getDuration() -> TimeInterval {
157
135
  var result: TimeInterval = 0
158
136
  owner?.executeOnAudioQueue { [weak self] in
159
- guard let self = self else { return }
160
-
161
- if channels.isEmpty || playIndex >= channels.count {
162
- result = 0
163
- return
164
- }
165
- let player = channels[playIndex]
166
- result = player.duration
137
+ guard let self else { return }
138
+ guard !channels.isEmpty, playIndex < channels.count else { return }
139
+ result = channels[playIndex].duration
167
140
  }
168
141
  return result
169
142
  }
170
143
 
171
- /**
172
- * Play the audio from the specified time with optional delay
173
- * - Parameters:
174
- * - time: Start time in seconds
175
- * - delay: Delay before playback in seconds
176
- */
177
- func play(time: TimeInterval, delay: TimeInterval) {
144
+ func play(time: TimeInterval, volume: Float? = nil) {
178
145
  stopCurrentTimeUpdates()
179
- stopFadeTimer()
180
-
181
146
  owner?.executeOnAudioQueue { [weak self] in
182
- guard let self = self else { return }
183
-
147
+ guard let self else { return }
184
148
  guard !channels.isEmpty else { return }
185
-
186
- // Reset play index if it's out of bounds
187
149
  if playIndex >= channels.count {
188
150
  playIndex = 0
189
151
  }
190
-
191
- // Ensure the audio session is active before playing
192
152
  owner?.activateSession()
153
+ cancelFade()
193
154
 
194
155
  let player = channels[playIndex]
195
- // Ensure time is within valid range
196
156
  let validTime = min(max(time, 0), player.duration)
197
157
  player.currentTime = validTime
198
158
  player.numberOfLoops = 0
199
-
200
- // Use a valid delay (non-negative)
201
- let validDelay = max(delay, 0)
202
-
203
- if validDelay > 0 {
204
- player.play(atTime: player.deviceCurrentTime + validDelay)
205
- } else {
206
- player.play()
207
- }
159
+ player.volume = volume ?? initialVolume
160
+ player.play()
208
161
 
209
162
  playIndex = (playIndex + 1) % channels.count
210
163
  startCurrentTimeUpdates()
211
164
  }
212
165
  }
213
166
 
167
+ // Backward-compatible signature
168
+ func play(time: TimeInterval, delay: TimeInterval) {
169
+ let validDelay = max(delay, 0)
170
+ if validDelay > 0 {
171
+ DispatchQueue.main.asyncAfter(deadline: .now() + validDelay) { [weak self] in
172
+ self?.play(time: time, volume: nil)
173
+ }
174
+ } else {
175
+ play(time: time, volume: nil)
176
+ }
177
+ }
178
+
214
179
  func playWithFade(time: TimeInterval) {
215
- owner?.executeOnAudioQueue { [weak self] in
216
- guard let self = self else { return }
180
+ playWithFade(time: time, volume: nil, fadeInDuration: TimeInterval(Constant.DefaultFadeDuration))
181
+ }
217
182
 
183
+ func playWithFade(time: TimeInterval, volume: Float?, fadeInDuration: TimeInterval) {
184
+ owner?.executeOnAudioQueue { [weak self] in
185
+ guard let self else { return }
218
186
  guard !channels.isEmpty else { return }
219
-
220
- // Reset play index if it's out of bounds
221
187
  if playIndex >= channels.count {
222
188
  playIndex = 0
223
189
  }
224
190
 
225
191
  let player = channels[playIndex]
226
192
  player.currentTime = time
227
-
228
- if !player.isPlaying {
229
- player.numberOfLoops = 0
230
- player.volume = 0 // Start with volume at 0
231
- player.play()
232
- playIndex = (playIndex + 1) % channels.count
233
- startCurrentTimeUpdates()
234
-
235
- // Start fade-in
236
- startVolumeRamp(from: 0, to: initialVolume, player: player)
237
- } else {
238
- if player.volume < initialVolume {
239
- // Continue fade-in if already in progress
240
- startVolumeRamp(from: player.volume, to: initialVolume, player: player)
241
- }
242
- }
243
- }
244
- }
245
-
246
- private func startVolumeRamp(from startVolume: Float, to endVolume: Float, player: AVAudioPlayer) {
247
- stopFadeTimer()
248
-
249
- let steps = abs(endVolume - startVolume) / FADESTEP
250
- guard steps > 0 else { return }
251
-
252
- let timeInterval = FADEDELAY / steps
253
- var currentStep = 0
254
- let totalSteps = Int(ceil(steps))
255
-
256
- player.volume = startVolume
257
-
258
- // Create timer on main thread
259
- DispatchQueue.main.async { [weak self, weak player] in
260
- guard let self = self else { return }
261
-
262
- let timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(timeInterval), repeats: true) { [weak self, weak player] timer in
263
- guard let strongSelf = self, let strongPlayer = player else {
264
- timer.invalidate()
265
- return
266
- }
267
-
268
- currentStep += 1
269
- let progress = Float(currentStep) / Float(totalSteps)
270
- let newVolume = startVolume + progress * (endVolume - startVolume)
271
-
272
- // Update player on audio queue
273
- strongSelf.owner?.executeOnAudioQueue {
274
- strongPlayer.volume = newVolume
275
- }
276
-
277
- if currentStep >= totalSteps {
278
- strongSelf.owner?.executeOnAudioQueue {
279
- strongPlayer.volume = endVolume
280
- }
281
- timer.invalidate()
282
- strongSelf.fadeTimer = nil
283
- }
284
- }
285
-
286
- self.fadeTimer = timer
287
- RunLoop.current.add(timer, forMode: .common)
288
- }
289
- }
290
-
291
- internal func stopFadeTimer() {
292
- if Thread.isMainThread {
293
- fadeTimer?.invalidate()
294
- fadeTimer = nil
295
- } else {
296
- DispatchQueue.main.async { [weak self] in
297
- self?.fadeTimer?.invalidate()
298
- self?.fadeTimer = nil
299
- }
193
+ player.numberOfLoops = 0
194
+ player.volume = 0
195
+ player.play()
196
+ playIndex = (playIndex + 1) % channels.count
197
+ startCurrentTimeUpdates()
198
+ fadeIn(audio: player, fadeInDuration: fadeInDuration, targetVolume: volume ?? initialVolume)
300
199
  }
301
200
  }
302
201
 
303
202
  func pause() {
304
203
  owner?.executeOnAudioQueue { [weak self] in
305
- guard let self = self else { return }
306
-
204
+ guard let self else { return }
205
+ guard !channels.isEmpty, playIndex < channels.count else { return }
206
+ cancelFade()
207
+ channels[playIndex].pause()
307
208
  stopCurrentTimeUpdates()
308
-
309
- // Check for valid playIndex
310
- guard !channels.isEmpty && playIndex < channels.count else { return }
311
-
312
- let player = channels[playIndex]
313
- player.pause()
314
209
  }
315
210
  }
316
211
 
317
212
  func resume() {
318
213
  owner?.executeOnAudioQueue { [weak self] in
319
- guard let self = self else { return }
320
-
321
- // Check for valid playIndex
322
- guard !channels.isEmpty && playIndex < channels.count else { return }
323
-
214
+ guard let self else { return }
215
+ guard !channels.isEmpty, playIndex < channels.count else { return }
324
216
  let player = channels[playIndex]
325
217
  let timeOffset = player.deviceCurrentTime + 0.01
326
218
  player.play(atTime: timeOffset)
@@ -330,11 +222,9 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
330
222
 
331
223
  func stop() {
332
224
  owner?.executeOnAudioQueue { [weak self] in
333
- guard let self = self else { return }
334
-
225
+ guard let self else { return }
226
+ cancelFade()
335
227
  stopCurrentTimeUpdates()
336
- stopFadeTimer()
337
-
338
228
  for player in channels {
339
229
  if player.isPlaying {
340
230
  player.stop()
@@ -343,32 +233,25 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
343
233
  player.numberOfLoops = 0
344
234
  }
345
235
  playIndex = 0
236
+ dispatchComplete()
346
237
  }
347
238
  }
348
239
 
349
240
  func stopWithFade() {
350
- // Store current player locally to avoid race conditions with playIndex
351
- owner?.executeOnAudioQueue { [weak self] in
352
- guard let self = self else { return }
241
+ stopWithFade(fadeOutDuration: TimeInterval(Constant.DefaultFadeDuration), toPause: false)
242
+ }
353
243
 
354
- guard !channels.isEmpty && playIndex < channels.count else {
355
- stop()
244
+ func stopWithFade(fadeOutDuration: TimeInterval, toPause: Bool = false) {
245
+ owner?.executeOnAudioQueue { [weak self] in
246
+ guard let self else { return }
247
+ guard !channels.isEmpty, playIndex < channels.count else {
248
+ if !toPause { stop() }
356
249
  return
357
250
  }
358
-
359
251
  let player = channels[playIndex]
360
252
  if player.isPlaying && player.volume > 0 {
361
- startVolumeRamp(from: player.volume, to: 0, player: player)
362
-
363
- // Schedule the stop when fade is complete
364
- DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(FADEDELAY * 1000))) { [weak self, weak player] in
365
- guard let strongSelf = self, let strongPlayer = player else { return }
366
-
367
- if strongPlayer.volume < strongSelf.FADESTEP {
368
- strongSelf.stop()
369
- }
370
- }
371
- } else {
253
+ fadeOut(audio: player, fadeOutDuration: fadeOutDuration, toPause: toPause)
254
+ } else if !toPause {
372
255
  stop()
373
256
  }
374
257
  }
@@ -376,12 +259,10 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
376
259
 
377
260
  func loop() {
378
261
  owner?.executeOnAudioQueue { [weak self] in
379
- guard let self = self else { return }
380
-
381
- self.stop()
382
-
383
- guard !channels.isEmpty && playIndex < channels.count else { return }
384
-
262
+ guard let self else { return }
263
+ cancelFade()
264
+ stop()
265
+ guard !channels.isEmpty, playIndex < channels.count else { return }
385
266
  let player = channels[playIndex]
386
267
  player.delegate = self
387
268
  player.numberOfLoops = -1
@@ -393,39 +274,36 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
393
274
 
394
275
  func unload() {
395
276
  owner?.executeOnAudioQueue { [weak self] in
396
- guard let self = self else { return }
397
-
398
- self.stop()
277
+ guard let self else { return }
278
+ cancelFade()
279
+ stop()
399
280
  stopCurrentTimeUpdates()
400
- stopFadeTimer()
401
281
  channels = []
402
282
  }
403
283
  }
404
284
 
405
- /**
406
- * Set the volume for all audio channels
407
- * - Parameter volume: Volume level (0.0-1.0)
408
- */
409
- func setVolume(volume: NSNumber!) {
410
- owner?.executeOnAudioQueue { [weak self] in
411
- guard let self = self else { return }
285
+ func setVolume(volume: NSNumber) {
286
+ setVolume(volume: volume, fadeDuration: 0)
287
+ }
412
288
 
413
- // Ensure volume is in valid range
289
+ func setVolume(volume: NSNumber, fadeDuration: Double) {
290
+ owner?.executeOnAudioQueue { [weak self] in
291
+ guard let self else { return }
292
+ cancelFade()
414
293
  let validVolume = min(max(volume.floatValue, Constant.MinVolume), Constant.MaxVolume)
415
294
  for player in channels {
416
- player.volume = validVolume
295
+ if player.isPlaying && fadeDuration > 0 {
296
+ fadeTo(audio: player, fadeDuration: fadeDuration, targetVolume: validVolume)
297
+ } else {
298
+ player.volume = validVolume
299
+ }
417
300
  }
418
301
  }
419
302
  }
420
303
 
421
- /// Sets the playback rate for every channel, clamping the provided value to the allowed range before applying it.
422
- /// - Parameters:
423
- /// - rate: Playback rate multiplier; the value is clamped to the asset's allowed rate range and applied to all channels.
424
- func setRate(rate: NSNumber!) {
304
+ func setRate(rate: NSNumber) {
425
305
  owner?.executeOnAudioQueue { [weak self] in
426
- guard let self = self else { return }
427
-
428
- // Ensure rate is in valid range
306
+ guard let self else { return }
429
307
  let validRate = min(max(rate.floatValue, Constant.MinRate), Constant.MaxRate)
430
308
  for player in channels {
431
309
  player.rate = validRate
@@ -433,92 +311,58 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
433
311
  }
434
312
  }
435
313
 
436
- /// Handles an AVAudioPlayer finishing playback by notifying listeners, invoking the public completion callback, and forwarding the completion to the owner.
437
- ///
438
- /// This is called when a player finishes playing; the notifications and callback are dispatched on the audio queue. Notifications are sent to listeners with the asset's `assetId`, then `onComplete` is invoked if set, and finally the event is forwarded to the owner.
439
- /// - Parameters:
440
- /// - player: The `AVAudioPlayer` instance that finished playback.
441
- /// Handle an AVAudioPlayer finishing playback by notifying listeners, invoking the optional `onComplete` callback, and forwarding the completion to the owner.
442
- /// - Note: The handler's work is dispatched on the audio queue.
443
- /// - Parameters:
444
- /// - player: The `AVAudioPlayer` instance that finished playback.
445
- /// - flag: `true` if playback finished successfully, `false` otherwise.
446
- public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
314
+ func isPlaying() -> Bool {
315
+ var result = false
447
316
  owner?.executeOnAudioQueue { [weak self] in
448
- guard let self = self else { return }
449
-
450
- self.owner?.notifyListeners("complete", data: [
451
- "assetId": self.assetId
452
- ])
453
-
454
- // Invoke completion callback if set
455
- self.onComplete?()
456
-
457
- // Notify the owner that this player finished
458
- // The owner will check if any other assets are still playing
459
- owner?.audioPlayerDidFinishPlaying(player, successfully: flag)
317
+ guard let self else { return }
318
+ result = channels.contains(where: { $0.isPlaying })
460
319
  }
320
+ return result
461
321
  }
462
322
 
463
- func playerDecodeError(player: AVAudioPlayer!, error: NSError!) {
464
- if let error = error {
465
- print("AudioAsset decode error: \(error.localizedDescription)")
323
+ public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
324
+ owner?.executeOnAudioQueue { [weak self] in
325
+ guard let self else { return }
326
+ dispatchComplete()
327
+ onComplete?()
328
+ owner?.audioPlayerDidFinishPlaying(player, successfully: flag)
466
329
  }
467
330
  }
468
331
 
469
- func isPlaying() -> Bool {
470
- var result: Bool = false
471
- owner?.executeOnAudioQueue { [weak self] in
472
- guard let self = self else { return }
473
-
474
- if channels.isEmpty || playIndex >= channels.count {
475
- result = false
476
- return
477
- }
478
- let player = channels[playIndex]
479
- result = player.isPlaying
332
+ func playerDecodeError(player: AVAudioPlayer, error: NSError?) {
333
+ if let error {
334
+ logger.error("AudioAsset decode error: %@", error.localizedDescription)
480
335
  }
481
- return result
482
336
  }
483
337
 
484
338
  internal func startCurrentTimeUpdates() {
339
+ stopCurrentTimeUpdates()
340
+ dispatchedCompleteMap[assetId] = false
485
341
  DispatchQueue.main.async { [weak self] in
486
- guard let strongSelf = self else { return }
487
-
488
- // Stop existing timer first (we're on main thread now)
489
- strongSelf.currentTimeTimer?.invalidate()
490
- strongSelf.currentTimeTimer = nil
491
-
492
- let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
493
- guard let strongSelf = self, let strongOwner = strongSelf.owner else {
342
+ guard let self else { return }
343
+ let timer = Timer(timeInterval: 0.1, repeats: true) { [weak self] _ in
344
+ guard let self, let owner else {
494
345
  self?.stopCurrentTimeUpdates()
495
346
  return
496
347
  }
497
-
498
- if strongSelf.isPlaying() {
499
- strongOwner.notifyCurrentTime(strongSelf)
348
+ if self.isPlaying() {
349
+ owner.notifyCurrentTime(self)
500
350
  } else {
501
- strongSelf.stopCurrentTimeUpdates()
351
+ self.stopCurrentTimeUpdates()
502
352
  }
503
353
  }
504
-
505
- strongSelf.currentTimeTimer = timer
354
+ self.currentTimeTimer = timer
506
355
  RunLoop.current.add(timer, forMode: .common)
507
356
  }
508
357
  }
509
358
 
510
- /// Stops and clears the periodic current-time update timer, ensuring invalidation occurs on the main thread.
511
- ///
512
- /// This method is safe to call from any thread; if not currently on the main thread the invalidation is dispatched to the main queue.
513
359
  internal func stopCurrentTimeUpdates() {
514
- if Thread.isMainThread {
515
- currentTimeTimer?.invalidate()
516
- currentTimeTimer = nil
517
- } else {
518
- DispatchQueue.main.async { [weak self] in
519
- self?.currentTimeTimer?.invalidate()
520
- self?.currentTimeTimer = nil
521
- }
360
+ DispatchQueue.main.async { [weak self] in
361
+ guard let self else { return }
362
+ logger.debug("Stop current time updates")
363
+ self.currentTimeTimer?.invalidate()
364
+ self.currentTimeTimer = nil
522
365
  }
523
366
  }
367
+
524
368
  }