@neoskola/auto-play 0.3.16 → 0.3.18

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.
package/ios/Types.swift CHANGED
@@ -20,4 +20,5 @@ enum AutoPlayError: Error {
20
20
  case invalidTemplateType(String)
21
21
  case noUiWindow(String)
22
22
  case initReactRootViewFailed(String)
23
+ case pushFailed(String)
23
24
  }
@@ -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,7 +66,28 @@ class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
61
66
  return true
62
67
  }
63
68
 
64
- return pushNowPlayingTemplateSafely(animated: animated)
69
+ let firstAttempt = await pushNowPlayingTemplateSafely(animated: animated)
70
+ if firstAttempt.success || interfaceController.topTemplate is CPNowPlayingTemplate {
71
+ return true
72
+ }
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
+ )
65
91
  }
66
92
 
67
93
  return try await interfaceController.pushTemplate(
@@ -71,19 +97,36 @@ class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
71
97
  }
72
98
 
73
99
  /// Pushes CPNowPlayingTemplate using ObjC @try/@catch to prevent crash from NSException.
74
- /// The push call is made entirely in Objective-C so NSExceptions never propagate through Swift frames.
75
- private func pushNowPlayingTemplateSafely(animated: Bool) -> Bool {
76
- do {
77
- try ObjCExceptionCatcher.push(
78
- CPNowPlayingTemplate.shared,
79
- on: self.interfaceController,
80
- animated: animated
81
- )
82
- print("[AutoPlay] CPNowPlayingTemplate pushed successfully")
83
- return true
84
- } catch {
85
- print("[AutoPlay] CPNowPlayingTemplate push threw ObjC exception: \(error.localizedDescription)")
86
- return false
100
+ /// Uses a completion handler to confirm the push actually succeeded.
101
+ private func pushNowPlayingTemplateSafely(animated: Bool) async -> NowPlayingPushResult {
102
+ return await withCheckedContinuation { continuation in
103
+ do {
104
+ try ObjCExceptionCatcher.push(
105
+ CPNowPlayingTemplate.shared,
106
+ on: self.interfaceController,
107
+ animated: animated,
108
+ completion: { success, error in
109
+ if let error = error {
110
+ print("[AutoPlay] CPNowPlayingTemplate push completion error: \(error.localizedDescription)")
111
+ }
112
+ print("[AutoPlay] CPNowPlayingTemplate push completion: success=\(success)")
113
+ continuation.resume(
114
+ returning: NowPlayingPushResult(
115
+ success: success,
116
+ errorDescription: error?.localizedDescription
117
+ )
118
+ )
119
+ }
120
+ )
121
+ } catch {
122
+ print("[AutoPlay] CPNowPlayingTemplate push threw ObjC exception: \(error.localizedDescription)")
123
+ continuation.resume(
124
+ returning: NowPlayingPushResult(
125
+ success: false,
126
+ errorDescription: error.localizedDescription
127
+ )
128
+ )
129
+ }
87
130
  }
88
131
  }
89
132
 
@@ -43,6 +43,12 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
43
43
  NowPlayingSessionManager.shared.ensureSessionActive()
44
44
  setupRemoteCommandCenter()
45
45
  updateNowPlayingInfo()
46
+
47
+ // Set playbackState before push so CarPlay sees an active now-playing session
48
+ if config.isPlaying {
49
+ MPNowPlayingInfoCenter.default().playbackState = .playing
50
+ }
51
+
46
52
  isSetupComplete = true
47
53
 
48
54
  if let image = config.image {
@@ -301,10 +307,13 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
301
307
  nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
302
308
  }
303
309
 
310
+ // Keep playback metadata populated even before duration is known.
311
+ // This helps CarPlay recognize an active now-playing session.
312
+ nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentElapsedTime
313
+ nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
314
+
304
315
  if currentDuration > 0 {
305
316
  nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = currentDuration
306
- nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentElapsedTime
307
- nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
308
317
  }
309
318
 
310
319
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
@@ -13,4 +13,10 @@ onInterfaceController:(CPInterfaceController * _Nonnull)interfaceController
13
13
  animated:(BOOL)animated
14
14
  error:(NSError * _Nullable * _Nullable)error;
15
15
 
16
+ + (BOOL)pushTemplate:(CPTemplate * _Nonnull)templateToPush
17
+ onInterfaceController:(CPInterfaceController * _Nonnull)interfaceController
18
+ animated:(BOOL)animated
19
+ completion:(void (^ _Nullable)(BOOL success, NSError * _Nullable error))completion
20
+ error:(NSError * _Nullable * _Nullable)error;
21
+
16
22
  @end
@@ -23,4 +23,26 @@ onInterfaceController:(CPInterfaceController *)interfaceController
23
23
  }
24
24
  }
25
25
 
26
+ + (BOOL)pushTemplate:(CPTemplate *)templateToPush
27
+ onInterfaceController:(CPInterfaceController *)interfaceController
28
+ animated:(BOOL)animated
29
+ completion:(void (^)(BOOL, NSError *))completion
30
+ error:(NSError **)error {
31
+ @try {
32
+ [interfaceController pushTemplate:templateToPush animated:animated completion:completion];
33
+ return YES;
34
+ } @catch (NSException *exception) {
35
+ NSLog(@"[ObjCExceptionCatcher] Caught exception during pushTemplate: %@ — %@", exception.name, exception.reason);
36
+ if (error) {
37
+ *error = [NSError errorWithDomain:@"ObjCExceptionCatcher"
38
+ code:0
39
+ userInfo:@{
40
+ NSLocalizedDescriptionKey: exception.reason ?: @"Unknown Objective-C exception",
41
+ @"ExceptionName": exception.name ?: @"Unknown"
42
+ }];
43
+ }
44
+ return NO;
45
+ }
46
+ }
47
+
26
48
  @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neoskola/auto-play",
3
- "version": "0.3.16",
3
+ "version": "0.3.18",
4
4
  "description": "Android Auto and Apple CarPlay for react-native",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",