react-native-audio-browser 0.2.0-next.14 → 0.2.0-next.15

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.
@@ -460,8 +460,14 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
460
460
  browserManager.config = buildConfig()
461
461
 
462
462
  // Notify player that browser configuration is ready (routes/tabs available)
463
- // This allows Android Auto to start browsing content
464
- connectedService?.player?.notifyBrowserConfigurationReady()
463
+ // This allows Android Auto to start browsing content.
464
+ // This reaches MediaSession.getConnectedControllers, which asserts the
465
+ // main thread, and this setter runs on the JS thread. Hop without
466
+ // blocking -- runBlockingOnMain would deadlock here for the same reason
467
+ // described for the navigation below (JS callbacks can't run while the
468
+ // JS thread waits). Ordering with navigationJob is preserved: both are
469
+ // queued on mainScope.
470
+ launchInScope { connectedService?.player?.notifyBrowserConfigurationReady() }
465
471
 
466
472
  clearNavigationError()
467
473
 
@@ -777,8 +783,9 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
777
783
  // Invalidate cached content so future navigations fetch fresh data
778
784
  browserManager.invalidateContentCache(path)
779
785
 
780
- // Notify external media controllers (Android Auto)
781
- connectedService?.player?.notifyContentChanged(path)
786
+ // Notify external media controllers (Android Auto). Reaches MediaSession,
787
+ // which asserts the main thread; this override runs on the JS thread.
788
+ launchInScope { connectedService?.player?.notifyContentChanged(path) }
782
789
 
783
790
  // If we're currently viewing this path, refresh the content
784
791
  if (browserManager.getPath() == path) {
@@ -792,8 +799,10 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
792
799
  // Clear all cached content so every path re-fetches fresh data
793
800
  browserManager.clearContentCache()
794
801
 
795
- // Notify external media controllers (Android Auto) to refresh subscribed paths
796
- connectedService?.player?.invalidateAllContent()
802
+ // Notify external media controllers (Android Auto) to refresh subscribed paths.
803
+ // Reaches MediaSession, which asserts the main thread; this override runs on
804
+ // the JS thread.
805
+ launchInScope { connectedService?.player?.invalidateAllContent() }
797
806
 
798
807
  // Refresh whatever the browser is currently viewing
799
808
  mainScope.launch { browserManager.refresh() }
@@ -834,14 +843,16 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
834
843
  // Re-query every subscribed parent so a connected controller (Android
835
844
  // Auto) swaps its lists for the gate tile without reconnecting. Notify
836
845
  // only — the content cache stays warm for when the gate clears.
837
- connectedService?.player?.invalidateAllContent()
846
+ // MediaSession asserts the main thread; this override runs on the JS thread.
847
+ launchInScope { connectedService?.player?.invalidateAllContent() }
838
848
  }
839
849
 
840
850
  override fun clearGate() {
841
851
  if (!gateState.active) return
842
852
  // Single atomic assignment — readers never see a half-cleared triple.
843
853
  gateState = GateState(active = false, chrome = null, hasResolver = false)
844
- connectedService?.player?.invalidateAllContent()
854
+ // MediaSession asserts the main thread; this override runs on the JS thread.
855
+ launchInScope { connectedService?.player?.invalidateAllContent() }
845
856
  }
846
857
 
