TEMP: HlsVerify error-level probes for on-device verification

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018uE2Db4pzmhi5cFKKkyq2m
This commit is contained in:
davotoula
2026-07-29 13:37:16 +02:00
co-authored by Claude Opus 5
parent cabe539e8e
commit 76b3323583
4 changed files with 68 additions and 1 deletions
@@ -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"
@@ -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"
@@ -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)
}
@@ -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))
}
}
/**