@neoskola/auto-play 0.3.17 → 0.3.19

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.
@@ -7,6 +7,11 @@
7
7
 
8
8
  import CarPlay
9
9
 
10
+ private struct NowPlayingPushResult {
11
+ let success: Bool
12
+ let errorDescription: String?
13
+ }
14
+
10
15
  @MainActor
11
16
  class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
12
17
  let interfaceController: CPInterfaceController
@@ -61,11 +66,28 @@ class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
61
66
  return true
62
67
  }
63
68
 
64
- let success = await pushNowPlayingTemplateSafely(animated: animated)
65
- if !success {
66
- throw AutoPlayError.pushFailed("CPNowPlayingTemplate push failed")
69
+ let firstAttempt = await pushNowPlayingTemplateSafely(animated: animated)
70
+ if firstAttempt.success || interfaceController.topTemplate is CPNowPlayingTemplate {
71
+ return true
67
72
  }
68
- return true
73
+
74
+ // CarPlay can reject pushes while handling list selection transitions.
75
+ // Retry once after a short delay to avoid transient failures.
76
+ try? await Task.sleep(nanoseconds: 250_000_000)
77
+ let secondAttempt = await pushNowPlayingTemplateSafely(animated: animated)
78
+ if secondAttempt.success || interfaceController.topTemplate is CPNowPlayingTemplate {
79
+ return true
80
+ }
81
+
82
+ let reason = secondAttempt.errorDescription
83
+ ?? firstAttempt.errorDescription
84
+ ?? "unknown reason"
85
+ let topTemplateDescription = interfaceController.topTemplate.map {
86
+ String(describing: type(of: $0))
87
+ } ?? "nil"
88
+ throw AutoPlayError.pushFailed(
89
+ "CPNowPlayingTemplate push failed (\(reason)). top=\(topTemplateDescription) stackCount=\(interfaceController.templates.count)"
90
+ )
69
91
  }
70
92
 
71
93
  return try await interfaceController.pushTemplate(
@@ -76,7 +98,7 @@ class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
76
98
 
77
99
  /// Pushes CPNowPlayingTemplate using ObjC @try/@catch to prevent crash from NSException.
78
100
  /// Uses a completion handler to confirm the push actually succeeded.
79
- private func pushNowPlayingTemplateSafely(animated: Bool) async -> Bool {
101
+ private func pushNowPlayingTemplateSafely(animated: Bool) async -> NowPlayingPushResult {
80
102
  return await withCheckedContinuation { continuation in
81
103
  do {
82
104
  try ObjCExceptionCatcher.push(
@@ -88,12 +110,22 @@ class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
88
110
  print("[AutoPlay] CPNowPlayingTemplate push completion error: \(error.localizedDescription)")
89
111
  }
90
112
  print("[AutoPlay] CPNowPlayingTemplate push completion: success=\(success)")
91
- continuation.resume(returning: success)
113
+ continuation.resume(
114
+ returning: NowPlayingPushResult(
115
+ success: success,
116
+ errorDescription: error?.localizedDescription
117
+ )
118
+ )
92
119
  }
93
120
  )
94
121
  } catch {
95
122
  print("[AutoPlay] CPNowPlayingTemplate push threw ObjC exception: \(error.localizedDescription)")
96
- continuation.resume(returning: false)
123
+ continuation.resume(
124
+ returning: NowPlayingPushResult(
125
+ success: false,
126
+ errorDescription: error.localizedDescription
127
+ )
128
+ )
97
129
  }
98
130
  }
99
131
  }
@@ -2,7 +2,8 @@ import CarPlay
2
2
  import MediaPlayer
3
3
  import AVFoundation
4
4
 
5
- class NowPlayingTemplate: NSObject, AutoPlayTemplate {
5
+ class NowPlayingTemplate: AutoPlayTemplate {
6
+ var template: CPListTemplate
6
7
  var config: NowPlayingTemplateConfig
7
8
  private var loadedImage: UIImage?
8
9
  private var isSetupComplete = false
@@ -27,33 +28,106 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
27
28
  }
28
29
 
29
30
  func getTemplate() -> CPTemplate {
30
- return CPNowPlayingTemplate.shared
31
+ return template
31
32
  }
32
33
 
33
34
  init(config: NowPlayingTemplateConfig) {
34
35
  self.config = config
35
36
 
36
- super.init()
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
+ )
69
+
70
+ DispatchQueue.main.async { [weak self] in
71
+ guard let self = self else { return }
72
+ NowPlayingSessionManager.shared.ensureSessionActive()
73
+ self.setupRemoteCommandCenter()
74
+ self.updateNowPlayingInfo()
75
+ self.isSetupComplete = true
37
76
 
38
- // Set the config ID on the shared singleton so TemplateStore can find it
39
- initTemplate(template: CPNowPlayingTemplate.shared, id: config.id)
77
+ if let image = config.image {
78
+ self.loadImageAsync(image: image)
79
+ }
80
+ }
81
+ }
40
82
 
41
- // Setup remote commands and now playing info synchronously
42
- // so they're ready before CPNowPlayingTemplate.shared is pushed
43
- NowPlayingSessionManager.shared.ensureSessionActive()
44
- setupRemoteCommandCenter()
45
- updateNowPlayingInfo()
83
+ // MARK: - Player UI
46
84
 
