2 changed files with 83 additions and 0 deletions
@ -0,0 +1,82 @@
|
||||
package com.utun.chat.ui.screens |
||||
|
||||
import android.Manifest |
||||
import android.content.pm.PackageManager |
||||
import android.view.SurfaceHolder |
||||
import android.view.SurfaceView |
||||
import androidx.activity.compose.rememberLauncherForActivityResult |
||||
import androidx.activity.result.contract.ActivityResultContracts |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.layout.* |
||||
import androidx.compose.material3.* |
||||
import androidx.compose.runtime.* |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.platform.LocalContext |
||||
import androidx.compose.ui.unit.dp |
||||
import androidx.compose.ui.viewinterop.AndroidView |
||||
import androidx.core.content.ContextCompat |
||||
import com.utun.chat.data.CallController |
||||
import com.utun.chat.data.CallVideoBitrateRange |
||||
|
||||
@Composable |
||||
internal fun CallVideoPanel() { |
||||
val engine by CallController.video.collectAsState() |
||||
val video = engine ?: return |
||||
val state by video.state.collectAsState() |
||||
val context = LocalContext.current |
||||
var denied by remember(video) { mutableStateOf(false) } |
||||
val permission = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> |
||||
denied = !granted |
||||
if (granted && engine === video) CallController.setVideo(state.settings.copy(enabled = true)) |
||||
} |
||||
DisposableEffect(video) { onDispose { video.surface(null) } } |
||||
Column(Modifier.fillMaxWidth().padding(horizontal = 16.dp)) { |
||||
AndroidView(factory = { ctx -> SurfaceView(ctx).apply { |
||||
holder.addCallback(object : SurfaceHolder.Callback { |
||||
override fun surfaceCreated(holder: SurfaceHolder) { video.surface(holder.surface) } |
||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { video.surface(holder.surface) } |
||||
override fun surfaceDestroyed(holder: SurfaceHolder) { video.surface(null) } |
||||
}) |
||||
} }, modifier = Modifier.fillMaxWidth().height(240.dp).background(Color.Black)) |
||||
Text(state.status, color = Color.White) |
||||
if (denied) Text("Camera permission is required to send video", color = MaterialTheme.colorScheme.error) |
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { |
||||
Text("Send video", color = Color.White, modifier = Modifier.weight(1f).padding(top = 12.dp)) |
||||
Switch(state.settings.enabled, enabled = state.ready && state.codecs.isNotEmpty(), onCheckedChange = { enabled -> |
||||
if (!enabled || ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) |
||||
CallController.setVideo(state.settings.copy(enabled = enabled)) |
||||
else permission.launch(Manifest.permission.CAMERA) |
||||
}) |
||||
} |
||||
VideoChoice("Camera", state.cameras.map { it.name }, state.cameras.indexOfFirst { it.id == state.settings.camera }) { index -> |
||||
val camera = state.cameras[index] |
||||
val size = camera.sizes.firstOrNull { it.width == state.settings.width && it.height == state.settings.height } ?: camera.sizes.first() |
||||
CallController.setVideo(state.settings.copy(camera = camera.id, width = size.width, height = size.height)) |
||||
} |
||||
val sizes = state.cameras.firstOrNull { it.id == state.settings.camera }?.sizes.orEmpty() |
||||
VideoChoice("Resolution", sizes.map { "${it.width}×${it.height}" }, sizes.indexOfFirst { it.width == state.settings.width && it.height == state.settings.height }) { i -> |
||||
CallController.setVideo(state.settings.copy(width = sizes[i].width, height = sizes[i].height)) |
||||
} |
||||
val codecs = listOf(0) + state.codecs.sortedDescending() |
||||
VideoChoice("Codec", codecs.map { when (it) { 0 -> "Auto (H.265 → H.264)"; 2 -> "H.265"; else -> "H.264" } }, |
||||
codecs.indexOf(state.settings.codec)) { i -> CallController.setVideo(state.settings.copy(codec = codecs[i])) } |
||||
var selected by remember(video) { mutableStateOf(state.range.minimum.toFloat()..state.range.maximum.toFloat()) } |
||||
var dragging by remember(video) { mutableStateOf(false) } |
||||
LaunchedEffect(state.range) { if (!dragging) selected = state.range.minimum.toFloat()..state.range.maximum.toFloat() } |
||||
Text("Bitrate: ${selected.start.toInt()}–${selected.endInclusive.toInt()} kbit/s · ${state.bitrate / 1000} kbit/s", color = Color.White) |
||||
RangeSlider(value = selected, onValueChange = { dragging = true; selected = it }, valueRange = 300f..4000f, |
||||
onValueChangeFinished = { dragging = false; video.range(CallVideoBitrateRange(selected.start.toInt(), selected.endInclusive.toInt())) }) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun VideoChoice(label: String, values: List<String>, selected: Int, choose: (Int) -> Unit) { |
||||
var expanded by remember { mutableStateOf(false) } |
||||
Box { |
||||
TextButton(onClick = { expanded = true }, enabled = values.isNotEmpty()) { Text("$label: ${values.getOrNull(selected).orEmpty()}") } |
||||
DropdownMenu(expanded, onDismissRequest = { expanded = false }) { |
||||
values.forEachIndexed { index, value -> DropdownMenuItem(text = { Text(value) }, onClick = { expanded = false; choose(index) }) } |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue