@neoskola/auto-play 0.3.4 → 0.3.5

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.
@@ -3,7 +3,7 @@ import MediaPlayer
3
3
  import AVFoundation
4
4
 
5
5
  class NowPlayingTemplate: AutoPlayTemplate {
6
- var template: CPNowPlayingTemplate
6
+ var template: CPListTemplate
7
7
  var config: NowPlayingTemplateConfig
8
8
  private var loadedImage: UIImage?
9
9
  private var isSetupComplete = false
@@ -34,19 +34,43 @@ class NowPlayingTemplate: AutoPlayTemplate {
34
34
  init(config: NowPlayingTemplateConfig) {
35
35
  self.config = config
36
36
 
37
- template = CPNowPlayingTemplate.shared
38
- initTemplate(template: template, id: config.id)
37
+ // Custom player screen using CPListTemplate instead of CPNowPlayingTemplate.shared
38
+ let titleText = Parser.parseText(text: config.title) ?? "Now Playing"
39
+ let subtitleText = config.subtitle.flatMap { Parser.parseText(text: $0) } ?? ""
40
+
41
+ let infoItem = CPListItem(
42
+ text: titleText,
43
+ detailText: subtitleText,
44
+ image: UIImage(systemName: "music.note"),
45
+ accessoryImage: nil,
46
+ accessoryType: .none
47
+ )
48
+
49
+ let statusItem = CPListItem(
50
+ text: "Yükleniyor...",
51
+ detailText: nil,
52
+ image: UIImage(systemName: "arrow.down.circle"),
53
+ accessoryImage: nil,
54
+ accessoryType: .none
55
+ )
56
+
57
+ let section = CPListSection(
58
+ items: [infoItem, statusItem],
59
+ header: nil,
60
+ sectionIndexTitle: nil
61
+ )
62
+
63
+ template = CPListTemplate(
64
+ title: "Now Playing",
65
+ sections: [section],
66
+ assistantCellConfiguration: nil,
67
+ id: config.id
68
+ )
39
69
 
40
- // Constructor runs on the JS thread. Dispatch all CarPlay UI setup to main thread.
41
- // CPNowPlayingTemplate.shared is Apple's singleton — must be modified on main thread.
42
70
  DispatchQueue.main.async { [weak self] in
43
71
  guard let self = self else { return }
44
-
45
- // Activate AVAudioSession FIRST — iOS needs this to recognize the app
46
- // as a media player before MPNowPlayingInfoCenter metadata is meaningful.
47
72
  NowPlayingSessionManager.shared.ensureSessionActive()
48
-
49
- self.setupNowPlayingButtons()
73
+ self.setupRemoteCommandCenter()
50
74
  self.updateNowPlayingInfo()
51
75
  self.isSetupComplete = true
52
76
 
@@ -56,6 +80,56 @@ class NowPlayingTemplate: AutoPlayTemplate {
56
80
  }
57
81
  }
58
82
 
83
+ // MARK: - Player UI
84
+
85
+ private func updatePlayerUI() {
86
+ let titleText = Parser.parseText(text: config.title) ?? "Now Playing"
87
+ let subtitleText = config.subtitle.flatMap { Parser.parseText(text: $0) } ?? ""
88
+
89
+ let infoItem = CPListItem(
90
+ text: titleText,
91
+ detailText: subtitleText,
92
+ image: loadedImage ?? UIImage(systemName: "music.note"),
93
+ accessoryImage: nil,
94
+ accessoryType: .none
95
+ )
96
+
97
+ let statusText: String
98
+ let statusIcon: String
99
+ if config.isPlaying {
100
+ let elapsed = formatTime(currentElapsedTime)
101
+ let total = currentDuration > 0 ? formatTime(currentDuration) : "--:--"
102
+ statusText = "Playing \(elapsed) / \(total)"
103
+ statusIcon = "play.circle.fill"
104
+ } else {
105
+ statusText = "Paused"
106
+ statusIcon = "pause.circle.fill"
107
+ }
108
+
109
+ let statusItem = CPListItem(
110
+ text: statusText,
111
+ detailText: nil,
112
+ image: UIImage(systemName: statusIcon),
113
+ accessoryImage: nil,
114
+ accessoryType: .none
115
+ )
116
+
117
+ let section = CPListSection(
118
+ items: [infoItem, statusItem],
119
+ header: nil,
120
+ sectionIndexTitle: nil
121
+ )
122
+
123
+ template.updateSections([section])
124
+ }
125
+
126
+ private func formatTime(_ seconds: Double) -> String {
127
+ guard !seconds.isNaN && !seconds.isInfinite && seconds >= 0 else { return "0:00" }
128
+ let mins = Int(seconds) / 60
129
+ let secs = Int(seconds) % 60
130
+ return String(format: "%d:%02d", mins, secs)
131
+ }
132
+
59
133
  // MARK: - Native Audio Playback
60
134
 
61
135
  @MainActor
@@ -73,12 +147,11 @@ class NowPlayingTemplate: AutoPlayTemplate {
73
147
  NowPlayingSessionManager.shared.ensureSessionActive()
74
148
  config.isPlaying = true
75
149
  updateNowPlayingInfo()
150
+ updatePlayerUI()
76
151
  MPNowPlayingInfoCenter.default().playbackState = .playing
77
152
 
78
153
  print("[NowPlayingTemplate] Downloading audio: \(url)")
79
154
 
80
- // Download file first, then play from local — R2/Cloudflare CDN streaming
81
- // causes FigFilePlayer errors with AVPlayer, local playback is reliable
82
155
  downloadTask = URLSession.shared.downloadTask(with: audioURL) { [weak self] tempURL, response, error in
83
156
  guard let self = self else { return }
84
157
 
@@ -92,7 +165,6 @@ class NowPlayingTemplate: AutoPlayTemplate {
92
165
  return
93
166
  }
94
167
 
95
- // Move to a persistent temp location (URLSession deletes the file after this block)
96
168
  let localURL = FileManager.default.temporaryDirectory
97
169
  .appendingPathComponent("carplay_audio_\(UUID().uuidString).mp3")
98
170
 
@@ -121,7 +193,6 @@ class NowPlayingTemplate: AutoPlayTemplate {
121
193
  let asset = AVURLAsset(url: localURL)
122
194
  playerItem = AVPlayerItem(asset: asset)
123
195
 
124
- // KVO: detect duration as soon as asset loads (critical for progress bar)
125
196
  statusObservation = playerItem?.observe(\.status, options: [.new]) { [weak self] item, _ in
126
197
  guard item.status == .readyToPlay else {
127
198
  if item.status == .failed {
@@ -135,6 +206,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
135
206
  if !duration.isNaN && !duration.isInfinite && duration > 0 {
136
207
  self.currentDuration = duration
137
208
  self.updateNowPlayingInfo()
209
+ self.updatePlayerUI()
138
210
  print("[NowPlayingTemplate] Duration resolved via KVO: \(duration)s")
139
211
  }
140
212
  }
@@ -147,7 +219,6 @@ class NowPlayingTemplate: AutoPlayTemplate {
147
219
  player?.seek(to: time)
148
220
  }
149
221
 
150
- // Periodic time observer (every 1 second) for MPNowPlayingInfoCenter updates
151
222
  let interval = CMTime(seconds: 1.0, preferredTimescale: 600)
152
223
  timeObserver = player?.addPeriodicTimeObserver(
153
224
  forInterval: interval,
@@ -156,7 +227,6 @@ class NowPlayingTemplate: AutoPlayTemplate {
156
227
  self?.handleTimeUpdate(time: time)
157
228
  }
158
229
 
159
- // Playback finished notification
160
230
  didFinishObserver = NotificationCenter.default.addObserver(
161
231
  forName: .AVPlayerItemDidPlayToEndTime,
162
232
  object: playerItem,
@@ -165,7 +235,6 @@ class NowPlayingTemplate: AutoPlayTemplate {
165
235
  self?.handlePlaybackFinished()
166
236
  }
167
237
 
168
- // Progress report timer (every 30 seconds) — calls JS callback for backend reporting
169
238
  progressReportTimer = Timer.scheduledTimer(
170
239
  withTimeInterval: 30.0,
171
240
  repeats: true
@@ -190,10 +259,8 @@ class NowPlayingTemplate: AutoPlayTemplate {
190
259
  currentDuration = duration
191
260
  }
192
261
 
193
- // Use existing info or create fresh if nil (race condition safety)
194
262
  var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
195
263
 
196
- // Ensure title/artist are present if this is a fresh dictionary
197
264
  if nowPlayingInfo[MPMediaItemPropertyTitle] == nil {
198
265
  let titleText = Parser.parseText(text: config.title) ?? ""
199
266
  nowPlayingInfo[MPMediaItemPropertyTitle] = titleText
@@ -209,6 +276,9 @@ class NowPlayingTemplate: AutoPlayTemplate {
209
276
  nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
210
277
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
211
278
 
279
+ // Update custom player UI
280
+ updatePlayerUI()
281
+
212
282
  // 95% completion check
213
283
  if !completionFired && currentDuration > 0 && currentTime / currentDuration >= 0.95 {
214
284
  completionFired = true
@@ -226,7 +296,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
226
296
  private func handlePlaybackFinished() {
227
297
  config.isPlaying = false
228
298
  MPNowPlayingInfoCenter.default().playbackState = .stopped
229
- // Fire completion if not already fired
299
+ updatePlayerUI()
230
300
  if !completionFired {
231
301
  completionFired = true
232
302
  config.onComplete?()
@@ -239,6 +309,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
239
309
  player?.pause()
240
310
  config.isPlaying = false
241
311
  updatePlaybackState(isPlaying: false)
312
+ updatePlayerUI()
242
313
  reportProgress()
243
314
  }
244
315
 
@@ -248,6 +319,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
248
319
  player?.play()
249
320
  config.isPlaying = true
250
321
  updatePlaybackState(isPlaying: true)
322
+ updatePlayerUI()
251
323
  }
252
324
 
253
325
  @MainActor
@@ -272,6 +344,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
272
344
  cleanupPlayer()
273
345
  config.isPlaying = false
274
346
  MPNowPlayingInfoCenter.default().playbackState = .stopped
347
+ updatePlayerUI()
275
348
  }
276
349
 
277
350
  private func cleanupPlayer() {
@@ -298,29 +371,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
298
371
  }
299
372
  }
300
373
 
301
- // MARK: - CarPlay UI
302
-
303
- private func setupNowPlayingButtons() {
304
- var buttons: [CPNowPlayingButton] = []
305
-
306
- let skipBackButton = CPNowPlayingImageButton(
307
- image: UIImage(systemName: "gobackward.30")!
308
- ) { [weak self] _ in
309
- self?.seekBackward(seconds: 30)
310
- self?.config.onSkipBackward?()
311
- }
312
- buttons.append(skipBackButton)
313
-
314
- let skipForwardButton = CPNowPlayingImageButton(
315
- image: UIImage(systemName: "goforward.30")!
316
- ) { [weak self] _ in
317
- self?.seekForward(seconds: 30)
318
- self?.config.onSkipForward?()
319
- }
320
- buttons.append(skipForwardButton)
321
-
322
- template.updateNowPlayingButtons(buttons)
323
- }
374
+ // MARK: - Now Playing Info & Remote Commands
324
375
 
325
376
  private func updateNowPlayingInfo() {
326
377
  let titleText = Parser.parseText(text: config.title) ?? ""
@@ -339,7 +390,6 @@ class NowPlayingTemplate: AutoPlayTemplate {
339
390
  nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
340
391
  }
341
392
 
342
- // Include duration and elapsed time for the progress bar
343
393
  if currentDuration > 0 {
344
394
  nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = currentDuration
345
395
  nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentElapsedTime
@@ -347,8 +397,6 @@ class NowPlayingTemplate: AutoPlayTemplate {
347
397
  }
348
398
 
349
399
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
350
-
351
- setupRemoteCommandCenter()
352
400
  }
353
401
 
354
402
  private func setupRemoteCommandCenter() {
@@ -399,6 +447,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
399
447
  self.player?.seek(to: time)
400
448
  self.currentElapsedTime = positionEvent.positionTime
401
449
  self.updateNowPlayingInfo()
450
+ self.updatePlayerUI()
402
451
  return .success
403
452
  }
404
453
  }
@@ -414,6 +463,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
414
463
  DispatchQueue.main.async {
415
464
  self.loadedImage = uiImage
416
465
  self.updateNowPlayingInfo()
466
+ self.updatePlayerUI()
417
467
  }
418
468
  }.resume()
419
469
  }
@@ -423,7 +473,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
423
473
 
424
474
  @MainActor
425
475
  func invalidate() {
426
- setupNowPlayingButtons()
476
+ updatePlayerUI()
427
477
  updateNowPlayingInfo()
428
478
 
429
479
  if loadedImage == nil, let image = config.image {
@@ -458,7 +508,6 @@ class NowPlayingTemplate: AutoPlayTemplate {
458
508
  commandCenter.skipBackwardCommand.removeTarget(nil)
459
509
  commandCenter.changePlaybackPositionCommand.removeTarget(nil)
460
510
 
461
- // Clear now playing info so CarPlay hides the Now Playing bar
462
511
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
463
512
  MPNowPlayingInfoCenter.default().playbackState = .stopped
464
513
  }
@@ -466,19 +515,14 @@ class NowPlayingTemplate: AutoPlayTemplate {
466
515
  @MainActor
467
516
  func updatePlaybackState(isPlaying: Bool) {
468
517
  config.isPlaying = isPlaying
469
-
470
- // Ensure AVAudioSession is active — required for CarPlay Now Playing bar
471
518
  NowPlayingSessionManager.shared.ensureSessionActive()
472
519
 
473
- // If constructor's DispatchQueue.main.async hasn't run yet,
474
- // nowPlayingInfo could be nil. Set it up now to fix the race condition.
475
520
  if !isSetupComplete {
476
- setupNowPlayingButtons()
521
+ updatePlayerUI()
477
522
  updateNowPlayingInfo()
478
523
  isSetupComplete = true
479
524
  }
480
525
 
481
- // Update playback rate — use existing info or create fresh if nil
482
526
  var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
483
527
 
484
528
  if nowPlayingInfo[MPMediaItemPropertyTitle] == nil {
@@ -501,6 +545,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
501
545
  config.title = AutoText(text: title, distance: nil, duration: nil)
502
546
  config.subtitle = AutoText(text: subtitle, distance: nil, duration: nil)
503
547
  updateNowPlayingInfo()
548
+ updatePlayerUI()
504
549
  }
505
550
 
506
551
  @MainActor
@@ -508,7 +553,6 @@ class NowPlayingTemplate: AutoPlayTemplate {
508
553
  self.currentElapsedTime = elapsedTime
509
554
  self.currentDuration = duration
510
555
 
511
- // Update time-related fields — use existing info or create fresh if nil
512
556
  var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
513
557
 
514
558
  if nowPlayingInfo[MPMediaItemPropertyTitle] == nil {
@@ -523,5 +567,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
523
567
  nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = elapsedTime
524
568
  nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
525
569
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
570
+
571
+ updatePlayerUI()
526
572
  }
527
573
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neoskola/auto-play",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Android Auto and Apple CarPlay for react-native",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",