diff --git a/build.gradle b/build.gradle index 878acfa19..901de05dc 100644 --- a/build.gradle +++ b/build.gradle @@ -78,8 +78,8 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05' implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0' - implementation "androidx.camera:camera-core:1.0.0-alpha04" - implementation "androidx.camera:camera-camera2:1.0.0-alpha04" + implementation "androidx.camera:camera-core:1.0.0-alpha06" + implementation "androidx.camera:camera-camera2:1.0.0-alpha06" implementation('com.google.firebase:firebase-messaging:17.3.4') { exclude group: 'com.google.firebase', module: 'firebase-core' diff --git a/res/values/styles.xml b/res/values/styles.xml index de1bddb27..e77c3b209 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -340,4 +340,27 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java b/src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java index 23fcaa609..a638596b9 100644 --- a/src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java +++ b/src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java @@ -1,9 +1,7 @@ package org.thoughtcrime.securesms.mediasend; import android.annotation.SuppressLint; -import android.app.Activity; import android.content.Context; -import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.os.Bundle; import android.view.GestureDetector; @@ -26,11 +24,13 @@ import androidx.annotation.RequiresApi; import androidx.camera.core.CameraX; import androidx.camera.core.ImageCapture; import androidx.camera.core.ImageProxy; +import androidx.camera.core.impl.utils.executor.CameraXExecutors; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProviders; import com.bumptech.glide.Glide; +import com.bumptech.glide.util.Executors; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.TooltipPopup; @@ -325,7 +325,7 @@ public class CameraXFragment extends Fragment implements CameraFragment { selfieFlash ); - camera.takePicture(new ImageCapture.OnImageCapturedListener() { + camera.takePicture(Executors.mainThreadExecutor(), new ImageCapture.OnImageCapturedListener() { @Override public void onCaptureSuccess(ImageProxy image, int rotationDegrees) { flashHelper.endFlash(); @@ -352,7 +352,7 @@ public class CameraXFragment extends Fragment implements CameraFragment { } @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(); controller.onCameraError(); } diff --git a/src/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java b/src/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java index 841a0736b..046162301 100644 --- a/src/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java +++ b/src/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java @@ -16,6 +16,8 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.fragment.app.Fragment; +import com.bumptech.glide.util.Executors; + import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.animation.AnimationCompleteListener; import org.thoughtcrime.securesms.components.TooltipPopup; @@ -119,7 +121,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener this.camera.setZoomLevel(0f); callback.onVideoRecordStarted(); shrinkCaptureArea(); - camera.startRecording(memoryFileDescriptor.getFileDescriptor(), videoSavedListener); + camera.startRecording(memoryFileDescriptor.getFileDescriptor(), Executors.mainThreadExecutor(), videoSavedListener); updateProgressAnimator.start(); } diff --git a/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java b/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java index 2389b93b7..5f4f47571 100644 --- a/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java +++ b/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java @@ -1,5 +1,3 @@ -package org.thoughtcrime.securesms.mediasend.camerax; - /* * Copyright (C) 2019 The Android Open Source Project * @@ -16,6 +14,8 @@ package org.thoughtcrime.securesms.mediasend.camerax; * limitations under the License. */ +package org.thoughtcrime.securesms.mediasend.camerax; + import android.Manifest.permission; import android.annotation.SuppressLint; import android.content.Context; @@ -25,23 +25,24 @@ import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; +import android.os.Build; import android.os.Looper; +import android.util.Log; import android.util.Rational; import android.util.Size; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RequiresPermission; import androidx.annotation.UiThread; +import androidx.camera.core.AspectRatio; import androidx.camera.core.CameraInfo; import androidx.camera.core.CameraInfoUnavailableException; import androidx.camera.core.CameraOrientationUtil; import androidx.camera.core.CameraX; -import androidx.camera.core.CameraX.LensFacing; import androidx.camera.core.FlashMode; 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.Preview; import androidx.camera.core.PreviewConfig; @@ -49,12 +50,10 @@ import androidx.camera.core.VideoCaptureConfig; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; 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.util.FeatureFlags; import org.thoughtcrime.securesms.video.VideoUtil; import java.io.File; @@ -62,11 +61,11 @@ import java.io.FileDescriptor; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; /** CameraX use case operation built on @{link androidx.camera.core}. */ @RequiresApi(21) -@SuppressLint("RestrictedApi") final class CameraXModule { public static final String TAG = "CameraXModule"; @@ -82,9 +81,9 @@ final class CameraXModule { private final PreviewConfig.Builder mPreviewConfigBuilder; private final VideoCaptureConfig.Builder mVideoCaptureConfigBuilder; private final ImageCaptureConfig.Builder mImageCaptureConfigBuilder; - private final CameraXView mCameraXView; + private final CameraXView mCameraView; 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 mMaxVideoSize = CameraXView.INDEFINITE_VIDEO_SIZE; private FlashMode mFlash = FlashMode.OFF; @@ -112,10 +111,10 @@ final class CameraXModule { @Nullable private Rect mCropRegion; @Nullable - private CameraX.LensFacing mCameraLensFacing = LensFacing.BACK; + private CameraX.LensFacing mCameraLensFacing = CameraX.LensFacing.BACK; CameraXModule(CameraXView view) { - this.mCameraXView = view; + this.mCameraView = view; mCameraManager = (CameraManager) view.getContext().getSystemService(Context.CAMERA_SERVICE); @@ -126,11 +125,10 @@ final class CameraXModule { // Begin Signal Custom Code Block mVideoCaptureConfigBuilder = - new VideoCaptureConfig.Builder() - .setAudioBitRate(VideoUtil.AUDIO_BIT_RATE) - .setVideoFrameRate(VideoUtil.VIDEO_FRAME_RATE) - .setBitRate(VideoUtil.VIDEO_BIT_RATE) - .setTargetName("VideoCapture"); + new VideoCaptureConfig.Builder().setTargetName("VideoCapture") + .setAudioBitRate(VideoUtil.AUDIO_BIT_RATE) + .setVideoFrameRate(VideoUtil.VIDEO_FRAME_RATE) + .setBitRate(VideoUtil.VIDEO_BIT_RATE); // End Signal Custom Code Block } @@ -192,8 +190,7 @@ final class CameraXModule { final int cameraOrientation; try { - String cameraId; - Set available = getAvailableCameraLensFacing(); + Set available = getAvailableCameraLensFacing(); if (available.isEmpty()) { Log.w(TAG, "Unable to bindToLifeCycle since no cameras available"); @@ -217,41 +214,32 @@ final class CameraXModule { if (mCameraLensFacing == null) { return; } - - cameraId = CameraX.getCameraWithLensFacing(mCameraLensFacing); - if (cameraId == null) { - return; - } - CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId); + CameraInfo cameraInfo = CameraX.getCameraInfo(getLensFacing()); cameraOrientation = cameraInfo.getSensorRotationDegrees(); + } catch (CameraInfoUnavailableException e) { + throw new IllegalStateException("Unable to get Camera Info.", e); } catch (Exception 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 // ratio as 16:9 if it is VIDEO or MIXED mode. Then, it will be WYSIWYG when the view finder - // is - // in CENTER_INSIDE mode. + // is in CENTER_INSIDE mode. boolean isDisplayPortrait = getDisplayRotationDegrees() == 0 || getDisplayRotationDegrees() == 180; - if (getCaptureMode() == CaptureMode.IMAGE) { - mImageCaptureConfigBuilder.setTargetAspectRatio( - isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3); - mPreviewConfigBuilder.setTargetAspectRatio( - isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3); + Rational targetAspectRatio; + if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) { + mImageCaptureConfigBuilder.setTargetAspectRatio(AspectRatio.RATIO_4_3); + targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3; } else { - mImageCaptureConfigBuilder.setTargetAspectRatio( - isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9); - mPreviewConfigBuilder.setTargetAspectRatio( - isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9); + mImageCaptureConfigBuilder.setTargetAspectRatio(AspectRatio.RATIO_16_9); + targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9; } mImageCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation()); mImageCaptureConfigBuilder.setLensFacing(mCameraLensFacing); - mImageCaptureConfigBuilder.setCaptureMode(CameraXUtil.getOptimalCaptureMode()); - mImageCaptureConfigBuilder.setTargetResolution(new Size(1920, 1920)); mImageCapture = new ImageCapture(mImageCaptureConfigBuilder.build()); // Begin Signal Custom Code Block @@ -267,25 +255,17 @@ final class CameraXModule { if (MediaConstraints.isVideoTranscodeAvailable()) { mVideoCapture = new VideoCapture(mVideoCaptureConfigBuilder.build()); } - // End Signal Custom Code Block - mPreviewConfigBuilder.setLensFacing(mCameraLensFacing); - int relativeCameraOrientation = getRelativeCameraOrientation(false); - - if (relativeCameraOrientation == 90 || relativeCameraOrientation == 270) { - mPreviewConfigBuilder.setTargetResolution( - new Size(getMeasuredHeight(), getMeasuredWidth())); - } else { - mPreviewConfigBuilder.setTargetResolution( - new Size(getMeasuredWidth(), getMeasuredHeight())); - } + // Adjusts the preview resolution according to the view size and the target aspect ratio. + int height = (int) (getMeasuredWidth() / targetAspectRatio.floatValue()); + mPreviewConfigBuilder.setTargetResolution(new Size(getMeasuredWidth(), height)); mPreview = new Preview(mPreviewConfigBuilder.build()); mPreview.setOnPreviewOutputUpdateListener( new Preview.OnPreviewOutputUpdateListener() { @Override - public void onUpdated(Preview.PreviewOutput output) { + public void onUpdated(@NonNull Preview.PreviewOutput output) { boolean needReverse = cameraOrientation != 0 && cameraOrientation != 180; int textureWidth = needReverse @@ -301,9 +281,9 @@ final class CameraXModule { } }); - if (getCaptureMode() == CaptureMode.IMAGE) { + if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) { CameraX.bindToLifecycle(mCurrentLifecycle, mImageCapture, mPreview); - } else if (getCaptureMode() == CaptureMode.VIDEO) { + } else if (getCaptureMode() == CameraXView.CaptureMode.VIDEO) { CameraX.bindToLifecycle(mCurrentLifecycle, mVideoCapture, mPreview); } else { CameraX.bindToLifecycle(mCurrentLifecycle, mImageCapture, mVideoCapture, mPreview); @@ -324,18 +304,12 @@ final class CameraXModule { "Explicit open/close of camera not yet supported. Use bindtoLifecycle() instead."); } - public void stopPreview() { - if (mPreview != null) { - mPreview.clear(); - } - } - - public void takePicture(OnImageCapturedListener listener) { + public void takePicture(Executor executor, ImageCapture.OnImageCapturedListener listener) { if (mImageCapture == null) { return; } - if (getCaptureMode() == CaptureMode.VIDEO) { + if (getCaptureMode() == CameraXView.CaptureMode.VIDEO) { 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"); } - 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) { return; } - if (getCaptureMode() == CaptureMode.VIDEO) { + if (getCaptureMode() == CameraXView.CaptureMode.VIDEO) { throw new IllegalStateException("Can not take picture under VIDEO capture mode."); } @@ -360,19 +334,19 @@ final class CameraXModule { } ImageCapture.Metadata metadata = new ImageCapture.Metadata(); - metadata.isReversedHorizontal = mCameraLensFacing == LensFacing.FRONT; - mImageCapture.takePicture(saveLocation, listener, metadata); + metadata.isReversedHorizontal = mCameraLensFacing == CameraX.LensFacing.FRONT; + mImageCapture.takePicture(saveLocation, metadata, executor, listener); } // Begin Signal Custom Code Block @RequiresApi(26) + public void startRecording(FileDescriptor file, Executor executor, final VideoCapture.OnVideoSavedListener listener) { // End Signal Custom Code Block - public void startRecording(FileDescriptor file, final VideoCapture.OnVideoSavedListener listener) { if (mVideoCapture == null) { return; } - if (getCaptureMode() == CaptureMode.IMAGE) { + if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) { throw new IllegalStateException("Can not record video under IMAGE capture mode."); } @@ -383,25 +357,24 @@ final class CameraXModule { mVideoIsRecording.set(true); mVideoCapture.startRecording( file, + executor, new VideoCapture.OnVideoSavedListener() { @Override - // Begin Signal Custom Code Block - public void onVideoSaved(FileDescriptor savedFileDescriptor) { + // Begin Signal Custom Code block + public void onVideoSaved(@NonNull FileDescriptor savedFile) { // End Signal Custom Code Block mVideoIsRecording.set(false); - // Begin Signal Custom Code Block - listener.onVideoSaved(savedFileDescriptor); - // End Signal Custom Code Block + listener.onVideoSaved(savedFile); } @Override public void onError( - VideoCapture.VideoCaptureError useCaseError, - String message, + @NonNull VideoCapture.VideoCaptureError videoCaptureError, + @NonNull String message, @Nullable Throwable cause) { mVideoIsRecording.set(false); 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. @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 if (mCameraLensFacing != lensFacing) { // 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) - public boolean hasCameraWithLensFacing(LensFacing lensFacing) { + public boolean hasCameraWithLensFacing(CameraX.LensFacing lensFacing) { String cameraId; try { cameraId = CameraX.getCameraWithLensFacing(lensFacing); @@ -450,14 +423,14 @@ final class CameraXModule { } @Nullable - public LensFacing getLensFacing() { + public CameraX.LensFacing getLensFacing() { return mCameraLensFacing; } public void toggleCamera() { // TODO(b/124269166): Rethink how we can handle permissions here. @SuppressLint("MissingPermission") - Set availableCameraLensFacing = getAvailableCameraLensFacing(); + Set availableCameraLensFacing = getAvailableCameraLensFacing(); if (availableCameraLensFacing.isEmpty()) { return; @@ -468,44 +441,19 @@ final class CameraXModule { return; } - if (mCameraLensFacing == LensFacing.BACK - && availableCameraLensFacing.contains(LensFacing.FRONT)) { - setCameraLensFacing(LensFacing.FRONT); + if (mCameraLensFacing == CameraX.LensFacing.BACK + && availableCameraLensFacing.contains(CameraX.LensFacing.FRONT)) { + setCameraLensFacing(CameraX.LensFacing.FRONT); return; } - if (mCameraLensFacing == LensFacing.FRONT - && availableCameraLensFacing.contains(LensFacing.BACK)) { - setCameraLensFacing(LensFacing.BACK); + if (mCameraLensFacing == CameraX.LensFacing.FRONT + && availableCameraLensFacing.contains(CameraX.LensFacing.BACK)) { + setCameraLensFacing(CameraX.LensFacing.BACK); 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() { return mZoomLevel; } @@ -604,17 +552,17 @@ final class CameraXModule { } int getRelativeCameraOrientation(boolean compensateForMirroring) { - int rotationDegrees; + int rotationDegrees = 0; try { - String cameraId = CameraX.getCameraWithLensFacing(getLensFacing()); - CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId); + CameraInfo cameraInfo = CameraX.getCameraInfo(getLensFacing()); rotationDegrees = cameraInfo.getSensorRotationDegrees(getDisplaySurfaceRotation()); if (compensateForMirroring) { rotationDegrees = (360 - rotationDegrees) % 360; } + } catch (CameraInfoUnavailableException e) { + Log.e(TAG, "Failed to get CameraInfo", e); } catch (Exception e) { Log.e(TAG, "Failed to query camera", e); - rotationDegrees = 0; } return rotationDegrees; @@ -678,30 +626,28 @@ final class CameraXModule { // Update view related information used in use cases private void updateViewInfo() { if (mImageCapture != null) { - mImageCapture.setTargetAspectRatio(new Rational(getWidth(), getHeight())); + mImageCapture.setTargetAspectRatioCustom(new Rational(getWidth(), getHeight())); mImageCapture.setTargetRotation(getDisplaySurfaceRotation()); } - // Begin Signal Custom Code Block - if (mImageCapture != null && MediaConstraints.isVideoTranscodeAvailable()) { - // End Signal Custom Code Block + if (mVideoCapture != null && MediaConstraints.isVideoTranscodeAvailable()) { mVideoCapture.setTargetRotation(getDisplaySurfaceRotation()); } } @RequiresPermission(permission.CAMERA) - private Set getAvailableCameraLensFacing() { + private Set getAvailableCameraLensFacing() { // Start with all camera directions - Set available = new LinkedHashSet<>(Arrays.asList(LensFacing.values())); + Set available = new LinkedHashSet<>(Arrays.asList(CameraX.LensFacing.values())); // If we're bound to a lifecycle, remove unavailable cameras if (mCurrentLifecycle != null) { - if (!hasCameraWithLensFacing(LensFacing.BACK)) { - available.remove(LensFacing.BACK); + if (!hasCameraWithLensFacing(CameraX.LensFacing.BACK)) { + available.remove(CameraX.LensFacing.BACK); } - if (!hasCameraWithLensFacing(LensFacing.FRONT)) { - available.remove(LensFacing.FRONT); + if (!hasCameraWithLensFacing(CameraX.LensFacing.FRONT)) { + available.remove(CameraX.LensFacing.FRONT); } } @@ -723,16 +669,6 @@ final class CameraXModule { 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) { if (mPreview == null) { return; @@ -748,48 +684,59 @@ final class CameraXModule { } public Context getContext() { - return mCameraXView.getContext(); + return mCameraView.getContext(); } public int getWidth() { - return mCameraXView.getWidth(); + return mCameraView.getWidth(); } public int getHeight() { - return mCameraXView.getHeight(); + return mCameraView.getHeight(); } public int getDisplayRotationDegrees() { return CameraOrientationUtil.surfaceRotationToDegrees(getDisplaySurfaceRotation()); } + // Begin Signal Custom Code Block + public boolean hasFlash() { + try { + LiveData isFlashAvailable = CameraX.getCameraInfo(getLensFacing()).isFlashAvailable(); + return isFlashAvailable.getValue() == Boolean.TRUE; + } catch (CameraInfoUnavailableException e) { + return false; + } + } + // End Signal Custom Code Block + protected int getDisplaySurfaceRotation() { - return mCameraXView.getDisplaySurfaceRotation(); + return mCameraView.getDisplaySurfaceRotation(); } public void setSurfaceTexture(SurfaceTexture st) { - mCameraXView.setSurfaceTexture(st); + mCameraView.setSurfaceTexture(st); } private int getPreviewWidth() { - return mCameraXView.getPreviewWidth(); + return mCameraView.getPreviewWidth(); } private int getPreviewHeight() { - return mCameraXView.getPreviewHeight(); + return mCameraView.getPreviewHeight(); } private int getMeasuredWidth() { - return mCameraXView.getMeasuredWidth(); + return mCameraView.getMeasuredWidth(); } private int getMeasuredHeight() { - return mCameraXView.getMeasuredHeight(); + return mCameraView.getMeasuredHeight(); } void setTransform(final Matrix matrix) { if (Looper.myLooper() != Looper.getMainLooper()) { - mCameraXView.post( + mCameraView.post( new Runnable() { @Override public void run() { @@ -797,7 +744,7 @@ final class CameraXModule { } }); } else { - mCameraXView.setTransform(matrix); + mCameraView.setTransform(matrix); } } @@ -810,14 +757,14 @@ final class CameraXModule { * @param height height of camera source buffers. */ void onPreviewSourceDimensUpdated(int width, int height) { - mCameraXView.onPreviewSourceDimensUpdated(width, height); + mCameraView.onPreviewSourceDimensUpdated(width, height); } - public CaptureMode getCaptureMode() { + public CameraXView.CaptureMode getCaptureMode() { return mCaptureMode; } - public void setCaptureMode(CaptureMode captureMode) { + public void setCaptureMode(CameraXView.CaptureMode captureMode) { this.mCaptureMode = captureMode; rebindToLifecycle(); } diff --git a/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXView.java b/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXView.java index 8364920d9..446a3f0fc 100644 --- a/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXView.java +++ b/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXView.java @@ -1,5 +1,3 @@ -package org.thoughtcrime.securesms.mediasend.camerax; - /* * Copyright (C) 2019 The Android Open Source Project * @@ -16,9 +14,12 @@ package org.thoughtcrime.securesms.mediasend.camerax; * limitations under the License. */ +package org.thoughtcrime.securesms.mediasend.camerax; + import android.Manifest.permission; import android.annotation.SuppressLint; import android.content.Context; +import android.content.res.TypedArray; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Rect; @@ -31,6 +32,7 @@ import android.os.Looper; import android.os.Parcelable; import android.text.TextUtils; import android.util.AttributeSet; +import android.util.Log; import android.util.Size; import android.view.Display; import android.view.MotionEvent; @@ -40,31 +42,35 @@ import android.view.TextureView; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.animation.BaseInterpolator; import android.view.animation.DecelerateInterpolator; -import android.view.animation.Interpolator; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RequiresPermission; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; 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.ImageCapture.OnImageCapturedListener; -import androidx.camera.core.ImageCapture.OnImageSavedListener; -import androidx.camera.core.ImageProxy; +import androidx.camera.core.FocusMeteringAction; +import androidx.camera.core.ImageCapture; +import androidx.camera.core.MeteringPoint; import androidx.lifecycle.LifecycleOwner; -import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.R; import java.io.File; import java.io.FileDescriptor; +import java.util.concurrent.Executor; /** * A {@link View} that displays a preview of the camera with methods {@link - * #takePicture(OnImageCapturedListener)}, {@link #takePicture(File, OnImageSavedListener)}, {@link - * #startRecording(File, OnVideoSavedListener)} and {@link #stopRecording()}. + * #takePicture(Executor, OnImageCapturedListener)}, + * {@link #takePicture(File, Executor, OnImageSavedListener)}, + * {@link #startRecording(File, Executor, OnVideoSavedListener)} and {@link #stopRecording()}. * *

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 @@ -88,8 +94,12 @@ public final class CameraXView extends ViewGroup { private static final String EXTRA_CAMERA_DIRECTION = "camera_direction"; private static final String EXTRA_CAPTURE_MODE = "captureMode"; - private final Rect mFocusingRect = new Rect(); - private final Rect mMeteringRect = new Rect(); + private static final int LENS_FACING_NONE = 0; + 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 private long mDownEventTimestamp; // For pinch-to-zoom @@ -116,8 +126,7 @@ public final class CameraXView extends ViewGroup { private ScaleType mScaleType = ScaleType.CENTER_CROP; // For accessibility event private MotionEvent mUpEvent; - private @Nullable - Paint mLayerPaint; + private @Nullable Paint mLayerPaint; public CameraXView(Context context) { this(context, null); @@ -188,11 +197,52 @@ public final class CameraXView extends ViewGroup { onPreviewSourceDimensUpdated(640, 480); } - setScaleType(ScaleType.CENTER_CROP); - setPinchToZoomEnabled(true); - setCaptureMode(CaptureMode.IMAGE); - setCameraLensFacing(LensFacing.FRONT); - setFlash(FlashMode.OFF); + if (attrs != null) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView); + setScaleType( + 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); + break; + default: + // Unhandled event. + } + + a.recycle(); + } if (getBackground() == null) { setBackgroundColor(0xFF111111); @@ -245,7 +295,7 @@ public final class CameraXView extends ViewGroup { setCameraLensFacing( TextUtils.isEmpty(lensFacingString) ? null - : LensFacing.valueOf(lensFacingString)); + : CameraX.LensFacing.valueOf(lensFacingString)); setCaptureMode(CaptureMode.fromId(state.getInt(EXTRA_CAPTURE_MODE))); } else { super.onRestoreInstanceState(savedState); @@ -578,33 +628,42 @@ public final class CameraXView extends ViewGroup { * Takes a picture, and calls {@link OnImageCapturedListener#onCaptureSuccess(ImageProxy, int)} * 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. */ - public void takePicture(OnImageCapturedListener listener) { - mCameraModule.takePicture(listener); + @SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901 + 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. * * @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. */ - public void takePicture(File file, OnImageSavedListener listener) { - mCameraModule.takePicture(file, listener); + @SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901 + 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. * - * @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) - public void startRecording(FileDescriptor fileDescriptor, VideoCapture.OnVideoSavedListener listener) { - mCameraModule.startRecording(fileDescriptor, listener); - } + @SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901 + public void startRecording(@NonNull FileDescriptor file, @NonNull Executor executor, // End Signal Custom Code Block + @NonNull VideoCapture.OnVideoSavedListener listener) { + mCameraModule.startRecording(file, executor, listener); + } /** Stops an in progress video. */ // 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. */ @RequiresPermission(permission.CAMERA) - public boolean hasCameraWithLensFacing(LensFacing lensFacing) { + public boolean hasCameraWithLensFacing(CameraX.LensFacing lensFacing) { return mCameraModule.hasCameraWithLensFacing(lensFacing); } @@ -655,29 +714,21 @@ public final class CameraXView extends ViewGroup { * * @param lensFacing The desired camera lensFacing. */ - public void setCameraLensFacing(@Nullable LensFacing lensFacing) { + public void setCameraLensFacing(@Nullable CameraX.LensFacing lensFacing) { mCameraModule.setCameraLensFacing(lensFacing); } /** Returns the currently selected {@link LensFacing}. */ @Nullable - public LensFacing getCameraLensFacing() { + public CameraX.LensFacing getCameraLensFacing() { return mCameraModule.getLensFacing(); } - /** - * Focuses the camera on the given area. - * - *

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); + // Begin Signal Custom Code Block + public boolean hasFlash() { + return mCameraModule.hasFlash(); } + // End Signal Custom Code Block /** Gets the active flash strategy. */ public FlashMode getFlash() { @@ -685,14 +736,10 @@ public final class CameraXView extends ViewGroup { } /** Sets the active flash strategy. */ - public void setFlash(FlashMode flashMode) { + public void setFlash(@NonNull FlashMode flashMode) { mCameraModule.setFlash(flashMode); } - public boolean hasFlash() { - return mCameraModule.hasFlash(); - } - private int getRelativeCameraOrientation(boolean compensateForMirroring) { return mCameraModule.getRelativeCameraOrientation(compensateForMirroring); } @@ -702,7 +749,7 @@ public final class CameraXView extends ViewGroup { } @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. if (mCameraModule.isPaused()) { return false; @@ -745,10 +792,21 @@ public final class CameraXView extends ViewGroup { final float x = (mUpEvent != null) ? mUpEvent.getX() : getX() + getWidth() / 2f; final float y = (mUpEvent != null) ? mUpEvent.getY() : getY() + getHeight() / 2f; mUpEvent = null; - calculateTapArea(mFocusingRect, x, y, 1f); - calculateTapArea(mMeteringRect, x, y, 1.5f); - if (area(mFocusingRect) > 0 && area(mMeteringRect) > 0) { - focus(mFocusingRect, mMeteringRect); + + TextureViewMeteringPointFactory pointFactory = new TextureViewMeteringPointFactory( + mCameraTextureView); + 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; @@ -759,80 +817,6 @@ public final class CameraXView extends ViewGroup { 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) { return Math.min(Math.max(val, min), max); } @@ -975,7 +959,7 @@ public final class CameraXView extends ViewGroup { * The capture mode used by CameraView. * *

This enum can be used to determine which capture mode will be enabled for {@link - * CameraXView}. + * CameraView}. */ public enum CaptureMode { /** A mode where image capture is enabled. */ @@ -1024,7 +1008,7 @@ public final class CameraXView extends ViewGroup { private class PinchToZoomGestureDetector extends ScaleGestureDetector implements ScaleGestureDetector.OnScaleGestureListener { 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; PinchToZoomGestureDetector(Context context) { diff --git a/src/org/thoughtcrime/securesms/mediasend/camerax/TextureViewMeteringPointFactory.java b/src/org/thoughtcrime/securesms/mediasend/camerax/TextureViewMeteringPointFactory.java new file mode 100644 index 000000000..de3d3c546 --- /dev/null +++ b/src/org/thoughtcrime/securesms/mediasend/camerax/TextureViewMeteringPointFactory.java @@ -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). + * + *

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; + } +} diff --git a/src/org/thoughtcrime/securesms/mediasend/camerax/VideoCapture.java b/src/org/thoughtcrime/securesms/mediasend/camerax/VideoCapture.java index 0e0a5b662..db4f1f26d 100644 --- a/src/org/thoughtcrime/securesms/mediasend/camerax/VideoCapture.java +++ b/src/org/thoughtcrime/securesms/mediasend/camerax/VideoCapture.java @@ -1,5 +1,3 @@ -package org.thoughtcrime.securesms.mediasend.camerax; - /* * Copyright (C) 2019 The Android Open Source Project * @@ -16,18 +14,24 @@ package org.thoughtcrime.securesms.mediasend.camerax; * limitations under the License. */ +package org.thoughtcrime.securesms.mediasend.camerax; + +import android.annotation.SuppressLint; import android.location.Location; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.CamcorderProfile; import android.media.MediaCodec; +import android.media.MediaCodec.BufferInfo; import android.media.MediaCodecInfo; +import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaFormat; import android.media.MediaMuxer; -import android.media.MediaRecorder; +import android.media.MediaRecorder.AudioSource; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; +import android.util.Log; import android.util.Size; import android.view.Display; import android.view.Surface; @@ -37,13 +41,16 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; 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.CameraX; +import androidx.camera.core.CameraX.LensFacing; import androidx.camera.core.CameraXThreads; import androidx.camera.core.ConfigProvider; import androidx.camera.core.DeferrableSurface; import androidx.camera.core.ImageOutputConfig; +import androidx.camera.core.ImageOutputConfig.RotationValue; import androidx.camera.core.ImmediateSurface; import androidx.camera.core.SessionConfig; import androidx.camera.core.UseCase; @@ -51,7 +58,6 @@ import androidx.camera.core.UseCaseConfig; import androidx.camera.core.VideoCaptureConfig; import androidx.camera.core.impl.utils.executor.CameraXExecutors; -import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.video.VideoUtil; import java.io.File; @@ -59,6 +65,8 @@ import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; 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. */ @RequiresApi(26) +@RestrictTo(Scope.LIBRARY_GROUP) public class VideoCapture extends UseCase { /** @@ -77,9 +86,9 @@ public class VideoCapture extends UseCase { * * @hide */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public static final VideoCapture.Defaults DEFAULT_CONFIG = new VideoCapture.Defaults(); - private static final VideoCapture.Metadata EMPTY_METADATA = new VideoCapture.Metadata(); + @RestrictTo(Scope.LIBRARY_GROUP) + public static final Defaults DEFAULT_CONFIG = new Defaults(); + private static final Metadata EMPTY_METADATA = new Metadata(); private static final String TAG = "VideoCapture"; /** Amount of time to wait for dequeuing a buffer from the videoEncoder. */ private static final int DEQUE_TIMEOUT_USEC = 10000; @@ -90,10 +99,10 @@ public class VideoCapture extends UseCase { // End Signal Custom Code Block /** Camcorder profiles quality list */ private static final int[] CamcorderQuality = { - CamcorderProfile.QUALITY_2160P, - CamcorderProfile.QUALITY_1080P, - CamcorderProfile.QUALITY_720P, - CamcorderProfile.QUALITY_480P + CamcorderProfile.QUALITY_2160P, + CamcorderProfile.QUALITY_1080P, + CamcorderProfile.QUALITY_720P, + CamcorderProfile.QUALITY_480P }; /** * Audio encoding @@ -101,28 +110,29 @@ public class VideoCapture extends UseCase { *

the result of PCM_8BIT and PCM_FLOAT are not good. Set PCM_16BIT as the first option. */ private static final short[] sAudioEncoding = { - AudioFormat.ENCODING_PCM_16BIT, - AudioFormat.ENCODING_PCM_8BIT, - AudioFormat.ENCODING_PCM_FLOAT + AudioFormat.ENCODING_PCM_16BIT, + AudioFormat.ENCODING_PCM_8BIT, + AudioFormat.ENCODING_PCM_FLOAT }; - private final MediaCodec.BufferInfo mVideoBufferInfo = new MediaCodec.BufferInfo(); + private final BufferInfo mVideoBufferInfo = new BufferInfo(); private final Object mMuxerLock = new Object(); /** Thread on which all encoding occurs. */ private final HandlerThread mVideoHandlerThread = - new HandlerThread(CameraXThreads.TAG + "video encoding thread"); + new HandlerThread(CameraXThreads.TAG + "video encoding thread"); private final Handler mVideoHandler; /** Thread on which audio encoding occurs. */ private final HandlerThread mAudioHandlerThread = - new HandlerThread(CameraXThreads.TAG + "audio encoding thread"); + new HandlerThread(CameraXThreads.TAG + "audio encoding thread"); private final Handler mAudioHandler; private final AtomicBoolean mEndOfVideoStreamSignal = new AtomicBoolean(true); private final AtomicBoolean mEndOfAudioStreamSignal = 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. */ private final AtomicBoolean mIsFirstVideoSampleWrite = new AtomicBoolean(false); private final AtomicBoolean mIsFirstAudioSampleWrite = new AtomicBoolean(false); private final VideoCaptureConfig.Builder mUseCaseConfigBuilder; + @NonNull MediaCodec mVideoEncoder; @NonNull @@ -138,6 +148,7 @@ public class VideoCapture extends UseCase { /** Surface the camera writes to, which the videoEncoder uses as input. */ Surface mCameraSurface; /** audio raw data */ + @NonNull private AudioRecord mAudioRecorder; private int mAudioBufferSize; private boolean mIsRecording = false; @@ -167,9 +178,9 @@ public class VideoCapture extends UseCase { /** Creates a {@link MediaFormat} using parameters from the configuration */ private static MediaFormat createMediaFormat(VideoCaptureConfig config, Size resolution) { MediaFormat format = - MediaFormat.createVideoFormat( - VIDEO_MIME_TYPE, resolution.getWidth(), resolution.getHeight()); - format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + MediaFormat.createVideoFormat( + VIDEO_MIME_TYPE, resolution.getWidth(), resolution.getHeight()); + format.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_BIT_RATE, config.getBitRate()); format.setInteger(MediaFormat.KEY_FRAME_RATE, config.getVideoFrameRate()); // Begin Signal Custom Code Block @@ -188,10 +199,10 @@ public class VideoCapture extends UseCase { */ @Override @Nullable - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - protected UseCaseConfig.Builder getDefaultBuilder(CameraX.LensFacing lensFacing) { + @RestrictTo(Scope.LIBRARY_GROUP) + protected UseCaseConfig.Builder getDefaultBuilder(LensFacing lensFacing) { VideoCaptureConfig defaults = CameraX.getDefaultUseCaseConfig( - VideoCaptureConfig.class, lensFacing); + VideoCaptureConfig.class, lensFacing); if (defaults != null) { return VideoCaptureConfig.Builder.fromConfig(defaults); } @@ -205,9 +216,9 @@ public class VideoCapture extends UseCase { * @hide */ @Override - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @RestrictTo(Scope.LIBRARY_GROUP) protected Map onSuggestedResolutionUpdated( - Map suggestedResolutionMap) { + Map suggestedResolutionMap) { VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig(); if (mCameraSurface != null) { mVideoEncoder.stop(); @@ -228,7 +239,7 @@ public class VideoCapture extends UseCase { Size resolution = suggestedResolutionMap.get(cameraId); if (resolution == null) { throw new IllegalArgumentException( - "Suggested resolution map missing resolution for camera " + cameraId); + "Suggested resolution map missing resolution for camera " + cameraId); } setupEncoder(resolution); @@ -240,17 +251,20 @@ public class VideoCapture extends UseCase { * called. * *

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 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 - 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 mIsFirstVideoSampleWrite.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. * *

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 listener Listener to call for 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 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 Log.i(TAG, "startRecording"); + OnVideoSavedListener postListener = new VideoSavedListenerWrapper(executor, listener); if (!mEndOfAudioVideoSignal.get()) { - listener.onError( - VideoCapture.VideoCaptureError.RECORDING_IN_PROGRESS, "It is still in video recording!", - null); + postListener.onError( + VideoCaptureError.RECORDING_IN_PROGRESS, "It is still in video recording!", + null); return; } - // Begin Signal Custom Code Block - if (mAudioRecorder != null) { - try { - // audioRecord start - mAudioRecorder.startRecording(); - } catch (IllegalStateException e) { - listener.onError(VideoCapture.VideoCaptureError.ENCODER_ERROR, "AudioRecorder start fail", e); - return; - } - } else { - Log.w(TAG, "Audio recorder was not initialized! Can't record audio."); + try { + // audioRecord start + mAudioRecorder.startRecording(); + } catch (IllegalStateException e) { + postListener.onError(VideoCaptureError.ENCODER_ERROR, "AudioRecorder start fail", e); + return; } - // End Signal Custom Code Block VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig(); String cameraId = getCameraIdUnchecked(config); @@ -303,18 +316,19 @@ public class VideoCapture extends UseCase { } catch (IllegalStateException e) { 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; } // Get the relative rotation or default to 0 if the camera info is unavailable int relativeRotation = 0; try { - CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId); + CameraInfoInternal cameraInfoInternal = CameraX.getCameraInfo(cameraId); relativeRotation = - cameraInfo.getSensorRotationDegrees( - ((ImageOutputConfig) getUseCaseConfig()) - .getTargetRotation(Surface.ROTATION_0)); + cameraInfoInternal.getSensorRotationDegrees( + ((ImageOutputConfig) getUseCaseConfig()) + .getTargetRotation(Surface.ROTATION_0)); } catch (CameraInfoUnavailableException e) { Log.e(TAG, "Unable to retrieve camera sensor orientation.", e); } @@ -322,22 +336,22 @@ public class VideoCapture extends UseCase { try { synchronized (mMuxerLock) { mMuxer = - new MediaMuxer( - // Begin Signal Custom Code Block - saveLocation, - // End Signal Custom Code Block - MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); + new MediaMuxer( + // Begin Signal Custom Code Block + saveLocation, + // End Signal Custom Code Block + MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); mMuxer.setOrientationHint(relativeRotation); if (metadata.location != null) { mMuxer.setLocation( - (float) metadata.location.getLatitude(), - (float) metadata.location.getLongitude()); + (float) metadata.location.getLatitude(), + (float) metadata.location.getLongitude()); } } } catch (IOException e) { setupEncoder(getAttachedSurfaceResolution(cameraId)); - listener.onError(VideoCapture.VideoCaptureError.MUXER_ERROR, "MediaMuxer creation failed!", e); + postListener.onError(VideoCaptureError.MUXER_ERROR, "MediaMuxer creation failed!", e); return; } @@ -348,32 +362,32 @@ public class VideoCapture extends UseCase { notifyActive(); mAudioHandler.post( - new Runnable() { - @Override - public void run() { - VideoCapture.this.audioEncode(listener); - } - }); + new Runnable() { + @Override + public void run() { + VideoCapture.this.audioEncode(postListener); + } + }); mVideoHandler.post( - new Runnable() { - @Override - public void run() { - boolean errorOccurred = VideoCapture.this.videoEncode(listener); - if (!errorOccurred) { - listener.onVideoSaved(saveLocation); - } + new Runnable() { + @Override + public void run() { + boolean errorOccurred = VideoCapture.this.videoEncode(postListener); + if (!errorOccurred) { + postListener.onVideoSaved(saveLocation); } - }); + } + }); } /** * 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. * *

stopRecording() is asynchronous API. User need to check if {@link - * VideoCapture.OnVideoSavedListener#onVideoSaved(File)} or - * {@link VideoCapture.OnVideoSavedListener#onError(VideoCapture.VideoCaptureError, String, Throwable)} be called + * OnVideoSavedListener#onVideoSaved(File)} or + * {@link OnVideoSavedListener#onError(VideoCaptureError, String, Throwable)} be called * before startRecording. */ public void stopRecording() { @@ -390,7 +404,7 @@ public class VideoCapture extends UseCase { * * @hide */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @RestrictTo(Scope.LIBRARY_GROUP) @Override public void clear() { mVideoHandlerThread.quitSafely(); @@ -423,19 +437,19 @@ public class VideoCapture extends UseCase { final MediaCodec videoEncoder = mVideoEncoder; mDeferrableSurface.setOnSurfaceDetachedListener( - CameraXExecutors.mainThreadExecutor(), - new DeferrableSurface.OnSurfaceDetachedListener() { - @Override - public void onSurfaceDetached() { - if (releaseVideoEncoder && videoEncoder != null) { - videoEncoder.release(); - } - - if (surface != null) { - surface.release(); - } + CameraXExecutors.mainThreadExecutor(), + new DeferrableSurface.OnSurfaceDetachedListener() { + @Override + public void onSurfaceDetached() { + if (releaseVideoEncoder && videoEncoder != null) { + videoEncoder.release(); } - }); + + if (surface != null) { + surface.release(); + } + } + }); if (releaseVideoEncoder) { mVideoEncoder = null; @@ -453,7 +467,7 @@ public class VideoCapture extends UseCase { * * @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(); int oldRotation = oldConfig.getTargetRotation(ImageOutputConfig.INVALID_ROTATION); if (oldRotation == ImageOutputConfig.INVALID_ROTATION || oldRotation != rotation) { @@ -468,35 +482,45 @@ public class VideoCapture extends UseCase { * Setup the {@link MediaCodec} for encoding video from a camera {@link Surface} and encoding * audio from selected audio source. */ - private void setupEncoder(Size resolution) { + @SuppressWarnings("WeakerAccess") /* synthetic accessor */ + void setupEncoder(Size resolution) { VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig(); // video encoder setup mVideoEncoder.reset(); mVideoEncoder.configure( - createMediaFormat(config, resolution), /*surface*/ - null, /*crypto*/ - null, - MediaCodec.CONFIGURE_FLAG_ENCODE); + createMediaFormat(config, resolution), /*surface*/ + null, /*crypto*/ + null, + MediaCodec.CONFIGURE_FLAG_ENCODE); if (mCameraSurface != null) { releaseCameraSurface(false); } mCameraSurface = mVideoEncoder.createInputSurface(); - SessionConfig.Builder builder = SessionConfig.Builder.createFrom(config); + SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config); mDeferrableSurface = new ImmediateSurface(mCameraSurface); - builder.addSurface(mDeferrableSurface); + sessionConfigBuilder.addSurface(mDeferrableSurface); 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 setAudioParametersByCamcorderProfile(resolution, cameraId); mAudioEncoder.reset(); mAudioEncoder.configure( - createAudioMediaFormat(), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + createAudioMediaFormat(), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); if (mAudioRecorder != null) { mAudioRecorder.release(); @@ -558,9 +582,9 @@ public class VideoCapture extends UseCase { ByteBuffer buffer = getOutputBuffer(mAudioEncoder, bufferIndex); buffer.position(mAudioBufferInfo.offset); if (mAudioTrackIndex >= 0 - && mVideoTrackIndex >= 0 - && mAudioBufferInfo.size > 0 - && mAudioBufferInfo.presentationTimeUs > 0) { + && mVideoTrackIndex >= 0 + && mAudioBufferInfo.size > 0 + && mAudioBufferInfo.presentationTimeUs > 0) { try { synchronized (mMuxerLock) { if (!mIsFirstAudioSampleWrite.get()) { @@ -571,13 +595,13 @@ public class VideoCapture extends UseCase { } } catch (Exception e) { Log.e( - TAG, - "audio error:size=" - + mAudioBufferInfo.size - + "/offset=" - + mAudioBufferInfo.offset - + "/timeUs=" - + mAudioBufferInfo.presentationTimeUs); + TAG, + "audio error:size=" + + mAudioBufferInfo.size + + "/offset=" + + mAudioBufferInfo.offset + + "/timeUs=" + + mAudioBufferInfo.presentationTimeUs); e.printStackTrace(); } } @@ -591,7 +615,7 @@ public class VideoCapture extends UseCase { * * @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(); // Main encoding loop. Exits on end of stream. boolean errorOccurred = false; @@ -605,14 +629,14 @@ public class VideoCapture extends UseCase { // Deque buffer to check for processing step int outputBufferId = - mVideoEncoder.dequeueOutputBuffer(mVideoBufferInfo, DEQUE_TIMEOUT_USEC); + mVideoEncoder.dequeueOutputBuffer(mVideoBufferInfo, DEQUE_TIMEOUT_USEC); switch (outputBufferId) { case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: if (mMuxerStarted) { videoSavedListener.onError( - VideoCapture.VideoCaptureError.ENCODER_ERROR, - "Unexpected change in video encoding format.", - null); + VideoCaptureError.ENCODER_ERROR, + "Unexpected change in video encoding format.", + null); errorOccurred = true; } @@ -640,8 +664,8 @@ public class VideoCapture extends UseCase { Log.i(TAG, "videoEncoder stop"); mVideoEncoder.stop(); } catch (IllegalStateException e) { - videoSavedListener.onError(VideoCapture.VideoCaptureError.ENCODER_ERROR, - "Video encoder stop failed!", e); + videoSavedListener.onError(VideoCaptureError.ENCODER_ERROR, + "Video encoder stop failed!", e); errorOccurred = true; } @@ -657,7 +681,7 @@ public class VideoCapture extends UseCase { } } } catch (IllegalStateException e) { - videoSavedListener.onError(VideoCapture.VideoCaptureError.MUXER_ERROR, "Muxer stop failed!", e); + videoSavedListener.onError(VideoCaptureError.MUXER_ERROR, "Muxer stop failed!", e); errorOccurred = true; } @@ -676,7 +700,7 @@ public class VideoCapture extends UseCase { return errorOccurred; } - boolean audioEncode(VideoCapture.OnVideoSavedListener videoSavedListener) { + boolean audioEncode(OnVideoSavedListener videoSavedListener) { // Audio encoding loop. Exits on end of stream. boolean audioEos = false; int outIndex; @@ -696,11 +720,11 @@ public class VideoCapture extends UseCase { int length = mAudioRecorder.read(buffer, mAudioBufferSize); if (length > 0) { mAudioEncoder.queueInputBuffer( - index, - 0, - length, - (System.nanoTime() / 1000), - mIsRecording ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM); + index, + 0, + length, + (System.nanoTime() / 1000), + mIsRecording ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM); } } @@ -729,25 +753,17 @@ public class VideoCapture extends UseCase { // Audio Stop try { Log.i(TAG, "audioRecorder stop"); - // Begin Signal Custom Code Block - if (mAudioRecorder != null) { - mAudioRecorder.stop(); - } - // End Signal Custom Code Block + mAudioRecorder.stop(); } catch (IllegalStateException e) { videoSavedListener.onError( - VideoCapture.VideoCaptureError.ENCODER_ERROR, "Audio recorder stop failed!", e); + VideoCaptureError.ENCODER_ERROR, "Audio recorder stop failed!", e); } try { - // Begin Signal Custom Code Block - if (mAudioRecorder != null) { - mAudioEncoder.stop(); - } - // End Signal Custom Code Block + mAudioEncoder.stop(); } catch (IllegalStateException e) { - videoSavedListener.onError(VideoCapture.VideoCaptureError.ENCODER_ERROR, - "Audio encoder stop failed!", e); + videoSavedListener.onError(VideoCaptureError.ENCODER_ERROR, + "Audio encoder stop failed!", e); } Log.i(TAG, "Audio encode thread end"); @@ -769,10 +785,10 @@ public class VideoCapture extends UseCase { /** Creates a {@link MediaFormat} using parameters for audio from the configuration */ private MediaFormat createAudioMediaFormat() { MediaFormat format = - MediaFormat.createAudioFormat(AUDIO_MIME_TYPE, mAudioSampleRate, - mAudioChannelCount); + MediaFormat.createAudioFormat(AUDIO_MIME_TYPE, mAudioSampleRate, + mAudioChannelCount); format.setInteger( - MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); + MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); format.setInteger(MediaFormat.KEY_BIT_RATE, mAudioBitRate); return format; @@ -784,41 +800,41 @@ public class VideoCapture extends UseCase { // Use channel count to determine stereo vs mono int channelConfig = - mAudioChannelCount == 1 - ? AudioFormat.CHANNEL_IN_MONO - : AudioFormat.CHANNEL_IN_STEREO; + mAudioChannelCount == 1 + ? AudioFormat.CHANNEL_IN_MONO + : AudioFormat.CHANNEL_IN_STEREO; int source = config.getAudioRecordSource(); try { int bufferSize = - AudioRecord.getMinBufferSize(mAudioSampleRate, channelConfig, audioFormat); + AudioRecord.getMinBufferSize(mAudioSampleRate, channelConfig, audioFormat); if (bufferSize <= 0) { bufferSize = config.getAudioMinBufferSize(); } AudioRecord recorder = - new AudioRecord( - source, - mAudioSampleRate, - channelConfig, - audioFormat, - bufferSize * 2); + new AudioRecord( + source, + mAudioSampleRate, + channelConfig, + audioFormat, + bufferSize * 2); if (recorder.getState() == AudioRecord.STATE_INITIALIZED) { mAudioBufferSize = bufferSize; Log.i( - TAG, - "source: " - + source - + " audioSampleRate: " - + mAudioSampleRate - + " channelConfig: " - + channelConfig - + " audioFormat: " - + audioFormat - + " bufferSize: " - + bufferSize); + TAG, + "source: " + + source + + " audioSampleRate: " + + mAudioSampleRate + + " channelConfig: " + + channelConfig + + " audioFormat: " + + audioFormat + + " bufferSize: " + + bufferSize); return recorder; } } catch (Exception e) { @@ -838,7 +854,7 @@ public class VideoCapture extends UseCase { if (CamcorderProfile.hasProfile(Integer.parseInt(cameraId), quality)) { profile = CamcorderProfile.get(Integer.parseInt(cameraId), quality); if (currentResolution.getWidth() == profile.videoFrameWidth - && currentResolution.getHeight() == profile.videoFrameHeight) { + && currentResolution.getHeight() == profile.videoFrameHeight) { mAudioChannelCount = profile.audioChannels; mAudioSampleRate = profile.audioSampleRate; mAudioBitRate = profile.audioBitRate; @@ -862,7 +878,7 @@ public class VideoCapture extends UseCase { * Describes the error that occurred during video capture operations. * *

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)}. * *

See message parameter in onError callback or log for more details. */ @@ -890,11 +906,11 @@ public class VideoCapture extends UseCase { public interface OnVideoSavedListener { /** Called when the video has been successfully saved. */ // Begin Signal Custom Code Block - void onVideoSaved(@NonNull FileDescriptor fileDescriptor); + void onVideoSaved(@NonNull FileDescriptor file); // End Signal Custom Code Block /** 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); } @@ -906,9 +922,9 @@ public class VideoCapture extends UseCase { * * @hide */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @RestrictTo(Scope.LIBRARY_GROUP) public static final class Defaults - implements ConfigProvider { + implements ConfigProvider { private static final Handler DEFAULT_HANDLER = new Handler(Looper.getMainLooper()); private static final int DEFAULT_VIDEO_FRAME_RATE = 30; /** 8Mb/s the recommend rate for 30fps 1080p */ @@ -922,7 +938,7 @@ public class VideoCapture extends UseCase { /** audio channel count */ private static final int DEFAULT_AUDIO_CHANNEL_COUNT = 1; /** 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 */ private static final int DEFAULT_AUDIO_MIN_BUFFER_SIZE = 1024; /** Current max resolution of VideoCapture is set as FHD */ @@ -934,24 +950,23 @@ public class VideoCapture extends UseCase { static { VideoCaptureConfig.Builder builder = - new VideoCaptureConfig.Builder() - .setCallbackHandler(DEFAULT_HANDLER) - .setVideoFrameRate(DEFAULT_VIDEO_FRAME_RATE) - .setBitRate(DEFAULT_BIT_RATE) - .setIFrameInterval(DEFAULT_INTRA_FRAME_INTERVAL) - .setAudioBitRate(DEFAULT_AUDIO_BIT_RATE) - .setAudioSampleRate(DEFAULT_AUDIO_SAMPLE_RATE) - .setAudioChannelCount(DEFAULT_AUDIO_CHANNEL_COUNT) - .setAudioRecordSource(DEFAULT_AUDIO_RECORD_SOURCE) - .setAudioMinBufferSize(DEFAULT_AUDIO_MIN_BUFFER_SIZE) - .setMaxResolution(DEFAULT_MAX_RESOLUTION) - .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY); + new VideoCaptureConfig.Builder() + .setVideoFrameRate(DEFAULT_VIDEO_FRAME_RATE) + .setBitRate(DEFAULT_BIT_RATE) + .setIFrameInterval(DEFAULT_INTRA_FRAME_INTERVAL) + .setAudioBitRate(DEFAULT_AUDIO_BIT_RATE) + .setAudioSampleRate(DEFAULT_AUDIO_SAMPLE_RATE) + .setAudioChannelCount(DEFAULT_AUDIO_CHANNEL_COUNT) + .setAudioRecordSource(DEFAULT_AUDIO_RECORD_SOURCE) + .setAudioMinBufferSize(DEFAULT_AUDIO_MIN_BUFFER_SIZE) + .setMaxResolution(DEFAULT_MAX_RESOLUTION) + .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY); DEFAULT_CONFIG = builder.build(); } @Override - public VideoCaptureConfig getConfig(CameraX.LensFacing lensFacing) { + public VideoCaptureConfig getConfig(LensFacing lensFacing) { return DEFAULT_CONFIG; } } @@ -962,4 +977,39 @@ public class VideoCapture extends UseCase { @Nullable 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."); + } + } + + } } diff --git a/witness-verifications.gradle b/witness-verifications.gradle index 67ae2a5f4..400e0fd00 100644 --- a/witness-verifications.gradle +++ b/witness-verifications.gradle @@ -24,11 +24,11 @@ dependencyVerification { ['androidx.asynclayoutinflater:asynclayoutinflater:1.0.0', 'f7eab60c57addd94bb06275832fe7600611beaaae1a1ec597c231956faf96c8b'], - ['androidx.camera:camera-camera2:1.0.0-alpha04', - 'b7897230aec96365d675712c92f5edcb8b464badfd61788c8f956ec2d6e49bfe'], + ['androidx.camera:camera-camera2:1.0.0-alpha06', + 'e50f20deb950ffebcd4d1de5408ef7a5404bec80ec77119e05663c890739b903'], - ['androidx.camera:camera-core:1.0.0-alpha04', - 'e1c70de55600a0caf826eb4f8a75c96c5ff8f0b626bf08413d31e80ffa55f8ba'], + ['androidx.camera:camera-core:1.0.0-alpha06', + '0096cabe539d9b4288f406acfb44264b137ebd600e38e33504ff425c979016c9'], ['androidx.cardview:cardview:1.0.0', '1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7'],