feat(android): SetNextAVTransportURI pre-queue for gap-free UPnP advance
android / Build + lint + test (push) Failing after 1m35s
android / Build + lint + test (push) Failing after 1m35s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,15 @@ class MinstrelForwardingPlayer(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
holder.active.collect { active -> onActiveChanged(active) }
|
holder.active.collect { active -> onActiveChanged(active) }
|
||||||
}
|
}
|
||||||
|
delegate.addListener(object : Player.Listener {
|
||||||
|
override fun onMediaItemTransition(
|
||||||
|
mediaItem: androidx.media3.common.MediaItem?,
|
||||||
|
reason: Int,
|
||||||
|
) {
|
||||||
|
val active = holder.active.value ?: return
|
||||||
|
scope.launch { preQueueNext(active) }
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isRemote(): Boolean = holder.active.value != null
|
private fun isRemote(): Boolean = holder.active.value != null
|
||||||
@@ -159,6 +168,34 @@ class MinstrelForwardingPlayer(
|
|||||||
}.onFailure { handleSoapFailure(active, it) }
|
}.onFailure { handleSoapFailure(active, it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Speculative pre-queue of the next track so Sonos can advance gap-free.
|
||||||
|
* Failures (token mint OR SetNextAVTransportURI rejection) silently fall
|
||||||
|
* back to the poll-driven advance path -- the pollLoop detects STOPPED +
|
||||||
|
* 0:00 RelTime and calls syncCurrentItemToRemote on whatever the new
|
||||||
|
* currentMediaItem is. The only consequence of failure here is a sub-second
|
||||||
|
* audible gap at track boundary, not a broken route.
|
||||||
|
*/
|
||||||
|
private suspend fun preQueueNext(active: ActiveUpnp) {
|
||||||
|
val nextMediaId = handlerEvaluate { nextItemMediaId() } ?: return
|
||||||
|
val token = runCatching { tokens.mint(nextMediaId) }.getOrNull() ?: return
|
||||||
|
runCatching {
|
||||||
|
active.avTransport.setNextAVTransportURI(
|
||||||
|
uri = token.url,
|
||||||
|
mime = token.mime,
|
||||||
|
title = token.title,
|
||||||
|
)
|
||||||
|
}.onFailure {
|
||||||
|
Timber.w(it, "SetNextAVTransportURI failed (poll-driven advance will fall back)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nextItemMediaId(): String? {
|
||||||
|
val nextIdx = delegate.nextMediaItemIndex
|
||||||
|
if (nextIdx == androidx.media3.common.C.INDEX_UNSET) return null
|
||||||
|
return delegate.getMediaItemAt(nextIdx).mediaId
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleSoapFailure(active: ActiveUpnp, t: Throwable) {
|
private fun handleSoapFailure(active: ActiveUpnp, t: Throwable) {
|
||||||
pollJob?.cancel()
|
pollJob?.cancel()
|
||||||
Timber.w(t, "UPnP transport call failed on %s", active.routeName)
|
Timber.w(t, "UPnP transport call failed on %s", active.routeName)
|
||||||
|
|||||||
Reference in New Issue
Block a user