react-native-audio-browser 0.2.0-next.16 → 0.2.0-next.17
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.
|
@@ -224,7 +224,11 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
224
224
|
private var updateOptions: PlayerUpdateOptions = PlayerUpdateOptions()
|
|
225
225
|
private var mediaBrowserFuture: ListenableFuture<MediaBrowser>? = null
|
|
226
226
|
private var setupOptions = PlayerSetupOptions()
|
|
227
|
-
|
|
227
|
+
// Written only on the main thread (ServiceConnection callbacks, dispose); read from main or,
|
|
228
|
+
// in a few places, straight from the JS thread. @Volatile gives those reads visibility — they
|
|
229
|
+
// must still tolerate staleness in both directions (a just-detached service, or null while
|
|
230
|
+
// onServiceConnected is pending). Keep writes on main; no read-modify-write.
|
|
231
|
+
@Volatile private var connectedService: Service? = null
|
|
228
232
|
|
|
229
233
|
private val serviceBinding = ServiceBinding(context)
|
|
230
234
|
|
|
@@ -45,8 +45,12 @@ import kotlinx.coroutines.withContext
|
|
|
45
45
|
import timber.log.Timber
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* Everything the MediaLibrarySession serves except transport: browse, search, item lookup, queue
|
|
49
|
+
* building, playback resumption, and the favorite and rating commands. Transport does not arrive
|
|
50
|
+
* here — it reaches the player through [InterceptingPlayer], and Assistant's play-from-search
|
|
51
|
+
* arrives as an intent handled in the service. The callbacks that answer through [scope] run on IO,
|
|
52
|
+
* so anything touching the ExoPlayer instance hops back to [Dispatchers.Main]; the rest answer
|
|
53
|
+
* synchronously on the Media3 application thread.
|
|
50
54
|
*/
|
|
51
55
|
class MediaSessionCallback(private val player: Player) :
|
|
52
56
|
MediaLibraryService.MediaLibrarySession.Callback {
|
|
@@ -69,14 +73,10 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
69
73
|
private set
|
|
70
74
|
|
|
71
75
|
init {
|
|
72
|
-
// Observe network state changes and notify subscribers
|
|
73
76
|
player.networkMonitor.observeOnline(scope) { _ -> notifySubscribedChildrenChanged() }
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
/**
|
|
77
|
-
* Apply pagination to a list of items. If pageSize is 0 or MAX_VALUE (Android Auto default),
|
|
78
|
-
* returns the full list.
|
|
79
|
-
*/
|
|
79
|
+
/** Media3's legacy browse bridge asks for a whole list with pageSize [Int.MAX_VALUE]. */
|
|
80
80
|
private fun <T> List<T>.paginate(page: Int, pageSize: Int): List<T> {
|
|
81
81
|
return if (pageSize in 1 until Int.MAX_VALUE) {
|
|
82
82
|
this.drop(page * pageSize).take(pageSize)
|
|
@@ -85,7 +85,6 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
/** Creates an offline error MediaItem. */
|
|
89
88
|
private fun createOfflineMediaItem(): MediaItem =
|
|
90
89
|
createErrorMediaItem(
|
|
91
90
|
mediaId = BrowserPathHelper.OFFLINE_PATH,
|
|
@@ -99,6 +98,12 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
99
98
|
* Auto / AAOS browse list. Rendered as a greyed-out, non-interactive tile, which is the only
|
|
100
99
|
* side-effect-free way to communicate a browse failure to legacy controllers (Media3 drops
|
|
101
100
|
* [LibraryResult.ofError] on the legacy browse bridge, leaving an empty "No items" screen).
|
|
101
|
+
*
|
|
102
|
+
* Tiles are the only in-browse error signal that exists. Verified on a head unit (2026-06): the
|
|
103
|
+
* Android Auto browse list renders no error text for a transient [SessionError], for sticky
|
|
104
|
+
* non-fatal replication, or for fatal replication (setLibraryErrorReplicationMode). Fatal
|
|
105
|
+
* replication does surface the message on the playback screen, but at the cost of presenting the
|
|
106
|
+
* session as STATE_ERROR with no actions, hiding the now-playing item and transport controls.
|
|
102
107
|
*/
|
|
103
108
|
private fun createErrorMediaItem(mediaId: String, title: String, subtitle: String): MediaItem =
|
|
104
109
|
MediaItem.Builder()
|
|
@@ -107,23 +112,15 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
107
112
|
MediaMetadata.Builder()
|
|
108
113
|
.setTitle(title)
|
|
109
114
|
.setSubtitle(subtitle)
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
115
|
+
// Android Auto ignores these flags and drills into a tapped tile anyway; onGetChildren
|
|
116
|
+
// returns an empty list for the sentinel paths, so it dead-ends at "No Items" rather
|
|
117
|
+
// than stacking error pages.
|
|
113
118
|
.setIsBrowsable(false)
|
|
114
119
|
.setIsPlayable(false)
|
|
115
120
|
.build()
|
|
116
121
|
)
|
|
117
122
|
.build()
|
|
118
123
|
|
|
119
|
-
/**
|
|
120
|
-
* Builds the gate tile from a per-request chrome: while a gate is active, tabs stay visible but a
|
|
121
|
-
* gated browse/search level serves this single tile (see the gate checks in [onGetChildren] /
|
|
122
|
-
* [onGetSearchResult]). Same shape as the error tiles — non-browsable, non-playable — and
|
|
123
|
-
* deliberately NOT accompanied by a SessionError: a gate is deliberate app state, not a failure.
|
|
124
|
-
* The message renders as the tile's subtitle (newlines collapse to spaces — list rows are
|
|
125
|
-
* single-line).
|
|
126
|
-
*/
|
|
127
124
|
/**
|
|
128
125
|
* Wraps a raw external-search query string into the structured [SearchParams] the gate request
|
|
129
126
|
* carries. The car search surfaces only give a free-text query, so the other fields stay null —
|
|
@@ -141,6 +138,14 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
141
138
|
reference = MediaReference.UNKNOWN,
|
|
142
139
|
)
|
|
143
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Builds the gate tile from a per-request chrome: while a gate is active, tabs stay visible but a
|
|
143
|
+
* gated browse/search level serves this single tile (see the gate checks in [onGetChildren] /
|
|
144
|
+
* [onGetSearchResult]). Same shape as the error tiles — non-browsable, non-playable — and
|
|
145
|
+
* deliberately NOT accompanied by a SessionError: a gate is deliberate app state, not a failure.
|
|
146
|
+
* The message renders as the tile's subtitle (newlines collapse to spaces — list rows are
|
|
147
|
+
* single-line).
|
|
148
|
+
*/
|
|
144
149
|
private fun createGateMediaItem(gate: Gate): MediaItem =
|
|
145
150
|
createErrorMediaItem(
|
|
146
151
|
mediaId = BrowserPathHelper.GATE_PATH,
|
|
@@ -150,7 +155,7 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
150
155
|
|
|
151
156
|
/**
|
|
152
157
|
* Builds a generic "something went wrong" error tile for a browse failure. The true offline case
|
|
153
|
-
* is handled separately by the
|
|
158
|
+
* is handled separately by the networkMonitor guard in [onGetChildren]; anything reaching the
|
|
154
159
|
* catch block is an online-but-failed request (e.g. server down, bad status), so it must NOT be
|
|
155
160
|
* labelled "no internet connection".
|
|
156
161
|
*/
|
|
@@ -170,7 +175,6 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
170
175
|
forwardJumpInterval: Double,
|
|
171
176
|
backwardJumpInterval: Double,
|
|
172
177
|
) {
|
|
173
|
-
// Store as MediaLibrarySession for notifyChildrenChanged support
|
|
174
178
|
this.mediaLibrarySession = mediaSession as? MediaLibraryService.MediaLibrarySession
|
|
175
179
|
commandManager.updateMediaSession(
|
|
176
180
|
mediaSession,
|
|
@@ -196,9 +200,8 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
196
200
|
command: SessionCommand,
|
|
197
201
|
args: Bundle,
|
|
198
202
|
): ListenableFuture<SessionResult> {
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
// track) must say INVALID_STATE instead of success-then-revert.
|
|
203
|
+
// A success here makes the controller flip its heart optimistically, so a no-op (no current
|
|
204
|
+
// track) must report INVALID_STATE instead of success-then-revert.
|
|
202
205
|
if (command.customAction == MediaSessionCommandManager.CUSTOM_ACTION_FAVORITE) {
|
|
203
206
|
val applied = player.toggleActiveTrackFavorited()
|
|
204
207
|
Timber.d("Favorite button tapped - toggle applied=$applied")
|
|
@@ -241,13 +244,6 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
241
244
|
browser: MediaSession.ControllerInfo,
|
|
242
245
|
params: MediaLibraryService.LibraryParams?,
|
|
243
246
|
): ListenableFuture<LibraryResult<MediaItem>> {
|
|
244
|
-
// Extract artwork size hint from root hints (e.g., from Android Auto)
|
|
245
|
-
// TODO: Also consider these other root hints in the future:
|
|
246
|
-
// - KEY_ROOT_HINT_MEDIA_HOST_VERSION
|
|
247
|
-
// - KEY_ROOT_HINT_MEDIA_SESSION_API
|
|
248
|
-
// - BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT
|
|
249
|
-
// - BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT
|
|
250
|
-
// - KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED
|
|
251
247
|
params?.extras?.getInt(MediaConstants.EXTRAS_KEY_MEDIA_ART_SIZE_PIXELS, 0)?.let { size ->
|
|
252
248
|
if (size > 0) {
|
|
253
249
|
artworkSizeHintPixels = size
|
|
@@ -290,7 +286,6 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
290
286
|
"onGetChildren: {parentId: $parentId, page: $page, pageSize: $pageSize, isSpecialPath: ${BrowserPathHelper.isSpecialPath(parentId)} }"
|
|
291
287
|
)
|
|
292
288
|
return scope.future {
|
|
293
|
-
// Wait for browser to be registered if it's not available yet
|
|
294
289
|
val audioBrowser =
|
|
295
290
|
player.awaitBrowser().also { Timber.d("Browser ready, proceeding with onGetChildren") }
|
|
296
291
|
val browserManager = audioBrowser.browserManager
|
|
@@ -330,7 +325,6 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
330
325
|
return@future LibraryResult.ofItemList(ImmutableList.of<MediaItem>(), params)
|
|
331
326
|
}
|
|
332
327
|
|
|
333
|
-
// Show offline error when offline:
|
|
334
328
|
if (
|
|
335
329
|
!player.networkMonitor.isOnline.value && browserManager.config.androidControllerOfflineError
|
|
336
330
|
) {
|
|
@@ -342,10 +336,8 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
342
336
|
try {
|
|
343
337
|
val children =
|
|
344
338
|
if (parentId == BrowserPathHelper.ROOT_PATH) {
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
// Disabled tabs hide (Track.disabled) — filtered before the cap so
|
|
348
|
-
// an unavailable tab never costs an available one its slot.
|
|
339
|
+
// Disabled tabs hide (Track.disabled) — filtered before the four-tab cap so an
|
|
340
|
+
// unavailable tab never costs an available one its slot.
|
|
349
341
|
val tabs = browserManager.queryTabs().filterNot { it.disabled == true }
|
|
350
342
|
if (tabs.size > 4) {
|
|
351
343
|
Timber.w(
|
|
@@ -354,9 +346,7 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
354
346
|
}
|
|
355
347
|
toFlatMediaItems(tabs.take(4))
|
|
356
348
|
} else {
|
|
357
|
-
//
|
|
358
|
-
// (paths are already set to contextual paths). Section styles come
|
|
359
|
-
// page-folded (`section ?? page`, ADR 0011).
|
|
349
|
+
// Section styles arrive page-folded (`section ?? page`, ADR 0011).
|
|
360
350
|
val resolvedTrack = browserManager.resolve(parentId)
|
|
361
351
|
val sections =
|
|
362
352
|
resolvedTrack.styleResolvedSections()
|
|
@@ -371,16 +361,8 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
371
361
|
// error tile. awaitBrowser's TimeoutCancellationException IS a real failure, keep that.
|
|
372
362
|
if (e is CancellationException && e !is TimeoutCancellationException) throw e
|
|
373
363
|
Timber.e(e, "Error getting children for parentId: $parentId")
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
// Auto. See https://github.com/androidx/media/issues/2901
|
|
377
|
-
//
|
|
378
|
-
// Verified on a head unit (2026-06): the AA browse list NEVER renders error text — not for
|
|
379
|
-
// transient sendError, sticky non-fatal replication, or fatal replication
|
|
380
|
-
// (setLibraryErrorReplicationMode). Fatal replication does show the message on the
|
|
381
|
-
// playback screen, but presents the session as STATE_ERROR with no actions, hiding the
|
|
382
|
-
// now-playing item and transport controls — not worth it. Tiles remain the only in-browse
|
|
383
|
-
// signal we control.
|
|
364
|
+
// A tile rather than a bare ofError() — see createErrorMediaItem and
|
|
365
|
+
// https://github.com/androidx/media/issues/2901
|
|
384
366
|
sendBrowseError(session, browser, offline = false)
|
|
385
367
|
LibraryResult.ofItemList(ImmutableList.of(createBrowseErrorMediaItem()), params)
|
|
386
368
|
}
|
|
@@ -393,8 +375,8 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
393
375
|
* Media3 transiently attaches the error code/message to the platform session's playback state
|
|
394
376
|
* without entering STATE_ERROR.
|
|
395
377
|
*
|
|
396
|
-
* NOTE: current Android Auto renders
|
|
397
|
-
*
|
|
378
|
+
* NOTE: current Android Auto renders nothing for this — its transient state is cleared
|
|
379
|
+
* microseconds after being set (see [createErrorMediaItem], androidx/media#2901). It is kept
|
|
398
380
|
* because it is the correct Media3-API error signal, costs nothing, and becomes user-visible if
|
|
399
381
|
* Android Auto moves to consuming SessionError via the Media3 controller API.
|
|
400
382
|
*
|
|
@@ -462,13 +444,12 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
462
444
|
): ListenableFuture<LibraryResult<MediaItem>> {
|
|
463
445
|
Timber.Forest.d("onGetItem: ${browser.packageName}, mediaId = $mediaId")
|
|
464
446
|
return scope.future {
|
|
465
|
-
//
|
|
447
|
+
// Offline, error and root answer ahead of awaitBrowser: they need no browser. The gate
|
|
448
|
+
// sentinel below still does, for gateDecision.
|
|
466
449
|
if (mediaId == BrowserPathHelper.OFFLINE_PATH) {
|
|
467
450
|
return@future LibraryResult.ofItem(createOfflineMediaItem(), null)
|
|
468
451
|
}
|
|
469
452
|
|
|
470
|
-
// Return the error tile as a non-browsable item so tapping it can't re-browse into a
|
|
471
|
-
// failing path (which would push another error page onto the stack).
|
|
472
453
|
if (mediaId == BrowserPathHelper.ERROR_PATH) {
|
|
473
454
|
return@future LibraryResult.ofItem(createBrowseErrorMediaItem(), null)
|
|
474
455
|
}
|
|
@@ -503,7 +484,6 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
503
484
|
)
|
|
504
485
|
}
|
|
505
486
|
|
|
506
|
-
// Wait for browser to be registered if it's not available yet
|
|
507
487
|
val browserManager = player.awaitBrowser().browserManager
|
|
508
488
|
|
|
509
489
|
// Serve tracks from the track cache first (keyed by path and src). Besides avoiding an HTTP
|
|
@@ -578,9 +558,6 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
578
558
|
}
|
|
579
559
|
|
|
580
560
|
/**
|
|
581
|
-
* Notifies all subscribed controllers to refresh their content. Called internally when network
|
|
582
|
-
* state changes to refresh all subscribed paths.
|
|
583
|
-
*
|
|
584
561
|
* Safe to call from any thread: snapshots the subscribed paths and dispatches the session calls
|
|
585
562
|
* to the main (Media3 application) thread.
|
|
586
563
|
*/
|
|
@@ -593,10 +570,8 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
593
570
|
}
|
|
594
571
|
|
|
595
572
|
/**
|
|
596
|
-
* Notifies external controllers that content at the given path has changed
|
|
597
|
-
* subscribed to
|
|
598
|
-
*
|
|
599
|
-
* @param path The path where content has changed
|
|
573
|
+
* Notifies external controllers that content at the given path has changed, so controllers
|
|
574
|
+
* subscribed to it refresh their UI. Safe to call from any thread.
|
|
600
575
|
*/
|
|
601
576
|
fun notifyContentChanged(path: String) {
|
|
602
577
|
Timber.d("Notifying content changed for path: $path")
|
|
@@ -614,10 +589,6 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
614
589
|
notifySubscribedChildrenChanged()
|
|
615
590
|
}
|
|
616
591
|
|
|
617
|
-
/**
|
|
618
|
-
* Called when the browser becomes available after a cold start. Notifies all subscribed
|
|
619
|
-
* controllers to refresh their content.
|
|
620
|
-
*/
|
|
621
592
|
fun notifyBrowserReady() {
|
|
622
593
|
Timber.d("Browser ready - notifying all subscribed paths")
|
|
623
594
|
notifySubscribedChildrenChanged()
|
|
@@ -636,8 +607,6 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
636
607
|
): ListenableFuture<LibraryResult<Void>> {
|
|
637
608
|
Timber.d("onSearch: ${browser.packageName}, query = $query")
|
|
638
609
|
return scope.future {
|
|
639
|
-
// Wait for browser registration like onGetChildren does, so a cold-start voice search
|
|
640
|
-
// doesn't fail before JS has configured the browser.
|
|
641
610
|
val audioBrowser =
|
|
642
611
|
try {
|
|
643
612
|
player.awaitBrowser()
|
|
@@ -659,14 +628,14 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
659
628
|
return@future LibraryResult.ofVoid()
|
|
660
629
|
}
|
|
661
630
|
|
|
662
|
-
// Check if search is configured
|
|
663
631
|
if (!browserManager.config.hasSearch) {
|
|
664
632
|
Timber.w("Search requested but no search source configured")
|
|
665
633
|
return@future LibraryResult.ofError(SessionError.ERROR_NOT_SUPPORTED)
|
|
666
634
|
}
|
|
667
635
|
|
|
668
636
|
try {
|
|
669
|
-
//
|
|
637
|
+
// Caches the result set under the query, which onGetSearchResult reads back through
|
|
638
|
+
// getCachedSearchResults.
|
|
670
639
|
val searchResults = browserManager.search(query)
|
|
671
640
|
// Count what onGetSearchResult will actually serve: disabled tracks
|
|
672
641
|
// hide on Android Auto, so they must not inflate the announced count.
|
|
@@ -677,7 +646,6 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
677
646
|
|
|
678
647
|
Timber.d("Search completed: $resultCount results for query '$query'")
|
|
679
648
|
|
|
680
|
-
// Notify Media3 of search results
|
|
681
649
|
session.notifySearchResultChanged(browser, query, resultCount, params)
|
|
682
650
|
|
|
683
651
|
LibraryResult.ofVoid()
|
|
@@ -724,10 +692,7 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
724
692
|
}
|
|
725
693
|
|
|
726
694
|
try {
|
|
727
|
-
// Get cached search results from BrowserManager
|
|
728
695
|
browserManager.getCachedSearchResults(query)?.let { tracks ->
|
|
729
|
-
// Through the shared browse conversion, so search results get the
|
|
730
|
-
// same artwork routing as browse rows.
|
|
731
696
|
val mediaItems = toFlatMediaItems(tracks.toList())
|
|
732
697
|
|
|
733
698
|
val paginatedItems = mediaItems.paginate(page, pageSize)
|
|
@@ -766,8 +731,9 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
766
731
|
return scope.future {
|
|
767
732
|
val audioBrowser = player.awaitBrowser()
|
|
768
733
|
|
|
769
|
-
//
|
|
770
|
-
//
|
|
734
|
+
// The "leave playback alone" answer for handleTrackLoad: returning the current state
|
|
735
|
+
// unchanged is how Media3 is told to do nothing. ExoPlayer is main-thread confined and this
|
|
736
|
+
// future runs on IO, so the reads hop to Main.
|
|
771
737
|
suspend fun currentPlayerState(): MediaSession.MediaItemsWithStartPosition =
|
|
772
738
|
withContext(Dispatchers.Main) {
|
|
773
739
|
val currentItems = player.tracks.map { TrackFactory.toMedia3(it) }
|
|
@@ -788,14 +754,12 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
788
754
|
?.takeIf { it.requestMetadata.searchQuery == null }
|
|
789
755
|
?.let { browserManager.contextualPathFor(it.mediaId) }
|
|
790
756
|
|
|
791
|
-
// Check if this is a single item from the current queue source
|
|
792
757
|
if (singleContextualPath != null) {
|
|
793
758
|
val parentPath = BrowserPathHelper.stripTrackId(singleContextualPath)
|
|
794
759
|
val trackId = BrowserPathHelper.extractTrackId(singleContextualPath)
|
|
795
760
|
|
|
796
|
-
//
|
|
797
|
-
//
|
|
798
|
-
// see indexOfTappedTrack)
|
|
761
|
+
// A tap inside the queue's own source list skips rather than rebuilds (exact path first,
|
|
762
|
+
// identity for index-less paths — see indexOfTappedTrack).
|
|
799
763
|
if (trackId != null && parentPath == player.queueSourcePath) {
|
|
800
764
|
val queueTracks = withContext(Dispatchers.Main) { player.tracks }
|
|
801
765
|
val index = queueTracks.indexOfTappedTrack(singleContextualPath, trackId)
|
|
@@ -809,7 +773,6 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
809
773
|
index.toDouble(),
|
|
810
774
|
::currentPlayerState,
|
|
811
775
|
) {
|
|
812
|
-
// Return the existing queue items with the new start index
|
|
813
776
|
val existingItems = queueTracks.map { TrackFactory.toMedia3(it) }
|
|
814
777
|
MediaSession.MediaItemsWithStartPosition(existingItems, index, startPositionMs)
|
|
815
778
|
}
|
|
@@ -830,8 +793,8 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
830
793
|
result.startIndex.toDouble(),
|
|
831
794
|
::currentPlayerState,
|
|
832
795
|
) {
|
|
833
|
-
//
|
|
834
|
-
//
|
|
796
|
+
// Inside handleTrackLoad's default branch: a consumer that handles the load itself owns
|
|
797
|
+
// the queue, so queueSourcePath must not claim this path on its behalf.
|
|
835
798
|
if (singleContextualPath != null) {
|
|
836
799
|
val parentPath = BrowserPathHelper.stripTrackId(singleContextualPath)
|
|
837
800
|
withContext(Dispatchers.Main) { player.queueSourcePath = parentPath }
|
|
@@ -849,8 +812,6 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
849
812
|
* using the browse callback. This enables seamless resumption after app restart with the same
|
|
850
813
|
* player settings (repeat mode, shuffle, playback speed).
|
|
851
814
|
*
|
|
852
|
-
* @param mediaSession The media session
|
|
853
|
-
* @param controller The controller requesting resumption
|
|
854
815
|
* @param isForPlayback True if this should start playback; false if just gathering info for the
|
|
855
816
|
* boot-time resumption notification (no network; local metadata only).
|
|
856
817
|
* @see https://developer.android.com/media/media3/session/background-playback#resumption
|
|
@@ -881,9 +842,8 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
881
842
|
throw IllegalStateException("Persisted track is disabled")
|
|
882
843
|
}
|
|
883
844
|
|
|
884
|
-
//
|
|
885
|
-
//
|
|
886
|
-
// already-local metadata; skip the settings restore and network queue expansion below.
|
|
845
|
+
// Info-gathering returns before the settings restore and the queue expansion below: both
|
|
846
|
+
// touch the live player or the network, and neither is permitted here.
|
|
887
847
|
if (!isForPlayback) {
|
|
888
848
|
Timber.d("Info-gathering resumption (boot-time); returning stored track without expansion")
|
|
889
849
|
return@future MediaSession.MediaItemsWithStartPosition(
|
|
@@ -899,10 +859,8 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
899
859
|
val path = state.track.path
|
|
900
860
|
Timber.d("Resuming from path=$path, positionMs=${state.positionMs}")
|
|
901
861
|
|
|
902
|
-
// Wait for browser to be available (JS needs to have configured it)
|
|
903
862
|
val browserManager = player.awaitBrowser().browserManager
|
|
904
863
|
|
|
905
|
-
// Try to expand the path into a full queue
|
|
906
864
|
val expanded = path?.let { browserManager.expandQueueFromContextualPath(it) }
|
|
907
865
|
|
|
908
866
|
if (expanded != null) {
|
|
@@ -911,7 +869,6 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
911
869
|
"Restored ${tracks.size} tracks, starting at index $selectedIndex at ${state.positionMs}ms"
|
|
912
870
|
)
|
|
913
871
|
|
|
914
|
-
// Track the source path if this was a contextual path expansion
|
|
915
872
|
if (BrowserPathHelper.isContextual(path)) {
|
|
916
873
|
val parentPath = BrowserPathHelper.stripTrackId(path)
|
|
917
874
|
withContext(Dispatchers.Main) { player.queueSourcePath = parentPath }
|
|
@@ -923,7 +880,6 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
923
880
|
state.positionMs,
|
|
924
881
|
)
|
|
925
882
|
} else {
|
|
926
|
-
// Fallback: play just the stored track
|
|
927
883
|
Timber.d("Queue expansion failed, using stored track: ${state.track.title}")
|
|
928
884
|
MediaSession.MediaItemsWithStartPosition(
|
|
929
885
|
ImmutableList.of(TrackFactory.toMedia3(state.track)),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
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.17",
|
|
4
4
|
"description": "react-native-audio-browser is a react native package built with Nitro",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"build": "yarn prepare",
|
|
56
56
|
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
|
|
57
57
|
"codegen": "node codegen/index.js --logLevel=\"debug\" && node post-script.cjs && node codegen/strip-struct-equality.cjs && yarn build",
|
|
58
|
-
"prepare": "bob build && yarn hooks:install",
|
|
58
|
+
"prepare": "bob build && echo '{\"type\":\"commonjs\"}' > lib/typescript/package.json && yarn hooks:install",
|
|
59
59
|
"hooks:install": "git config core.hooksPath .githooks || exit 0",
|
|
60
60
|
"example": "yarn workspace example-native",
|
|
61
61
|
"ci:android": "yarn android:format:check && yarn android:types && yarn android:test",
|