|
@@ -1,15 +1,21 @@
|
|
|
package com.joe.camera.webcam.camera
|
|
package com.joe.camera.webcam.camera
|
|
|
|
|
|
|
|
import android.content.Context
|
|
import android.content.Context
|
|
|
|
|
+import android.hardware.camera2.CameraCharacteristics
|
|
|
|
|
+import android.hardware.camera2.CaptureRequest
|
|
|
import android.provider.MediaStore
|
|
import android.provider.MediaStore
|
|
|
|
|
+import android.util.Range
|
|
|
import androidx.camera.core.Camera
|
|
import androidx.camera.core.Camera
|
|
|
import androidx.camera.core.CameraSelector
|
|
import androidx.camera.core.CameraSelector
|
|
|
import androidx.camera.core.ImageCapture
|
|
import androidx.camera.core.ImageCapture
|
|
|
import androidx.camera.core.ImageCaptureException
|
|
import androidx.camera.core.ImageCaptureException
|
|
|
import androidx.camera.core.Preview
|
|
import androidx.camera.core.Preview
|
|
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
|
-import androidx.camera.video.MediaStoreOutputOptions
|
|
|
|
|
-import androidx.camera.video.FallbackStrategy
|
|
|
|
|
|
|
+import androidx.camera.camera2.interop.Camera2CameraControl
|
|
|
|
|
+import androidx.camera.camera2.interop.Camera2CameraInfo
|
|
|
|
|
+import androidx.camera.camera2.interop.CaptureRequestOptions
|
|
|
|
|
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop
|
|
|
|
|
+import androidx.camera.video.FileOutputOptions
|
|
|
import androidx.camera.video.PendingRecording
|
|
import androidx.camera.video.PendingRecording
|
|
|
import androidx.camera.video.Quality
|
|
import androidx.camera.video.Quality
|
|
|
import androidx.camera.video.QualitySelector
|
|
import androidx.camera.video.QualitySelector
|
|
@@ -23,14 +29,18 @@ import androidx.lifecycle.LifecycleOwner
|
|
|
import com.joe.camera.webcam.data.MediaRepository
|
|
import com.joe.camera.webcam.data.MediaRepository
|
|
|
import com.joe.camera.webcam.model.CameraState
|
|
import com.joe.camera.webcam.model.CameraState
|
|
|
import java.util.concurrent.Executor
|
|
import java.util.concurrent.Executor
|
|
|
|
|
+import java.util.concurrent.Executors
|
|
|
|
|
+import java.io.File
|
|
|
|
|
|
|
|
class CameraController(private val context: Context, private val onState: (CameraState) -> Unit, private val onMessage: (String) -> Unit) {
|
|
class CameraController(private val context: Context, private val onState: (CameraState) -> Unit, private val onMessage: (String) -> Unit) {
|
|
|
private val executor: Executor = ContextCompat.getMainExecutor(context)
|
|
private val executor: Executor = ContextCompat.getMainExecutor(context)
|
|
|
|
|
+ private val fileExecutor = Executors.newSingleThreadExecutor()
|
|
|
private var provider: ProcessCameraProvider? = null
|
|
private var provider: ProcessCameraProvider? = null
|
|
|
private var preview: Preview? = null
|
|
private var preview: Preview? = null
|
|
|
private var imageCapture: ImageCapture? = null
|
|
private var imageCapture: ImageCapture? = null
|
|
|
private var videoCapture: VideoCapture<Recorder>? = null
|
|
private var videoCapture: VideoCapture<Recorder>? = null
|
|
|
private var recording: Recording? = null
|
|
private var recording: Recording? = null
|
|
|
|
|
+ private var afterRecordingStopped: (() -> Unit)? = null
|
|
|
private var camera: Camera? = null
|
|
private var camera: Camera? = null
|
|
|
private var lifecycleOwner: LifecycleOwner? = null
|
|
private var lifecycleOwner: LifecycleOwner? = null
|
|
|
private var previewView: PreviewView? = null
|
|
private var previewView: PreviewView? = null
|
|
@@ -38,6 +48,7 @@ class CameraController(private val context: Context, private val onState: (Camer
|
|
|
private set
|
|
private set
|
|
|
var previewPaused = false
|
|
var previewPaused = false
|
|
|
private set
|
|
private set
|
|
|
|
|
+ private var linearZoom = 0f
|
|
|
|
|
|
|
|
fun bind(owner: LifecycleOwner, view: PreviewView, facing: Int = lensFacing) {
|
|
fun bind(owner: LifecycleOwner, view: PreviewView, facing: Int = lensFacing) {
|
|
|
lifecycleOwner = owner
|
|
lifecycleOwner = owner
|
|
@@ -49,11 +60,16 @@ class CameraController(private val context: Context, private val onState: (Camer
|
|
|
provider = future.get()
|
|
provider = future.get()
|
|
|
provider?.unbindAll()
|
|
provider?.unbindAll()
|
|
|
preview = Preview.Builder().build().also { it.surfaceProvider = view.surfaceProvider }
|
|
preview = Preview.Builder().build().also { it.surfaceProvider = view.surfaceProvider }
|
|
|
- imageCapture = ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY).build()
|
|
|
|
|
- val recorder = Recorder.Builder().setQualitySelector(QualitySelector.fromOrderedList(listOf(Quality.UHD, Quality.FHD, Quality.HD), FallbackStrategy.lowerQualityOrHigherThan(Quality.HD))).build()
|
|
|
|
|
|
|
+ imageCapture = ImageCapture.Builder()
|
|
|
|
|
+ .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
|
|
|
|
+ .setFlashMode(ImageCapture.FLASH_MODE_OFF)
|
|
|
|
|
+ .build()
|
|
|
|
|
+ val recorder = Recorder.Builder().setQualitySelector(QualitySelector.from(Quality.FHD)).build()
|
|
|
videoCapture = VideoCapture.withOutput(recorder)
|
|
videoCapture = VideoCapture.withOutput(recorder)
|
|
|
camera = provider?.bindToLifecycle(owner, CameraSelector.Builder().requireLensFacing(facing).build(), preview, imageCapture, videoCapture)
|
|
camera = provider?.bindToLifecycle(owner, CameraSelector.Builder().requireLensFacing(facing).build(), preview, imageCapture, videoCapture)
|
|
|
camera?.cameraControl?.enableTorch(false)
|
|
camera?.cameraControl?.enableTorch(false)
|
|
|
|
|
+ camera?.cameraControl?.setLinearZoom(linearZoom)
|
|
|
|
|
+ camera?.let(::configureLowLight)
|
|
|
onState(CameraState.FREE)
|
|
onState(CameraState.FREE)
|
|
|
}.onFailure { onState(CameraState.ERROR); onMessage("相机初始化失败:${it.message.orEmpty()}") }
|
|
}.onFailure { onState(CameraState.ERROR); onMessage("相机初始化失败:${it.message.orEmpty()}") }
|
|
|
}, executor)
|
|
}, executor)
|
|
@@ -66,20 +82,67 @@ class CameraController(private val context: Context, private val onState: (Camer
|
|
|
preview?.surfaceProvider = if (previewPaused) null else previewView?.surfaceProvider
|
|
preview?.surfaceProvider = if (previewPaused) null else previewView?.surfaceProvider
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ fun setLinearZoom(value: Float) {
|
|
|
|
|
+ linearZoom = value.coerceIn(0f, 1f)
|
|
|
|
|
+ camera?.cameraControl?.setLinearZoom(linearZoom)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @OptIn(ExperimentalCamera2Interop::class)
|
|
|
|
|
+ private fun configureLowLight(boundCamera: Camera) {
|
|
|
|
|
+ runCatching {
|
|
|
|
|
+ val cameraInfo = Camera2CameraInfo.from(boundCamera.cameraInfo)
|
|
|
|
|
+ val availableFpsRanges = cameraInfo.getCameraCharacteristic(
|
|
|
|
|
+ CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES,
|
|
|
|
|
+ ) ?: emptyArray()
|
|
|
|
|
+ val lowLightRange = availableFpsRanges
|
|
|
|
|
+ .filter { it.lower <= 15 && it.upper >= 30 }
|
|
|
|
|
+ .minByOrNull { it.lower }
|
|
|
|
|
+
|
|
|
|
|
+ val noiseReductionModes = cameraInfo.getCameraCharacteristic(
|
|
|
|
|
+ CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES,
|
|
|
|
|
+ ) ?: intArrayOf()
|
|
|
|
|
+ val options = CaptureRequestOptions.Builder().apply {
|
|
|
|
|
+ lowLightRange?.let {
|
|
|
|
|
+ setCaptureRequestOption(
|
|
|
|
|
+ CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
|
|
|
|
|
+ Range(it.lower, it.upper),
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ if (noiseReductionModes.contains(CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY)) {
|
|
|
|
|
+ setCaptureRequestOption(
|
|
|
|
|
+ CaptureRequest.NOISE_REDUCTION_MODE,
|
|
|
|
|
+ CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }.build()
|
|
|
|
|
+ Camera2CameraControl.from(boundCamera.cameraControl).setCaptureRequestOptions(options)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
fun takePicture() {
|
|
fun takePicture() {
|
|
|
val capture = imageCapture ?: return onMessage("相机尚未就绪")
|
|
val capture = imageCapture ?: return onMessage("相机尚未就绪")
|
|
|
|
|
+ capture.flashMode = ImageCapture.FLASH_MODE_OFF
|
|
|
|
|
+ camera?.cameraControl?.enableTorch(false)
|
|
|
onState(CameraState.WORKING)
|
|
onState(CameraState.WORKING)
|
|
|
val options = ImageCapture.OutputFileOptions.Builder(context.contentResolver, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaRepository(context).imageValues()).build()
|
|
val options = ImageCapture.OutputFileOptions.Builder(context.contentResolver, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaRepository(context).imageValues()).build()
|
|
|
capture.takePicture(options, executor, object : ImageCapture.OnImageSavedCallback {
|
|
capture.takePicture(options, executor, object : ImageCapture.OnImageSavedCallback {
|
|
|
- override fun onImageSaved(output: ImageCapture.OutputFileResults) { onState(CameraState.FREE); onMessage("照片已保存到 Download/jf") }
|
|
|
|
|
- override fun onError(exception: ImageCaptureException) { onState(CameraState.ERROR); onMessage("拍照失败:${exception.message}") }
|
|
|
|
|
|
|
+ override fun onImageSaved(output: ImageCapture.OutputFileResults) {
|
|
|
|
|
+ onState(CameraState.FREE)
|
|
|
|
|
+ onMessage("照片已保存到 Download/jf")
|
|
|
|
|
+ }
|
|
|
|
|
+ override fun onError(exception: ImageCaptureException) {
|
|
|
|
|
+ onState(CameraState.ERROR)
|
|
|
|
|
+ onMessage("拍照失败:${exception.message}")
|
|
|
|
|
+ }
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fun toggleRecording() {
|
|
fun toggleRecording() {
|
|
|
recording?.let { it.stop(); return }
|
|
recording?.let { it.stop(); return }
|
|
|
val capture = videoCapture ?: return onMessage("相机尚未就绪")
|
|
val capture = videoCapture ?: return onMessage("相机尚未就绪")
|
|
|
- val options = MediaStoreOutputOptions.Builder(context.contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI).setContentValues(MediaRepository(context).videoValues()).build()
|
|
|
|
|
|
|
+ camera?.cameraControl?.enableTorch(false)
|
|
|
|
|
+ val temporaryFile = File.createTempFile("webcam-recording-", ".mp4", context.cacheDir)
|
|
|
|
|
+ val options = FileOutputOptions.Builder(temporaryFile).build()
|
|
|
val pending: PendingRecording = capture.output.prepareRecording(context, options)
|
|
val pending: PendingRecording = capture.output.prepareRecording(context, options)
|
|
|
onState(CameraState.WORKING)
|
|
onState(CameraState.WORKING)
|
|
|
recording = pending.start(executor) { event ->
|
|
recording = pending.start(executor) { event ->
|
|
@@ -87,13 +150,35 @@ class CameraController(private val context: Context, private val onState: (Camer
|
|
|
is VideoRecordEvent.Start -> onState(CameraState.RECORDING)
|
|
is VideoRecordEvent.Start -> onState(CameraState.RECORDING)
|
|
|
is VideoRecordEvent.Finalize -> {
|
|
is VideoRecordEvent.Finalize -> {
|
|
|
recording = null
|
|
recording = null
|
|
|
- if (event.hasError()) { onState(CameraState.ERROR); onMessage("录像失败:${event.error}") }
|
|
|
|
|
- else { onState(CameraState.FREE); onMessage("视频已保存到 Download/jf") }
|
|
|
|
|
|
|
+ if (event.hasError()) {
|
|
|
|
|
+ temporaryFile.delete()
|
|
|
|
|
+ onState(CameraState.ERROR)
|
|
|
|
|
+ onMessage("录像失败:${event.error}")
|
|
|
|
|
+ afterRecordingStopped?.also { callback -> afterRecordingStopped = null; callback() }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ onState(CameraState.WORKING)
|
|
|
|
|
+ fileExecutor.execute {
|
|
|
|
|
+ val saved = MediaRepository(context).importRecordedVideo(temporaryFile)
|
|
|
|
|
+ executor.execute {
|
|
|
|
|
+ if (saved) { onState(CameraState.FREE); onMessage("视频已保存到 Download/jf") }
|
|
|
|
|
+ else { onState(CameraState.ERROR); onMessage("录像保存失败") }
|
|
|
|
|
+ afterRecordingStopped?.also { callback -> afterRecordingStopped = null; callback() }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fun stopRecording() { recording?.stop() }
|
|
fun stopRecording() { recording?.stop() }
|
|
|
|
|
+ fun stopRecording(onStopped: () -> Unit) {
|
|
|
|
|
+ val activeRecording = recording
|
|
|
|
|
+ if (activeRecording == null) onStopped()
|
|
|
|
|
+ else {
|
|
|
|
|
+ afterRecordingStopped = onStopped
|
|
|
|
|
+ activeRecording.stop()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
fun release() { recording?.stop(); recording = null; provider?.unbindAll() }
|
|
fun release() { recording?.stop(); recording = null; provider?.unbindAll() }
|
|
|
}
|
|
}
|