mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 04:58:45 +00:00
committed by
Alex Hart
parent
39bc6d5eb3
commit
e91ed88785
@@ -15,6 +15,7 @@ import android.util.Size;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.OrientationEventListener;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -30,18 +31,12 @@ import androidx.camera.core.CameraSelector;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
import androidx.camera.core.ImageCaptureException;
|
||||
import androidx.camera.core.ImageProxy;
|
||||
import androidx.camera.video.FallbackStrategy;
|
||||
import androidx.camera.video.Quality;
|
||||
import androidx.camera.video.QualitySelector;
|
||||
import androidx.camera.view.CameraController;
|
||||
import androidx.camera.view.LifecycleCameraController;
|
||||
import androidx.camera.view.PreviewView;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.util.Executors;
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
|
||||
import org.signal.core.util.Stopwatch;
|
||||
@@ -54,6 +49,7 @@ import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXFlashToggleView;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModePolicy;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.SignalCameraController;
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaAnimations;
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaCountIndicatorButton;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
@@ -87,7 +83,8 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
||||
private Controller controller;
|
||||
private View selfieFlash;
|
||||
private MemoryFileDescriptor videoFileDescriptor;
|
||||
private LifecycleCameraController cameraController;
|
||||
private SignalCameraController cameraController;
|
||||
private CameraXOrientationListener orientationListener;
|
||||
private Disposable mostRecentItemDisposable = Disposable.disposed();
|
||||
private CameraXModePolicy cameraXModePolicy;
|
||||
private CameraScreenBrightnessController cameraScreenBrightnessController;
|
||||
@@ -124,6 +121,8 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
||||
if (controller == null) {
|
||||
throw new IllegalStateException("Parent must implement controller interface.");
|
||||
}
|
||||
|
||||
this.orientationListener = new CameraXOrientationListener(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -144,11 +143,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
||||
|
||||
Log.d(TAG, "Starting CameraX with mode policy " + cameraXModePolicy.getClass().getSimpleName());
|
||||
|
||||
cameraController = new LifecycleCameraController(requireContext());
|
||||
cameraController.bindToLifecycle(getViewLifecycleOwner());
|
||||
cameraController.setCameraSelector(CameraXUtil.toCameraSelector(TextSecurePreferences.getDirectCaptureCameraId(requireContext())));
|
||||
cameraController.setTapToFocusEnabled(true);
|
||||
cameraController.setImageCaptureMode(CameraXUtil.getOptimalCaptureMode());
|
||||
cameraController = new SignalCameraController(requireContext(), getViewLifecycleOwner(), previewView);
|
||||
cameraXModePolicy.initialize(cameraController);
|
||||
|
||||
cameraScreenBrightnessController = new CameraScreenBrightnessController(
|
||||
@@ -157,9 +152,9 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
||||
);
|
||||
|
||||
previewView.setScaleType(PREVIEW_SCALE_TYPE);
|
||||
previewView.setController(cameraController);
|
||||
|
||||
onOrientationChanged();
|
||||
cameraController.bindToLifecycle(() -> Log.d(TAG, "Camera init complete from onViewCreated"));
|
||||
|
||||
view.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
||||
// Let's assume portrait for now, so 9:16
|
||||
@@ -175,16 +170,29 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
||||
params.height = (int) height;
|
||||
|
||||
cameraParent.setLayoutParams(params);
|
||||
cameraController.setPreviewTargetSize(new CameraController.OutputSize(new Size((int) width, (int) height)));
|
||||
cameraController.setPreviewTargetSize(new Size((int) width, (int) height));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
orientationListener.enable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
orientationListener.disable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
cameraController.bindToLifecycle(getViewLifecycleOwner());
|
||||
cameraController.bindToLifecycle(() -> Log.d(TAG, "Camera init complete from onResume"));
|
||||
|
||||
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
}
|
||||
|
||||
@@ -237,10 +245,8 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
||||
|
||||
int resolution = CameraXUtil.getIdealResolution(Resources.getSystem().getDisplayMetrics().widthPixels, Resources.getSystem().getDisplayMetrics().heightPixels);
|
||||
Size size = CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, true);
|
||||
CameraController.OutputSize outputSize = new CameraController.OutputSize(size);
|
||||
|
||||
cameraController.setImageCaptureTargetSize(outputSize);
|
||||
cameraController.setVideoCaptureQualitySelector(QualitySelector.from(Quality.HD, FallbackStrategy.lowerQualityThan(Quality.HD)));
|
||||
cameraController.setImageCaptureTargetSize(size);
|
||||
|
||||
controlsContainer.removeAllViews();
|
||||
controlsContainer.addView(LayoutInflater.from(getContext()).inflate(layout, controlsContainer, false));
|
||||
@@ -339,8 +345,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
||||
|
||||
previewView.setScaleType(PREVIEW_SCALE_TYPE);
|
||||
|
||||
cameraController.getInitializationFuture()
|
||||
.addListener(() -> initializeFlipButton(flipButton, flashButton), Executors.mainThreadExecutor());
|
||||
cameraController.addInitializationCompletedListener(cameraProvider -> initializeFlipButton(flipButton, flashButton));
|
||||
|
||||
flashButton.setAutoFlashEnabled(cameraController.getImageCaptureFlashMode() >= ImageCapture.FLASH_MODE_AUTO);
|
||||
flashButton.setFlash(cameraController.getImageCaptureFlashMode());
|
||||
@@ -476,7 +481,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
||||
);
|
||||
|
||||
flashHelper.onWillTakePicture();
|
||||
cameraController.takePicture(Executors.mainThreadExecutor(), new ImageCapture.OnImageCapturedCallback() {
|
||||
cameraController.takePicture(ContextCompat.getMainExecutor(requireContext()), new ImageCapture.OnImageCapturedCallback() {
|
||||
@Override
|
||||
public void onCaptureSuccess(@NonNull ImageProxy image) {
|
||||
flashHelper.endFlash();
|
||||
@@ -505,8 +510,8 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ImageCaptureException exception) {
|
||||
Log.w(TAG, "Failed to capture image", exception);
|
||||
public void onError(@NonNull ImageCaptureException exception) {
|
||||
Log.w(TAG, "Failed to capture image due to error " + exception.getImageCaptureError(), exception.getCause());
|
||||
flashHelper.endFlash();
|
||||
controller.onCameraError();
|
||||
}
|
||||
@@ -571,9 +576,9 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
||||
|
||||
private static class CameraStateProvider implements CameraScreenBrightnessController.CameraStateProvider {
|
||||
|
||||
private final CameraController cameraController;
|
||||
private final SignalCameraController cameraController;
|
||||
|
||||
private CameraStateProvider(CameraController cameraController) {
|
||||
private CameraStateProvider(SignalCameraController cameraController) {
|
||||
this.cameraController = cameraController;
|
||||
}
|
||||
|
||||
@@ -587,4 +592,18 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
||||
return cameraController.getImageCaptureFlashMode() == ImageCapture.FLASH_MODE_ON;
|
||||
}
|
||||
}
|
||||
|
||||
private class CameraXOrientationListener extends OrientationEventListener {
|
||||
|
||||
public CameraXOrientationListener(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOrientationChanged(int orientation) {
|
||||
if (cameraController != null) {
|
||||
cameraController.setImageRotation(orientation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,23 +7,24 @@ import android.view.WindowManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.camera.core.CameraSelector;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
import androidx.camera.view.CameraController;
|
||||
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.SignalCameraController;
|
||||
|
||||
final class CameraXSelfieFlashHelper {
|
||||
|
||||
private static final float MAX_SCREEN_BRIGHTNESS = 1f;
|
||||
private static final float MAX_SELFIE_FLASH_ALPHA = 0.9f;
|
||||
|
||||
private final Window window;
|
||||
private final CameraController camera;
|
||||
private final View selfieFlash;
|
||||
private final Window window;
|
||||
private final SignalCameraController camera;
|
||||
private final View selfieFlash;
|
||||
|
||||
private float brightnessBeforeFlash;
|
||||
private boolean inFlash;
|
||||
private int flashMode = -1;
|
||||
|
||||
CameraXSelfieFlashHelper(@NonNull Window window,
|
||||
@NonNull CameraController camera,
|
||||
@NonNull SignalCameraController camera,
|
||||
@NonNull View selfieFlash)
|
||||
{
|
||||
this.window = window;
|
||||
@@ -68,7 +69,7 @@ final class CameraXSelfieFlashHelper {
|
||||
}
|
||||
|
||||
private boolean shouldUseViewBasedFlash() {
|
||||
CameraSelector cameraSelector = camera.getCameraSelector() ;
|
||||
CameraSelector cameraSelector = camera.getCameraSelector();
|
||||
|
||||
return (camera.getImageCaptureFlashMode() == ImageCapture.FLASH_MODE_ON || flashMode == ImageCapture.FLASH_MODE_ON) &&
|
||||
cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA;
|
||||
|
||||
@@ -17,7 +17,6 @@ import androidx.camera.core.ZoomState;
|
||||
import androidx.camera.video.FileDescriptorOutputOptions;
|
||||
import androidx.camera.video.Recording;
|
||||
import androidx.camera.video.VideoRecordEvent;
|
||||
import androidx.camera.view.CameraController;
|
||||
import androidx.camera.view.PreviewView;
|
||||
import androidx.camera.view.video.AudioConfig;
|
||||
import androidx.core.util.Consumer;
|
||||
@@ -27,6 +26,7 @@ import com.bumptech.glide.util.Executors;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.SignalCameraController;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModePolicy;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
@@ -47,14 +47,14 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
|
||||
private static final String VIDEO_DEBUG_LABEL = "video-capture";
|
||||
private static final long VIDEO_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
private final @NonNull Fragment fragment;
|
||||
private final @NonNull PreviewView previewView;
|
||||
private final @NonNull CameraController cameraController;
|
||||
private final @NonNull Callback callback;
|
||||
private final @NonNull MemoryFileDescriptor memoryFileDescriptor;
|
||||
private final @NonNull ValueAnimator updateProgressAnimator;
|
||||
private final @NonNull Debouncer debouncer;
|
||||
private final @NonNull CameraXModePolicy cameraXModePolicy;
|
||||
private final @NonNull Fragment fragment;
|
||||
private final @NonNull PreviewView previewView;
|
||||
private final @NonNull SignalCameraController cameraController;
|
||||
private final @NonNull Callback callback;
|
||||
private final @NonNull MemoryFileDescriptor memoryFileDescriptor;
|
||||
private final @NonNull ValueAnimator updateProgressAnimator;
|
||||
private final @NonNull Debouncer debouncer;
|
||||
private final @NonNull CameraXModePolicy cameraXModePolicy;
|
||||
|
||||
private ValueAnimator cameraMetricsAnimator;
|
||||
|
||||
@@ -88,7 +88,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
|
||||
|
||||
CameraXVideoCaptureHelper(@NonNull Fragment fragment,
|
||||
@NonNull CameraButtonView captureButton,
|
||||
@NonNull CameraController cameraController,
|
||||
@NonNull SignalCameraController cameraController,
|
||||
@NonNull PreviewView previewView,
|
||||
@NonNull MemoryFileDescriptor memoryFileDescriptor,
|
||||
@NonNull CameraXModePolicy cameraXModePolicy,
|
||||
@@ -150,7 +150,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
|
||||
FileDescriptorOutputOptions outputOptions = new FileDescriptorOutputOptions.Builder(memoryFileDescriptor.getParcelFileDescriptor()).build();
|
||||
AudioConfig audioConfig = AudioConfig.create(true);
|
||||
|
||||
activeRecording = cameraController.startRecording(outputOptions, audioConfig, Executors.mainThreadExecutor(), videoSavedListener);
|
||||
activeRecording = cameraController.startRecording(outputOptions, audioConfig, videoSavedListener);
|
||||
|
||||
updateProgressAnimator.start();
|
||||
debouncer.publish(this::onVideoCaptureComplete);
|
||||
|
||||
@@ -15,11 +15,11 @@ sealed class CameraXModePolicy {
|
||||
|
||||
abstract val isVideoSupported: Boolean
|
||||
|
||||
abstract fun initialize(cameraController: CameraController)
|
||||
abstract fun initialize(cameraController: SignalCameraController)
|
||||
|
||||
open fun setToImage(cameraController: CameraController) = Unit
|
||||
open fun setToImage(cameraController: SignalCameraController) = Unit
|
||||
|
||||
open fun setToVideo(cameraController: CameraController) = Unit
|
||||
open fun setToVideo(cameraController: SignalCameraController) = Unit
|
||||
|
||||
/**
|
||||
* The device supports having Image and Video enabled at the same time
|
||||
@@ -28,7 +28,7 @@ sealed class CameraXModePolicy {
|
||||
|
||||
override val isVideoSupported: Boolean = true
|
||||
|
||||
override fun initialize(cameraController: CameraController) {
|
||||
override fun initialize(cameraController: SignalCameraController) {
|
||||
cameraController.setEnabledUseCases(CameraController.IMAGE_CAPTURE or CameraController.VIDEO_CAPTURE)
|
||||
}
|
||||
}
|
||||
@@ -40,15 +40,15 @@ sealed class CameraXModePolicy {
|
||||
|
||||
override val isVideoSupported: Boolean = true
|
||||
|
||||
override fun initialize(cameraController: CameraController) {
|
||||
override fun initialize(cameraController: SignalCameraController) {
|
||||
setToImage(cameraController)
|
||||
}
|
||||
|
||||
override fun setToImage(cameraController: CameraController) {
|
||||
override fun setToImage(cameraController: SignalCameraController) {
|
||||
cameraController.setEnabledUseCases(CameraController.IMAGE_CAPTURE)
|
||||
}
|
||||
|
||||
override fun setToVideo(cameraController: CameraController) {
|
||||
override fun setToVideo(cameraController: SignalCameraController) {
|
||||
cameraController.setEnabledUseCases(CameraController.VIDEO_CAPTURE)
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ sealed class CameraXModePolicy {
|
||||
|
||||
override val isVideoSupported: Boolean = false
|
||||
|
||||
override fun initialize(cameraController: CameraController) {
|
||||
override fun initialize(cameraController: SignalCameraController) {
|
||||
cameraController.setEnabledUseCases(CameraController.IMAGE_CAPTURE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.mediasend.camerax
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.util.Size
|
||||
import android.view.MotionEvent
|
||||
import android.view.Surface
|
||||
import android.view.View
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.camera.core.Camera
|
||||
import androidx.camera.core.CameraProvider
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.FocusMeteringAction
|
||||
import androidx.camera.core.FocusMeteringResult
|
||||
import androidx.camera.core.ImageCapture
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.core.UseCase
|
||||
import androidx.camera.core.UseCaseGroup
|
||||
import androidx.camera.core.ViewPort
|
||||
import androidx.camera.core.ZoomState
|
||||
import androidx.camera.core.resolutionselector.AspectRatioStrategy
|
||||
import androidx.camera.core.resolutionselector.ResolutionSelector
|
||||
import androidx.camera.core.resolutionselector.ResolutionStrategy
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.video.FallbackStrategy
|
||||
import androidx.camera.video.FileDescriptorOutputOptions
|
||||
import androidx.camera.video.Quality
|
||||
import androidx.camera.video.QualitySelector
|
||||
import androidx.camera.video.Recorder
|
||||
import androidx.camera.video.Recording
|
||||
import androidx.camera.video.VideoCapture
|
||||
import androidx.camera.video.VideoRecordEvent
|
||||
import androidx.camera.view.CameraController
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.camera.view.video.AudioConfig
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.util.Consumer
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/**
|
||||
* This is a class to manage the camera resource, and relies on the AndroidX CameraX library.
|
||||
*
|
||||
* The API is a subset of the [CameraController] class, but with a few additions such as [setImageRotation].
|
||||
*/
|
||||
class SignalCameraController(val context: Context, val lifecycleOwner: LifecycleOwner, private val previewView: PreviewView) {
|
||||
companion object {
|
||||
val TAG = Log.tag(SignalCameraController::class.java)
|
||||
|
||||
@JvmStatic
|
||||
private fun isLandscape(surfaceRotation: Int): Boolean {
|
||||
return surfaceRotation == Surface.ROTATION_90 || surfaceRotation == Surface.ROTATION_270
|
||||
}
|
||||
}
|
||||
|
||||
private val videoQualitySelector: QualitySelector = QualitySelector.from(Quality.HD, FallbackStrategy.lowerQualityThan(Quality.HD))
|
||||
private val imageMode = CameraXUtil.getOptimalCaptureMode()
|
||||
|
||||
private val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> = ProcessCameraProvider.getInstance(context)
|
||||
private val viewPort: ViewPort? = previewView.getViewPort(Surface.ROTATION_0)
|
||||
private val initializationCompleteListeners: MutableSet<InitializationListener> = mutableSetOf()
|
||||
|
||||
private var imageRotation = 0
|
||||
private var recording: Recording? = null
|
||||
private var previewTargetSize: Size? = null
|
||||
private var imageCaptureTargetSize: Size? = null
|
||||
private var cameraSelector: CameraSelector = CameraXUtil.toCameraSelector(TextSecurePreferences.getDirectCaptureCameraId(context))
|
||||
private var enabledUseCases: Int = CameraController.IMAGE_CAPTURE
|
||||
|
||||
private var previewUseCase: Preview = createPreviewUseCase()
|
||||
private var imageCaptureUseCase: ImageCapture = createImageCaptureUseCase()
|
||||
private var videoCaptureUseCase: VideoCapture<Recorder> = createVideoCaptureRecorder()
|
||||
|
||||
private lateinit var cameraProvider: ProcessCameraProvider
|
||||
private lateinit var camera: Camera
|
||||
|
||||
@RequiresPermission(Manifest.permission.CAMERA)
|
||||
fun bindToLifecycle(onCameraBoundListener: Runnable) {
|
||||
ThreadUtil.assertMainThread()
|
||||
if (this::cameraProvider.isInitialized) {
|
||||
bindToLifecycleInternal()
|
||||
onCameraBoundListener.run()
|
||||
} else {
|
||||
cameraProviderFuture.addListener({
|
||||
cameraProvider = cameraProviderFuture.get()
|
||||
initializationCompleteListeners.forEach { it.onInitialized(cameraProvider) }
|
||||
bindToLifecycleInternal()
|
||||
onCameraBoundListener.run()
|
||||
}, ContextCompat.getMainExecutor(context))
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun unbind() {
|
||||
ThreadUtil.assertMainThread()
|
||||
cameraProvider.unbindAll()
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun bindToLifecycleInternal() {
|
||||
ThreadUtil.assertMainThread()
|
||||
try {
|
||||
if (!this::cameraProvider.isInitialized) {
|
||||
Log.d(TAG, "Camera provider not yet initialized.")
|
||||
return
|
||||
}
|
||||
camera = cameraProvider.bindToLifecycle(
|
||||
lifecycleOwner,
|
||||
cameraSelector,
|
||||
buildUseCaseGroup()
|
||||
)
|
||||
|
||||
initializeTapToFocus()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Use case binding failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun takePicture(executor: Executor, callback: ImageCapture.OnImageCapturedCallback) {
|
||||
ThreadUtil.assertMainThread()
|
||||
assertImageEnabled()
|
||||
imageCaptureUseCase.takePicture(executor, callback)
|
||||
}
|
||||
|
||||
@RequiresApi(26)
|
||||
@MainThread
|
||||
fun startRecording(outputOptions: FileDescriptorOutputOptions, audioConfig: AudioConfig, videoSavedListener: Consumer<VideoRecordEvent>): Recording {
|
||||
ThreadUtil.assertMainThread()
|
||||
assertVideoEnabled()
|
||||
|
||||
recording?.stop()
|
||||
recording = null
|
||||
val startedRecording = videoCaptureUseCase.output
|
||||
.prepareRecording(context, outputOptions)
|
||||
.apply {
|
||||
if (audioConfig.audioEnabled) {
|
||||
withAudioEnabled()
|
||||
}
|
||||
}
|
||||
.start(ContextCompat.getMainExecutor(context)) {
|
||||
videoSavedListener.accept(it)
|
||||
if (it is VideoRecordEvent.Finalize) {
|
||||
recording = null
|
||||
}
|
||||
}
|
||||
|
||||
recording = startedRecording
|
||||
return startedRecording
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun setEnabledUseCases(useCaseFlags: Int) {
|
||||
ThreadUtil.assertMainThread()
|
||||
if (enabledUseCases == useCaseFlags) {
|
||||
return
|
||||
}
|
||||
|
||||
val oldEnabledUseCases = enabledUseCases
|
||||
enabledUseCases = useCaseFlags
|
||||
if (isRecording()) {
|
||||
stopRecording()
|
||||
}
|
||||
tryToBindCamera { enabledUseCases = oldEnabledUseCases }
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun getImageCaptureFlashMode(): Int {
|
||||
ThreadUtil.assertMainThread()
|
||||
return imageCaptureUseCase.flashMode
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun setPreviewTargetSize(size: Size) {
|
||||
ThreadUtil.assertMainThread()
|
||||
if (size == previewTargetSize || previewTargetSize?.equals(size) == true) {
|
||||
return
|
||||
}
|
||||
Log.d(TAG, "Setting Preview dimensions to $size")
|
||||
previewTargetSize = size
|
||||
if (this::cameraProvider.isInitialized) {
|
||||
cameraProvider.unbind(previewUseCase)
|
||||
}
|
||||
previewUseCase = createPreviewUseCase()
|
||||
|
||||
tryToBindCamera(null)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun setImageCaptureTargetSize(size: Size) {
|
||||
ThreadUtil.assertMainThread()
|
||||
if (size == imageCaptureTargetSize || imageCaptureTargetSize?.equals(size) == true) {
|
||||
return
|
||||
}
|
||||
imageCaptureTargetSize = size
|
||||
if (this::cameraProvider.isInitialized) {
|
||||
cameraProvider.unbind(imageCaptureUseCase)
|
||||
}
|
||||
imageCaptureUseCase = createImageCaptureUseCase()
|
||||
tryToBindCamera(null)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun setImageRotation(rotation: Int) {
|
||||
ThreadUtil.assertMainThread()
|
||||
val newRotation = UseCase.snapToSurfaceRotation(rotation.coerceIn(0, 359))
|
||||
|
||||
if (newRotation == imageRotation) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isLandscape(newRotation) != isLandscape(imageRotation)) {
|
||||
imageCaptureTargetSize = imageCaptureTargetSize?.swap()
|
||||
}
|
||||
|
||||
videoCaptureUseCase.targetRotation = newRotation
|
||||
imageCaptureUseCase.targetRotation = newRotation
|
||||
|
||||
imageRotation = newRotation
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun setImageCaptureFlashMode(flashMode: Int) {
|
||||
ThreadUtil.assertMainThread()
|
||||
imageCaptureUseCase.flashMode = flashMode
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun setZoomRatio(ratio: Float): ListenableFuture<Void> {
|
||||
ThreadUtil.assertMainThread()
|
||||
return camera.cameraControl.setZoomRatio(ratio)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun getZoomState(): LiveData<ZoomState> {
|
||||
ThreadUtil.assertMainThread()
|
||||
return camera.cameraInfo.zoomState
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun setCameraSelector(selector: CameraSelector) {
|
||||
ThreadUtil.assertMainThread()
|
||||
if (selector == cameraSelector) {
|
||||
return
|
||||
}
|
||||
|
||||
val oldCameraSelector: CameraSelector = cameraSelector
|
||||
cameraSelector = selector
|
||||
if (!this::cameraProvider.isInitialized) {
|
||||
return
|
||||
}
|
||||
cameraProvider.unbindAll()
|
||||
tryToBindCamera { cameraSelector = oldCameraSelector }
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun getCameraSelector(): CameraSelector {
|
||||
ThreadUtil.assertMainThread()
|
||||
return cameraSelector
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun hasCamera(selectedCamera: CameraSelector): Boolean {
|
||||
ThreadUtil.assertMainThread()
|
||||
return cameraProvider.hasCamera(selectedCamera)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun addInitializationCompletedListener(listener: InitializationListener) {
|
||||
ThreadUtil.assertMainThread()
|
||||
initializationCompleteListeners.add(listener)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun tryToBindCamera(restoreStateRunnable: (() -> Unit)?) {
|
||||
ThreadUtil.assertMainThread()
|
||||
try {
|
||||
bindToLifecycleInternal()
|
||||
} catch (e: RuntimeException) {
|
||||
Log.i(TAG, "Could not re-bind camera!", e)
|
||||
restoreStateRunnable?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun stopRecording() {
|
||||
ThreadUtil.assertMainThread()
|
||||
recording?.close()
|
||||
}
|
||||
|
||||
private fun createVideoCaptureRecorder() = VideoCapture.Builder(
|
||||
Recorder.Builder()
|
||||
.setQualitySelector(videoQualitySelector)
|
||||
.build()
|
||||
)
|
||||
.setTargetRotation(imageRotation)
|
||||
.build()
|
||||
|
||||
private fun createPreviewUseCase() = Preview.Builder()
|
||||
.apply {
|
||||
setTargetRotation(Surface.ROTATION_0)
|
||||
val size = previewTargetSize
|
||||
if (size != null) {
|
||||
setResolutionSelector(
|
||||
ResolutionSelector.Builder()
|
||||
.setResolutionStrategy(ResolutionStrategy(size, ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}.build()
|
||||
.also {
|
||||
it.setSurfaceProvider(previewView.surfaceProvider)
|
||||
}
|
||||
|
||||
private fun createImageCaptureUseCase(): ImageCapture = ImageCapture.Builder()
|
||||
.apply {
|
||||
setCaptureMode(imageMode)
|
||||
setTargetRotation(imageRotation)
|
||||
|
||||
val size = imageCaptureTargetSize
|
||||
if (size != null) {
|
||||
setResolutionSelector(
|
||||
ResolutionSelector.Builder()
|
||||
.setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY)
|
||||
.setResolutionStrategy(ResolutionStrategy(size, ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}.build()
|
||||
|
||||
private fun buildUseCaseGroup() = UseCaseGroup.Builder().apply {
|
||||
addUseCase(previewUseCase)
|
||||
if (isUseCaseEnabled(CameraController.IMAGE_CAPTURE)) {
|
||||
addUseCase(imageCaptureUseCase)
|
||||
} else {
|
||||
cameraProvider.unbind(imageCaptureUseCase)
|
||||
}
|
||||
if (isUseCaseEnabled(CameraController.VIDEO_CAPTURE)) {
|
||||
addUseCase(videoCaptureUseCase)
|
||||
} else {
|
||||
cameraProvider.unbind(videoCaptureUseCase)
|
||||
}
|
||||
if (viewPort != null) {
|
||||
setViewPort(viewPort)
|
||||
} else {
|
||||
Log.d(TAG, "ViewPort was null, not adding to UseCase builder.")
|
||||
}
|
||||
}.build()
|
||||
|
||||
@MainThread
|
||||
private fun initializeTapToFocus() {
|
||||
ThreadUtil.assertMainThread()
|
||||
previewView.setOnTouchListener { v: View?, event: MotionEvent ->
|
||||
if (event.action == MotionEvent.ACTION_DOWN) {
|
||||
return@setOnTouchListener true
|
||||
}
|
||||
if (event.action == MotionEvent.ACTION_UP) {
|
||||
focusAndMeterOnPoint(event.x, event.y)
|
||||
v?.performClick()
|
||||
return@setOnTouchListener true
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun focusAndMeterOnPoint(x: Float, y: Float) {
|
||||
ThreadUtil.assertMainThread()
|
||||
if (this::camera.isInitialized) {
|
||||
Log.d(TAG, "Can't tap to focus before camera is initialized.")
|
||||
return
|
||||
}
|
||||
val factory = previewView.meteringPointFactory
|
||||
val point = factory.createPoint(x, y)
|
||||
val action = FocusMeteringAction.Builder(point).build()
|
||||
|
||||
val future: ListenableFuture<FocusMeteringResult> = camera.cameraControl.startFocusAndMetering(action)
|
||||
future.addListener({
|
||||
try {
|
||||
val result = future.get()
|
||||
Log.d(TAG, "Tap to focus was successful? ${result.isFocusSuccessful}")
|
||||
} catch (e: ExecutionException) {
|
||||
Log.d(TAG, "Tap to focus could not be completed due to an exception.", e)
|
||||
} catch (e: InterruptedException) {
|
||||
Log.d(TAG, "Tap to focus could not be completed due to an exception.", e)
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(context))
|
||||
}
|
||||
|
||||
private fun isRecording(): Boolean {
|
||||
return recording != null
|
||||
}
|
||||
|
||||
private fun isUseCaseEnabled(mask: Int): Boolean {
|
||||
return (enabledUseCases and mask) != 0
|
||||
}
|
||||
|
||||
private fun assertVideoEnabled() {
|
||||
if (!isUseCaseEnabled(CameraController.VIDEO_CAPTURE)) {
|
||||
throw IllegalStateException("VideoCapture disabled.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertImageEnabled() {
|
||||
if (!isUseCaseEnabled(CameraController.IMAGE_CAPTURE)) {
|
||||
throw IllegalStateException("ImageCapture disabled.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun Size.swap(): Size {
|
||||
return Size(this.height, this.width)
|
||||
}
|
||||
|
||||
interface InitializationListener {
|
||||
fun onInitialized(cameraProvider: CameraProvider)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user