From 76b33235837350c17a03993f31ff51a6c71f7488 Mon Sep 17 00:00:00 2001 From: davotoula Date: Wed, 29 Jul 2026 13:37:16 +0200 Subject: [PATCH] TEMP: HlsVerify error-level probes for on-device verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DO NOT MERGE — revert this commit before the branch goes anywhere. PLAYBACK_DIAG_TAG logs at DEBUG and Amethyst.onCreate sets `Log.minLevel = if (BuildConfig.DEBUG) DEBUG else ERROR`, so nothing on that tag survives in the benchmark build. Three logcat captures during this work had zero PlaybackDiag lines for exactly that reason, which left the routing decision unverifiable — the fix was only ever confirmed indirectly, by the crash no longer happening. Adds one ERROR-level tag, HlsVerify, on the three paths unit tests cannot reach: - CustomMediaSourceFactory.createMediaSource — the routing decision, so isHlsMediaItem's verdict and the chosen factory are visible. This is the predicate with no test coverage, since android.net.Uri stubs to null under unitTests.isReturnDefaultValues. - LowLatencyStrippingParser.parse — whether the stripper actually engaged and how many bytes it removed, so a silent stop-stripping regression is visible rather than inferred. - HlsLivenessRecorder.maybeRecord — the other consumer of the shared predicate, unreachable without a Player. The pure predicates underneath are already unit-tested and remain the durable regression guard; this is only for confirming the wiring on a device. Capture with `adb logcat -s HlsVerify:E`. Remove with `grep -rn HlsVerify` or by reverting this commit. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018uE2Db4pzmhi5cFKKkyq2m --- .../amethyst/service/playback/HlsVerifyLog.kt | 44 +++++++++++++++++++ .../playerPool/CustomMediaSourceFactory.kt | 6 +++ .../playerPool/HlsLivenessRecorder.kt | 3 ++ .../playerPool/LowLatencyHlsStripper.kt | 16 ++++++- 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/HlsVerifyLog.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/HlsVerifyLog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/HlsVerifyLog.kt new file mode 100644 index 0000000000..9f89e66b1c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/HlsVerifyLog.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.playback + +/** + * TEMPORARY — delete this file and its three call sites before merging. + * `grep -rn HlsVerify` finds all of them; reverting the commit that added it does the same job. + * + * [PLAYBACK_DIAG_TAG] logs at DEBUG, and `Amethyst.onCreate` sets + * `Log.minLevel = if (BuildConfig.DEBUG) DEBUG else ERROR` — so nothing on that tag survives in a + * benchmark or release build. On-device verification of the HLS work therefore has to log at ERROR + * to be visible at all. + * + * Deliberately placed only on the paths that unit tests *cannot* reach: + * - `isHlsMediaItem` and the factory choice — needs `android.net.Uri`, which stubs to null under + * `unitTests.isReturnDefaultValues`. + * - `LowLatencyStrippingParser.parse` — needs a `Uri` for the same reason. + * - `HlsLivenessRecorder.maybeRecord` — needs a `Player`. + * + * The pure predicates underneath (`stripLowLatencyTags`, `isHlsMedia`, `shouldBypassCache`, + * `livenessVerdictToRecord`) already have unit tests, which are the durable regression guard. This + * tag is only for confirming the wiring on a real device. + * + * Capture with: `adb logcat -s HlsVerify:E` + */ +const val HLS_VERIFY_TAG = "HlsVerify" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/CustomMediaSourceFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/CustomMediaSourceFactory.kt index d9ec09d6e9..bfabbd885d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/CustomMediaSourceFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/CustomMediaSourceFactory.kt @@ -30,6 +30,7 @@ import androidx.media3.exoplayer.hls.HlsMediaSource import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy +import com.vitorpamplona.amethyst.service.playback.HLS_VERIFY_TAG import com.vitorpamplona.amethyst.service.playback.PLAYBACK_DIAG_TAG import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemCache import com.vitorpamplona.amethyst.service.playback.diskCache.HlsLivenessCache @@ -165,6 +166,11 @@ class CustomMediaSourceFactory( // Logs the routing inputs directly rather than a re-derived label, so it can't drift from // shouldBypassCache. + // TEMPORARY HlsVerify — see HlsVerifyLog.kt. Remove before merge. + Log.e(HLS_VERIFY_TAG) { + "ROUTE hls=$hls bypassCache=$bypassCache flaggedLive=$flaggedLive knownOnDemand=$knownOnDemand " + + "mime=${mediaItem.localConfiguration?.mimeType} -> ${source::class.java.simpleName} id=$id" + } Log.d(PLAYBACK_DIAG_TAG) { "SOURCE ${if (bypassCache) "BYPASS" else "CACHE"} flaggedLive=$flaggedLive hls=$hls knownOnDemand=$knownOnDemand " + "mime=${mediaItem.localConfiguration?.mimeType} -> ${source::class.java.simpleName} id=$id" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/HlsLivenessRecorder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/HlsLivenessRecorder.kt index 9b95564fed..7ff88465db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/HlsLivenessRecorder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/HlsLivenessRecorder.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service.playback.playerPool import androidx.media3.common.C import androidx.media3.common.Player import androidx.media3.common.Timeline +import com.vitorpamplona.amethyst.service.playback.HLS_VERIFY_TAG import com.vitorpamplona.amethyst.service.playback.PLAYBACK_DIAG_TAG import com.vitorpamplona.amethyst.service.playback.diskCache.HlsLivenessCache import com.vitorpamplona.quartz.utils.Log @@ -105,6 +106,8 @@ class HlsLivenessRecorder( // refresh of a live stream, and the verdict is stable once learned, so re-putting the same // value would take a ConcurrentHashMap bin lock on every callback for nothing. if (known != toRecord) { + // TEMPORARY HlsVerify — see HlsVerifyLog.kt. Remove before merge. + Log.e(HLS_VERIFY_TAG) { "LIVENESS ${if (toRecord) "LIVE" else "ON-DEMAND"} learned for $url" } Log.d(PLAYBACK_DIAG_TAG) { "LIVENESS ${if (toRecord) "LIVE" else "ON-DEMAND"} learned for $url" } HlsLivenessCache.record(url, toRecord) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/LowLatencyHlsStripper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/LowLatencyHlsStripper.kt index 45ee84bc05..50d61e1af1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/LowLatencyHlsStripper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/LowLatencyHlsStripper.kt @@ -28,6 +28,8 @@ import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist import androidx.media3.exoplayer.hls.playlist.HlsPlaylist import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParserFactory import androidx.media3.exoplayer.upstream.ParsingLoadable +import com.vitorpamplona.amethyst.service.playback.HLS_VERIFY_TAG +import com.vitorpamplona.quartz.utils.Log import java.io.ByteArrayInputStream import java.io.InputStream @@ -104,7 +106,19 @@ internal class LowLatencyStrippingParser( override fun parse( uri: Uri, inputStream: InputStream, - ): HlsPlaylist = delegate.parse(uri, ByteArrayInputStream(stripLowLatencyTags(inputStream.readBytes()))) + ): HlsPlaylist { + val original = inputStream.readBytes() + val stripped = stripLowLatencyTags(original) + // TEMPORARY HlsVerify — see HlsVerifyLog.kt. Remove before merge. + Log.e(HLS_VERIFY_TAG) { + if (stripped === original) { + "PARSE no LL tags (${original.size}B) $uri" + } else { + "PARSE STRIPPED ${original.size - stripped.size}B of ${original.size}B $uri" + } + } + return delegate.parse(uri, ByteArrayInputStream(stripped)) + } } /**