o.optString("alias", null) returns the string "null" when the JSON value
is JSON null. Use o.isNull() check first. Also handle empty fingerprint
edge case with "unknown" fallback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
98 lines
3.7 KiB
Kotlin
98 lines
3.7 KiB
Kotlin
package com.wzp.engine
|
|
|
|
import org.json.JSONArray
|
|
import org.json.JSONObject
|
|
|
|
/**
|
|
* Snapshot of call statistics, mirroring the Rust `CallStats` struct.
|
|
*
|
|
* Constructed from the JSON string returned by [WzpEngine.getStats].
|
|
*/
|
|
data class CallStats(
|
|
/** Current call state ordinal (see [CallStateConstants]). */
|
|
val state: Int = 0,
|
|
/** Call duration in seconds. */
|
|
val durationSecs: Double = 0.0,
|
|
/** Quality tier: 0 = Good, 1 = Degraded, 2 = Catastrophic. */
|
|
val qualityTier: Int = 0,
|
|
/** Observed packet loss percentage (0..100). */
|
|
val lossPct: Float = 0f,
|
|
/** Smoothed round-trip time in milliseconds. */
|
|
val rttMs: Int = 0,
|
|
/** Jitter in milliseconds. */
|
|
val jitterMs: Int = 0,
|
|
/** Current jitter buffer depth in packets. */
|
|
val jitterBufferDepth: Int = 0,
|
|
/** Total frames encoded since call start. */
|
|
val framesEncoded: Long = 0,
|
|
/** Total frames decoded since call start. */
|
|
val framesDecoded: Long = 0,
|
|
/** Number of playout underruns (buffer empty when audio was needed). */
|
|
val underruns: Long = 0,
|
|
/** Frames recovered by FEC. */
|
|
val fecRecovered: Long = 0,
|
|
/** Current mic audio level (RMS, 0-32767). */
|
|
val audioLevel: Int = 0,
|
|
/** Number of participants in the room. */
|
|
val roomParticipantCount: Int = 0,
|
|
/** Participants in the room (fingerprint + optional alias). */
|
|
val roomParticipants: List<RoomMember> = emptyList(),
|
|
) {
|
|
/** Human-readable quality label. */
|
|
val qualityLabel: String
|
|
get() = when (qualityTier) {
|
|
0 -> "Good"
|
|
1 -> "Degraded"
|
|
2 -> "Catastrophic"
|
|
else -> "Unknown"
|
|
}
|
|
|
|
companion object {
|
|
private fun parseParticipants(arr: JSONArray?): List<RoomMember> {
|
|
if (arr == null) return emptyList()
|
|
return (0 until arr.length()).map { i ->
|
|
val o = arr.getJSONObject(i)
|
|
RoomMember(
|
|
fingerprint = o.optString("fingerprint", ""),
|
|
alias = if (o.isNull("alias")) null else o.optString("alias", null)
|
|
)
|
|
}
|
|
}
|
|
|
|
/** Deserialise from the JSON string produced by the native engine. */
|
|
fun fromJson(json: String): CallStats {
|
|
return try {
|
|
val obj = JSONObject(json)
|
|
CallStats(
|
|
state = obj.optInt("state", 0),
|
|
durationSecs = obj.optDouble("duration_secs", 0.0),
|
|
qualityTier = obj.optInt("quality_tier", 0),
|
|
lossPct = obj.optDouble("loss_pct", 0.0).toFloat(),
|
|
rttMs = obj.optInt("rtt_ms", 0),
|
|
jitterMs = obj.optInt("jitter_ms", 0),
|
|
jitterBufferDepth = obj.optInt("jitter_buffer_depth", 0),
|
|
framesEncoded = obj.optLong("frames_encoded", 0),
|
|
framesDecoded = obj.optLong("frames_decoded", 0),
|
|
underruns = obj.optLong("underruns", 0),
|
|
fecRecovered = obj.optLong("fec_recovered", 0),
|
|
audioLevel = obj.optInt("audio_level", 0),
|
|
roomParticipantCount = obj.optInt("room_participant_count", 0),
|
|
roomParticipants = parseParticipants(obj.optJSONArray("room_participants"))
|
|
)
|
|
} catch (e: Exception) {
|
|
CallStats()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
data class RoomMember(
|
|
val fingerprint: String,
|
|
val alias: String? = null
|
|
) {
|
|
/** Short display name: alias if set, otherwise first 8 chars of fingerprint. */
|
|
val displayName: String
|
|
get() = alias?.takeIf { it.isNotBlank() }
|
|
?: fingerprint.take(8).ifEmpty { "unknown" }
|
|
}
|