feat: add camera preview

This commit is contained in:
Fritz Heiden 2025-03-22 13:32:02 +01:00
parent 04c93a0ce7
commit 8d14f3575f
8 changed files with 121 additions and 17 deletions

View File

@ -52,6 +52,13 @@ dependencies {
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.ktor.client.core) implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio) implementation(libs.ktor.client.cio)
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.video)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.mlkit.vision)
implementation(libs.androidx.camera.extensions)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@ -1,10 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application <application
android:allowBackup="true" android:allowBackup="true"

View File

@ -29,15 +29,20 @@ import com.example.tvcontroller.ui.theme.TVControllerTheme
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.core.app.ActivityCompat
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.tvcontroller.services.BluetoothService import com.example.tvcontroller.services.BluetoothService
import com.example.tvcontroller.services.CameraService
import com.example.tvcontroller.services.DeviceService import com.example.tvcontroller.services.DeviceService
import com.example.tvcontroller.ui.AppViewModel import com.example.tvcontroller.ui.AppViewModel
import com.example.tvcontroller.ui.views.CameraView
import com.example.tvcontroller.ui.views.SettingsView
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private lateinit var bluetoothService: BluetoothService private lateinit var bluetoothService: BluetoothService
private lateinit var deviceService: DeviceService private lateinit var deviceService: DeviceService
private lateinit var cameraService: CameraService
private val appViewModel by viewModels<AppViewModel>() private val appViewModel by viewModels<AppViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -46,6 +51,8 @@ class MainActivity : ComponentActivity() {
bluetoothService.onBluetoothStateChanged { state -> appViewModel.setBluetoothEnabled(state == BluetoothAdapter.STATE_ON) } bluetoothService.onBluetoothStateChanged { state -> appViewModel.setBluetoothEnabled(state == BluetoothAdapter.STATE_ON) }
appViewModel.setBluetoothEnabled(bluetoothService.isBluetoothEnabled()) appViewModel.setBluetoothEnabled(bluetoothService.isBluetoothEnabled())
deviceService = DeviceService(this.applicationContext) deviceService = DeviceService(this.applicationContext)
cameraService = CameraService(this.applicationContext)
checkPermissions()
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
TVControllerTheme { TVControllerTheme {
@ -57,6 +64,12 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
private fun checkPermissions() {
if (!cameraService.hasRequiredPermissions()) {
ActivityCompat.requestPermissions(this, CameraService.CAMERAX_PERMISSIONS, 0)
}
}
} }
@Composable @Composable
@ -115,31 +128,19 @@ fun TvControllerApp(
modifier = Modifier.padding(innerPadding) modifier = Modifier.padding(innerPadding)
) { ) {
composable(route = Screen.Camera.name) { composable(route = Screen.Camera.name) {
CameraScreen() CameraView()
} }
composable(route = Screen.Remote.name) { composable(route = Screen.Remote.name) {
RemoteScreen() RemoteScreen()
} }
composable(route = Screen.Settings.name) { composable(route = Screen.Settings.name) {
SettingsScreen(appViewModel = appViewModel, deviceService = deviceService) SettingsView(appViewModel = appViewModel, deviceService = deviceService)
} }
} }
} }
} }
} }
@Composable
fun CameraScreen(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "Camera Screen", modifier = modifier)
}
}
@Composable @Composable
fun RemoteScreen(modifier: Modifier = Modifier) { fun RemoteScreen(modifier: Modifier = Modifier) {
Column( Column(

View File

@ -0,0 +1,19 @@
package com.example.tvcontroller.services
import android.content.Context
import android.content.pm.PackageManager
class CameraService(private val context: Context) {
fun hasRequiredPermissions(): Boolean {
return CAMERAX_PERMISSIONS.all {
context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED
}
}
companion object {
val CAMERAX_PERMISSIONS = arrayOf(
android.Manifest.permission.CAMERA,
android.Manifest.permission.RECORD_AUDIO
)
}
}

View File

@ -0,0 +1,25 @@
package com.example.tvcontroller.ui.components
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.compose.LocalLifecycleOwner
@Composable
fun CameraPreview(
controller: LifecycleCameraController,
modifier: Modifier = Modifier
) {
val lifecycleOwner = LocalLifecycleOwner.current
AndroidView(
modifier = modifier,
factory = {
PreviewView(it).apply {
this.controller = controller
controller.bindToLifecycle(lifecycleOwner)
}
},
)
}

View File

@ -0,0 +1,34 @@
package com.example.tvcontroller.ui.views
import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.example.tvcontroller.ui.components.CameraPreview
@Composable
fun CameraView() {
val context = LocalContext.current
val controller = remember {
LifecycleCameraController(context).apply {
setEnabledUseCases(CameraController.VIDEO_CAPTURE)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.padding(all = 16.dp),
) {
CameraPreview(controller = controller, modifier = Modifier.fillMaxSize())
}
}

View File

@ -1,4 +1,4 @@
package com.example.tvcontroller package com.example.tvcontroller.ui.views
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -16,13 +16,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.tvcontroller.R
import com.example.tvcontroller.Settings
import com.example.tvcontroller.SettingsViewModel
import com.example.tvcontroller.services.DeviceService import com.example.tvcontroller.services.DeviceService
import com.example.tvcontroller.ui.AppViewModel import com.example.tvcontroller.ui.AppViewModel
@Composable @Composable
fun SettingsScreen(deviceService: DeviceService, appViewModel: AppViewModel) { fun SettingsView(deviceService: DeviceService, appViewModel: AppViewModel) {
val viewModel = val viewModel =
viewModel<SettingsViewModel>(factory = SettingsViewModel.provideFactory(deviceService)) viewModel<SettingsViewModel>(factory = SettingsViewModel.Companion.provideFactory(deviceService))
Column( Column(
modifier = Modifier modifier = Modifier

View File

@ -1,5 +1,6 @@
[versions] [versions]
agp = "8.9.0" agp = "8.9.0"
cameraCore = "1.4.1"
kotlin = "2.0.0" kotlin = "2.0.0"
coreKtx = "1.10.1" coreKtx = "1.10.1"
junit = "4.13.2" junit = "4.13.2"
@ -12,6 +13,13 @@ composeBom = "2024.04.01"
navigationCompose = "2.8.4" navigationCompose = "2.8.4"
[libraries] [libraries]
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraCore" }
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "cameraCore" }
androidx-camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "cameraCore" }
androidx-camera-mlkit-vision = { module = "androidx.camera:camera-mlkit-vision", version.ref = "cameraCore" }
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "cameraCore" }
androidx-camera-video = { module = "androidx.camera:camera-video", version.ref = "cameraCore" }
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "cameraCore" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }