0.2.63 update

This commit is contained in:
ponzischeme89
2026-08-14 11:47:32 +12:00
parent 9a8ecbdceb
commit 06b6490ac9
41 changed files with 921 additions and 179 deletions
@@ -41,6 +41,7 @@ object EmbyServiceFactory {
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(EmbyAuthInterceptor(deviceIdProvider, tokenProvider))
.addInterceptor(DiagnosticNetworkInterceptor("emby"))
.addInterceptor(logging)
.build()
@@ -9,6 +9,7 @@ import com.ponzischeme89.memby.data.playback.devicePlaybackCapabilities
import com.ponzischeme89.memby.data.playback.gatewayAudioTokens
import com.ponzischeme89.memby.data.playback.gatewayCapabilityTokens
import com.ponzischeme89.memby.update.RequiredUpdateSignal
import com.ponzischeme89.memby.diagnostics.MembyDiagnostics
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
@@ -37,6 +38,7 @@ object GatewayServiceFactory {
// read timeout here only ever means Emby itself is struggling behind it.
.readTimeout(20, TimeUnit.SECONDS)
.addInterceptor(GatewayAuthInterceptor(tokenProvider))
.addInterceptor(DiagnosticNetworkInterceptor("gateway"))
.addInterceptor(RequiredUpdateInterceptor)
.build()
@@ -49,6 +51,28 @@ object GatewayServiceFactory {
}
}
/** Timings and outcomes only: request URLs are stripped of query parameters and no headers are read. */
internal class DiagnosticNetworkInterceptor(private val backend: String) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val started = android.os.SystemClock.elapsedRealtime()
MembyDiagnostics.trace("http_started", "backend" to backend, "method" to request.method, "url" to MembyDiagnostics.safeUrl(request.url))
return try {
chain.proceed(request).also { response ->
MembyDiagnostics.debug("http_finished", "backend" to backend, "method" to request.method,
"url" to MembyDiagnostics.safeUrl(request.url), "status" to response.code,
"duration_ms" to (android.os.SystemClock.elapsedRealtime() - started),
"correlation" to response.header("X-Memby-Correlation"))
}
} catch (error: Exception) {
MembyDiagnostics.debug("http_failed", "backend" to backend, "method" to request.method,
"url" to MembyDiagnostics.safeUrl(request.url), "duration_ms" to (android.os.SystemClock.elapsedRealtime() - started),
"exception" to error.javaClass.simpleName, "detail" to error.message)
throw error
}
}
}
/**
* Sends the gateway token as a bearer header. Image URLs cannot carry headers, so those
* are built with a `t=` query parameter instead (see EmbyRepository's URL helpers).
@@ -0,0 +1,34 @@
package com.ponzischeme89.memby.diagnostics
import android.util.Log
import com.ponzischeme89.memby.BuildConfig
import okhttp3.HttpUrl
/** Small, dependency-free diagnostic log shared by network and playback code. */
object MembyDiagnostics {
private const val TAG = "MembyDiagnostic"
private val rank = mapOf("TRACE" to 0, "DEBUG" to 1, "INFO" to 2)
private val configured = rank[BuildConfig.DIAGNOSTIC_LOG_LEVEL] ?: 2
fun debug(event: String, vararg fields: Pair<String, Any?>) = write("DEBUG", event, fields)
fun trace(event: String, vararg fields: Pair<String, Any?>) = write("TRACE", event, fields)
fun info(event: String, vararg fields: Pair<String, Any?>) = write("INFO", event, fields)
private fun write(level: String, event: String, fields: Array<out Pair<String, Any?>>) {
if ((rank[level] ?: Int.MAX_VALUE) < configured) return
val text = buildString {
append("event=").append(event)
fields.forEach { (key, value) -> append(' ').append(key).append('=').append(safe(key, value)) }
}
when (level) { "INFO" -> Log.i(TAG, text); else -> Log.d(TAG, text) }
}
fun safeUrl(url: HttpUrl): String = url.newBuilder().query(null).build().toString()
private fun safe(key: String, value: Any?): String {
val lower = key.lowercase()
if (lower.contains("token") || lower.contains("password") || lower.contains("secret") || lower.contains("cookie") || lower.contains("authorization") || lower.contains("key")) return "[redacted]"
return value?.toString()?.replace(SENSITIVE_QUERY_VALUE, "$1[redacted]") ?: "null"
}
private val SENSITIVE_QUERY_VALUE = Regex("([?&](?:api[_-]?key|token|auth(?:orization)?|t)=)[^&\\s]+", RegexOption.IGNORE_CASE)
}
@@ -60,6 +60,7 @@ import coil.load
import coil.request.ImageRequest
import com.ponzischeme89.memby.R
import com.ponzischeme89.memby.ServiceLocator
import com.ponzischeme89.memby.diagnostics.MembyDiagnostics
import com.ponzischeme89.memby.data.audioPassthroughPreference
import com.ponzischeme89.memby.data.DEFAULT_SEEK_INTERVAL_SECONDS
import com.ponzischeme89.memby.data.IntroSegment
@@ -609,6 +610,9 @@ class PlayerActivity : ComponentActivity() {
trace.mark(PlaybackTrace.PLAYER_BUILT)
playback.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
MembyDiagnostics.debug("media3_state", "playback" to playSessionId, "item" to itemId, "state" to media3StateName(playbackState),
"play_when_ready" to playback.playWhenReady, "position_ms" to playback.currentPosition,
"buffered_ms" to playback.bufferedPosition)
recordPlaybackState(playbackState)
when (playbackState) {
Player.STATE_BUFFERING -> {
@@ -640,6 +644,8 @@ class PlayerActivity : ComponentActivity() {
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
MembyDiagnostics.debug("media3_playing", "playback" to playSessionId, "item" to itemId, "is_playing" to isPlaying,
"position_ms" to playback.currentPosition)
if (playbackStarted && !stopReported) {
reportProgress(
playback.currentPosition,
@@ -664,6 +670,7 @@ class PlayerActivity : ComponentActivity() {
}
override fun onTracksChanged(tracks: Tracks) {
MembyDiagnostics.trace("media3_tracks_changed", "playback" to playSessionId, "item" to itemId, "groups" to tracks.groups.size)
updateStreamStatus(tracks)
ensureSubtitleSelected(tracks)
}
@@ -674,10 +681,14 @@ class PlayerActivity : ComponentActivity() {
}
override fun onPlayerError(error: PlaybackException) {
MembyDiagnostics.debug("media3_error", "playback" to playSessionId, "item" to itemId, "code" to error.errorCodeName,
"exception" to error.cause?.javaClass?.simpleName, "detail" to error.message)
handlePlaybackError(error)
}
override fun onRenderedFirstFrame() {
MembyDiagnostics.info("media3_first_frame", "playback" to playSessionId, "item" to itemId,
"startup_ms" to (SystemClock.elapsedRealtime() - requestStartedAtMs))
renderedFirstFrame = true
trailerStartupTimeoutJob?.cancel()
trailerStartupTimeoutJob = null
@@ -1726,6 +1737,14 @@ class PlayerActivity : ComponentActivity() {
)
}
private fun media3StateName(state: Int): String = when (state) {
Player.STATE_IDLE -> "idle"
Player.STATE_BUFFERING -> "buffering"
Player.STATE_READY -> "ready"
Player.STATE_ENDED -> "ended"
else -> "unknown($state)"
}
private fun updateStreamStatus(tracks: Tracks) {
val selected = tracks.groups.flatMap { group ->
(0 until group.length)