diff --git a/app/src/main/kotlin/com/kimgo/posefit/MainActivity.kt b/app/src/main/kotlin/com/kimgo/posefit/MainActivity.kt index 0e56669..53f20cc 100644 --- a/app/src/main/kotlin/com/kimgo/posefit/MainActivity.kt +++ b/app/src/main/kotlin/com/kimgo/posefit/MainActivity.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.kimgo.posefit.sender.CameraFacing import com.kimgo.posefit.sender.WebRtcSenderClient import timber.log.Timber @@ -23,7 +24,8 @@ class MainActivity : ComponentActivity() { registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> if (granted) { val url = getSavedSignalingUrl() - startWebRtc(url) + val cameraFacing = getSavedCameraFacing() + startWebRtc(url, cameraFacing) } else { Timber.w("Camera permission denied") } @@ -48,6 +50,7 @@ class MainActivity : ComponentActivity() { @Composable fun ConfigScreen() { var url by remember { mutableStateOf(getSavedSignalingUrl()) } + var cameraFacing by remember { mutableStateOf(getSavedCameraFacing()) } var isStreaming by remember { mutableStateOf(false) } Column( @@ -71,6 +74,32 @@ class MainActivity : ComponentActivity() { enabled = !isStreaming ) Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "选择摄像头", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + CameraFacingButton( + text = "前置摄像头", + selected = cameraFacing == CameraFacing.FRONT, + enabled = !isStreaming, + modifier = Modifier.weight(1f), + onClick = { cameraFacing = CameraFacing.FRONT } + ) + CameraFacingButton( + text = "后置摄像头", + selected = cameraFacing == CameraFacing.BACK, + enabled = !isStreaming, + modifier = Modifier.weight(1f), + onClick = { cameraFacing = CameraFacing.BACK } + ) + } + Spacer(modifier = Modifier.height(16.dp)) Button( onClick = { if (isStreaming) { @@ -78,6 +107,7 @@ class MainActivity : ComponentActivity() { isStreaming = false } else { saveSignalingUrl(url) + saveCameraFacing(cameraFacing) requestPermission.launch(Manifest.permission.CAMERA) isStreaming = true } @@ -89,6 +119,40 @@ class MainActivity : ComponentActivity() { } } + @Composable + private fun CameraFacingButton( + text: String, + selected: Boolean, + enabled: Boolean, + modifier: Modifier = Modifier, + onClick: () -> Unit + ) { + val colors = if (selected) { + ButtonDefaults.buttonColors() + } else { + ButtonDefaults.outlinedButtonColors() + } + + if (selected) { + Button( + onClick = onClick, + enabled = enabled, + modifier = modifier + ) { + Text(text) + } + } else { + OutlinedButton( + onClick = onClick, + enabled = enabled, + modifier = modifier, + colors = colors + ) { + Text(text) + } + } + } + private fun getSavedSignalingUrl(): String { val prefs = getSharedPreferences("posefit_prefs", Context.MODE_PRIVATE) return prefs.getString("signaling_url", "ws://192.168.2.10:8765") ?: "ws://192.168.2.10:8765" @@ -99,12 +163,25 @@ class MainActivity : ComponentActivity() { prefs.edit().putString("signaling_url", url).apply() } - private fun startWebRtc(url: String) { - Timber.i("startWebRtc: %s", url) + private fun getSavedCameraFacing(): CameraFacing { + val prefs = getSharedPreferences("posefit_prefs", Context.MODE_PRIVATE) + val value = prefs.getString("camera_facing", CameraFacing.FRONT.name) + return runCatching { CameraFacing.valueOf(value ?: CameraFacing.FRONT.name) } + .getOrDefault(CameraFacing.FRONT) + } + + private fun saveCameraFacing(cameraFacing: CameraFacing) { + val prefs = getSharedPreferences("posefit_prefs", Context.MODE_PRIVATE) + prefs.edit().putString("camera_facing", cameraFacing.name).apply() + } + + private fun startWebRtc(url: String, cameraFacing: CameraFacing) { + Timber.i("startWebRtc: %s, cameraFacing=%s", url, cameraFacing) webRtcClient?.release() webRtcClient = WebRtcSenderClient( context = this, - signalingUrl = url + signalingUrl = url, + cameraFacing = cameraFacing ) webRtcClient?.start() } diff --git a/app/src/main/kotlin/com/kimgo/posefit/sender/CameraFacing.kt b/app/src/main/kotlin/com/kimgo/posefit/sender/CameraFacing.kt new file mode 100644 index 0000000..cab1a5b --- /dev/null +++ b/app/src/main/kotlin/com/kimgo/posefit/sender/CameraFacing.kt @@ -0,0 +1,6 @@ +package com.kimgo.posefit.sender + +enum class CameraFacing { + FRONT, + BACK +} diff --git a/app/src/main/kotlin/com/kimgo/posefit/sender/WebRtcSenderClient.kt b/app/src/main/kotlin/com/kimgo/posefit/sender/WebRtcSenderClient.kt index f2c3926..b342424 100644 --- a/app/src/main/kotlin/com/kimgo/posefit/sender/WebRtcSenderClient.kt +++ b/app/src/main/kotlin/com/kimgo/posefit/sender/WebRtcSenderClient.kt @@ -8,7 +8,8 @@ import timber.log.Timber class WebRtcSenderClient( private val context: Context, - private val signalingUrl: String + private val signalingUrl: String, + private val cameraFacing: CameraFacing = CameraFacing.FRONT ) { private val eglBase = EglBase.create() @@ -24,7 +25,7 @@ class WebRtcSenderClient( private var surfaceTextureHelper: SurfaceTextureHelper? = null fun start() { - Timber.i("WebRTC starting, signalingUrl=%s", signalingUrl) + Timber.i("WebRTC starting, signalingUrl=%s, cameraFacing=%s", signalingUrl, cameraFacing) initPeerConnectionFactory() connectSignaling() } @@ -129,9 +130,7 @@ class WebRtcSenderClient( private fun startCamera() { val enumerator = Camera2Enumerator(context) - val cameraName = enumerator.deviceNames.firstOrNull { - enumerator.isFrontFacing(it) - } ?: enumerator.deviceNames.first() + val cameraName = selectCamera(enumerator) videoCapturer = enumerator.createCapturer(cameraName, null) @@ -160,6 +159,22 @@ class WebRtcSenderClient( Timber.d("Camera started: %s, resolution=1280x720@30fps", cameraName) } + private fun selectCamera(enumerator: Camera2Enumerator): String { + val preferredCamera = enumerator.deviceNames.firstOrNull { cameraName -> + when (cameraFacing) { + CameraFacing.FRONT -> enumerator.isFrontFacing(cameraName) + CameraFacing.BACK -> enumerator.isBackFacing(cameraName) + } + } + + if (preferredCamera != null) { + return preferredCamera + } + + Timber.w("Preferred camera not found: %s, falling back to first available camera", cameraFacing) + return enumerator.deviceNames.first() + } + private fun createOffer() { val constraints = MediaConstraints().apply { mandatory.add( @@ -256,4 +271,4 @@ class WebRtcSenderClient( Timber.d("WebRTC released") } -} \ No newline at end of file +}