Browse Source

Add video codec and bitrate controls with aggregate send buffer indicators

master
evgeny 6 days ago
parent
commit
c815d06ab0
  1. 67
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamScreen.kt
  2. 5
      tools/chatgui-android/app/src/main/res/values-ru/strings.xml
  3. 5
      tools/chatgui-android/app/src/main/res/values/strings.xml

67
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamScreen.kt

@ -71,7 +71,8 @@ fun StreamScreen(channelId: String, channelName: String, sources: List<StreamSou
var audio by remember { mutableStateOf(true) }
var mode by remember { mutableStateOf<StreamCaptureMode?>(null) }
var orientation by rememberSaveable { mutableStateOf(StreamOrientation.UPRIGHT) }
val config = mode?.let { StreamCaptureConfig(it, orientation) }
var videoBitrate by rememberSaveable { mutableStateOf<Int?>(null) }
val config = mode?.let { StreamCaptureConfig(it, orientation, videoBitrate) }
var cameras by remember { mutableStateOf<List<StreamCamera>>(emptyList()) }
var loadingModes by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
@ -114,6 +115,7 @@ fun StreamScreen(channelId: String, channelName: String, sources: List<StreamSou
CaptureOptions(cameras, local.pendingConfig ?: local.config, local.phase == StreamPhase.LIVE && local.pendingConfig == null,
StreamController::change)
CaptureModeStatus(local.config, local.pendingConfig, local.notice)
StreamTxPanel(local.id)
}
Box(Modifier.weight(1f).fillMaxWidth()) {
key(group, selected.id, selected.epoch) { LiveStreamView(group, selected.id, selected.epoch, if (selected.self) local?.preview else null) }
@ -126,6 +128,7 @@ fun StreamScreen(channelId: String, channelName: String, sources: List<StreamSou
CaptureOptions(cameras, local.pendingConfig ?: local.config, local.phase == StreamPhase.LIVE && local.pendingConfig == null,
StreamController::change)
CaptureModeStatus(local.config, local.pendingConfig, local.notice)
StreamTxPanel(local.id)
Text(stringResource(when (local.phase) {
StreamPhase.LIVE -> R.string.stream_live
StreamPhase.STOPPING -> R.string.stream_stopping
@ -133,7 +136,7 @@ fun StreamScreen(channelId: String, channelName: String, sources: List<StreamSou
}))
Button(onClick = StreamController::stop, enabled = local.phase != StreamPhase.STOPPING) { Text(stringResource(R.string.stream_stop)) }
} else {
CaptureOptions(cameras, config, !loadingModes) { mode = it.mode; orientation = it.orientation }
CaptureOptions(cameras, config, !loadingModes) { mode = it.mode; orientation = it.orientation; videoBitrate = it.videoBitrate }
if (!loadingModes && cameras.isEmpty()) Text(stringResource(R.string.stream_no_modes), color = MaterialTheme.colorScheme.error)
Row { Checkbox(checked = audio, onCheckedChange = { audio = it }); Text(stringResource(R.string.microphone), Modifier.padding(top = 12.dp)) }
Button(onClick = {
@ -158,11 +161,24 @@ fun StreamScreen(channelId: String, channelName: String, sources: List<StreamSou
}
@Composable
private fun CaptureModeStatus(config: StreamCaptureConfig?, pending: StreamCaptureConfig?, notice: Int?) {
config?.let { Text(it.mode.label, style = MaterialTheme.typography.bodySmall) }
config?.let { Text("${it.mode.label} · ${it.mode.videoCodec.label} · " + stringResource(R.string.stream_bitrate_value, it.bitrate / 1_000_000.0),
style = MaterialTheme.typography.bodySmall) }
if (pending != null) Text(stringResource(if (config == null) R.string.stream_connecting else R.string.stream_switching))
notice?.let { Text(stringResource(it), color = MaterialTheme.colorScheme.error) }
}
@Composable
private fun StreamTxPanel(id: Long) {
val snapshot by StreamController.txStats.collectAsState()
val s = snapshot?.takeIf { it.id == id } ?: return
Column {
Text(stringResource(R.string.stream_tx_queues, s.videoBytes / 1024.0, s.inflightBytes / 1024.0),
style = MaterialTheme.typography.bodySmall)
Text(stringResource(R.string.stream_tx_total, s.totalBytes / 1024.0, s.acked1s / 1024.0, s.acked1s * 8 / 1_000_000.0),
style = MaterialTheme.typography.bodySmall)
}
}
@Composable
private fun CaptureOptions(cameras: List<StreamCamera>, config: StreamCaptureConfig?, enabled: Boolean,
onChange: (StreamCaptureConfig) -> Unit) {
@ -170,12 +186,51 @@ private fun CaptureOptions(cameras: List<StreamCamera>, config: StreamCaptureCon
Row(Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
cameras.forEach { camera ->
FilterChip(selected = camera.id == mode?.camera, enabled = enabled, onClick = {
StreamCameraModes.choose(camera.modes, mode)?.let { onChange(StreamCaptureConfig(it, config?.orientation ?: StreamOrientation.UPRIGHT)) }
StreamCameraModes.choose(camera.modes, mode)?.let { onChange(config?.withMode(it) ?: StreamCaptureConfig(it)) }
}, label = { Text("${camera.lens.label} ${camera.lens.mm}".trim()) })
}
}
val camera = cameras.firstOrNull { it.id == mode?.camera }
val modes = camera?.modes.orEmpty()
val allModes = camera?.modes.orEmpty()
val modes = allModes.filter { it.videoCodec == mode?.videoCodec }
var codecsExpanded by remember { mutableStateOf(false) }
var bitrateExpanded by remember { mutableStateOf(false) }
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Box(Modifier.weight(1f)) {
OutlinedButton(onClick = { codecsExpanded = true }, enabled = enabled && config != null, modifier = Modifier.fillMaxWidth()) {
Text(stringResource(R.string.stream_video_codec, mode?.videoCodec?.label.orEmpty()))
}
DropdownMenu(expanded = codecsExpanded && enabled, onDismissRequest = { codecsExpanded = false }) {
allModes.map { it.videoCodec }.distinct().sortedBy { it.id }.forEach { codec ->
DropdownMenuItem(text = { Text(codec.label) }, onClick = {
codecsExpanded = false
StreamCameraModes.choose(allModes.filter { it.videoCodec == codec }, mode)?.let { next ->
config?.let { onChange(it.withMode(next)) }
}
})
}
}
}
Box(Modifier.weight(1f)) {
OutlinedButton(onClick = { bitrateExpanded = true }, enabled = enabled && config != null, modifier = Modifier.fillMaxWidth()) {
Text(if (config?.videoBitrate == null) stringResource(R.string.stream_bitrate_auto)
else stringResource(R.string.stream_bitrate_value, config.bitrate / 1_000_000.0))
}
DropdownMenu(expanded = bitrateExpanded && enabled, onDismissRequest = { bitrateExpanded = false }) {
DropdownMenuItem(text = { Text(stringResource(R.string.stream_bitrate_auto)) }, onClick = {
bitrateExpanded = false; config?.let { onChange(it.copy(videoBitrate = null)) }
})
mode?.let { m ->
listOf(256000, 512000, 1000000, 2000000, 4000000, 6000000, 8000000, 12000000, 16000000, 24000000, 32000000, 48000000, 64000000)
.map { it.coerceIn(m.minimumBitrate, m.maximumBitrate) }.distinct().forEach { value ->
DropdownMenuItem(text = { Text(stringResource(R.string.stream_bitrate_value, value / 1_000_000.0)) }, onClick = {
bitrateExpanded = false; config?.let { onChange(it.copy(videoBitrate = value)) }
})
}
}
}
}
}
val context = LocalContext.current
val configuration = LocalConfiguration.current
val display = remember(configuration) { ((context as? Activity)?.windowManager?.defaultDisplay?.rotation ?: 0) * 90 }
@ -185,7 +240,7 @@ private fun CaptureOptions(cameras: List<StreamCamera>, config: StreamCaptureCon
OutlinedButton(onClick = { expanded = true }, enabled = enabled && modes.isNotEmpty()) { Text(mode?.label.orEmpty()) }
DropdownMenu(expanded = expanded && enabled, onDismissRequest = { expanded = false }) {
modes.forEach { option -> DropdownMenuItem(text = { Text(option.label) }, onClick = {
expanded = false; config?.let { onChange(it.copy(mode = option)) }
expanded = false; config?.let { onChange(it.withMode(option)) }
}) }
}
}

5
tools/chatgui-android/app/src/main/res/values-ru/strings.xml

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="stream_video_codec">Кодек: %1$s</string>
<string name="stream_bitrate_auto">Битрейт видео: Авто</string>
<string name="stream_bitrate_value">%1$.2f Мбит/с</string>
<string name="stream_tx_queues">Очередь видео: %1$.1f КиБ · Неподтверждено: %2$.1f КиБ</string>
<string name="stream_tx_total">Буфер отправки: %1$.1f КиБ · ACK / 1 с: %2$.1f КиБ (%3$.2f Мбит/с)</string>
<string name="stream_title">Трансляции</string>
<string name="stream_available">Трансляции: %1$d</string>
<string name="stream_choose_source">Выберите трансляцию</string>

5
tools/chatgui-android/app/src/main/res/values/strings.xml

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="stream_video_codec">Codec: %1$s</string>
<string name="stream_bitrate_auto">Video bitrate: Auto</string>
<string name="stream_bitrate_value">%1$.2f Mbps</string>
<string name="stream_tx_queues">Video queue: %1$.1f KiB · Unacknowledged: %2$.1f KiB</string>
<string name="stream_tx_total">Send buffer: %1$.1f KiB · ACK / 1 s: %2$.1f KiB (%3$.2f Mbps)</string>
<string name="stream_title">Live streams</string>
<string name="stream_available">Live streams: %1$d</string>
<string name="stream_choose_source">Choose a stream</string>

Loading…
Cancel
Save