847
858
  /**
@@ -927,8 +938,17 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
927
938
 
928
939
  override fun updateOptions(options: NativeUpdateOptions) {
929
940
  updateOptions.updateFromBridge(options)
930
- // Only update the options if the service is around
931
- connectedService?.let { player.applyOptions(updateOptions) }
941
+ // Only update the options if the service is around.
942
+ // applyOptions touches the Media3 player, which asserts it is only ever
943
+ // driven from the main thread ("Player callback method is called from a
944
+ // wrong thread"). Nitro invokes this from the JS thread, so hop.
945
+ //
946
+ // The hop blocks rather than launching because `updateOptions` is a single
947
+ // mutable holder: updateFromBridge mutates it in place on the JS thread and
948
+ // applyOptions reads it on main. Blocking is what keeps the two ordered.
949
+ connectedService?.let { service ->
950
+ runBlockingOnMain { service.player.applyOptions(updateOptions) }
951
+ }
932
952
  }
933
953
 
934
954
  override fun getOptions(): Options {
@@ -72,8 +72,10 @@ final class CarPlayImageLoader {
72
72
  return
73
73
  }
74
74
 
75
- // Build URL resolver closure that wraps BrowserManager
76
- let urlResolver: ((Double, Double) async -> ArtworkResolvedImage?)? = browserManager.map { bm in
75
+ // The annotation goes on the closure's return type, not the binding:
76
+ // @Sendable isn't inferred through `map`, and omitting it fails with
77
+ // "type of expression is ambiguous" — which doesn't mention @Sendable.
78
+ let urlResolver = browserManager.map { bm -> @Sendable (Double, Double) async -> ArtworkResolvedImage? in
77
79
  { [weak bm] pixelWidth, pixelHeight in
78
80
  guard let bm else { return nil }
79
81
  let imageContext = ImageContext(width: pixelWidth, height: pixelHeight)
@@ -259,7 +259,7 @@ final class CarPlayListItemFactory {
259
259
  // API (the elements are captured per availability branch — their type
260
260
  // doesn't exist before iOS 26). nil = the legacy gridImages path.
261
261
  // @MainActor makes the closure Sendable for the artwork completion hop.
262
- var applyImage: (@MainActor (Int, UIImage) -> Void)?
262
+ var applyImage: (@MainActor @Sendable (Int, UIImage) -> Void)?
263
263
  // Resolved once per track, shared by element construction and the
264
264
  // artwork loads below.
265
265
  let resolvedStyles = tracks.map { StyleResolver.trackStyle(track: $0.style, section: style) }
@@ -280,7 +280,7 @@ final class CarPlayListItemFactory {
280
280
  }
281
281
  // The image setter every element family shares (`image` is on the base
282
282
  // class); bounds-checked against the elements the row was built with.
283
- func imageApplier(_ elements: [some CPListImageRowItemElement]) -> @MainActor (Int, UIImage) -> Void {
283
+ func imageApplier(_ elements: [some CPListImageRowItemElement]) -> @MainActor @Sendable (Int, UIImage) -> Void {
284
284
  { index, image in
285
285
  guard index < elements.count else { return }
286
286
  elements[index].image = image
@@ -386,7 +386,7 @@ final class CarPlayListItemFactory {
386
386
  for: track,
387
387
  style: resolved,
388
388
  size: Self.tileImageSize(gridTile: style.gridTile, cardImage: resolved.cardImage),
389
- ) { [weak item] image in
389
+ ) { [weak item, applyImage] image in
390
390
  Task { @MainActor in
391
391
  guard let item, let image else { return }
392
392
  if let applyImage {
@@ -1,3 +1,5 @@
1
- @MainActor protocol SeekCompletionHandler: AnyObject {
1
+ // Sendable so LoadSeekCoordinator can carry a delegate into AVPlayer's
2
+ // @Sendable seek completion closure; @MainActor is what keeps that sound.
3
+ @MainActor protocol SeekCompletionHandler: AnyObject, Sendable {
2
4
  func handleSeekCompleted(to seconds: Double, didFinish: Bool)
3
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-audio-browser",
3
- "version": "0.2.0-next.14",
3
+ "version": "0.2.0-next.15",
4
4
  "description": "react-native-audio-browser is a react native package built with Nitro",
5
5
  "keywords": [
6
6
  "react-native",
@@ -60,11 +60,11 @@
60
60
  "example": "yarn workspace example-native",
61
61
  "ci:android": "yarn android:format:check && yarn android:types && yarn android:test",
62
62
  "ci:format": "oxfmt --check --ignore-path .gitignore --ignore-path .oxfmtignore .",
63
- "ci:format:ios": "swiftformat --lint ios",
63
+ "ci:format:ios": "scripts/swiftformat.sh --lint ios",
64
64
  "ci:ios": "yarn ci:format:ios && yarn ios:test",
65
65
  "ci:lint": "oxlint --type-aware src",
66
66
  "format": "oxfmt --write --ignore-path .gitignore --ignore-path .oxfmtignore .",
67
- "ios:format": "swiftformat ios",
67
+ "ios:format": "scripts/swiftformat.sh ios",
68
68
  "ios:ide": "yarn workspace example-native ios:ide",
69
69
  "ios:rebuild": "yarn workspace example-native ios:rebuild",
70
70
  "ios:test": "swift test --disable-sandbox",
@@ -80,21 +80,21 @@
80
80
  "@testing-library/dom": "^10.4.1",
81
81
  "@testing-library/react": "^16.3.2",
82
82
  "@types/node": "^22",
83
- "@types/react": "19.1.1",
84
- "@types/react-dom": "19.1.7",
83
+ "@types/react": "19.2.3",
84
+ "@types/react-dom": "^19.2.0",
85
85
  "conventional-changelog-conventionalcommits": "^9.3.0",
86
86
  "del-cli": "^5.1.0",
87
87
  "happy-dom": "^20.11.2",
88
- "nitrogen": "0.35.0",
88
+ "nitrogen": "0.35.10",
89
89
  "oxfmt": "^0.27.0",
90
90
  "oxlint": "^1.42.0",
91
91
  "oxlint-tsgolint": "^0.11.2",
92
- "react": "19.1.1",
93
- "react-dom": "19.1.1",
94
- "react-native": "0.83.0",
92
+ "react": "19.2.3",
93
+ "react-dom": "19.2.3",
94
+ "react-native": "0.86.2",
95
95
  "react-native-builder-bob": "^0.37.0",
96
96
  "react-native-monorepo-config": "^0.3.1",
97
- "react-native-nitro-modules": "0.35.0",
97
+ "react-native-nitro-modules": "0.35.10",
98
98
  "semantic-release": "^25.0.3",
99
99
  "shaka-player": "^4.15.4",
100
100
  "typescript": "^7.0.2",