retry a read that lands past a cold transcode

A transcoded stream is chunked with no length, so the app declares an upper-bound
length, and on the first request the server is still producing the stream: a seek
near that bound lands past the data and comes back as POSITION_OUT_OF_RANGE
(Navidrome answers 416 until it has cached the transcode). That first request is
what warms the cache, so give the load one retry after a second: the retry sees a
real Content-Length and the stream plays and seeks. Further out-of-range errors
are given up on rather than looped.
This commit is contained in:
austinried
2026-10-04 00:30:04 +00:00
parent 9a999498ab
commit dd6b0ea00c
3 changed files with 96 additions and 0 deletions
@@ -0,0 +1,57 @@
package com.subtracks.playback
import android.net.Uri
import androidx.media3.common.C
import androidx.media3.datasource.DataSourceException
import androidx.media3.datasource.DataSpec
import androidx.media3.exoplayer.source.LoadEventInfo
import androidx.media3.exoplayer.source.MediaLoadData
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import java.io.IOException
@RunWith(AndroidJUnit4::class)
class RetryOutOfRangePolicyTest {
private val policy = RetryOutOfRangePolicy(delayMs = 123L)
@Test
fun anOutOfRangeReadIsRetriedOnce() {
assertEquals(123L, policy.getRetryDelayMsFor(info(outOfRange(), errorCount = 1)))
}
@Test
fun anOutOfRangeReadIsNotRetriedForever() {
assertEquals(C.TIME_UNSET.toLong(), policy.getRetryDelayMsFor(info(outOfRange(), errorCount = 2)))
}
@Test
fun aWrappedOutOfRangeIsStillSeen() {
val wrapped = IOException("load failed", outOfRange())
assertEquals(123L, policy.getRetryDelayMsFor(info(wrapped, errorCount = 1)))
}
@Test
fun otherErrorsGoToTheDefaultPolicy() {
val plain = IOException("network")
val expected = DefaultLoadErrorHandlingPolicy().getRetryDelayMsFor(info(plain, errorCount = 1))
assertEquals(expected, policy.getRetryDelayMsFor(info(plain, errorCount = 1)))
}
private fun outOfRange() = DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE)
private fun info(
error: IOException,
errorCount: Int,
) = LoadErrorHandlingPolicy.LoadErrorInfo(
LoadEventInfo(0, DataSpec(Uri.EMPTY), 0),
MediaLoadData(C.DATA_TYPE_MEDIA),
error,
errorCount,
)
}