@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
|
-
|
|
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
|
|
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
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
}
|