diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/BaseInterceptorTest.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/BaseInterceptorTest.kt index ebdeaf68..c30e9227 100644 --- a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/BaseInterceptorTest.kt +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/BaseInterceptorTest.kt @@ -32,4 +32,4 @@ abstract class BaseInterceptorTest { .url(mockWebServerRule.mockWebServer.url("/")) .also { additionalParams(it) } .build() -} \ No newline at end of file +} diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/RangeHeaderInterceptorTest.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/RangeHeaderInterceptorTest.kt index 95f46e04..e458fc0d 100644 --- a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/RangeHeaderInterceptorTest.kt +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/RangeHeaderInterceptorTest.kt @@ -31,7 +31,7 @@ class RangeHeaderInterceptorTest : BaseInterceptorTest() { } @Test - fun `Should not add range header if request doens't contain it`() { + fun `Should not add range header if request doesnt contain it`() { mockWebServerRule.mockWebServer.enqueue(MockResponse()) val request = createRequest { } @@ -56,4 +56,4 @@ class RangeHeaderInterceptorTest : BaseInterceptorTest() { executedRequest.headers.names() `should contain` "Range" executedRequest.headers["Range"]!! `should equal to` "bytes=$offset-" } -} \ No newline at end of file +} diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/RangeHeaderInterceptor.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/RangeHeaderInterceptor.kt index 3690b0d7..cbb5d031 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/RangeHeaderInterceptor.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/RangeHeaderInterceptor.kt @@ -3,10 +3,17 @@ package org.moire.ultrasonic.api.subsonic.interceptors import okhttp3.Interceptor import okhttp3.Interceptor.Chain import okhttp3.Response +import java.util.concurrent.TimeUnit.MILLISECONDS + +internal const val SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000 +// Allow 20 seconds extra timeout pear MB offset. +internal const val TIMEOUT_MILLIS_PER_OFFSET_BYTE = 0.02 /** * Modifies request "Range" header to be according to HTTP standard. * + * Also increases read timeout to allow server to transcode response and offset it. + * * See [range rfc](https://tools.ietf.org/html/rfc7233). */ internal class RangeHeaderInterceptor : Interceptor { @@ -14,12 +21,21 @@ internal class RangeHeaderInterceptor : Interceptor { val originalRequest = chain.request() val headers = originalRequest.headers() return if (headers.names().contains("Range")) { - val offset = "bytes=${headers["Range"]}-" - chain.proceed(originalRequest.newBuilder() + val offsetValue = headers["Range"] ?: "0" + val offset = "bytes=$offsetValue-" + chain.withReadTimeout(getReadTimeout(offsetValue.toInt()), MILLISECONDS) + .proceed(originalRequest.newBuilder() .removeHeader("Range").addHeader("Range", offset) .build()) } else { chain.proceed(originalRequest) } } -} \ No newline at end of file + + // Set socket read timeout. Note: The timeout increases as the offset gets larger. This is + // to avoid the thrashing effect seen when offset is combined with transcoding/downsampling + // on the server. In that case, the server uses a long time before sending any data, + // causing the client to time out. + private fun getReadTimeout(offset: Int) + = (SOCKET_READ_TIMEOUT_DOWNLOAD + offset * TIMEOUT_MILLIS_PER_OFFSET_BYTE).toInt() +}