Joburgess 1 săptămână în urmă
părinte
comite
72c460750c

BIN
WebCam-release-v1.0.0.apk


+ 1 - 0
app/src/main/java/com/joe/camera/webcam/MainViewModel.kt

@@ -54,6 +54,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
     }
     fun togglePreview() { cameraController.togglePreview(); _uiState.value = _uiState.value.copy(previewPaused = cameraController.previewPaused) }
     fun setCameraZoom(value: Float) = cameraController.setLinearZoom(value)
+    fun focusCameraAt(x: Float, y: Float) = cameraController.focusAt(x, y)
     fun toggleRemoteMode() {
         val remote = !_uiState.value.remoteMode
         preferences.edit().putBoolean("remote_mode", remote).apply()

+ 26 - 0
app/src/main/java/com/joe/camera/webcam/camera/CameraController.kt

@@ -9,6 +9,7 @@ import androidx.camera.core.Camera
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.FocusMeteringAction
 import androidx.camera.core.Preview
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.camera2.interop.Camera2CameraControl
@@ -31,6 +32,7 @@ import com.joe.camera.webcam.model.CameraState
 import java.util.concurrent.Executor
 import java.util.concurrent.Executors
 import java.io.File
+import java.util.concurrent.TimeUnit
 
 class CameraController(private val context: Context, private val onState: (CameraState) -> Unit, private val onMessage: (String) -> Unit) {
     private val executor: Executor = ContextCompat.getMainExecutor(context)
@@ -70,6 +72,9 @@ class CameraController(private val context: Context, private val onState: (Camer
                 camera?.cameraControl?.enableTorch(false)
                 camera?.cameraControl?.setLinearZoom(linearZoom)
                 camera?.let(::configureLowLight)
+                view.post {
+                    if (view.width > 0 && view.height > 0) focusAt(view.width / 2f, view.height / 2f)
+                }
                 onState(CameraState.FREE)
             }.onFailure { onState(CameraState.ERROR); onMessage("相机初始化失败:${it.message.orEmpty()}") }
         }, executor)
@@ -87,6 +92,18 @@ class CameraController(private val context: Context, private val onState: (Camer
         camera?.cameraControl?.setLinearZoom(linearZoom)
     }
 
+    fun focusAt(x: Float, y: Float) {
+        val view = previewView ?: return
+        val point = view.meteringPointFactory.createPoint(x, y)
+        val action = FocusMeteringAction.Builder(
+            point,
+            FocusMeteringAction.FLAG_AF or FocusMeteringAction.FLAG_AE or FocusMeteringAction.FLAG_AWB,
+        )
+            .setAutoCancelDuration(4, TimeUnit.SECONDS)
+            .build()
+        camera?.cameraControl?.startFocusAndMetering(action)
+    }
+
     @OptIn(ExperimentalCamera2Interop::class)
     private fun configureLowLight(boundCamera: Camera) {
         runCatching {
@@ -101,6 +118,9 @@ class CameraController(private val context: Context, private val onState: (Camer
             val noiseReductionModes = cameraInfo.getCameraCharacteristic(
                 CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES,
             ) ?: intArrayOf()
+            val autoFocusModes = cameraInfo.getCameraCharacteristic(
+                CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES,
+            ) ?: intArrayOf()
             val options = CaptureRequestOptions.Builder().apply {
                 lowLightRange?.let {
                     setCaptureRequestOption(
@@ -114,6 +134,12 @@ class CameraController(private val context: Context, private val onState: (Camer
                         CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY,
                     )
                 }
+                if (autoFocusModes.contains(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)) {
+                    setCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_MODE,
+                        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO,
+                    )
+                }
             }.build()
             Camera2CameraControl.from(boundCamera.cameraControl).setCaptureRequestOptions(options)
         }

+ 3 - 0
app/src/main/java/com/joe/camera/webcam/ui/HomeScreen.kt

@@ -127,6 +127,9 @@ fun HomeScreen(viewModel: MainViewModel, state: MainUiState, onMedia: () -> Unit
                     .aspectRatio(9f / 16f)
                     .alpha(previewOpacity)
                     .clip(RoundedCornerShape((previewWidth / 9f).dp))
+                    .pointerInput(Unit) {
+                        detectTapGestures { point -> viewModel.focusCameraAt(point.x, point.y) }
+                    }
                     .pointerInput(maxWidthPx, maxHeightPx, previewWidthPx, previewHeightPx) {
                         detectDragGestures { change, drag ->
                             change.consume()

+ 36 - 8
app/src/main/java/com/joe/camera/webcam/ui/MediaScreens.kt

@@ -1,7 +1,8 @@
 package com.joe.camera.webcam.ui
 
-import android.content.Context
+import android.media.MediaMetadataRetriever
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
 import androidx.compose.foundation.combinedClickable
 import androidx.compose.foundation.layout.*
@@ -15,6 +16,8 @@ import androidx.compose.runtime.*
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asImageBitmap
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.LocalContext
@@ -24,15 +27,13 @@ import androidx.media3.common.MediaItem as ExoMediaItem
 import androidx.media3.common.MimeTypes
 import androidx.media3.exoplayer.ExoPlayer
 import androidx.media3.ui.PlayerView
-import coil.ImageLoader
 import coil.compose.AsyncImage
-import coil.decode.VideoFrameDecoder
-import coil.request.ImageRequest
-import coil.request.videoFrameMillis
 import com.joe.camera.webcam.MainViewModel
 import com.joe.camera.webcam.model.MediaItem
 import com.joe.camera.webcam.model.MediaKind
 import kotlinx.coroutines.delay
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
 
 @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
 @Composable
@@ -58,9 +59,36 @@ fun MediaGridScreen(viewModel: MainViewModel, media: List<MediaItem>, onBack: ()
 @Composable
 private fun MediaThumbnail(item: MediaItem, modifier: Modifier = Modifier) {
     val context = LocalContext.current
-    val imageLoader = remember(context) { ImageLoader.Builder(context).components { add(VideoFrameDecoder.Factory()) }.build() }
-    val request = remember(item.uri) { ImageRequest.Builder(context).data(item.uri).apply { if (item.kind == MediaKind.VIDEO) videoFrameMillis(0) }.crossfade(true).build() }
-    AsyncImage(model = request, imageLoader = imageLoader, contentDescription = item.displayName, contentScale = ContentScale.Crop, modifier = modifier)
+    if (item.kind == MediaKind.IMAGE) {
+        AsyncImage(
+            model = item.uri,
+            contentDescription = item.displayName,
+            contentScale = ContentScale.Crop,
+            modifier = modifier,
+        )
+    } else {
+        val thumbnail by produceState<ImageBitmap?>(initialValue = null, item.uri) {
+            value = withContext(Dispatchers.IO) {
+                val retriever = MediaMetadataRetriever()
+                try {
+                    retriever.setDataSource(context, item.uri)
+                    retriever.getFrameAtTime(0L, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)?.asImageBitmap()
+                } catch (_: Exception) {
+                    null
+                } finally {
+                    retriever.release()
+                }
+            }
+        }
+        thumbnail?.let {
+            Image(
+                bitmap = it,
+                contentDescription = item.displayName,
+                contentScale = ContentScale.Crop,
+                modifier = modifier,
+            )
+        }
+    }
 }
 
 @OptIn(ExperimentalMaterial3Api::class)