0.2.63 update
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user