From cfb8bf04d6fb0afd3ebfa2656d49b866d5a1147d Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 14 Mar 2026 12:15:53 +0100 Subject: [PATCH] fix: reduce remote playback startup latency - Lower OkHttp readTimeout from 30s to 10s so stalled Range requests retry faster - Reduce bufferForPlaybackMs from 2500 to 1000 for quicker playback start - Add HTTP transfer timing logs (TTFB, bytes, duration) to diagnose startup delays --- .../edde746/plezy/exoplayer/ExoPlayerCore.kt | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt index edc11db8..10ef7539 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt @@ -30,7 +30,9 @@ import androidx.media3.common.Tracks import androidx.media3.common.VideoSize import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.analytics.AnalyticsListener +import androidx.media3.datasource.DataSpec import androidx.media3.datasource.DefaultDataSource +import androidx.media3.datasource.TransferListener import androidx.media3.datasource.okhttp.OkHttpDataSource import okhttp3.OkHttpClient import java.util.concurrent.TimeUnit @@ -93,7 +95,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { private val okHttpClient: OkHttpClient by lazy { OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(15, TimeUnit.SECONDS) .build() } @@ -352,6 +354,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { // OkHttp DataSource for TCP connection pooling — prevents reconnect stutter at high bitrates httpDataSourceFactory = OkHttpDataSource.Factory(okHttpClient) dataSourceFactory = DefaultDataSource.Factory(activity, httpDataSourceFactory!!) + .setTransferListener(HttpTimingTransferListener()) val extractorsFactory = DefaultExtractorsFactory() // Inline buildWithAssSupport to retain AssHandler reference for font scale control. @@ -422,9 +425,9 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { setTargetBufferBytes(targetBufferBytes) setPrioritizeTimeOverSizeThresholds(false) if (availableMB <= 2048) { - setBufferDurationsMs(15_000, 50_000, 2_500, 5_000) + setBufferDurationsMs(15_000, 50_000, 1_000, 5_000) } else { - setBufferDurationsMs(30_000, 60_000, 2_500, 5_000) + setBufferDurationsMs(30_000, 60_000, 1_000, 5_000) } }.build() emitLog("info", "init", "Buffer: ${targetBufferBytes / 1024 / 1024}MB limit, available=${availableMB}MB, tunneling=${tunnelingUserEnabled}, dataSource=OkHttp") @@ -1627,4 +1630,43 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { isInitialized = false Log.d(TAG, "Disposed") } + + /** Logs HTTP request timing: TTFB, bytes transferred, errors. */ + private inner class HttpTimingTransferListener : TransferListener { + private val startTimes = HashMap() + private val firstByteTimes = HashMap() + private val byteCounts = HashMap() + + override fun onTransferInitializing(source: androidx.media3.datasource.DataSource, dataSpec: DataSpec, isNetwork: Boolean) { + if (!isNetwork) return + startTimes[dataSpec] = System.currentTimeMillis() + val range = dataSpec.position.let { if (it > 0) "bytes=$it-" else "" } + val uri = dataSpec.uri.path?.let { if (it.length > 60) "…${it.takeLast(50)}" else it } ?: dataSpec.uri.toString() + emitLog("info", "http", "GET $uri ${if (range.isNotEmpty()) range else ""}") + } + + override fun onTransferStart(source: androidx.media3.datasource.DataSource, dataSpec: DataSpec, isNetwork: Boolean) { + if (!isNetwork) return + val start = startTimes[dataSpec] ?: return + val ttfb = System.currentTimeMillis() - start + firstByteTimes[dataSpec] = System.currentTimeMillis() + byteCounts[dataSpec] = 0 + emitLog("info", "http", "→ first byte in ${ttfb}ms") + } + + override fun onBytesTransferred(source: androidx.media3.datasource.DataSource, dataSpec: DataSpec, isNetwork: Boolean, bytesTransferred: Int) { + if (!isNetwork) return + byteCounts[dataSpec] = (byteCounts[dataSpec] ?: 0) + bytesTransferred + } + + override fun onTransferEnd(source: androidx.media3.datasource.DataSource, dataSpec: DataSpec, isNetwork: Boolean) { + if (!isNetwork) return + val start = startTimes.remove(dataSpec) ?: return + firstByteTimes.remove(dataSpec) + val bytes = byteCounts.remove(dataSpec) ?: 0 + val elapsed = System.currentTimeMillis() - start + val sizeStr = if (bytes > 1_000_000) "${"%.1f".format(bytes / 1_000_000.0)}MB" else "${bytes / 1000}KB" + emitLog("info", "http", "← $sizeStr in ${elapsed}ms") + } + } }