@neoskola/auto-play 0.3.0 → 0.3.2

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.
@@ -17,7 +17,11 @@ class NowPlayingTemplate: AutoPlayTemplate {
17
17
  private var progressReportTimer: Timer?
18
18
  private var lastReportedSecond: Int = 0
19
19
  private var didFinishObserver: NSObjectProtocol?
20
+ private var statusObservation: NSKeyValueObservation?
20
21
  private var completionFired = false
22
+ private var downloadTask: URLSessionDownloadTask?
23
+ private var localAudioFileURL: URL?
24
+ private var pendingStartFrom: Double = 0
21
25
 
22
26
  var autoDismissMs: Double? {
23
27
  return config.autoDismissMs
@@ -60,6 +64,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
60
64
  cleanupPlayer()
61
65
  completionFired = false
62
66
  lastReportedSecond = Int(startFrom)
67
+ pendingStartFrom = startFrom
63
68
 
64
69
  guard let audioURL = URL(string: url) else {
65
70
  print("[NowPlayingTemplate] Invalid audio URL: \(url)")
@@ -69,8 +74,77 @@ class NowPlayingTemplate: AutoPlayTemplate {
69
74
  // Ensure AVAudioSession is active
70
75
  NowPlayingSessionManager.shared.ensureSessionActive()
71
76
 
72
- let asset = AVURLAsset(url: audioURL)
77
+ // Update NowPlaying UI immediately (before download completes)
78
+ config.isPlaying = true
79
+ updateNowPlayingInfo()
80
+ MPNowPlayingInfoCenter.default().playbackState = .playing
81
+
82
+ print("[NowPlayingTemplate] Downloading audio: \(url)")
83
+
84
+ // Download file first, then play from local — avoids FigFilePlayer streaming errors
85
+ downloadTask = URLSession.shared.downloadTask(with: audioURL) { [weak self] tempURL, response, error in
86
+ guard let self = self else { return }
87
+
88
+ if let error = error {
89
+ print("[NowPlayingTemplate] Download failed: \(error.localizedDescription)")
90
+ return
91
+ }
92
+
93
+ guard let tempURL = tempURL else {
94
+ print("[NowPlayingTemplate] Download returned no file")
95
+ return
96
+ }
97
+
98
+ // Move to a persistent temp location (URLSession deletes the file after this block)
99
+ let localURL = FileManager.default.temporaryDirectory
100
+ .appendingPathComponent("carplay_audio_\(UUID().uuidString).mp3")
101
+
102
+ do {
103
+ // Remove old file if exists
104
+ if let oldFile = self.localAudioFileURL {
105
+ try? FileManager.default.removeItem(at: oldFile)
106
+ }
107
+ try FileManager.default.moveItem(at: tempURL, to: localURL)
108
+ self.localAudioFileURL = localURL
109
+ print("[NowPlayingTemplate] Audio downloaded to: \(localURL.lastPathComponent)")
110
+ } catch {
111
+ print("[NowPlayingTemplate] Failed to move downloaded file: \(error)")
112
+ return
113
+ }
114
+
115
+ // Start playback on main thread
116
+ DispatchQueue.main.async { [weak self] in
117
+ self?.startPlaybackFromLocalFile(localURL: localURL, startFrom: self?.pendingStartFrom ?? 0)
118
+ }
119
+ }
120
+ downloadTask?.resume()
121
+
122
+ return true
123
+ }
124
+
125
+ private func startPlaybackFromLocalFile(localURL: URL, startFrom: Double) {
126
+ let asset = AVURLAsset(url: localURL)
73
127
  playerItem = AVPlayerItem(asset: asset)
128
+
129
+ // KVO: detect duration as soon as asset loads (critical for progress bar)
130
+ statusObservation = playerItem?.observe(\.status, options: [.new]) { [weak self] item, _ in
131
+ guard item.status == .readyToPlay else {
132
+ if item.status == .failed {
133
+ print("[NowPlayingTemplate] AVPlayerItem failed: \(item.error?.localizedDescription ?? "unknown")")
134
+ }
135
+ return
136
+ }
137
+ DispatchQueue.main.async { [weak self] in
138
+ guard let self = self else { return }
139
+ let duration = CMTimeGetSeconds(item.duration)
140
+ if !duration.isNaN && !duration.isInfinite && duration > 0 {
141
+ self.currentDuration = duration
142
+ self.updateNowPlayingInfo()
143
+ print("[NowPlayingTemplate] Duration resolved via KVO: \(duration)s")
144
+ }
145
+ }
146
+ }
147
+
74
148
  player = AVPlayer(playerItem: playerItem)
75
149
 
76
150
  // Seek to start position
@@ -107,14 +181,9 @@ class NowPlayingTemplate: AutoPlayTemplate {
107
181
 
108
182
  // Play
109
183
  player?.play()
110
-
111
- // Update NowPlaying UI
112
- config.isPlaying = true
113
- updateNowPlayingInfo()
114
184
  MPNowPlayingInfoCenter.default().playbackState = .playing
115
185
 
116
- print("[NowPlayingTemplate] Native audio playback started: \(url)")
117
- return true
186
+ print("[NowPlayingTemplate] Native audio playback started from local file")
118
187
  }
119
188
 
120
189
  private func handleTimeUpdate(time: CMTime) {
@@ -124,20 +193,29 @@ class NowPlayingTemplate: AutoPlayTemplate {
124
193
  guard !currentTime.isNaN else { return }
125
194
 
126
195
  currentElapsedTime = currentTime
127
- if !duration.isNaN && duration > 0 {
196
+ if !duration.isNaN && !duration.isInfinite && duration > 0 {
128
197
  currentDuration = duration
129
198
  }
130
199
 
131
- // Update MPNowPlayingInfoCenter no JS roundtrip needed
132
- if var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo {
133
- if currentDuration > 0 {
134
- nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = currentDuration
200
+ // Use existing info or create fresh if nil (race condition safety)
201
+ var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
202
+
203
+ // Ensure title/artist are present if this is a fresh dictionary
204
+ if nowPlayingInfo[MPMediaItemPropertyTitle] == nil {
205
+ let titleText = Parser.parseText(text: config.title) ?? ""
206
+ nowPlayingInfo[MPMediaItemPropertyTitle] = titleText
207
+ if let subtitle = config.subtitle {
208
+ nowPlayingInfo[MPMediaItemPropertyArtist] = Parser.parseText(text: subtitle)
135
209
  }
136
- nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentElapsedTime
137
- nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
138
- MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
139
210
  }
140
211
 
212
+ if currentDuration > 0 {
213
+ nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = currentDuration
214
+ }
215
+ nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentElapsedTime
216
+ nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
217
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
218
+
141
219
  // 95% completion check
142
220
  if !completionFired && currentDuration > 0 && currentTime / currentDuration >= 0.95 {
143
221
  completionFired = true
@@ -204,6 +282,10 @@ class NowPlayingTemplate: AutoPlayTemplate {
204
282
  }
205
283
 
206
284
  private func cleanupPlayer() {
285
+ downloadTask?.cancel()
286
+ downloadTask = nil
287
+ statusObservation?.invalidate()
288
+ statusObservation = nil
207
289
  if let timeObserver = timeObserver {
208
290
  player?.removeTimeObserver(timeObserver)
209
291
  self.timeObserver = nil
@@ -217,6 +299,11 @@ class NowPlayingTemplate: AutoPlayTemplate {
217
299
  player?.pause()
218
300
  player = nil
219
301
  playerItem = nil
302
+ // Clean up temp audio file
303
+ if let localFile = localAudioFileURL {
304
+ try? FileManager.default.removeItem(at: localFile)
305
+ localAudioFileURL = nil
306
+ }
220
307
  }
221
308
 
222
309
  // MARK: - CarPlay UI
@@ -342,6 +429,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
342
429
 
343
430
  // MARK: - AutoPlayTemplate Protocol
344
431
 
432
+ @MainActor
345
433
  func invalidate() {
346
434
  setupNowPlayingButtons()
347
435
  updateNowPlayingInfo()
@@ -398,13 +486,21 @@ class NowPlayingTemplate: AutoPlayTemplate {
398
486
  isSetupComplete = true
399
487
  }
400
488
 
401
- // Update playback rate in existing nowPlayingInfo
402
- if var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo {
403
- nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0
404
- nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentElapsedTime
405
- MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
489
+ // Update playback rate use existing info or create fresh if nil
490
+ var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
491
+
492
+ if nowPlayingInfo[MPMediaItemPropertyTitle] == nil {
493
+ let titleText = Parser.parseText(text: config.title) ?? ""
494
+ nowPlayingInfo[MPMediaItemPropertyTitle] = titleText
495
+ if let subtitle = config.subtitle {
496
+ nowPlayingInfo[MPMediaItemPropertyArtist] = Parser.parseText(text: subtitle)
497
+ }
406
498
  }
407
499
 
500
+ nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0
501
+ nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentElapsedTime
502
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
503
+
408
504
  MPNowPlayingInfoCenter.default().playbackState = isPlaying ? .playing : .paused
409
505
  }
410
506
 
@@ -420,12 +516,20 @@ class NowPlayingTemplate: AutoPlayTemplate {
420
516
  self.currentElapsedTime = elapsedTime
421
517
  self.currentDuration = duration
422
518
 
423
- // Update only time-related fields without rebuilding everything
424
- if var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo {
425
- nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
426
- nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = elapsedTime
427
- nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
428
- MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
519
+ // Update time-related fields use existing info or create fresh if nil
520
+ var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
521
+
522
+ if nowPlayingInfo[MPMediaItemPropertyTitle] == nil {
523
+ let titleText = Parser.parseText(text: config.title) ?? ""
524
+ nowPlayingInfo[MPMediaItemPropertyTitle] = titleText
525
+ if let subtitle = config.subtitle {
526
+ nowPlayingInfo[MPMediaItemPropertyArtist] = Parser.parseText(text: subtitle)
527
+ }
429
528
  }
529
+
530
+ nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
531
+ nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = elapsedTime
532
+ nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
533
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
430
534
  }
431
535
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neoskola/auto-play",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Android Auto and Apple CarPlay for react-native",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",