mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
playback: cache streamed audio to stop redundant refetches
A streamed MP3 with no Xing/Info/VBRI header gets an estimated duration from Mp3Extractor. At end of stream the extractor rewrites it, which refreshes the timeline; MediaPeriodQueue.updateQueuedPeriods then sees the playing period's duration change, drops every period after it and re-enqueues the next one, so the next track is opened from byte 0 again. After a seek near the end that repeats a few times. Route streamed bytes through a bounded SimpleCache (DefaultDataSource -> CacheDataSource -> KnownLengthDataSource -> OkHttp). A re-opened next period is served from the bytes the prefetch already committed, so the redundant network fetches stop without changing playback length or gapless padding. DefaultDataSource still handles file:/content:, so downloads bypass the cache. The cache is a process-scoped 64 MiB LRU in cacheDir/stream-cache; a construction failure falls back to no cache so playback still starts.
This commit is contained in:
@@ -2,7 +2,12 @@ package com.subtracks.playback
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.DataSpec
|
||||
import androidx.media3.datasource.cache.NoOpCacheEvictor
|
||||
import androidx.media3.datasource.cache.SimpleCache
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
@@ -13,13 +18,16 @@ import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MediaDataSourceTest {
|
||||
private val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
|
||||
private fun dataSource() = mediaDataSourceFactory(context, OkHttpDataSource.Factory(OkHttpClient())).createDataSource()
|
||||
private fun dataSource(cache: SimpleCache? = null) =
|
||||
mediaDataSourceFactory(context, cache, OkHttpDataSource.Factory(OkHttpClient())).createDataSource()
|
||||
|
||||
@Test
|
||||
fun aDownloadedFileIsReadThroughTheStreamingChain() {
|
||||
@@ -50,4 +58,44 @@ class MediaDataSourceTest {
|
||||
|
||||
assertEquals(1_000_000L, length)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reopeningAStreamIsServedFromTheCache() {
|
||||
val server = MockWebServer()
|
||||
server.enqueue(MockResponse().setBody("0123456789"))
|
||||
server.start()
|
||||
val url = "http://${server.hostName}:${server.port}/rest/stream"
|
||||
val cache =
|
||||
SimpleCache(
|
||||
File(context.cacheDir, "stream-cache-${System.nanoTime()}"),
|
||||
NoOpCacheEvictor(),
|
||||
StandaloneDatabaseProvider(context),
|
||||
)
|
||||
|
||||
val first = readAll(dataSource(cache), url)
|
||||
val second = readAll(dataSource(cache), url)
|
||||
val requests = server.requestCount
|
||||
server.shutdown()
|
||||
cache.release()
|
||||
|
||||
assertEquals("0123456789", String(first, StandardCharsets.UTF_8))
|
||||
assertEquals(first.toList(), second.toList())
|
||||
assertEquals(1, requests)
|
||||
}
|
||||
|
||||
private fun readAll(
|
||||
dataSource: DataSource,
|
||||
url: String,
|
||||
): ByteArray {
|
||||
dataSource.open(DataSpec(Uri.parse(url)))
|
||||
val out = ByteArrayOutputStream()
|
||||
val buffer = ByteArray(1024)
|
||||
while (true) {
|
||||
val read = dataSource.read(buffer, 0, buffer.size)
|
||||
if (read == C.RESULT_END_OF_INPUT) break
|
||||
out.write(buffer, 0, read)
|
||||
}
|
||||
dataSource.close()
|
||||
return out.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user