Add video codec and bitrate controls with aggregate send buffer indicators
This commit is contained in:
1 parent
7a5a94bb64
commit
c815d06ab0
3 files changed
+71
-6
No files matched your search
@@ -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)) }
|
||||
}) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user