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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
76
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
84
|
-
"@types/react-dom": "19.
|
|
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.
|
|
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.
|
|
93
|
-
"react-dom": "19.
|
|
94
|
-
"react-native": "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.
|
|
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",
|