47
- // Set playbackState before push so CarPlay sees an active now-playing session
48
- if config.isPlaying {
49
- MPNowPlayingInfoCenter.default().playbackState = .playing
50
- }
85
+ private func updatePlayerUI() {
86
+ let titleText = Parser.parseText(text: config.title) ?? "Now Playing"
87
+ let subtitleText = config.subtitle.flatMap { Parser.parseText(text: $0) } ?? ""
51
88
 
52
- isSetupComplete = true
89
+ let infoItem = CPListItem(
90
+ text: titleText,
91
+ detailText: subtitleText,
92
+ image: loadedImage ?? UIImage(systemName: "music.note"),
93
+ accessoryImage: nil,
94
+ accessoryType: .none
95
+ )
53
96
 
54
- if let image = config.image {
55
- loadImageAsync(image: image)
56
- }
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)
57
131
  }
58
132
 
59
133
  // MARK: - Native Audio Playback
@@ -73,6 +147,7 @@ class NowPlayingTemplate: NSObject, 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)")
@@ -131,6 +206,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
131
206
  if !duration.isNaN && !duration.isInfinite && duration > 0 {
132
207
  self.currentDuration = duration
133
208
  self.updateNowPlayingInfo()
209
+ self.updatePlayerUI()
134
210
  print("[NowPlayingTemplate] Duration resolved via KVO: \(duration)s")
135
211
  }
136
212
  }
@@ -200,6 +276,9 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
200
276
  nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
201
277
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
202
278
 
279
+ // Update custom player UI
280
+ updatePlayerUI()
281
+
203
282
  // 95% completion check
204
283
  if !completionFired && currentDuration > 0 && currentTime / currentDuration >= 0.95 {
205
284
  completionFired = true
@@ -217,6 +296,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
217
296
  private func handlePlaybackFinished() {
218
297
  config.isPlaying = false
219
298
  MPNowPlayingInfoCenter.default().playbackState = .stopped
299
+ updatePlayerUI()
220
300
  if !completionFired {
221
301
  completionFired = true
222
302
  config.onComplete?()
@@ -229,6 +309,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
229
309
  player?.pause()
230
310
  config.isPlaying = false
231
311
  updatePlaybackState(isPlaying: false)
312
+ updatePlayerUI()
232
313
  reportProgress()
233
314
  }
234
315
 
@@ -238,6 +319,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
238
319
  player?.play()
239
320
  config.isPlaying = true
240
321
  updatePlaybackState(isPlaying: true)
322
+ updatePlayerUI()
241
323
  }
242
324
 
243
325
  @MainActor
@@ -262,6 +344,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
262
344
  cleanupPlayer()
263
345
  config.isPlaying = false
264
346
  MPNowPlayingInfoCenter.default().playbackState = .stopped
347
+ updatePlayerUI()
265
348
  }
266
349
 
267
350
  private func cleanupPlayer() {
@@ -307,10 +390,11 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
307
390
  nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
308
391
  }
309
392
 
393
+ nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentElapsedTime
394
+ nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
395
+
310
396
  if currentDuration > 0 {
311
397
  nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = currentDuration
312
- nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentElapsedTime
313
- nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
314
398
  }
315
399
 
316
400
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
@@ -378,6 +462,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
378
462
  self.player?.seek(to: time)
379
463
  self.currentElapsedTime = positionEvent.positionTime
380
464
  self.updateNowPlayingInfo()
465
+ self.updatePlayerUI()
381
466
  return .success
382
467
  }
383
468
  }
@@ -393,6 +478,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
393
478
  DispatchQueue.main.async {
394
479
  self.loadedImage = uiImage
395
480
  self.updateNowPlayingInfo()
481
+ self.updatePlayerUI()
396
482
  }
397
483
  }.resume()
398
484
  }
@@ -402,6 +488,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
402
488
 
403
489
  @MainActor
404
490
  func invalidate() {
491
+ updatePlayerUI()
405
492
  updateNowPlayingInfo()
406
493
 
407
494
  if loadedImage == nil, let image = config.image {
@@ -448,6 +535,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
448
535
  NowPlayingSessionManager.shared.ensureSessionActive()
449
536
 
450
537
  if !isSetupComplete {
538
+ updatePlayerUI()
451
539
  updateNowPlayingInfo()
452
540
  isSetupComplete = true
453
541
  }
@@ -474,6 +562,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
474
562
  config.title = AutoText(text: title, distance: nil, duration: nil)
475
563
  config.subtitle = AutoText(text: subtitle, distance: nil, duration: nil)
476
564
  updateNowPlayingInfo()
565
+ updatePlayerUI()
477
566
  }
478
567
 
479
568
  @MainActor
@@ -495,5 +584,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
495
584
  nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = elapsedTime
496
585
  nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
497
586
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
587
+
588
+ updatePlayerUI()
498
589
  }
499
590
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neoskola/auto-play",
3
- "version": "0.3.17",
3
+ "version": "0.3.19",
4
4
  "description": "Android Auto and Apple CarPlay for react-native",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",