feat: adaptive quality engine + codec indicator UI
Wire AdaptiveQualityController into Android engine for auto codec switching based on network quality reports. Add color-coded TX/RX codec badges to the in-call screen showing active codecs and Auto mode. - Recv task: ingest QualityReports, feed to controller, signal profile changes via AtomicU8 to send task - Send task: check for pending profile switch at frame boundaries, update encoder/FEC/frame size - Track peer codec from incoming packet headers - Kotlin UI: codec badges (blue=studio, green=good, amber=degraded, red=catastrophic) with Auto tag - Add .taskmaster to .gitignore Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,12 @@ data class CallStats(
|
||||
val fecRecovered: Long = 0,
|
||||
/** Current mic audio level (RMS, 0-32767). */
|
||||
val audioLevel: Int = 0,
|
||||
/** Our current outgoing codec (e.g. "Opus24k"). */
|
||||
val currentCodec: String = "",
|
||||
/** Last seen incoming codec from peers. */
|
||||
val peerCodec: String = "",
|
||||
/** Whether auto quality mode is active. */
|
||||
val autoMode: Boolean = false,
|
||||
/** Number of participants in the room. */
|
||||
val roomParticipantCount: Int = 0,
|
||||
/** Participants in the room (fingerprint + optional alias). */
|
||||
@@ -76,6 +82,9 @@ data class CallStats(
|
||||
underruns = obj.optLong("underruns", 0),
|
||||
fecRecovered = obj.optLong("fec_recovered", 0),
|
||||
audioLevel = obj.optInt("audio_level", 0),
|
||||
currentCodec = obj.optString("current_codec", ""),
|
||||
peerCodec = obj.optString("peer_codec", ""),
|
||||
autoMode = obj.optBoolean("auto_mode", false),
|
||||
roomParticipantCount = obj.optInt("room_participant_count", 0),
|
||||
roomParticipants = parseParticipants(obj.optJSONArray("room_participants"))
|
||||
)
|
||||
|
||||
@@ -463,7 +463,51 @@ fun InCallScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Stats
|
||||
// Codec + Stats
|
||||
if (stats.currentCodec.isNotEmpty()) {
|
||||
val codecLabel = formatCodecName(stats.currentCodec)
|
||||
val peerLabel = if (stats.peerCodec.isNotEmpty()) formatCodecName(stats.peerCodec) else null
|
||||
val autoTag = if (stats.autoMode) " [Auto]" else ""
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Our codec badge
|
||||
Surface(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = codecColor(stats.currentCodec)
|
||||
) {
|
||||
Text(
|
||||
text = "TX $codecLabel$autoTag",
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
style = MaterialTheme.typography.labelSmall.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 10.sp
|
||||
),
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
if (peerLabel != null) {
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Surface(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = codecColor(stats.peerCodec)
|
||||
) {
|
||||
Text(
|
||||
text = "RX $peerLabel",
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
style = MaterialTheme.typography.labelSmall.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 10.sp
|
||||
),
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
Text(
|
||||
text = "TX: ${stats.framesEncoded} | RX: ${stats.framesDecoded}",
|
||||
style = MaterialTheme.typography.labelSmall.copy(fontFamily = FontFamily.Monospace),
|
||||
@@ -825,3 +869,25 @@ private fun DebugReportCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Map Rust CodecId debug name to a human-readable label. */
|
||||
private fun formatCodecName(codecId: String): String = when (codecId) {
|
||||
"Opus64k" -> "Opus 64k"
|
||||
"Opus48k" -> "Opus 48k"
|
||||
"Opus32k" -> "Opus 32k"
|
||||
"Opus24k" -> "Opus 24k"
|
||||
"Opus16k" -> "Opus 16k"
|
||||
"Opus6k" -> "Opus 6k"
|
||||
"Codec2_3200" -> "C2 3.2k"
|
||||
"Codec2_1200" -> "C2 1.2k"
|
||||
else -> codecId
|
||||
}
|
||||
|
||||
/** Color-code codec badges by quality tier. */
|
||||
private fun codecColor(codecId: String): Color = when (codecId) {
|
||||
"Opus64k", "Opus48k", "Opus32k" -> Color(0xFF0D6EFD) // blue — studio
|
||||
"Opus24k", "Opus16k" -> Color(0xFF198754) // green — good
|
||||
"Opus6k" -> Color(0xFFCC8800) // amber — degraded
|
||||
"Codec2_3200", "Codec2_1200" -> Color(0xFFDC3545) // red — catastrophic
|
||||
else -> Color(0xFF6C757D) // gray
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user