Update CameraX to Alpha06 and bring in new View / Module code.

This commit is contained in:
Alex Hart 2019-10-18 17:36:00 -03:00 committed by GitHub
parent 46ebff3659
commit c2da4fcd7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 572 additions and 473 deletions

View File

@ -78,8 +78,8 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05' implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0' implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
implementation "androidx.camera:camera-core:1.0.0-alpha04" implementation "androidx.camera:camera-core:1.0.0-alpha06"
implementation "androidx.camera:camera-camera2:1.0.0-alpha04" implementation "androidx.camera:camera-camera2:1.0.0-alpha06"
implementation('com.google.firebase:firebase-messaging:17.3.4') { implementation('com.google.firebase:firebase-messaging:17.3.4') {
exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-core'

View File

@ -340,4 +340,27 @@
<attr name="recordSize" format="dimension" /> <attr name="recordSize" format="dimension" />
</declare-styleable> </declare-styleable>
<declare-styleable name="CameraXView">
<attr format="enum" name="scaleType">
<enum name="centerCrop" value="0"/>
<enum name="centerInside" value="1"/>
</attr>
<attr format="enum" name="lensFacing">
<enum name="none" value="0"/>
<enum name="front" value="1"/>
<enum name="back" value="2"/>
</attr>
<attr format="enum" name="captureMode">
<enum name="image" value="0"/>
<enum name="video" value="1"/>
<enum name="mixed" value="2"/>
</attr>
<attr format="enum" name="flash">
<enum name="auto" value="1"/>
<enum name="on" value="2"/>
<enum name="off" value="4"/>
</attr>
<attr format="boolean" name="pinchToZoomEnabled"/>
</declare-styleable>
</resources> </resources>

View File

@ -1,9 +1,7 @@
package org.thoughtcrime.securesms.mediasend; package org.thoughtcrime.securesms.mediasend;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.view.GestureDetector; import android.view.GestureDetector;
@ -26,11 +24,13 @@ import androidx.annotation.RequiresApi;
import androidx.camera.core.CameraX; import androidx.camera.core.CameraX;
import androidx.camera.core.ImageCapture; import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageProxy; import androidx.camera.core.ImageProxy;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProviders;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.util.Executors;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.TooltipPopup; import org.thoughtcrime.securesms.components.TooltipPopup;
@ -325,7 +325,7 @@ public class CameraXFragment extends Fragment implements CameraFragment {
selfieFlash selfieFlash
); );
camera.takePicture(new ImageCapture.OnImageCapturedListener() { camera.takePicture(Executors.mainThreadExecutor(), new ImageCapture.OnImageCapturedListener() {
@Override @Override
public void onCaptureSuccess(ImageProxy image, int rotationDegrees) { public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
flashHelper.endFlash(); flashHelper.endFlash();
@ -352,7 +352,7 @@ public class CameraXFragment extends Fragment implements CameraFragment {
} }
@Override @Override
public void onError(ImageCapture.UseCaseError useCaseError, String message, @Nullable Throwable cause) { public void onError(ImageCapture.ImageCaptureError useCaseError, String message, @Nullable Throwable cause) {
flashHelper.endFlash(); flashHelper.endFlash();
controller.onCameraError(); controller.onCameraError();
} }

View File

@ -16,6 +16,8 @@ import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.bumptech.glide.util.Executors;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener; import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.components.TooltipPopup; import org.thoughtcrime.securesms.components.TooltipPopup;
@ -119,7 +121,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
this.camera.setZoomLevel(0f); this.camera.setZoomLevel(0f);
callback.onVideoRecordStarted(); callback.onVideoRecordStarted();
shrinkCaptureArea(); shrinkCaptureArea();
camera.startRecording(memoryFileDescriptor.getFileDescriptor(), videoSavedListener); camera.startRecording(memoryFileDescriptor.getFileDescriptor(), Executors.mainThreadExecutor(), videoSavedListener);
updateProgressAnimator.start(); updateProgressAnimator.start();
} }

View File

@ -1,5 +1,3 @@
package org.thoughtcrime.securesms.mediasend.camerax;
/* /*
* Copyright (C) 2019 The Android Open Source Project * Copyright (C) 2019 The Android Open Source Project
* *
@ -16,6 +14,8 @@ package org.thoughtcrime.securesms.mediasend.camerax;
* limitations under the License. * limitations under the License.
*/ */
package org.thoughtcrime.securesms.mediasend.camerax;
import android.Manifest.permission; import android.Manifest.permission;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
@ -25,23 +25,24 @@ import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraManager;
import android.os.Build;
import android.os.Looper; import android.os.Looper;
import android.util.Log;
import android.util.Rational; import android.util.Rational;
import android.util.Size; import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission; import androidx.annotation.RequiresPermission;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.camera.core.AspectRatio;
import androidx.camera.core.CameraInfo; import androidx.camera.core.CameraInfo;
import androidx.camera.core.CameraInfoUnavailableException; import androidx.camera.core.CameraInfoUnavailableException;
import androidx.camera.core.CameraOrientationUtil; import androidx.camera.core.CameraOrientationUtil;
import androidx.camera.core.CameraX; import androidx.camera.core.CameraX;
import androidx.camera.core.CameraX.LensFacing;
import androidx.camera.core.FlashMode; import androidx.camera.core.FlashMode;
import androidx.camera.core.ImageCapture; import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCapture.OnImageCapturedListener;
import androidx.camera.core.ImageCapture.OnImageSavedListener;
import androidx.camera.core.ImageCaptureConfig; import androidx.camera.core.ImageCaptureConfig;
import androidx.camera.core.Preview; import androidx.camera.core.Preview;
import androidx.camera.core.PreviewConfig; import androidx.camera.core.PreviewConfig;
@ -49,12 +50,10 @@ import androidx.camera.core.VideoCaptureConfig;
import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.OnLifecycleEvent; import androidx.lifecycle.OnLifecycleEvent;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXView.CaptureMode;
import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.video.VideoUtil; import org.thoughtcrime.securesms.video.VideoUtil;
import java.io.File; import java.io.File;
@ -62,11 +61,11 @@ import java.io.FileDescriptor;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
/** CameraX use case operation built on @{link androidx.camera.core}. */ /** CameraX use case operation built on @{link androidx.camera.core}. */
@RequiresApi(21) @RequiresApi(21)
@SuppressLint("RestrictedApi")
final class CameraXModule { final class CameraXModule {
public static final String TAG = "CameraXModule"; public static final String TAG = "CameraXModule";
@ -82,9 +81,9 @@ final class CameraXModule {
private final PreviewConfig.Builder mPreviewConfigBuilder; private final PreviewConfig.Builder mPreviewConfigBuilder;
private final VideoCaptureConfig.Builder mVideoCaptureConfigBuilder; private final VideoCaptureConfig.Builder mVideoCaptureConfigBuilder;
private final ImageCaptureConfig.Builder mImageCaptureConfigBuilder; private final ImageCaptureConfig.Builder mImageCaptureConfigBuilder;
private final CameraXView mCameraXView; private final CameraXView mCameraView;
final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false); final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false);
private CaptureMode mCaptureMode = CaptureMode.IMAGE; private CameraXView.CaptureMode mCaptureMode = CameraXView.CaptureMode.IMAGE;
private long mMaxVideoDuration = CameraXView.INDEFINITE_VIDEO_DURATION; private long mMaxVideoDuration = CameraXView.INDEFINITE_VIDEO_DURATION;
private long mMaxVideoSize = CameraXView.INDEFINITE_VIDEO_SIZE; private long mMaxVideoSize = CameraXView.INDEFINITE_VIDEO_SIZE;
private FlashMode mFlash = FlashMode.OFF; private FlashMode mFlash = FlashMode.OFF;
@ -112,10 +111,10 @@ final class CameraXModule {
@Nullable @Nullable
private Rect mCropRegion; private Rect mCropRegion;
@Nullable @Nullable
private CameraX.LensFacing mCameraLensFacing = LensFacing.BACK; private CameraX.LensFacing mCameraLensFacing = CameraX.LensFacing.BACK;
CameraXModule(CameraXView view) { CameraXModule(CameraXView view) {
this.mCameraXView = view; this.mCameraView = view;
mCameraManager = (CameraManager) view.getContext().getSystemService(Context.CAMERA_SERVICE); mCameraManager = (CameraManager) view.getContext().getSystemService(Context.CAMERA_SERVICE);
@ -126,11 +125,10 @@ final class CameraXModule {
// Begin Signal Custom Code Block // Begin Signal Custom Code Block
mVideoCaptureConfigBuilder = mVideoCaptureConfigBuilder =
new VideoCaptureConfig.Builder() new VideoCaptureConfig.Builder().setTargetName("VideoCapture")
.setAudioBitRate(VideoUtil.AUDIO_BIT_RATE) .setAudioBitRate(VideoUtil.AUDIO_BIT_RATE)
.setVideoFrameRate(VideoUtil.VIDEO_FRAME_RATE) .setVideoFrameRate(VideoUtil.VIDEO_FRAME_RATE)
.setBitRate(VideoUtil.VIDEO_BIT_RATE) .setBitRate(VideoUtil.VIDEO_BIT_RATE);
.setTargetName("VideoCapture");
// End Signal Custom Code Block // End Signal Custom Code Block
} }
@ -192,8 +190,7 @@ final class CameraXModule {
final int cameraOrientation; final int cameraOrientation;
try { try {
String cameraId; Set<CameraX.LensFacing> available = getAvailableCameraLensFacing();
Set<LensFacing> available = getAvailableCameraLensFacing();
if (available.isEmpty()) { if (available.isEmpty()) {
Log.w(TAG, "Unable to bindToLifeCycle since no cameras available"); Log.w(TAG, "Unable to bindToLifeCycle since no cameras available");
@ -217,41 +214,32 @@ final class CameraXModule {
if (mCameraLensFacing == null) { if (mCameraLensFacing == null) {
return; return;
} }
CameraInfo cameraInfo = CameraX.getCameraInfo(getLensFacing());
cameraId = CameraX.getCameraWithLensFacing(mCameraLensFacing);
if (cameraId == null) {
return;
}
CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
cameraOrientation = cameraInfo.getSensorRotationDegrees(); cameraOrientation = cameraInfo.getSensorRotationDegrees();
} catch (CameraInfoUnavailableException e) {
throw new IllegalStateException("Unable to get Camera Info.", e);
} catch (Exception e) { } catch (Exception e) {
throw new IllegalStateException("Unable to bind to lifecycle.", e); throw new IllegalStateException("Unable to bind to lifecycle.", e);
} }
// Set the preferred aspect ratio as 4:3 if it is IMAGE only mode. Set the preferred aspect // Set the preferred aspect ratio as 4:3 if it is IMAGE only mode. Set the preferred aspect
// ratio as 16:9 if it is VIDEO or MIXED mode. Then, it will be WYSIWYG when the view finder // ratio as 16:9 if it is VIDEO or MIXED mode. Then, it will be WYSIWYG when the view finder
// is // is in CENTER_INSIDE mode.
// in CENTER_INSIDE mode.
boolean isDisplayPortrait = getDisplayRotationDegrees() == 0 boolean isDisplayPortrait = getDisplayRotationDegrees() == 0
|| getDisplayRotationDegrees() == 180; || getDisplayRotationDegrees() == 180;
if (getCaptureMode() == CaptureMode.IMAGE) { Rational targetAspectRatio;
mImageCaptureConfigBuilder.setTargetAspectRatio( if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) {
isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3); mImageCaptureConfigBuilder.setTargetAspectRatio(AspectRatio.RATIO_4_3);
mPreviewConfigBuilder.setTargetAspectRatio( targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3;
isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3);
} else { } else {
mImageCaptureConfigBuilder.setTargetAspectRatio( mImageCaptureConfigBuilder.setTargetAspectRatio(AspectRatio.RATIO_16_9);
isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9); targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
mPreviewConfigBuilder.setTargetAspectRatio(
isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9);
} }
mImageCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation()); mImageCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation());
mImageCaptureConfigBuilder.setLensFacing(mCameraLensFacing); mImageCaptureConfigBuilder.setLensFacing(mCameraLensFacing);
mImageCaptureConfigBuilder.setCaptureMode(CameraXUtil.getOptimalCaptureMode());
mImageCaptureConfigBuilder.setTargetResolution(new Size(1920, 1920));
mImageCapture = new ImageCapture(mImageCaptureConfigBuilder.build()); mImageCapture = new ImageCapture(mImageCaptureConfigBuilder.build());
// Begin Signal Custom Code Block // Begin Signal Custom Code Block
@ -267,25 +255,17 @@ final class CameraXModule {
if (MediaConstraints.isVideoTranscodeAvailable()) { if (MediaConstraints.isVideoTranscodeAvailable()) {
mVideoCapture = new VideoCapture(mVideoCaptureConfigBuilder.build()); mVideoCapture = new VideoCapture(mVideoCaptureConfigBuilder.build());
} }
// End Signal Custom Code Block
mPreviewConfigBuilder.setLensFacing(mCameraLensFacing); mPreviewConfigBuilder.setLensFacing(mCameraLensFacing);
int relativeCameraOrientation = getRelativeCameraOrientation(false); // Adjusts the preview resolution according to the view size and the target aspect ratio.
int height = (int) (getMeasuredWidth() / targetAspectRatio.floatValue());
if (relativeCameraOrientation == 90 || relativeCameraOrientation == 270) { mPreviewConfigBuilder.setTargetResolution(new Size(getMeasuredWidth(), height));
mPreviewConfigBuilder.setTargetResolution(
new Size(getMeasuredHeight(), getMeasuredWidth()));
} else {
mPreviewConfigBuilder.setTargetResolution(
new Size(getMeasuredWidth(), getMeasuredHeight()));
}
mPreview = new Preview(mPreviewConfigBuilder.build()); mPreview = new Preview(mPreviewConfigBuilder.build());
mPreview.setOnPreviewOutputUpdateListener( mPreview.setOnPreviewOutputUpdateListener(
new Preview.OnPreviewOutputUpdateListener() { new Preview.OnPreviewOutputUpdateListener() {
@Override @Override
public void onUpdated(Preview.PreviewOutput output) { public void onUpdated(@NonNull Preview.PreviewOutput output) {
boolean needReverse = cameraOrientation != 0 && cameraOrientation != 180; boolean needReverse = cameraOrientation != 0 && cameraOrientation != 180;
int textureWidth = int textureWidth =
needReverse needReverse
@ -301,9 +281,9 @@ final class CameraXModule {
} }
}); });
if (getCaptureMode() == CaptureMode.IMAGE) { if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) {
CameraX.bindToLifecycle(mCurrentLifecycle, mImageCapture, mPreview); CameraX.bindToLifecycle(mCurrentLifecycle, mImageCapture, mPreview);
} else if (getCaptureMode() == CaptureMode.VIDEO) { } else if (getCaptureMode() == CameraXView.CaptureMode.VIDEO) {
CameraX.bindToLifecycle(mCurrentLifecycle, mVideoCapture, mPreview); CameraX.bindToLifecycle(mCurrentLifecycle, mVideoCapture, mPreview);
} else { } else {
CameraX.bindToLifecycle(mCurrentLifecycle, mImageCapture, mVideoCapture, mPreview); CameraX.bindToLifecycle(mCurrentLifecycle, mImageCapture, mVideoCapture, mPreview);
@ -324,18 +304,12 @@ final class CameraXModule {
"Explicit open/close of camera not yet supported. Use bindtoLifecycle() instead."); "Explicit open/close of camera not yet supported. Use bindtoLifecycle() instead.");
} }
public void stopPreview() { public void takePicture(Executor executor, ImageCapture.OnImageCapturedListener listener) {
if (mPreview != null) {
mPreview.clear();
}
}
public void takePicture(OnImageCapturedListener listener) {
if (mImageCapture == null) { if (mImageCapture == null) {
return; return;
} }
if (getCaptureMode() == CaptureMode.VIDEO) { if (getCaptureMode() == CameraXView.CaptureMode.VIDEO) {
throw new IllegalStateException("Can not take picture under VIDEO capture mode."); throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
} }
@ -343,15 +317,15 @@ final class CameraXModule {
throw new IllegalArgumentException("OnImageCapturedListener should not be empty"); throw new IllegalArgumentException("OnImageCapturedListener should not be empty");
} }
mImageCapture.takePicture(listener); mImageCapture.takePicture(executor, listener);
} }
public void takePicture(File saveLocation, OnImageSavedListener listener) { public void takePicture(File saveLocation, Executor executor, ImageCapture.OnImageSavedListener listener) {
if (mImageCapture == null) { if (mImageCapture == null) {
return; return;
} }
if (getCaptureMode() == CaptureMode.VIDEO) { if (getCaptureMode() == CameraXView.CaptureMode.VIDEO) {
throw new IllegalStateException("Can not take picture under VIDEO capture mode."); throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
} }
@ -360,19 +334,19 @@ final class CameraXModule {
} }
ImageCapture.Metadata metadata = new ImageCapture.Metadata(); ImageCapture.Metadata metadata = new ImageCapture.Metadata();
metadata.isReversedHorizontal = mCameraLensFacing == LensFacing.FRONT; metadata.isReversedHorizontal = mCameraLensFacing == CameraX.LensFacing.FRONT;
mImageCapture.takePicture(saveLocation, listener, metadata); mImageCapture.takePicture(saveLocation, metadata, executor, listener);
} }
// Begin Signal Custom Code Block // Begin Signal Custom Code Block
@RequiresApi(26) @RequiresApi(26)
public void startRecording(FileDescriptor file, Executor executor, final VideoCapture.OnVideoSavedListener listener) {
// End Signal Custom Code Block // End Signal Custom Code Block
public void startRecording(FileDescriptor file, final VideoCapture.OnVideoSavedListener listener) {
if (mVideoCapture == null) { if (mVideoCapture == null) {
return; return;
} }
if (getCaptureMode() == CaptureMode.IMAGE) { if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) {
throw new IllegalStateException("Can not record video under IMAGE capture mode."); throw new IllegalStateException("Can not record video under IMAGE capture mode.");
} }
@ -383,25 +357,24 @@ final class CameraXModule {
mVideoIsRecording.set(true); mVideoIsRecording.set(true);
mVideoCapture.startRecording( mVideoCapture.startRecording(
file, file,
executor,
new VideoCapture.OnVideoSavedListener() { new VideoCapture.OnVideoSavedListener() {
@Override @Override
// Begin Signal Custom Code Block // Begin Signal Custom Code block
public void onVideoSaved(FileDescriptor savedFileDescriptor) { public void onVideoSaved(@NonNull FileDescriptor savedFile) {
// End Signal Custom Code Block // End Signal Custom Code Block
mVideoIsRecording.set(false); mVideoIsRecording.set(false);
// Begin Signal Custom Code Block listener.onVideoSaved(savedFile);
listener.onVideoSaved(savedFileDescriptor);
// End Signal Custom Code Block
} }
@Override @Override
public void onError( public void onError(
VideoCapture.VideoCaptureError useCaseError, @NonNull VideoCapture.VideoCaptureError videoCaptureError,
String message, @NonNull String message,
@Nullable Throwable cause) { @Nullable Throwable cause) {
mVideoIsRecording.set(false); mVideoIsRecording.set(false);
Log.e(TAG, message, cause); Log.e(TAG, message, cause);
listener.onError(useCaseError, message, cause); listener.onError(videoCaptureError, message, cause);
} }
}); });
} }
@ -423,7 +396,7 @@ final class CameraXModule {
// TODO(b/124269166): Rethink how we can handle permissions here. // TODO(b/124269166): Rethink how we can handle permissions here.
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
public void setCameraLensFacing(@Nullable LensFacing lensFacing) { public void setCameraLensFacing(@Nullable CameraX.LensFacing lensFacing) {
// Setting same lens facing is a no-op, so check for that first // Setting same lens facing is a no-op, so check for that first
if (mCameraLensFacing != lensFacing) { if (mCameraLensFacing != lensFacing) {
// If we're not bound to a lifecycle, just update the camera that will be opened when we // If we're not bound to a lifecycle, just update the camera that will be opened when we
@ -438,7 +411,7 @@ final class CameraXModule {
} }
@RequiresPermission(permission.CAMERA) @RequiresPermission(permission.CAMERA)
public boolean hasCameraWithLensFacing(LensFacing lensFacing) { public boolean hasCameraWithLensFacing(CameraX.LensFacing lensFacing) {
String cameraId; String cameraId;
try { try {
cameraId = CameraX.getCameraWithLensFacing(lensFacing); cameraId = CameraX.getCameraWithLensFacing(lensFacing);
@ -450,14 +423,14 @@ final class CameraXModule {
} }
@Nullable @Nullable
public LensFacing getLensFacing() { public CameraX.LensFacing getLensFacing() {
return mCameraLensFacing; return mCameraLensFacing;
} }
public void toggleCamera() { public void toggleCamera() {
// TODO(b/124269166): Rethink how we can handle permissions here. // TODO(b/124269166): Rethink how we can handle permissions here.
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
Set<LensFacing> availableCameraLensFacing = getAvailableCameraLensFacing(); Set<CameraX.LensFacing> availableCameraLensFacing = getAvailableCameraLensFacing();
if (availableCameraLensFacing.isEmpty()) { if (availableCameraLensFacing.isEmpty()) {
return; return;
@ -468,44 +441,19 @@ final class CameraXModule {
return; return;
} }
if (mCameraLensFacing == LensFacing.BACK if (mCameraLensFacing == CameraX.LensFacing.BACK
&& availableCameraLensFacing.contains(LensFacing.FRONT)) { && availableCameraLensFacing.contains(CameraX.LensFacing.FRONT)) {
setCameraLensFacing(LensFacing.FRONT); setCameraLensFacing(CameraX.LensFacing.FRONT);
return; return;
} }
if (mCameraLensFacing == LensFacing.FRONT if (mCameraLensFacing == CameraX.LensFacing.FRONT
&& availableCameraLensFacing.contains(LensFacing.BACK)) { && availableCameraLensFacing.contains(CameraX.LensFacing.BACK)) {
setCameraLensFacing(LensFacing.BACK); setCameraLensFacing(CameraX.LensFacing.BACK);
return; return;
} }
} }
public void focus(Rect focus, Rect metering) {
if (mPreview == null) {
// Nothing to focus on since we don't yet have a preview
return;
}
Rect rescaledFocus;
Rect rescaledMetering;
try {
Rect sensorRegion;
if (mCropRegion != null) {
sensorRegion = mCropRegion;
} else {
sensorRegion = getSensorSize(getActiveCamera());
}
rescaledFocus = rescaleViewRectToSensorRect(focus, sensorRegion);
rescaledMetering = rescaleViewRectToSensorRect(metering, sensorRegion);
} catch (Exception e) {
Log.e(TAG, "Failed to rescale the focus and metering rectangles.", e);
return;
}
mPreview.focus(rescaledFocus, rescaledMetering);
}
public float getZoomLevel() { public float getZoomLevel() {
return mZoomLevel; return mZoomLevel;
} }
@ -604,17 +552,17 @@ final class CameraXModule {
} }
int getRelativeCameraOrientation(boolean compensateForMirroring) { int getRelativeCameraOrientation(boolean compensateForMirroring) {
int rotationDegrees; int rotationDegrees = 0;
try { try {
String cameraId = CameraX.getCameraWithLensFacing(getLensFacing()); CameraInfo cameraInfo = CameraX.getCameraInfo(getLensFacing());
CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
rotationDegrees = cameraInfo.getSensorRotationDegrees(getDisplaySurfaceRotation()); rotationDegrees = cameraInfo.getSensorRotationDegrees(getDisplaySurfaceRotation());
if (compensateForMirroring) { if (compensateForMirroring) {
rotationDegrees = (360 - rotationDegrees) % 360; rotationDegrees = (360 - rotationDegrees) % 360;
} }
} catch (CameraInfoUnavailableException e) {
Log.e(TAG, "Failed to get CameraInfo", e);
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "Failed to query camera", e); Log.e(TAG, "Failed to query camera", e);
rotationDegrees = 0;
} }
return rotationDegrees; return rotationDegrees;
@ -678,30 +626,28 @@ final class CameraXModule {
// Update view related information used in use cases // Update view related information used in use cases
private void updateViewInfo() { private void updateViewInfo() {
if (mImageCapture != null) { if (mImageCapture != null) {
mImageCapture.setTargetAspectRatio(new Rational(getWidth(), getHeight())); mImageCapture.setTargetAspectRatioCustom(new Rational(getWidth(), getHeight()));
mImageCapture.setTargetRotation(getDisplaySurfaceRotation()); mImageCapture.setTargetRotation(getDisplaySurfaceRotation());
} }
// Begin Signal Custom Code Block if (mVideoCapture != null && MediaConstraints.isVideoTranscodeAvailable()) {
if (mImageCapture != null && MediaConstraints.isVideoTranscodeAvailable()) {
// End Signal Custom Code Block
mVideoCapture.setTargetRotation(getDisplaySurfaceRotation()); mVideoCapture.setTargetRotation(getDisplaySurfaceRotation());
} }
} }
@RequiresPermission(permission.CAMERA) @RequiresPermission(permission.CAMERA)
private Set<LensFacing> getAvailableCameraLensFacing() { private Set<CameraX.LensFacing> getAvailableCameraLensFacing() {
// Start with all camera directions // Start with all camera directions
Set<LensFacing> available = new LinkedHashSet<>(Arrays.asList(LensFacing.values())); Set<CameraX.LensFacing> available = new LinkedHashSet<>(Arrays.asList(CameraX.LensFacing.values()));
// If we're bound to a lifecycle, remove unavailable cameras // If we're bound to a lifecycle, remove unavailable cameras
if (mCurrentLifecycle != null) { if (mCurrentLifecycle != null) {
if (!hasCameraWithLensFacing(LensFacing.BACK)) { if (!hasCameraWithLensFacing(CameraX.LensFacing.BACK)) {
available.remove(LensFacing.BACK); available.remove(CameraX.LensFacing.BACK);
} }
if (!hasCameraWithLensFacing(LensFacing.FRONT)) { if (!hasCameraWithLensFacing(CameraX.LensFacing.FRONT)) {
available.remove(LensFacing.FRONT); available.remove(CameraX.LensFacing.FRONT);
} }
} }
@ -723,16 +669,6 @@ final class CameraXModule {
mImageCapture.setFlashMode(flash); mImageCapture.setFlashMode(flash);
} }
public boolean hasFlash() {
try {
Boolean flashInfoAvailable = mCameraManager.getCameraCharacteristics(getActiveCamera())
.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
return flashInfoAvailable == Boolean.TRUE;
} catch (CameraInfoUnavailableException | CameraAccessException e) {
return false;
}
}
public void enableTorch(boolean torch) { public void enableTorch(boolean torch) {
if (mPreview == null) { if (mPreview == null) {
return; return;
@ -748,48 +684,59 @@ final class CameraXModule {
} }
public Context getContext() { public Context getContext() {
return mCameraXView.getContext(); return mCameraView.getContext();
} }
public int getWidth() { public int getWidth() {
return mCameraXView.getWidth(); return mCameraView.getWidth();
} }
public int getHeight() { public int getHeight() {
return mCameraXView.getHeight(); return mCameraView.getHeight();
} }
public int getDisplayRotationDegrees() { public int getDisplayRotationDegrees() {
return CameraOrientationUtil.surfaceRotationToDegrees(getDisplaySurfaceRotation()); return CameraOrientationUtil.surfaceRotationToDegrees(getDisplaySurfaceRotation());
} }
// Begin Signal Custom Code Block
public boolean hasFlash() {
try {
LiveData<Boolean> isFlashAvailable = CameraX.getCameraInfo(getLensFacing()).isFlashAvailable();
return isFlashAvailable.getValue() == Boolean.TRUE;
} catch (CameraInfoUnavailableException e) {
return false;
}
}
// End Signal Custom Code Block
protected int getDisplaySurfaceRotation() { protected int getDisplaySurfaceRotation() {
return mCameraXView.getDisplaySurfaceRotation(); return mCameraView.getDisplaySurfaceRotation();
} }
public void setSurfaceTexture(SurfaceTexture st) { public void setSurfaceTexture(SurfaceTexture st) {
mCameraXView.setSurfaceTexture(st); mCameraView.setSurfaceTexture(st);
} }
private int getPreviewWidth() { private int getPreviewWidth() {
return mCameraXView.getPreviewWidth(); return mCameraView.getPreviewWidth();
} }
private int getPreviewHeight() { private int getPreviewHeight() {
return mCameraXView.getPreviewHeight(); return mCameraView.getPreviewHeight();
} }
private int getMeasuredWidth() { private int getMeasuredWidth() {
return mCameraXView.getMeasuredWidth(); return mCameraView.getMeasuredWidth();
} }
private int getMeasuredHeight() { private int getMeasuredHeight() {
return mCameraXView.getMeasuredHeight(); return mCameraView.getMeasuredHeight();
} }
void setTransform(final Matrix matrix) { void setTransform(final Matrix matrix) {
if (Looper.myLooper() != Looper.getMainLooper()) { if (Looper.myLooper() != Looper.getMainLooper()) {
mCameraXView.post( mCameraView.post(
new Runnable() { new Runnable() {
@Override @Override
public void run() { public void run() {
@ -797,7 +744,7 @@ final class CameraXModule {
} }
}); });
} else { } else {
mCameraXView.setTransform(matrix); mCameraView.setTransform(matrix);
} }
} }
@ -810,14 +757,14 @@ final class CameraXModule {
* @param height height of camera source buffers. * @param height height of camera source buffers.
*/ */
void onPreviewSourceDimensUpdated(int width, int height) { void onPreviewSourceDimensUpdated(int width, int height) {
mCameraXView.onPreviewSourceDimensUpdated(width, height); mCameraView.onPreviewSourceDimensUpdated(width, height);
} }
public CaptureMode getCaptureMode() { public CameraXView.CaptureMode getCaptureMode() {
return mCaptureMode; return mCaptureMode;
} }
public void setCaptureMode(CaptureMode captureMode) { public void setCaptureMode(CameraXView.CaptureMode captureMode) {
this.mCaptureMode = captureMode; this.mCaptureMode = captureMode;
rebindToLifecycle(); rebindToLifecycle();
} }

View File

@ -1,5 +1,3 @@
package org.thoughtcrime.securesms.mediasend.camerax;
/* /*
* Copyright (C) 2019 The Android Open Source Project * Copyright (C) 2019 The Android Open Source Project
* *
@ -16,9 +14,12 @@ package org.thoughtcrime.securesms.mediasend.camerax;
* limitations under the License. * limitations under the License.
*/ */
package org.thoughtcrime.securesms.mediasend.camerax;
import android.Manifest.permission; import android.Manifest.permission;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Rect; import android.graphics.Rect;
@ -31,6 +32,7 @@ import android.os.Looper;
import android.os.Parcelable; import android.os.Parcelable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log;
import android.util.Size; import android.util.Size;
import android.view.Display; import android.view.Display;
import android.view.MotionEvent; import android.view.MotionEvent;
@ -40,31 +42,35 @@ import android.view.TextureView;
import android.view.View; import android.view.View;
import android.view.ViewConfiguration; import android.view.ViewConfiguration;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.BaseInterpolator;
import android.view.animation.DecelerateInterpolator; import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission; import androidx.annotation.RequiresPermission;
import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope; import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.camera.core.CameraX.LensFacing; import androidx.camera.core.CameraInfoUnavailableException;
import androidx.camera.core.CameraX;
import androidx.camera.core.FlashMode; import androidx.camera.core.FlashMode;
import androidx.camera.core.ImageCapture.OnImageCapturedListener; import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.ImageCapture.OnImageSavedListener; import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageProxy; import androidx.camera.core.MeteringPoint;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.R;
import java.io.File; import java.io.File;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.util.concurrent.Executor;
/** /**
* A {@link View} that displays a preview of the camera with methods {@link * A {@link View} that displays a preview of the camera with methods {@link
* #takePicture(OnImageCapturedListener)}, {@link #takePicture(File, OnImageSavedListener)}, {@link * #takePicture(Executor, OnImageCapturedListener)},
* #startRecording(File, OnVideoSavedListener)} and {@link #stopRecording()}. * {@link #takePicture(File, Executor, OnImageSavedListener)},
* {@link #startRecording(File, Executor, OnVideoSavedListener)} and {@link #stopRecording()}.
* *
* <p>Because the Camera is a limited resource and consumes a high amount of power, CameraView must * <p>Because the Camera is a limited resource and consumes a high amount of power, CameraView must
* be opened/closed. CameraView will handle opening/closing automatically through use of a {@link * be opened/closed. CameraView will handle opening/closing automatically through use of a {@link
@ -88,8 +94,12 @@ public final class CameraXView extends ViewGroup {
private static final String EXTRA_CAMERA_DIRECTION = "camera_direction"; private static final String EXTRA_CAMERA_DIRECTION = "camera_direction";
private static final String EXTRA_CAPTURE_MODE = "captureMode"; private static final String EXTRA_CAPTURE_MODE = "captureMode";
private final Rect mFocusingRect = new Rect(); private static final int LENS_FACING_NONE = 0;
private final Rect mMeteringRect = new Rect(); private static final int LENS_FACING_FRONT = 1;
private static final int LENS_FACING_BACK = 2;
private static final int FLASH_MODE_AUTO = 1;
private static final int FLASH_MODE_ON = 2;
private static final int FLASH_MODE_OFF = 4;
// For tap-to-focus // For tap-to-focus
private long mDownEventTimestamp; private long mDownEventTimestamp;
// For pinch-to-zoom // For pinch-to-zoom
@ -116,8 +126,7 @@ public final class CameraXView extends ViewGroup {
private ScaleType mScaleType = ScaleType.CENTER_CROP; private ScaleType mScaleType = ScaleType.CENTER_CROP;
// For accessibility event // For accessibility event
private MotionEvent mUpEvent; private MotionEvent mUpEvent;
private @Nullable private @Nullable Paint mLayerPaint;
Paint mLayerPaint;
public CameraXView(Context context) { public CameraXView(Context context) {
this(context, null); this(context, null);
@ -188,11 +197,52 @@ public final class CameraXView extends ViewGroup {
onPreviewSourceDimensUpdated(640, 480); onPreviewSourceDimensUpdated(640, 480);
} }
setScaleType(ScaleType.CENTER_CROP); if (attrs != null) {
setPinchToZoomEnabled(true); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView);
setCaptureMode(CaptureMode.IMAGE); setScaleType(
setCameraLensFacing(LensFacing.FRONT); ScaleType.fromId(
a.getInteger(R.styleable.CameraXView_scaleType,
getScaleType().getId())));
setPinchToZoomEnabled(
a.getBoolean(
R.styleable.CameraXView_pinchToZoomEnabled, isPinchToZoomEnabled()));
setCaptureMode(
CaptureMode.fromId(
a.getInteger(R.styleable.CameraXView_captureMode,
getCaptureMode().getId())));
int lensFacing = a.getInt(R.styleable.CameraXView_lensFacing, LENS_FACING_BACK);
switch (lensFacing) {
case LENS_FACING_NONE:
setCameraLensFacing(null);
break;
case LENS_FACING_FRONT:
setCameraLensFacing(CameraX.LensFacing.FRONT);
break;
case LENS_FACING_BACK:
setCameraLensFacing(CameraX.LensFacing.BACK);
break;
default:
// Unhandled event.
}
int flashMode = a.getInt(R.styleable.CameraXView_flash, 0);
switch (flashMode) {
case FLASH_MODE_AUTO:
setFlash(FlashMode.AUTO);
break;
case FLASH_MODE_ON:
setFlash(FlashMode.ON);
break;
case FLASH_MODE_OFF:
setFlash(FlashMode.OFF); setFlash(FlashMode.OFF);
break;
default:
// Unhandled event.
}
a.recycle();
}
if (getBackground() == null) { if (getBackground() == null) {
setBackgroundColor(0xFF111111); setBackgroundColor(0xFF111111);
@ -245,7 +295,7 @@ public final class CameraXView extends ViewGroup {
setCameraLensFacing( setCameraLensFacing(
TextUtils.isEmpty(lensFacingString) TextUtils.isEmpty(lensFacingString)
? null ? null
: LensFacing.valueOf(lensFacingString)); : CameraX.LensFacing.valueOf(lensFacingString));
setCaptureMode(CaptureMode.fromId(state.getInt(EXTRA_CAPTURE_MODE))); setCaptureMode(CaptureMode.fromId(state.getInt(EXTRA_CAPTURE_MODE)));
} else { } else {
super.onRestoreInstanceState(savedState); super.onRestoreInstanceState(savedState);
@ -578,33 +628,42 @@ public final class CameraXView extends ViewGroup {
* Takes a picture, and calls {@link OnImageCapturedListener#onCaptureSuccess(ImageProxy, int)} * Takes a picture, and calls {@link OnImageCapturedListener#onCaptureSuccess(ImageProxy, int)}
* once when done. * once when done.
* *
* @param executor The executor in which the listener callback methods will be run.
* @param listener Listener which will receive success or failure callbacks. * @param listener Listener which will receive success or failure callbacks.
*/ */
public void takePicture(OnImageCapturedListener listener) { @SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
mCameraModule.takePicture(listener); public void takePicture(@NonNull Executor executor, @NonNull ImageCapture.OnImageCapturedListener listener) {
mCameraModule.takePicture(executor, listener);
} }
/** /**
* Takes a picture and calls {@link OnImageSavedListener#onImageSaved(File)} when done. * Takes a picture and calls {@link OnImageSavedListener#onImageSaved(File)} when done.
* *
* @param file The destination. * @param file The destination.
* @param executor The executor in which the listener callback methods will be run.
* @param listener Listener which will receive success or failure callbacks. * @param listener Listener which will receive success or failure callbacks.
*/ */
public void takePicture(File file, OnImageSavedListener listener) { @SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
mCameraModule.takePicture(file, listener); public void takePicture(@NonNull File file, @NonNull Executor executor,
@NonNull ImageCapture.OnImageSavedListener listener) {
mCameraModule.takePicture(file, executor, listener);
} }
// Begin Signal Custom Code Block
/** /**
* Takes a video and calls the OnVideoSavedListener when done. * Takes a video and calls the OnVideoSavedListener when done.
* *
* @param fileDescriptor The destination. * @param file The destination.
* @param executor The executor in which the listener callback methods will be run.
* @param listener Listener which will receive success or failure callbacks.
*/ */
// Begin Signal Custom Code Block
@RequiresApi(26) @RequiresApi(26)
public void startRecording(FileDescriptor fileDescriptor, VideoCapture.OnVideoSavedListener listener) { @SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
mCameraModule.startRecording(fileDescriptor, listener); public void startRecording(@NonNull FileDescriptor file, @NonNull Executor executor,
}
// End Signal Custom Code Block // End Signal Custom Code Block
@NonNull VideoCapture.OnVideoSavedListener listener) {
mCameraModule.startRecording(file, executor, listener);
}
/** Stops an in progress video. */ /** Stops an in progress video. */
// Begin Signal Custom Code Block // Begin Signal Custom Code Block
@ -626,7 +685,7 @@ public final class CameraXView extends ViewGroup {
* @throws IllegalStateException if the CAMERA permission is not currently granted. * @throws IllegalStateException if the CAMERA permission is not currently granted.
*/ */
@RequiresPermission(permission.CAMERA) @RequiresPermission(permission.CAMERA)
public boolean hasCameraWithLensFacing(LensFacing lensFacing) { public boolean hasCameraWithLensFacing(CameraX.LensFacing lensFacing) {
return mCameraModule.hasCameraWithLensFacing(lensFacing); return mCameraModule.hasCameraWithLensFacing(lensFacing);
} }
@ -655,29 +714,21 @@ public final class CameraXView extends ViewGroup {
* *
* @param lensFacing The desired camera lensFacing. * @param lensFacing The desired camera lensFacing.
*/ */
public void setCameraLensFacing(@Nullable LensFacing lensFacing) { public void setCameraLensFacing(@Nullable CameraX.LensFacing lensFacing) {
mCameraModule.setCameraLensFacing(lensFacing); mCameraModule.setCameraLensFacing(lensFacing);
} }
/** Returns the currently selected {@link LensFacing}. */ /** Returns the currently selected {@link LensFacing}. */
@Nullable @Nullable
public LensFacing getCameraLensFacing() { public CameraX.LensFacing getCameraLensFacing() {
return mCameraModule.getLensFacing(); return mCameraModule.getLensFacing();
} }
/** // Begin Signal Custom Code Block
* Focuses the camera on the given area. public boolean hasFlash() {
* return mCameraModule.hasFlash();
* <p>Sets the focus and exposure metering rectangles. Coordinates for both X and Y dimensions
* are Limited from -1000 to 1000, where (0, 0) is the center of the image and the width/height
* represent the values from -1000 to 1000.
*
* @param focus Area used to focus the camera.
* @param metering Area used for exposure metering.
*/
public void focus(Rect focus, Rect metering) {
mCameraModule.focus(focus, metering);
} }
// End Signal Custom Code Block
/** Gets the active flash strategy. */ /** Gets the active flash strategy. */
public FlashMode getFlash() { public FlashMode getFlash() {
@ -685,14 +736,10 @@ public final class CameraXView extends ViewGroup {
} }
/** Sets the active flash strategy. */ /** Sets the active flash strategy. */
public void setFlash(FlashMode flashMode) { public void setFlash(@NonNull FlashMode flashMode) {
mCameraModule.setFlash(flashMode); mCameraModule.setFlash(flashMode);
} }
public boolean hasFlash() {
return mCameraModule.hasFlash();
}
private int getRelativeCameraOrientation(boolean compensateForMirroring) { private int getRelativeCameraOrientation(boolean compensateForMirroring) {
return mCameraModule.getRelativeCameraOrientation(compensateForMirroring); return mCameraModule.getRelativeCameraOrientation(compensateForMirroring);
} }
@ -702,7 +749,7 @@ public final class CameraXView extends ViewGroup {
} }
@Override @Override
public boolean onTouchEvent(MotionEvent event) { public boolean onTouchEvent(@NonNull MotionEvent event) {
// Disable pinch-to-zoom and tap-to-focus while the camera module is paused. // Disable pinch-to-zoom and tap-to-focus while the camera module is paused.
if (mCameraModule.isPaused()) { if (mCameraModule.isPaused()) {
return false; return false;
@ -745,10 +792,21 @@ public final class CameraXView extends ViewGroup {
final float x = (mUpEvent != null) ? mUpEvent.getX() : getX() + getWidth() / 2f; final float x = (mUpEvent != null) ? mUpEvent.getX() : getX() + getWidth() / 2f;
final float y = (mUpEvent != null) ? mUpEvent.getY() : getY() + getHeight() / 2f; final float y = (mUpEvent != null) ? mUpEvent.getY() : getY() + getHeight() / 2f;
mUpEvent = null; mUpEvent = null;
calculateTapArea(mFocusingRect, x, y, 1f);
calculateTapArea(mMeteringRect, x, y, 1.5f); TextureViewMeteringPointFactory pointFactory = new TextureViewMeteringPointFactory(
if (area(mFocusingRect) > 0 && area(mMeteringRect) > 0) { mCameraTextureView);
focus(mFocusingRect, mMeteringRect); float afPointWidth = 1.0f / 6.0f; // 1/6 total area
float aePointWidth = afPointWidth * 1.5f;
MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth, 1.0f);
MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth, 1.0f);
try {
CameraX.getCameraControl(getCameraLensFacing()).startFocusAndMetering(
FocusMeteringAction.Builder.from(afPoint, FocusMeteringAction.MeteringMode.AF_ONLY)
.addPoint(aePoint, FocusMeteringAction.MeteringMode.AE_ONLY)
.build());
} catch (CameraInfoUnavailableException e) {
Log.d(TAG, "cannot access camera", e);
} }
return true; return true;
@ -759,80 +817,6 @@ public final class CameraXView extends ViewGroup {
return rect.width() * rect.height(); return rect.width() * rect.height();
} }
/** The area must be between -1000,-1000 and 1000,1000 */
private void calculateTapArea(Rect rect, float x, float y, float coefficient) {
int max = 1000;
int min = -1000;
// Default to 300 (1/6th the total area) and scale by the coefficient
int areaSize = (int) (300 * coefficient);
// Rotate the coordinates if the camera orientation is different
int width = getWidth();
int height = getHeight();
// Compensate orientation as it's mirrored on preview for forward facing cameras
boolean compensateForMirroring = (getCameraLensFacing() == LensFacing.FRONT);
int relativeCameraOrientation = getRelativeCameraOrientation(compensateForMirroring);
int temp;
float tempf;
switch (relativeCameraOrientation) {
case 90:
// Fall-through
case 270:
// We're horizontal. Swap width/height. Swap x/y.
temp = width;
//noinspection SuspiciousNameCombination
width = height;
height = temp;
tempf = x;
//noinspection SuspiciousNameCombination
x = y;
y = tempf;
break;
default:
break;
}
switch (relativeCameraOrientation) {
// Map to correct coordinates according to relativeCameraOrientation
case 90:
y = height - y;
break;
case 180:
x = width - x;
y = height - y;
break;
case 270:
x = width - x;
break;
default:
break;
}
// Swap x if it's a mirrored preview
if (compensateForMirroring) {
x = width - x;
}
// Grab the x, y position from within the View and normalize it to -1000 to 1000
x = min + distance(max, min) * (x / width);
y = min + distance(max, min) * (y / height);
// Modify the rect to the bounding area
rect.top = (int) y - areaSize / 2;
rect.left = (int) x - areaSize / 2;
rect.bottom = rect.top + areaSize;
rect.right = rect.left + areaSize;
// Cap at -1000 to 1000
rect.top = rangeLimit(rect.top, max, min);
rect.left = rangeLimit(rect.left, max, min);
rect.bottom = rangeLimit(rect.bottom, max, min);
rect.right = rangeLimit(rect.right, max, min);
}
private int rangeLimit(int val, int max, int min) { private int rangeLimit(int val, int max, int min) {
return Math.min(Math.max(val, min), max); return Math.min(Math.max(val, min), max);
} }
@ -975,7 +959,7 @@ public final class CameraXView extends ViewGroup {
* The capture mode used by CameraView. * The capture mode used by CameraView.
* *
* <p>This enum can be used to determine which capture mode will be enabled for {@link * <p>This enum can be used to determine which capture mode will be enabled for {@link
* CameraXView}. * CameraView}.
*/ */
public enum CaptureMode { public enum CaptureMode {
/** A mode where image capture is enabled. */ /** A mode where image capture is enabled. */
@ -1024,7 +1008,7 @@ public final class CameraXView extends ViewGroup {
private class PinchToZoomGestureDetector extends ScaleGestureDetector private class PinchToZoomGestureDetector extends ScaleGestureDetector
implements ScaleGestureDetector.OnScaleGestureListener { implements ScaleGestureDetector.OnScaleGestureListener {
private static final float SCALE_MULTIPIER = 0.75f; private static final float SCALE_MULTIPIER = 0.75f;
private final Interpolator mInterpolator = new DecelerateInterpolator(2f); private final BaseInterpolator mInterpolator = new DecelerateInterpolator(2f);
private float mNormalizedScaleFactor = 0; private float mNormalizedScaleFactor = 0;
PinchToZoomGestureDetector(Context context) { PinchToZoomGestureDetector(Context context) {

View File

@ -0,0 +1,93 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thoughtcrime.securesms.mediasend.camerax;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.SurfaceTexture;
import android.view.TextureView;
import androidx.annotation.NonNull;
import androidx.camera.core.MeteringPoint;
import androidx.camera.core.MeteringPointFactory;
/**
* A {@link MeteringPointFactory} for creating a {@link MeteringPoint} by {@link TextureView} and
* (x,y).
*
* <p>SurfaceTexture in TextureView could be cropped, scaled or rotated by
* {@link TextureView#getTransform(Matrix)}. This factory translates the (x, y) into the sensor
* crop region normalized (x,y) by this transform. {@link SurfaceTexture#getTransformMatrix} is
* also used during the translation. No lens facing information is required because
* {@link SurfaceTexture#getTransformMatrix} contains the necessary transformation corresponding
* to the lens face of current camera ouput.
*/
public class TextureViewMeteringPointFactory extends MeteringPointFactory {
private final TextureView mTextureView;
public TextureViewMeteringPointFactory(@NonNull TextureView textureView) {
mTextureView = textureView;
}
/**
* Translates a (x,y) from TextureView.
*/
@NonNull
@Override
protected PointF translatePoint(float x, float y) {
Matrix transform = new Matrix();
mTextureView.getTransform(transform);
// applying reverse of TextureView#getTransform
Matrix inverse = new Matrix();
transform.invert(inverse);
float[] pt = new float[]{x, y};
inverse.mapPoints(pt);
// get SurfaceTexture#getTransformMatrix
float[] surfaceTextureMat = new float[16];
mTextureView.getSurfaceTexture().getTransformMatrix(surfaceTextureMat);
// convert SurfaceTexture#getTransformMatrix(4x4 column major 3D matrix) to
// android.graphics.Matrix(3x3 row major 2D matrix)
Matrix surfaceTextureTransform = glMatrixToGraphicsMatrix(surfaceTextureMat);
float[] pt2 = new float[2];
// convert to texture coordinates first.
pt2[0] = pt[0] / mTextureView.getWidth();
pt2[1] = (mTextureView.getHeight() - pt[1]) / mTextureView.getHeight();
surfaceTextureTransform.mapPoints(pt2);
return new PointF(pt2[0], pt2[1]);
}
private Matrix glMatrixToGraphicsMatrix(float[] glMatrix) {
float[] convert = new float[9];
convert[0] = glMatrix[0];
convert[1] = glMatrix[4];
convert[2] = glMatrix[12];
convert[3] = glMatrix[1];
convert[4] = glMatrix[5];
convert[5] = glMatrix[13];
convert[6] = glMatrix[3];
convert[7] = glMatrix[7];
convert[8] = glMatrix[15];
Matrix graphicsMatrix = new Matrix();
graphicsMatrix.setValues(convert);
return graphicsMatrix;
}
}

View File

@ -1,5 +1,3 @@
package org.thoughtcrime.securesms.mediasend.camerax;
/* /*
* Copyright (C) 2019 The Android Open Source Project * Copyright (C) 2019 The Android Open Source Project
* *
@ -16,18 +14,24 @@ package org.thoughtcrime.securesms.mediasend.camerax;
* limitations under the License. * limitations under the License.
*/ */
package org.thoughtcrime.securesms.mediasend.camerax;
import android.annotation.SuppressLint;
import android.location.Location; import android.location.Location;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.AudioRecord; import android.media.AudioRecord;
import android.media.CamcorderProfile; import android.media.CamcorderProfile;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodecInfo; import android.media.MediaCodecInfo;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.media.MediaMuxer; import android.media.MediaMuxer;
import android.media.MediaRecorder; import android.media.MediaRecorder.AudioSource;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Looper; import android.os.Looper;
import android.util.Log;
import android.util.Size; import android.util.Size;
import android.view.Display; import android.view.Display;
import android.view.Surface; import android.view.Surface;
@ -37,13 +41,16 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo;
import androidx.camera.core.CameraInfo; import androidx.annotation.RestrictTo.Scope;
import androidx.camera.core.CameraInfoInternal;
import androidx.camera.core.CameraInfoUnavailableException; import androidx.camera.core.CameraInfoUnavailableException;
import androidx.camera.core.CameraX; import androidx.camera.core.CameraX;
import androidx.camera.core.CameraX.LensFacing;
import androidx.camera.core.CameraXThreads; import androidx.camera.core.CameraXThreads;
import androidx.camera.core.ConfigProvider; import androidx.camera.core.ConfigProvider;
import androidx.camera.core.DeferrableSurface; import androidx.camera.core.DeferrableSurface;
import androidx.camera.core.ImageOutputConfig; import androidx.camera.core.ImageOutputConfig;
import androidx.camera.core.ImageOutputConfig.RotationValue;
import androidx.camera.core.ImmediateSurface; import androidx.camera.core.ImmediateSurface;
import androidx.camera.core.SessionConfig; import androidx.camera.core.SessionConfig;
import androidx.camera.core.UseCase; import androidx.camera.core.UseCase;
@ -51,7 +58,6 @@ import androidx.camera.core.UseCaseConfig;
import androidx.camera.core.VideoCaptureConfig; import androidx.camera.core.VideoCaptureConfig;
import androidx.camera.core.impl.utils.executor.CameraXExecutors; import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.video.VideoUtil; import org.thoughtcrime.securesms.video.VideoUtil;
import java.io.File; import java.io.File;
@ -59,6 +65,8 @@ import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
/** /**
@ -70,6 +78,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
* @hide In the earlier stage, the VideoCapture is deprioritized. * @hide In the earlier stage, the VideoCapture is deprioritized.
*/ */
@RequiresApi(26) @RequiresApi(26)
@RestrictTo(Scope.LIBRARY_GROUP)
public class VideoCapture extends UseCase { public class VideoCapture extends UseCase {
/** /**
@ -77,9 +86,9 @@ public class VideoCapture extends UseCase {
* *
* @hide * @hide
*/ */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @RestrictTo(Scope.LIBRARY_GROUP)
public static final VideoCapture.Defaults DEFAULT_CONFIG = new VideoCapture.Defaults(); public static final Defaults DEFAULT_CONFIG = new Defaults();
private static final VideoCapture.Metadata EMPTY_METADATA = new VideoCapture.Metadata(); private static final Metadata EMPTY_METADATA = new Metadata();
private static final String TAG = "VideoCapture"; private static final String TAG = "VideoCapture";
/** Amount of time to wait for dequeuing a buffer from the videoEncoder. */ /** Amount of time to wait for dequeuing a buffer from the videoEncoder. */
private static final int DEQUE_TIMEOUT_USEC = 10000; private static final int DEQUE_TIMEOUT_USEC = 10000;
@ -105,7 +114,7 @@ public class VideoCapture extends UseCase {
AudioFormat.ENCODING_PCM_8BIT, AudioFormat.ENCODING_PCM_8BIT,
AudioFormat.ENCODING_PCM_FLOAT AudioFormat.ENCODING_PCM_FLOAT
}; };
private final MediaCodec.BufferInfo mVideoBufferInfo = new MediaCodec.BufferInfo(); private final BufferInfo mVideoBufferInfo = new BufferInfo();
private final Object mMuxerLock = new Object(); private final Object mMuxerLock = new Object();
/** Thread on which all encoding occurs. */ /** Thread on which all encoding occurs. */
private final HandlerThread mVideoHandlerThread = private final HandlerThread mVideoHandlerThread =
@ -118,11 +127,12 @@ public class VideoCapture extends UseCase {
private final AtomicBoolean mEndOfVideoStreamSignal = new AtomicBoolean(true); private final AtomicBoolean mEndOfVideoStreamSignal = new AtomicBoolean(true);
private final AtomicBoolean mEndOfAudioStreamSignal = new AtomicBoolean(true); private final AtomicBoolean mEndOfAudioStreamSignal = new AtomicBoolean(true);
private final AtomicBoolean mEndOfAudioVideoSignal = new AtomicBoolean(true); private final AtomicBoolean mEndOfAudioVideoSignal = new AtomicBoolean(true);
private final MediaCodec.BufferInfo mAudioBufferInfo = new MediaCodec.BufferInfo(); private final BufferInfo mAudioBufferInfo = new BufferInfo();
/** For record the first sample written time. */ /** For record the first sample written time. */
private final AtomicBoolean mIsFirstVideoSampleWrite = new AtomicBoolean(false); private final AtomicBoolean mIsFirstVideoSampleWrite = new AtomicBoolean(false);
private final AtomicBoolean mIsFirstAudioSampleWrite = new AtomicBoolean(false); private final AtomicBoolean mIsFirstAudioSampleWrite = new AtomicBoolean(false);
private final VideoCaptureConfig.Builder mUseCaseConfigBuilder; private final VideoCaptureConfig.Builder mUseCaseConfigBuilder;
@NonNull @NonNull
MediaCodec mVideoEncoder; MediaCodec mVideoEncoder;
@NonNull @NonNull
@ -138,6 +148,7 @@ public class VideoCapture extends UseCase {
/** Surface the camera writes to, which the videoEncoder uses as input. */ /** Surface the camera writes to, which the videoEncoder uses as input. */
Surface mCameraSurface; Surface mCameraSurface;
/** audio raw data */ /** audio raw data */
@NonNull
private AudioRecord mAudioRecorder; private AudioRecord mAudioRecorder;
private int mAudioBufferSize; private int mAudioBufferSize;
private boolean mIsRecording = false; private boolean mIsRecording = false;
@ -169,7 +180,7 @@ public class VideoCapture extends UseCase {
MediaFormat format = MediaFormat format =
MediaFormat.createVideoFormat( MediaFormat.createVideoFormat(
VIDEO_MIME_TYPE, resolution.getWidth(), resolution.getHeight()); VIDEO_MIME_TYPE, resolution.getWidth(), resolution.getHeight());
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, config.getBitRate()); format.setInteger(MediaFormat.KEY_BIT_RATE, config.getBitRate());
format.setInteger(MediaFormat.KEY_FRAME_RATE, config.getVideoFrameRate()); format.setInteger(MediaFormat.KEY_FRAME_RATE, config.getVideoFrameRate());
// Begin Signal Custom Code Block // Begin Signal Custom Code Block
@ -188,8 +199,8 @@ public class VideoCapture extends UseCase {
*/ */
@Override @Override
@Nullable @Nullable
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @RestrictTo(Scope.LIBRARY_GROUP)
protected UseCaseConfig.Builder<?, ?, ?> getDefaultBuilder(CameraX.LensFacing lensFacing) { protected UseCaseConfig.Builder<?, ?, ?> getDefaultBuilder(LensFacing lensFacing) {
VideoCaptureConfig defaults = CameraX.getDefaultUseCaseConfig( VideoCaptureConfig defaults = CameraX.getDefaultUseCaseConfig(
VideoCaptureConfig.class, lensFacing); VideoCaptureConfig.class, lensFacing);
if (defaults != null) { if (defaults != null) {
@ -205,7 +216,7 @@ public class VideoCapture extends UseCase {
* @hide * @hide
*/ */
@Override @Override
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @RestrictTo(Scope.LIBRARY_GROUP)
protected Map<String, Size> onSuggestedResolutionUpdated( protected Map<String, Size> onSuggestedResolutionUpdated(
Map<String, Size> suggestedResolutionMap) { Map<String, Size> suggestedResolutionMap) {
VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig(); VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
@ -240,17 +251,20 @@ public class VideoCapture extends UseCase {
* called. * called.
* *
* <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the * <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the
* {@link VideoCapture.OnVideoSavedListener#onError(VideoCapture.VideoCaptureError, String, Throwable)}. * {@link OnVideoSavedListener#onError(VideoCaptureError, String, Throwable)}.
* *
* @param saveLocation Location to save the video capture * @param saveLocation Location to save the video capture
* @param executor The executor in which the listener callback methods will be run.
* @param listener Listener to call for the recorded video * @param listener Listener to call for the recorded video
*/ */
@SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
// Begin Signal Custom Code Block // Begin Signal Custom Code Block
public void startRecording(FileDescriptor saveLocation, VideoCapture.OnVideoSavedListener listener) { public void startRecording(@NonNull FileDescriptor saveLocation,
@NonNull Executor executor, @NonNull OnVideoSavedListener listener) {
// End Signal Custom Code Block // End Signal Custom Code Block
mIsFirstVideoSampleWrite.set(false); mIsFirstVideoSampleWrite.set(false);
mIsFirstAudioSampleWrite.set(false); mIsFirstAudioSampleWrite.set(false);
startRecording(saveLocation, listener, EMPTY_METADATA); startRecording(saveLocation, EMPTY_METADATA, executor, listener);
} }
/** /**
@ -258,38 +272,37 @@ public class VideoCapture extends UseCase {
* called. * called.
* *
* <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the * <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the
* {@link VideoCapture.OnVideoSavedListener#onError(VideoCapture.VideoCaptureError, String, Throwable)}. * {@link OnVideoSavedListener#onError(VideoCaptureError, String, Throwable)}.
* *
* @param saveLocation Location to save the video capture * @param saveLocation Location to save the video capture
* @param listener Listener to call for the recorded video
* @param metadata Metadata to save with the recorded video * @param metadata Metadata to save with the recorded video
* @param executor The executor in which the listener callback methods will be run.
* @param listener Listener to call for the recorded video
*/ */
@SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
// Begin Signal Custom Code Block // Begin Signal Custom Code Block
public void startRecording( public void startRecording(
final FileDescriptor saveLocation, final VideoCapture.OnVideoSavedListener listener, VideoCapture.Metadata metadata) { @NonNull FileDescriptor saveLocation, @NonNull Metadata metadata,
@NonNull Executor executor,
@NonNull OnVideoSavedListener listener) {
// End Signal Custom Code Block // End Signal Custom Code Block
Log.i(TAG, "startRecording"); Log.i(TAG, "startRecording");
OnVideoSavedListener postListener = new VideoSavedListenerWrapper(executor, listener);
if (!mEndOfAudioVideoSignal.get()) { if (!mEndOfAudioVideoSignal.get()) {
listener.onError( postListener.onError(
VideoCapture.VideoCaptureError.RECORDING_IN_PROGRESS, "It is still in video recording!", VideoCaptureError.RECORDING_IN_PROGRESS, "It is still in video recording!",
null); null);
return; return;
} }
// Begin Signal Custom Code Block
if (mAudioRecorder != null) {
try { try {
// audioRecord start // audioRecord start
mAudioRecorder.startRecording(); mAudioRecorder.startRecording();
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
listener.onError(VideoCapture.VideoCaptureError.ENCODER_ERROR, "AudioRecorder start fail", e); postListener.onError(VideoCaptureError.ENCODER_ERROR, "AudioRecorder start fail", e);
return; return;
} }
} else {
Log.w(TAG, "Audio recorder was not initialized! Can't record audio.");
}
// End Signal Custom Code Block
VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig(); VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
String cameraId = getCameraIdUnchecked(config); String cameraId = getCameraIdUnchecked(config);
@ -303,16 +316,17 @@ public class VideoCapture extends UseCase {
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
setupEncoder(getAttachedSurfaceResolution(cameraId)); setupEncoder(getAttachedSurfaceResolution(cameraId));
listener.onError(VideoCapture.VideoCaptureError.ENCODER_ERROR, "Audio/Video encoder start fail", e); postListener.onError(VideoCaptureError.ENCODER_ERROR, "Audio/Video encoder start fail",
e);
return; return;
} }
// Get the relative rotation or default to 0 if the camera info is unavailable // Get the relative rotation or default to 0 if the camera info is unavailable
int relativeRotation = 0; int relativeRotation = 0;
try { try {
CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId); CameraInfoInternal cameraInfoInternal = CameraX.getCameraInfo(cameraId);
relativeRotation = relativeRotation =
cameraInfo.getSensorRotationDegrees( cameraInfoInternal.getSensorRotationDegrees(
((ImageOutputConfig) getUseCaseConfig()) ((ImageOutputConfig) getUseCaseConfig())
.getTargetRotation(Surface.ROTATION_0)); .getTargetRotation(Surface.ROTATION_0));
} catch (CameraInfoUnavailableException e) { } catch (CameraInfoUnavailableException e) {
@ -337,7 +351,7 @@ public class VideoCapture extends UseCase {
} }
} catch (IOException e) { } catch (IOException e) {
setupEncoder(getAttachedSurfaceResolution(cameraId)); setupEncoder(getAttachedSurfaceResolution(cameraId));
listener.onError(VideoCapture.VideoCaptureError.MUXER_ERROR, "MediaMuxer creation failed!", e); postListener.onError(VideoCaptureError.MUXER_ERROR, "MediaMuxer creation failed!", e);
return; return;
} }
@ -351,7 +365,7 @@ public class VideoCapture extends UseCase {
new Runnable() { new Runnable() {
@Override @Override
public void run() { public void run() {
VideoCapture.this.audioEncode(listener); VideoCapture.this.audioEncode(postListener);
} }
}); });
@ -359,9 +373,9 @@ public class VideoCapture extends UseCase {
new Runnable() { new Runnable() {
@Override @Override
public void run() { public void run() {
boolean errorOccurred = VideoCapture.this.videoEncode(listener); boolean errorOccurred = VideoCapture.this.videoEncode(postListener);
if (!errorOccurred) { if (!errorOccurred) {
listener.onVideoSaved(saveLocation); postListener.onVideoSaved(saveLocation);
} }
} }
}); });
@ -369,11 +383,11 @@ public class VideoCapture extends UseCase {
/** /**
* Stops recording video, this must be called after {@link * Stops recording video, this must be called after {@link
* VideoCapture#startRecording(File, VideoCapture.OnVideoSavedListener, VideoCapture.Metadata)} is called. * VideoCapture#startRecording(File, Metadata, Executor, OnVideoSavedListener)} is called.
* *
* <p>stopRecording() is asynchronous API. User need to check if {@link * <p>stopRecording() is asynchronous API. User need to check if {@link
* VideoCapture.OnVideoSavedListener#onVideoSaved(File)} or * OnVideoSavedListener#onVideoSaved(File)} or
* {@link VideoCapture.OnVideoSavedListener#onError(VideoCapture.VideoCaptureError, String, Throwable)} be called * {@link OnVideoSavedListener#onError(VideoCaptureError, String, Throwable)} be called
* before startRecording. * before startRecording.
*/ */
public void stopRecording() { public void stopRecording() {
@ -390,7 +404,7 @@ public class VideoCapture extends UseCase {
* *
* @hide * @hide
*/ */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @RestrictTo(Scope.LIBRARY_GROUP)
@Override @Override
public void clear() { public void clear() {
mVideoHandlerThread.quitSafely(); mVideoHandlerThread.quitSafely();
@ -453,7 +467,7 @@ public class VideoCapture extends UseCase {
* *
* @param rotation Desired rotation of the output video. * @param rotation Desired rotation of the output video.
*/ */
public void setTargetRotation(@ImageOutputConfig.RotationValue int rotation) { public void setTargetRotation(@RotationValue int rotation) {
ImageOutputConfig oldConfig = (ImageOutputConfig) getUseCaseConfig(); ImageOutputConfig oldConfig = (ImageOutputConfig) getUseCaseConfig();
int oldRotation = oldConfig.getTargetRotation(ImageOutputConfig.INVALID_ROTATION); int oldRotation = oldConfig.getTargetRotation(ImageOutputConfig.INVALID_ROTATION);
if (oldRotation == ImageOutputConfig.INVALID_ROTATION || oldRotation != rotation) { if (oldRotation == ImageOutputConfig.INVALID_ROTATION || oldRotation != rotation) {
@ -468,7 +482,8 @@ public class VideoCapture extends UseCase {
* Setup the {@link MediaCodec} for encoding video from a camera {@link Surface} and encoding * Setup the {@link MediaCodec} for encoding video from a camera {@link Surface} and encoding
* audio from selected audio source. * audio from selected audio source.
*/ */
private void setupEncoder(Size resolution) { @SuppressWarnings("WeakerAccess") /* synthetic accessor */
void setupEncoder(Size resolution) {
VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig(); VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
// video encoder setup // video encoder setup
@ -483,14 +498,23 @@ public class VideoCapture extends UseCase {
} }
mCameraSurface = mVideoEncoder.createInputSurface(); mCameraSurface = mVideoEncoder.createInputSurface();
SessionConfig.Builder builder = SessionConfig.Builder.createFrom(config); SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
mDeferrableSurface = new ImmediateSurface(mCameraSurface); mDeferrableSurface = new ImmediateSurface(mCameraSurface);
builder.addSurface(mDeferrableSurface); sessionConfigBuilder.addSurface(mDeferrableSurface);
String cameraId = getCameraIdUnchecked(config); String cameraId = getCameraIdUnchecked(config);
attachToCamera(cameraId, builder.build());
sessionConfigBuilder.addErrorListener(new SessionConfig.ErrorListener() {
@Override
public void onError(@NonNull SessionConfig sessionConfig,
@NonNull SessionConfig.SessionError error) {
setupEncoder(resolution);
}
});
attachToCamera(cameraId, sessionConfigBuilder.build());
// audio encoder setup // audio encoder setup
setAudioParametersByCamcorderProfile(resolution, cameraId); setAudioParametersByCamcorderProfile(resolution, cameraId);
@ -591,7 +615,7 @@ public class VideoCapture extends UseCase {
* *
* @return returns {@code true} if an error condition occurred, otherwise returns {@code false} * @return returns {@code true} if an error condition occurred, otherwise returns {@code false}
*/ */
boolean videoEncode(VideoCapture.OnVideoSavedListener videoSavedListener) { boolean videoEncode(OnVideoSavedListener videoSavedListener) {
VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig(); VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
// Main encoding loop. Exits on end of stream. // Main encoding loop. Exits on end of stream.
boolean errorOccurred = false; boolean errorOccurred = false;
@ -610,7 +634,7 @@ public class VideoCapture extends UseCase {
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
if (mMuxerStarted) { if (mMuxerStarted) {
videoSavedListener.onError( videoSavedListener.onError(
VideoCapture.VideoCaptureError.ENCODER_ERROR, VideoCaptureError.ENCODER_ERROR,
"Unexpected change in video encoding format.", "Unexpected change in video encoding format.",
null); null);
errorOccurred = true; errorOccurred = true;
@ -640,7 +664,7 @@ public class VideoCapture extends UseCase {
Log.i(TAG, "videoEncoder stop"); Log.i(TAG, "videoEncoder stop");
mVideoEncoder.stop(); mVideoEncoder.stop();
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
videoSavedListener.onError(VideoCapture.VideoCaptureError.ENCODER_ERROR, videoSavedListener.onError(VideoCaptureError.ENCODER_ERROR,
"Video encoder stop failed!", e); "Video encoder stop failed!", e);
errorOccurred = true; errorOccurred = true;
} }
@ -657,7 +681,7 @@ public class VideoCapture extends UseCase {
} }
} }
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
videoSavedListener.onError(VideoCapture.VideoCaptureError.MUXER_ERROR, "Muxer stop failed!", e); videoSavedListener.onError(VideoCaptureError.MUXER_ERROR, "Muxer stop failed!", e);
errorOccurred = true; errorOccurred = true;
} }
@ -676,7 +700,7 @@ public class VideoCapture extends UseCase {
return errorOccurred; return errorOccurred;
} }
boolean audioEncode(VideoCapture.OnVideoSavedListener videoSavedListener) { boolean audioEncode(OnVideoSavedListener videoSavedListener) {
// Audio encoding loop. Exits on end of stream. // Audio encoding loop. Exits on end of stream.
boolean audioEos = false; boolean audioEos = false;
int outIndex; int outIndex;
@ -729,24 +753,16 @@ public class VideoCapture extends UseCase {
// Audio Stop // Audio Stop
try { try {
Log.i(TAG, "audioRecorder stop"); Log.i(TAG, "audioRecorder stop");
// Begin Signal Custom Code Block
if (mAudioRecorder != null) {
mAudioRecorder.stop(); mAudioRecorder.stop();
}
// End Signal Custom Code Block
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
videoSavedListener.onError( videoSavedListener.onError(
VideoCapture.VideoCaptureError.ENCODER_ERROR, "Audio recorder stop failed!", e); VideoCaptureError.ENCODER_ERROR, "Audio recorder stop failed!", e);
} }
try { try {
// Begin Signal Custom Code Block
if (mAudioRecorder != null) {
mAudioEncoder.stop(); mAudioEncoder.stop();
}
// End Signal Custom Code Block
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
videoSavedListener.onError(VideoCapture.VideoCaptureError.ENCODER_ERROR, videoSavedListener.onError(VideoCaptureError.ENCODER_ERROR,
"Audio encoder stop failed!", e); "Audio encoder stop failed!", e);
} }
@ -862,7 +878,7 @@ public class VideoCapture extends UseCase {
* Describes the error that occurred during video capture operations. * Describes the error that occurred during video capture operations.
* *
* <p>This is a parameter sent to the error callback functions set in listeners such as {@link * <p>This is a parameter sent to the error callback functions set in listeners such as {@link
* .VideoCapture.OnVideoSavedListener#onError(VideoCapture.VideoCaptureError, String, Throwable)}. * VideoCapture.OnVideoSavedListener#onError(VideoCaptureError, String, Throwable)}.
* *
* <p>See message parameter in onError callback or log for more details. * <p>See message parameter in onError callback or log for more details.
*/ */
@ -890,11 +906,11 @@ public class VideoCapture extends UseCase {
public interface OnVideoSavedListener { public interface OnVideoSavedListener {
/** Called when the video has been successfully saved. */ /** Called when the video has been successfully saved. */
// Begin Signal Custom Code Block // Begin Signal Custom Code Block
void onVideoSaved(@NonNull FileDescriptor fileDescriptor); void onVideoSaved(@NonNull FileDescriptor file);
// End Signal Custom Code Block // End Signal Custom Code Block
/** Called when an error occurs while attempting to save the video. */ /** Called when an error occurs while attempting to save the video. */
void onError(@NonNull VideoCapture.VideoCaptureError videoCaptureError, @NonNull String message, void onError(@NonNull VideoCaptureError videoCaptureError, @NonNull String message,
@Nullable Throwable cause); @Nullable Throwable cause);
} }
@ -906,7 +922,7 @@ public class VideoCapture extends UseCase {
* *
* @hide * @hide
*/ */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @RestrictTo(Scope.LIBRARY_GROUP)
public static final class Defaults public static final class Defaults
implements ConfigProvider<VideoCaptureConfig> { implements ConfigProvider<VideoCaptureConfig> {
private static final Handler DEFAULT_HANDLER = new Handler(Looper.getMainLooper()); private static final Handler DEFAULT_HANDLER = new Handler(Looper.getMainLooper());
@ -922,7 +938,7 @@ public class VideoCapture extends UseCase {
/** audio channel count */ /** audio channel count */
private static final int DEFAULT_AUDIO_CHANNEL_COUNT = 1; private static final int DEFAULT_AUDIO_CHANNEL_COUNT = 1;
/** audio record source */ /** audio record source */
private static final int DEFAULT_AUDIO_RECORD_SOURCE = MediaRecorder.AudioSource.MIC; private static final int DEFAULT_AUDIO_RECORD_SOURCE = AudioSource.MIC;
/** audio default minimum buffer size */ /** audio default minimum buffer size */
private static final int DEFAULT_AUDIO_MIN_BUFFER_SIZE = 1024; private static final int DEFAULT_AUDIO_MIN_BUFFER_SIZE = 1024;
/** Current max resolution of VideoCapture is set as FHD */ /** Current max resolution of VideoCapture is set as FHD */
@ -935,7 +951,6 @@ public class VideoCapture extends UseCase {
static { static {
VideoCaptureConfig.Builder builder = VideoCaptureConfig.Builder builder =
new VideoCaptureConfig.Builder() new VideoCaptureConfig.Builder()
.setCallbackHandler(DEFAULT_HANDLER)
.setVideoFrameRate(DEFAULT_VIDEO_FRAME_RATE) .setVideoFrameRate(DEFAULT_VIDEO_FRAME_RATE)
.setBitRate(DEFAULT_BIT_RATE) .setBitRate(DEFAULT_BIT_RATE)
.setIFrameInterval(DEFAULT_INTRA_FRAME_INTERVAL) .setIFrameInterval(DEFAULT_INTRA_FRAME_INTERVAL)
@ -951,7 +966,7 @@ public class VideoCapture extends UseCase {
} }
@Override @Override
public VideoCaptureConfig getConfig(CameraX.LensFacing lensFacing) { public VideoCaptureConfig getConfig(LensFacing lensFacing) {
return DEFAULT_CONFIG; return DEFAULT_CONFIG;
} }
} }
@ -962,4 +977,39 @@ public class VideoCapture extends UseCase {
@Nullable @Nullable
public Location location; public Location location;
} }
private final class VideoSavedListenerWrapper implements OnVideoSavedListener {
@NonNull Executor mExecutor;
@NonNull OnVideoSavedListener mOnVideoSavedListener;
VideoSavedListenerWrapper(@NonNull Executor executor,
@NonNull OnVideoSavedListener onVideoSavedListener) {
mExecutor = executor;
mOnVideoSavedListener = onVideoSavedListener;
}
@Override
// Begin Signal Custom Code Block
public void onVideoSaved(@NonNull FileDescriptor file) {
// End Signal Custom Code Block
try {
mExecutor.execute(() -> mOnVideoSavedListener.onVideoSaved(file));
} catch (RejectedExecutionException e) {
Log.e(TAG, "Unable to post to the supplied executor.");
}
}
@Override
public void onError(@NonNull VideoCaptureError videoCaptureError, @NonNull String message,
@Nullable Throwable cause) {
try {
mExecutor.execute(
() -> mOnVideoSavedListener.onError(videoCaptureError, message, cause));
} catch (RejectedExecutionException e) {
Log.e(TAG, "Unable to post to the supplied executor.");
}
}
}
} }

View File

@ -24,11 +24,11 @@ dependencyVerification {
['androidx.asynclayoutinflater:asynclayoutinflater:1.0.0', ['androidx.asynclayoutinflater:asynclayoutinflater:1.0.0',
'f7eab60c57addd94bb06275832fe7600611beaaae1a1ec597c231956faf96c8b'], 'f7eab60c57addd94bb06275832fe7600611beaaae1a1ec597c231956faf96c8b'],
['androidx.camera:camera-camera2:1.0.0-alpha04', ['androidx.camera:camera-camera2:1.0.0-alpha06',
'b7897230aec96365d675712c92f5edcb8b464badfd61788c8f956ec2d6e49bfe'], 'e50f20deb950ffebcd4d1de5408ef7a5404bec80ec77119e05663c890739b903'],
['androidx.camera:camera-core:1.0.0-alpha04', ['androidx.camera:camera-core:1.0.0-alpha06',
'e1c70de55600a0caf826eb4f8a75c96c5ff8f0b626bf08413d31e80ffa55f8ba'], '0096cabe539d9b4288f406acfb44264b137ebd600e38e33504ff425c979016c9'],
['androidx.cardview:cardview:1.0.0', ['androidx.cardview:cardview:1.0.0',
'1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7'], '1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7'],