|
|
|
|
@ -9,25 +9,34 @@ import android.view.TextureView
|
|
|
|
|
import androidx.activity.compose.rememberLauncherForActivityResult |
|
|
|
|
import androidx.activity.result.contract.ActivityResultContracts |
|
|
|
|
import androidx.compose.foundation.layout.* |
|
|
|
|
import androidx.compose.foundation.BorderStroke |
|
|
|
|
import androidx.compose.foundation.Canvas |
|
|
|
|
import androidx.compose.foundation.shape.RoundedCornerShape |
|
|
|
|
import androidx.compose.foundation.horizontalScroll |
|
|
|
|
import androidx.compose.foundation.rememberScrollState |
|
|
|
|
import androidx.compose.foundation.verticalScroll |
|
|
|
|
import androidx.compose.material.icons.Icons |
|
|
|
|
import androidx.compose.material.icons.filled.ArrowUpward |
|
|
|
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack |
|
|
|
|
import androidx.compose.material3.* |
|
|
|
|
import androidx.compose.runtime.* |
|
|
|
|
import androidx.compose.runtime.saveable.rememberSaveable |
|
|
|
|
import androidx.compose.ui.Modifier |
|
|
|
|
import androidx.compose.ui.Alignment |
|
|
|
|
import androidx.compose.ui.draw.rotate |
|
|
|
|
import androidx.compose.ui.geometry.CornerRadius |
|
|
|
|
import androidx.compose.ui.geometry.Offset |
|
|
|
|
import androidx.compose.ui.geometry.Size |
|
|
|
|
import androidx.compose.ui.graphics.Color |
|
|
|
|
import androidx.compose.ui.graphics.StrokeCap |
|
|
|
|
import androidx.compose.ui.graphics.drawscope.Stroke |
|
|
|
|
import androidx.compose.ui.semantics.contentDescription |
|
|
|
|
import androidx.compose.ui.semantics.selected |
|
|
|
|
import androidx.compose.ui.semantics.semantics |
|
|
|
|
import androidx.compose.ui.platform.LocalContext |
|
|
|
|
import androidx.compose.ui.platform.LocalConfiguration |
|
|
|
|
import androidx.compose.ui.platform.LocalLifecycleOwner |
|
|
|
|
import androidx.compose.ui.res.stringResource |
|
|
|
|
import androidx.compose.ui.unit.dp |
|
|
|
|
import androidx.compose.ui.unit.sp |
|
|
|
|
import androidx.compose.ui.viewinterop.AndroidView |
|
|
|
|
import androidx.core.content.ContextCompat |
|
|
|
|
import androidx.lifecycle.Lifecycle |
|
|
|
|
@ -166,10 +175,13 @@ private fun CaptureOptions(cameras: List<StreamCamera>, config: StreamCaptureCon
|
|
|
|
|
}, label = { Text("${camera.lens.label} ${camera.lens.mm}".trim()) }) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
val modes = cameras.firstOrNull { it.id == mode?.camera }?.modes.orEmpty() |
|
|
|
|
val camera = cameras.firstOrNull { it.id == mode?.camera } |
|
|
|
|
val modes = camera?.modes.orEmpty() |
|
|
|
|
val context = LocalContext.current |
|
|
|
|
val configuration = LocalConfiguration.current |
|
|
|
|
val display = remember(configuration) { ((context as? Activity)?.windowManager?.defaultDisplay?.rotation ?: 0) * 90 } |
|
|
|
|
var expanded by remember { mutableStateOf(false) } |
|
|
|
|
var orientationsExpanded by remember { mutableStateOf(false) } |
|
|
|
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { |
|
|
|
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) { |
|
|
|
|
Box { |
|
|
|
|
OutlinedButton(onClick = { expanded = true }, enabled = enabled && modes.isNotEmpty()) { Text(mode?.label.orEmpty()) } |
|
|
|
|
DropdownMenu(expanded = expanded && enabled, onDismissRequest = { expanded = false }) { |
|
|
|
|
@ -178,16 +190,45 @@ private fun CaptureOptions(cameras: List<StreamCamera>, config: StreamCaptureCon
|
|
|
|
|
}) } |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
Box { |
|
|
|
|
OutlinedButton(onClick = { orientationsExpanded = true }, enabled = enabled && config != null) { |
|
|
|
|
Text(stringResource(R.string.stream_orientation, config?.orientation?.degrees ?: 0)) |
|
|
|
|
for (option in listOf(StreamOrientation.UPRIGHT, StreamOrientation.RIGHT, StreamOrientation.LEFT)) { |
|
|
|
|
val active = config?.orientation == option |
|
|
|
|
val label = stringResource(R.string.stream_orientation, option.degrees) |
|
|
|
|
val top = if (camera?.lens?.type == LensType.SELFIE) (display - option.degrees + 360) % 360 |
|
|
|
|
else (option.degrees - display + 360) % 360 |
|
|
|
|
OutlinedButton(onClick = { config?.let { onChange(it.copy(orientation = option)) } }, enabled = enabled && config != null, |
|
|
|
|
modifier = Modifier.size(44.dp).semantics { selected = active; contentDescription = label }, |
|
|
|
|
shape = RoundedCornerShape(6.dp), contentPadding = PaddingValues(2.dp), |
|
|
|
|
border = BorderStroke(if (active) 2.dp else 1.dp, |
|
|
|
|
if (active) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline), |
|
|
|
|
colors = ButtonDefaults.outlinedButtonColors( |
|
|
|
|
containerColor = if (active) MaterialTheme.colorScheme.primaryContainer else Color.Transparent)) { |
|
|
|
|
Column(horizontalAlignment = Alignment.CenterHorizontally) { |
|
|
|
|
StreamRotationMark(top) |
|
|
|
|
Text("${option.degrees}°", fontSize = 10.sp, lineHeight = 10.sp) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
DropdownMenu(expanded = orientationsExpanded && enabled, onDismissRequest = { orientationsExpanded = false }) { |
|
|
|
|
StreamOrientation.values().forEach { option -> DropdownMenuItem(text = { Text("${option.degrees}°") }, onClick = { |
|
|
|
|
orientationsExpanded = false; config?.let { onChange(it.copy(orientation = option)) } |
|
|
|
|
}) } |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Composable |
|
|
|
|
private fun StreamRotationMark(top: Int) { |
|
|
|
|
val frame = LocalContentColor.current |
|
|
|
|
val marker = MaterialTheme.colorScheme.error |
|
|
|
|
Box(Modifier.size(24.dp), contentAlignment = Alignment.Center) { |
|
|
|
|
Canvas(Modifier.matchParentSize()) { |
|
|
|
|
val left = size.width * 0.15f; val right = size.width * 0.85f |
|
|
|
|
val upper = size.height * 0.08f; val lower = size.height * 0.92f |
|
|
|
|
drawRoundRect(frame, Offset(left, upper), Size(right - left, lower - upper), CornerRadius(2.dp.toPx()), style = Stroke(1.dp.toPx())) |
|
|
|
|
val edge = when (top) { |
|
|
|
|
90 -> Offset(right, upper) to Offset(right, lower) |
|
|
|
|
180 -> Offset(left, lower) to Offset(right, lower) |
|
|
|
|
270 -> Offset(left, upper) to Offset(left, lower) |
|
|
|
|
else -> Offset(left, upper) to Offset(right, upper) |
|
|
|
|
} |
|
|
|
|
drawLine(marker, edge.first, edge.second, 3.dp.toPx(), StrokeCap.Round) |
|
|
|
|
} |
|
|
|
|
Text(stringResource(R.string.stream_top), fontSize = 7.sp, lineHeight = 7.sp) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -215,7 +256,6 @@ private fun LiveStreamView(group: Long, source: Long, epoch: Long, localPreview:
|
|
|
|
|
private fun LiveStreamSurface(group: Long, source: Long, epoch: Long, localPreview: StreamPreview?, displayDegrees: Int) { |
|
|
|
|
val engine = remember(group, source, epoch, localPreview) { StreamPlayerEngine(group, source, epoch, localPreview, displayDegrees) } |
|
|
|
|
val aspect by engine.aspect.collectAsState() |
|
|
|
|
val top by engine.top.collectAsState() |
|
|
|
|
val status by engine.status.collectAsState() |
|
|
|
|
var surface by remember { mutableStateOf<Surface?>(null) } |
|
|
|
|
DisposableEffect(engine) { onDispose { engine.stop() } } |
|
|
|
|
@ -240,25 +280,8 @@ private fun LiveStreamSurface(group: Long, source: Long, epoch: Long, localPrevi
|
|
|
|
|
}, modifier = Modifier.matchParentSize(), onRelease = { view -> |
|
|
|
|
engine.stop(); view.surfaceTextureListener = null; surface?.release(); surface = null |
|
|
|
|
}) |
|
|
|
|
top?.let { StreamTopMarker(it) } |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Composable |
|
|
|
|
private fun BoxScope.StreamTopMarker(direction: Int) { |
|
|
|
|
val alignment = when (direction) { |
|
|
|
|
90 -> Alignment.CenterEnd |
|
|
|
|
180 -> Alignment.BottomCenter |
|
|
|
|
270 -> Alignment.CenterStart |
|
|
|
|
else -> Alignment.TopCenter |
|
|
|
|
} |
|
|
|
|
Surface(modifier = Modifier.align(alignment).padding(8.dp), shape = RoundedCornerShape(6.dp), color = Color.Black.copy(alpha = 0.65f)) { |
|
|
|
|
Row(Modifier.padding(horizontal = 8.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) { |
|
|
|
|
Icon(Icons.Filled.ArrowUpward, contentDescription = null, tint = Color.White, modifier = Modifier.size(16.dp).rotate(direction.toFloat())) |
|
|
|
|
Spacer(Modifier.width(4.dp)) |
|
|
|
|
Text(stringResource(R.string.stream_top), color = Color.White, style = MaterialTheme.typography.bodySmall) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|