This controls how the view finder should be scaled and positioned within the view. * - * @param scaleType The desired {@link ScaleType}. + * @param scaleType The desired {@link PreviewView.ScaleType}. */ - public void setScaleType(@NonNull ScaleType scaleType) { - if (scaleType != mScaleType) { - mScaleType = scaleType; - requestLayout(); - } + public void setScaleType(@NonNull PreviewView.ScaleType scaleType) { + mPreviewView.setScaleType(scaleType); } /** @@ -401,8 +413,10 @@ public final class CameraXView extends FrameLayout { } /** - * Sets the maximum video duration before {@link VideoCapture.OnVideoSavedCallback#onVideoSaved(FileDescriptor)} is - * called automatically. Use {@link #INDEFINITE_VIDEO_DURATION} to disable the timeout. + * Sets the maximum video duration before + * {@link OnVideoSavedCallback#onVideoSaved(VideoCapture.OutputFileResults)} is called + * automatically. + * Use {@link #INDEFINITE_VIDEO_DURATION} to disable the timeout. */ private void setMaxVideoDuration(long duration) { mCameraModule.setMaxVideoDuration(duration); @@ -417,7 +431,8 @@ public final class CameraXView extends FrameLayout { } /** - * Sets the maximum video size in bytes before {@link VideoCapture.OnVideoSavedCallback#onVideoSaved(FileDescriptor)} + * Sets the maximum video size in bytes before + * {@link OnVideoSavedCallback#onVideoSaved(VideoCapture.OutputFileResults)} * is called automatically. Use {@link #INDEFINITE_VIDEO_SIZE} to disable the size restriction. */ private void setMaxVideoSize(long size) { @@ -435,28 +450,38 @@ public final class CameraXView extends FrameLayout { mCameraModule.takePicture(executor, callback); } + /** + * Takes a picture and calls + * {@link OnImageSavedCallback#onImageSaved(ImageCapture.OutputFileResults)} when done. + * + *
The value of {@link ImageCapture.Metadata#isReversedHorizontal()} in the
+ * {@link ImageCapture.OutputFileOptions} will be overwritten based on camera direction. For
+ * front camera, it will be set to true; for back camera, it will be set to false.
+ *
+ * @param outputFileOptions Options to store the newly captured image.
+ * @param executor The executor in which the callback methods will be run.
+ * @param callback Callback which will receive success or failure.
+ */
+ public void takePicture(@NonNull ImageCapture.OutputFileOptions outputFileOptions,
+ @NonNull Executor executor,
+ @NonNull OnImageSavedCallback callback) {
+ mCameraModule.takePicture(outputFileOptions, executor, callback);
+ }
+
/**
* Takes a video and calls the OnVideoSavedCallback when done.
*
- * @param file The destination.
- * @param executor The executor in which the callback methods will be run.
- * @param callback Callback which will receive success or failure.
+ * @param outputFileOptions Options to store the newly captured video.
+ * @param executor The executor in which the callback methods will be run.
+ * @param callback Callback which will receive success or failure.
*/
- // Begin Signal Custom Code Block
- @RequiresApi(26)
- // End Signal Custom Code Block
- public void startRecording(// Begin Signal Custom Code Block
- @NonNull FileDescriptor file,
- // End Signal Custom Code Block
+ public void startRecording(@NonNull VideoCapture.OutputFileOptions outputFileOptions,
@NonNull Executor executor,
- @NonNull VideoCapture.OnVideoSavedCallback callback) {
- mCameraModule.startRecording(file, executor, callback);
+ @NonNull OnVideoSavedCallback callback) {
+ mCameraModule.startRecording(outputFileOptions, executor, callback);
}
/** Stops an in progress video. */
- // Begin Signal Custom Code Block
- @RequiresApi(26)
- // End Signal Custom Code Block
public void stopRecording() {
mCameraModule.stopRecording();
}
@@ -554,7 +579,8 @@ public final class CameraXView extends FrameLayout {
mDownEventTimestamp = System.currentTimeMillis();
break;
case MotionEvent.ACTION_UP:
- if (delta() < ViewConfiguration.getLongPressTimeout()) {
+ if (delta() < ViewConfiguration.getLongPressTimeout()
+ && mCameraModule.isBoundToLifecycle()) {
mUpEvent = event;
performClick();
}
@@ -578,19 +604,14 @@ public final class CameraXView extends FrameLayout {
final float y = (mUpEvent != null) ? mUpEvent.getY() : getY() + getHeight() / 2f;
mUpEvent = null;
- CameraSelector cameraSelector =
- new CameraSelector.Builder().requireLensFacing(
- mCameraModule.getLensFacing()).build();
-
- DisplayOrientedMeteringPointFactory pointFactory = new DisplayOrientedMeteringPointFactory(
- getDisplay(), cameraSelector, mPreviewView.getWidth(), mPreviewView.getHeight());
- float afPointWidth = 1.0f / 6.0f; // 1/6 total area
- float aePointWidth = afPointWidth * 1.5f;
- MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth);
- MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth);
-
Camera camera = mCameraModule.getCamera();
if (camera != null) {
+ MeteringPointFactory pointFactory = mPreviewView.getMeteringPointFactory();
+ float afPointWidth = 1.0f / 6.0f; // 1/6 total area
+ float aePointWidth = afPointWidth * 1.5f;
+ MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth);
+ MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth);
+
ListenableFuture This enum can be used to determine which capture mode will be enabled for {@link
- * CameraXView}.
+ * SignalCameraView}.
*/
public enum CaptureMode {
/** A mode where image capture is enabled. */
@@ -832,4 +819,4 @@ public final class CameraXView extends FrameLayout {
public void onScaleEnd(ScaleGestureDetector detector) {
}
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java b/app/src/main/java/androidx/camera/view/SignalCameraXModule.java
similarity index 80%
rename from app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java
rename to app/src/main/java/androidx/camera/view/SignalCameraXModule.java
index c762b98ccf..daac8bcdad 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java
+++ b/app/src/main/java/androidx/camera/view/SignalCameraXModule.java
@@ -14,7 +14,9 @@
* limitations under the License.
*/
-package org.thoughtcrime.securesms.mediasend.camerax;
+package androidx.camera.view;
+
+import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
import android.Manifest.permission;
import android.annotation.SuppressLint;
@@ -27,17 +29,21 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission;
+import androidx.camera.core.AspectRatio;
import androidx.camera.core.Camera;
+import androidx.camera.core.CameraInfoUnavailableException;
import androidx.camera.core.CameraSelector;
-import androidx.camera.core.CameraX;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCapture.OnImageCapturedCallback;
+import androidx.camera.core.ImageCapture.OnImageSavedCallback;
+import androidx.camera.core.Logger;
import androidx.camera.core.Preview;
import androidx.camera.core.TorchState;
import androidx.camera.core.UseCase;
+import androidx.camera.core.VideoCapture;
+import androidx.camera.core.VideoCapture.OnVideoSavedCallback;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.LensFacingConverter;
-import androidx.camera.core.impl.VideoCaptureConfig;
import androidx.camera.core.impl.utils.CameraOrientationUtil;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback;
@@ -51,11 +57,10 @@ import androidx.lifecycle.OnLifecycleEvent;
import com.google.common.util.concurrent.ListenableFuture;
-import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.video.VideoUtil;
-import java.io.FileDescriptor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
@@ -65,13 +70,10 @@ import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
-import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
-
/** CameraX use case operation built on @{link androidx.camera.core}. */
-// Begin Signal Custom Code Block
@RequiresApi(21)
-// End Signal Custom Code Block
-final class CameraXModule {
+@SuppressLint("RestrictedApi")
+final class SignalCameraXModule {
public static final String TAG = "CameraXModule";
private static final float UNITY_ZOOM_SCALE = 1f;
@@ -82,13 +84,13 @@ final class CameraXModule {
private static final Rational ASPECT_RATIO_3_4 = new Rational(3, 4);
private final Preview.Builder mPreviewBuilder;
- private final VideoCaptureConfig.Builder mVideoCaptureConfigBuilder;
+ private final VideoCapture.Builder mVideoCaptureBuilder;
private final ImageCapture.Builder mImageCaptureBuilder;
- private final CameraXView mCameraXView;
+ private final SignalCameraView mCameraView;
final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false);
- private CameraXView.CaptureMode mCaptureMode = CameraXView.CaptureMode.IMAGE;
- private long mMaxVideoDuration = CameraXView.INDEFINITE_VIDEO_DURATION;
- private long mMaxVideoSize = CameraXView.INDEFINITE_VIDEO_SIZE;
+ private SignalCameraView.CaptureMode mCaptureMode = SignalCameraView.CaptureMode.IMAGE;
+ private long mMaxVideoDuration = SignalCameraView.INDEFINITE_VIDEO_DURATION;
+ private long mMaxVideoSize = SignalCameraView.INDEFINITE_VIDEO_SIZE;
@ImageCapture.FlashMode
private int mFlash = FLASH_MODE_OFF;
@Nullable
@@ -110,7 +112,6 @@ final class CameraXModule {
public void onDestroy(LifecycleOwner owner) {
if (owner == mCurrentLifecycle) {
clearCurrentLifecycle();
- mPreview.setSurfaceProvider(null);
}
}
};
@@ -123,8 +124,8 @@ final class CameraXModule {
@Nullable
ProcessCameraProvider mCameraProvider;
- CameraXModule(CameraXView view) {
- mCameraXView = view;
+ SignalCameraXModule(SignalCameraView view) {
+ mCameraView = view;
Futures.addCallback(ProcessCameraProvider.getInstance(view.getContext()),
new FutureCallback This class manages the Surface lifecycle, as well as the preview aspect ratio and
- * orientation. Internally, it uses either a {@link android.view.TextureView} or
- * {@link android.view.SurfaceView} to display the camera feed.
- */
-// Begin Signal Custom Code Block
-@RequiresApi(21)
-// End Signal Custom Code Block
-public class PreviewView extends FrameLayout {
-
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
- Implementation mImplementation;
-
- private ImplementationMode mImplementationMode;
-
- private final DisplayManager.DisplayListener mDisplayListener =
- new DisplayManager.DisplayListener() {
- @Override
- public void onDisplayAdded(int displayId) {
- }
-
- @Override
- public void onDisplayRemoved(int displayId) {
- }
- @Override
- public void onDisplayChanged(int displayId) {
- mImplementation.onDisplayChanged();
- }
- };
-
- public PreviewView(@NonNull Context context) {
- this(context, null);
- }
-
- public PreviewView(@NonNull Context context, @Nullable AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public PreviewView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public PreviewView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
-
- final TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs,
- R.styleable.PreviewView, defStyleAttr, defStyleRes);
-
- try {
- final int implementationModeId = attributes.getInteger(
- R.styleable.PreviewView_implementationMode,
- ImplementationMode.TEXTURE_VIEW.getId());
- mImplementationMode = ImplementationMode.fromId(implementationModeId);
- } finally {
- attributes.recycle();
- }
- setUp();
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- final DisplayManager displayManager =
- (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
- if (displayManager != null) {
- displayManager.registerDisplayListener(mDisplayListener, getHandler());
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- final DisplayManager displayManager =
- (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
- if (displayManager != null) {
- displayManager.unregisterDisplayListener(mDisplayListener);
- }
- }
-
- private void setUp() {
- removeAllViews();
- switch (mImplementationMode) {
- case SURFACE_VIEW:
- mImplementation = new SurfaceViewImplementation();
- break;
- case TEXTURE_VIEW:
- mImplementation = new TextureViewImplementation();
- break;
- default:
- throw new IllegalStateException(
- "Unsupported implementation mode " + mImplementationMode);
- }
- mImplementation.init(this);
- }
-
- /**
- * Specifies the {@link ImplementationMode} to use for the preview.
- *
- * @param implementationMode Implementation might need to adjust transform by latest display properties such as
- * display orientation in order to show the preview correctly.
- */
- void onDisplayChanged();
- }
-
- /**
- * The implementation mode of a {@link PreviewView}
- *
- * Specifies how the Preview surface will be implemented internally: Using a
- * {@link android.view.SurfaceView} or a {@link android.view.TextureView} (which is the default)
- *
- * Using the current app's window to determine whether the device is a natural
- * portrait-oriented device doesn't work in all scenarios, one example of this is multi-window
- * mode.
- * Taking a natural portrait-oriented device in multi-window mode, rotating it 90 degrees (so
- * that it's in landscape), with the app open, and its window's width being smaller than its
- * height. Using the app's width and height would determine that the device isn't
- * naturally portrait-oriented, where in fact it is, which is why it is important to use the
- * size of the device instead.
- * Valid values for the relative rotation are {@link Surface#ROTATION_0}, {@link
- * * Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
- */
- static int rotationDegreesFromSurfaceRotation(int rotationConstant) {
- switch (rotationConstant) {
- case Surface.ROTATION_0:
- return 0;
- case Surface.ROTATION_90:
- return 90;
- case Surface.ROTATION_180:
- return 180;
- case Surface.ROTATION_270:
- return 270;
- default:
- throw new UnsupportedOperationException(
- "Unsupported surface rotation constant: " + rotationConstant);
- }
- }
-
- /** Prevents construction */
- private SurfaceRotation() {}
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/SurfaceViewImplementation.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/SurfaceViewImplementation.java
deleted file mode 100644
index 25255ef67e..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/SurfaceViewImplementation.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * 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.util.Size;
-import android.view.Surface;
-import android.view.SurfaceHolder;
-import android.widget.FrameLayout;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.UiThread;
-import androidx.camera.core.Preview;
-import androidx.camera.core.SurfaceRequest;
-import androidx.core.content.ContextCompat;
-
-import org.thoughtcrime.securesms.logging.Log;
-
-/**
- * The SurfaceView implementation for {@link PreviewView}.
- */
-@RequiresApi(21)
-final class SurfaceViewImplementation implements PreviewView.Implementation {
-
- private static final String TAG = "SurfaceViewPreviewView";
-
- // Synthetic Accessor
- @SuppressWarnings("WeakerAccess")
- TransformableSurfaceView mSurfaceView;
-
- // Synthetic Accessor
- @SuppressWarnings("WeakerAccess")
- final SurfaceRequestCallback mSurfaceRequestCallback =
- new SurfaceRequestCallback();
-
- private Preview.SurfaceProvider mSurfaceProvider =
- new Preview.SurfaceProvider() {
- @Override
- public void onSurfaceRequested(@NonNull SurfaceRequest surfaceRequest) {
- mSurfaceView.post(
- () -> mSurfaceRequestCallback.setSurfaceRequest(surfaceRequest));
- }
- };
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void init(@NonNull FrameLayout parent) {
- mSurfaceView = new TransformableSurfaceView(parent.getContext());
- mSurfaceView.setLayoutParams(
- new FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.MATCH_PARENT,
- FrameLayout.LayoutParams.MATCH_PARENT));
- parent.addView(mSurfaceView);
- mSurfaceView.getHolder().addCallback(mSurfaceRequestCallback);
- }
-
- /**
- * {@inheritDoc}
- */
- @NonNull
- @Override
- public Preview.SurfaceProvider getSurfaceProvider() {
- return mSurfaceProvider;
- }
-
- @Override
- public void onDisplayChanged() {
-
- }
-
- /**
- * The {@link SurfaceHolder.Callback} on mSurfaceView.
- *
- * SurfaceView creates Surface on its own before we can do anything. This class makes
- * sure only the Surface with correct size will be returned to Preview.
- */
- class SurfaceRequestCallback implements SurfaceHolder.Callback {
-
- // Target Surface size. Only complete the SurfaceRequest when the size of the Surface
- // matches this value.
- // Guarded by UI thread.
- @Nullable
- private Size mTargetSize;
-
- // SurfaceRequest to set when the target size is met.
- // Guarded by UI thread.
- @Nullable
- private SurfaceRequest mSurfaceRequest;
-
- // The cached size of the current Surface.
- // Guarded by UI thread.
- @Nullable
- private Size mCurrentSurfaceSize;
-
- /**
- * Sets the completer and the size. The completer will only be set if the current size of
- * the Surface matches the target size.
- */
- @UiThread
- void setSurfaceRequest(@NonNull SurfaceRequest surfaceRequest) {
- cancelPreviousRequest();
- mSurfaceRequest = surfaceRequest;
- Size targetSize = surfaceRequest.getResolution();
- mTargetSize = targetSize;
- if (!tryToComplete()) {
- // The current size is incorrect. Wait for it to change.
- Log.d(TAG, "Wait for new Surface creation.");
- mSurfaceView.getHolder().setFixedSize(targetSize.getWidth(),
- targetSize.getHeight());
- }
- }
-
- /**
- * Sets the completer if size matches.
- *
- * @return true if the completer is set.
- */
- @UiThread
- private boolean tryToComplete() {
- Surface surface = mSurfaceView.getHolder().getSurface();
- if (mSurfaceRequest != null && mTargetSize != null && mTargetSize.equals(
- mCurrentSurfaceSize)) {
- Log.d(TAG, "Surface set on Preview.");
- mSurfaceRequest.provideSurface(surface,
- ContextCompat.getMainExecutor(mSurfaceView.getContext()),
- (result) -> Log.d(TAG, "Safe to release surface."));
- mSurfaceRequest = null;
- mTargetSize = null;
- return true;
- }
- return false;
- }
-
- @UiThread
- private void cancelPreviousRequest() {
- if (mSurfaceRequest != null) {
- Log.d(TAG, "Request canceled: " + mSurfaceRequest);
- mSurfaceRequest.willNotProvideSurface();
- mSurfaceRequest = null;
- }
- mTargetSize = null;
- }
-
- @Override
- public void surfaceCreated(SurfaceHolder surfaceHolder) {
- Log.d(TAG, "Surface created.");
- // No-op. Handling surfaceChanged() is enough because it's always called afterwards.
- }
-
- @Override
- public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
- Log.d(TAG, "Surface changed. Size: " + width + "x" + height);
- mCurrentSurfaceSize = new Size(width, height);
- tryToComplete();
- }
-
- @Override
- public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
- Log.d(TAG, "Surface destroyed.");
- mCurrentSurfaceSize = null;
- cancelPreviousRequest();
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/TextureViewImplementation.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/TextureViewImplementation.java
deleted file mode 100644
index ccc76669c8..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/TextureViewImplementation.java
+++ /dev/null
@@ -1,239 +0,0 @@
-/*
- * 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 static androidx.camera.core.SurfaceRequest.Result;
-
-import android.annotation.SuppressLint;
-import android.graphics.Point;
-import android.graphics.SurfaceTexture;
-import android.util.Pair;
-import android.util.Size;
-import android.view.Surface;
-import android.view.TextureView;
-import android.view.View;
-import android.widget.FrameLayout;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.Preview;
-import androidx.camera.core.SurfaceRequest;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
-import androidx.camera.core.impl.utils.futures.FutureCallback;
-import androidx.camera.core.impl.utils.futures.Futures;
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-import androidx.core.content.ContextCompat;
-import androidx.core.util.Preconditions;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import org.thoughtcrime.securesms.logging.Log;
-
-/**
- * The {@link TextureView} implementation for {@link PreviewView}
- */
-// Begin Signal Custom Code Block
-@RequiresApi(21)
-@SuppressLint("RestrictedApi")
-// End Signal Custom Code Block
-public class TextureViewImplementation implements PreviewView.Implementation {
-
- private static final String TAG = "TextureViewImpl";
-
- private FrameLayout mParent;
- TextureView mTextureView;
- SurfaceTexture mSurfaceTexture;
- private Size mResolution;
- ListenableFuture
- * The camera produces a preview that depends on its sensor orientation and that has a
- * specific resolution. In order to display it correctly, this preview must be rotated to
- * match the UI orientation, and must be scaled up/down to fit inside the view that's
- * displaying it. This method takes care of doing so while keeping the preview centered.
- * 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.
- *
- * @hide
- */
- @NonNull
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @Override
- protected PointF convertPoint(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/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/TransformableSurfaceView.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/TransformableSurfaceView.java
deleted file mode 100644
index 7e06677a3d..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/TransformableSurfaceView.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * 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.content.Context;
-import android.graphics.Matrix;
-import android.graphics.RectF;
-import android.util.AttributeSet;
-import android.view.SurfaceView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-
-/**
- * A subclass of {@link SurfaceView} that supports translation and scaling transformations.
- */
-// Begin Signal Custom Code Block
-@RequiresApi(21)
-// End Signal Custom Code Block
-final class TransformableSurfaceView extends SurfaceView {
-
- private RectF mOverriddenLayoutRect;
-
- TransformableSurfaceView(@NonNull Context context) {
- super(context);
- }
-
- TransformableSurfaceView(@NonNull Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- }
-
- TransformableSurfaceView(@NonNull Context context, @Nullable AttributeSet attrs,
- int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- TransformableSurfaceView(@NonNull Context context, @Nullable AttributeSet attrs,
- int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- if (mOverriddenLayoutRect == null) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- } else {
- setMeasuredDimension((int) mOverriddenLayoutRect.width(),
- (int) mOverriddenLayoutRect.height());
- }
- }
-
- /**
- * Sets the transform to associate with this surface view. Only translation and scaling are
- * supported. If a rotated transformation is passed in, an exception is thrown.
- *
- * @param transform The transform to apply to the content of this view.
- */
- void setTransform(final Matrix transform) {
- if (hasRotation(transform)) {
- throw new IllegalArgumentException("TransformableSurfaceView does not support "
- + "rotation transformations.");
- }
-
- final RectF rect = new RectF(getLeft(), getTop(), getRight(), getBottom());
- transform.mapRect(rect);
- overrideLayout(rect);
- }
-
- private boolean hasRotation(final Matrix matrix) {
- final float[] values = new float[9];
- matrix.getValues(values);
-
- /*
- A translation matrix can be represented as:
- (1 0 transX)
- (0 1 transX)
- (0 0 1)
-
- A rotation Matrix of ψ degrees can be represented as:
- (cosψ -sinψ 0)
- (sinψ cosψ 0)
- (0 0 1)
-
- A scale matrix can be represented as:
- (scaleX 0 0)
- (0 scaleY 0)
- (0 0 0)
-
- Meaning a transformed matrix can be represented as:
- (scaleX * cosψ -scaleX * sinψ transX)
- (scaleY * sinψ scaleY * cosψ transY)
- (0 0 1)
-
- Using the following 2 equalities:
- scaleX * cosψ = matrix[0][0]
- -scaleX * sinψ = matrix[0][1]
-
- The following is deduced:
- -tanψ = matrix[0][1] / matrix[0][0]
-
- Or:
- ψ = -arctan(matrix[0][1] / matrix[0][0])
- */
- final double angle = -Math.atan2(values[Matrix.MSKEW_X], values[Matrix.MSCALE_X]);
-
- return Math.round(angle * (180 / Math.PI)) != 0;
- }
-
- private void overrideLayout(final RectF overriddenLayoutRect) {
- mOverriddenLayoutRect = overriddenLayoutRect;
- setX(overriddenLayoutRect.left);
- setY(overriddenLayoutRect.top);
- requestLayout();
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/VideoCapture.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/VideoCapture.java
deleted file mode 100644
index a9afd717d2..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/VideoCapture.java
+++ /dev/null
@@ -1,1048 +0,0 @@
-/*
- * Copyright (C) 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;
-
-/*
- * Copyright (C) 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.
- */
-
-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.AudioSource;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.util.Size;
-import android.view.Display;
-import android.view.Surface;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.RestrictTo.Scope;
-import androidx.camera.core.CameraInfo;
-import androidx.camera.core.CameraX;
-import androidx.camera.core.CameraXThreads;
-import androidx.camera.core.UseCase;
-import androidx.camera.core.impl.CameraInfoInternal;
-import androidx.camera.core.impl.CameraInternal;
-import androidx.camera.core.impl.ConfigProvider;
-import androidx.camera.core.impl.DeferrableSurface;
-import androidx.camera.core.impl.ImageOutputConfig;
-import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
-import androidx.camera.core.impl.ImmediateSurface;
-import androidx.camera.core.impl.SessionConfig;
-import androidx.camera.core.impl.UseCaseConfig;
-import androidx.camera.core.impl.VideoCaptureConfig;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
-import androidx.camera.core.internal.utils.UseCaseConfigUtil;
-
-import org.thoughtcrime.securesms.logging.Log;
-import org.thoughtcrime.securesms.video.VideoUtil;
-
-import java.io.File;
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.nio.ByteBuffer;
-import java.util.Map;
-import java.util.concurrent.Executor;
-import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * A use case for taking a video.
- *
- * This class is designed for simple video capturing. It gives basic configuration of the
- * recorded video such as resolution and file format.
- *
- * @hide In the earlier stage, the VideoCapture is deprioritized.
- */
-// Begin Signal Custom Code Block
-@RequiresApi(26)
-// End Signal Custom Code Block
-@RestrictTo(Scope.LIBRARY_GROUP)
-@SuppressWarnings("ClassCanBeStatic") // TODO(b/141958189): Suppressed during upgrade to AGP 3.6.
-public class VideoCapture extends UseCase {
-
- /**
- * An unknown error occurred.
- *
- * See message parameter in onError callback or log for more details.
- */
- public static final int ERROR_UNKNOWN = 0;
- /**
- * An error occurred with encoder state, either when trying to change state or when an
- * unexpected state change occurred.
- */
- public static final int ERROR_ENCODER = 1;
- /** An error with muxer state such as during creation or when stopping. */
- public static final int ERROR_MUXER = 2;
- /**
- * An error indicating start recording was called when video recording is still in progress.
- */
- public static final int ERROR_RECORDING_IN_PROGRESS = 3;
-
- /**
- * Provides a static configuration with implementation-agnostic options.
- *
- * @hide
- */
- @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;
- /** Android preferred mime type for AVC video. */
- // Begin Signal Custom Code Block
- private static final String VIDEO_MIME_TYPE = VideoUtil.VIDEO_MIME_TYPE;
- private static final String AUDIO_MIME_TYPE = VideoUtil.AUDIO_MIME_TYPE;
- // 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
- };
- /**
- * Audio encoding
- *
- * 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
- };
- 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");
- private final Handler mVideoHandler;
- /** Thread on which audio encoding occurs. */
- private final HandlerThread mAudioHandlerThread =
- 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 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);
-
- @NonNull
- MediaCodec mVideoEncoder;
- @NonNull
- private MediaCodec mAudioEncoder;
- /** The muxer that writes the encoding data to file. */
- @GuardedBy("mMuxerLock")
- private MediaMuxer mMuxer;
- private boolean mMuxerStarted = false;
- /** The index of the video track used by the muxer. */
- private int mVideoTrackIndex;
- /** The index of the audio track used by the muxer. */
- private int mAudioTrackIndex;
- /** 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;
- private int mAudioChannelCount;
- private int mAudioSampleRate;
- private int mAudioBitRate;
- private DeferrableSurface mDeferrableSurface;
-
- /**
- * Creates a new video capture use case from the given configuration.
- *
- * @param config for this use case instance
- */
- public VideoCapture(VideoCaptureConfig config) {
- super(config);
-
- // video thread start
- mVideoHandlerThread.start();
- mVideoHandler = new Handler(mVideoHandlerThread.getLooper());
-
- // audio thread start
- mAudioHandlerThread.start();
- mAudioHandler = new Handler(mAudioHandlerThread.getLooper());
- }
-
- /** 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, CodecCapabilities.COLOR_FormatSurface);
- format.setInteger(MediaFormat.KEY_BIT_RATE, config.getBitRate());
- format.setInteger(MediaFormat.KEY_FRAME_RATE, config.getVideoFrameRate());
- format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, config.getIFrameInterval());
-
- return format;
- }
-
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @Override
- @Nullable
- @RestrictTo(Scope.LIBRARY_GROUP)
- protected UseCaseConfig.Builder, ?, ?> getDefaultBuilder(@Nullable CameraInfo cameraInfo) {
- VideoCaptureConfig defaults = CameraX.getDefaultUseCaseConfig(VideoCaptureConfig.class,
- cameraInfo);
- if (defaults != null) {
- return VideoCaptureConfig.Builder.fromConfig(defaults);
- }
-
- return null;
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @Override
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- protected Map StartRecording() is asynchronous. User needs to check if any error occurs by setting the
- * {@link OnVideoSavedCallback#onError(int, String, Throwable)}.
- *
- * @param saveLocation Location to save the video capture
- * @param executor The executor in which the callback methods will be run.
- * @param callback Callback for when the recorded video saving completion or failure.
- */
- // Begin Signal Custom Code Block
- public void startRecording(@NonNull FileDescriptor saveLocation,
- // End Signal Custom Code Block
- @NonNull Executor executor, @NonNull OnVideoSavedCallback callback) {
- mIsFirstVideoSampleWrite.set(false);
- mIsFirstAudioSampleWrite.set(false);
- startRecording(saveLocation, EMPTY_METADATA, executor, callback);
- }
-
- /**
- * Starts recording video, which continues until {@link VideoCapture#stopRecording()} is
- * called.
- *
- * StartRecording() is asynchronous. User needs to check if any error occurs by setting the
- * {@link OnVideoSavedCallback#onError(int, String, Throwable)}.
- *
- * @param saveLocation Location to save the video capture
- * @param metadata Metadata to save with the recorded video
- * @param executor The executor in which the callback methods will be run.
- * @param callback Callback for when the recorded video saving completion or failure.
- */
- public void startRecording(
- // Begin Signal Custom Code Block
- @NonNull FileDescriptor saveLocation,
- // End Signal Custom Code Block
- @NonNull Metadata metadata,
- @NonNull Executor executor,
- @NonNull OnVideoSavedCallback callback) {
- Log.i(TAG, "startRecording");
- OnVideoSavedCallback postListener = new VideoSavedListenerWrapper(executor, callback);
-
- if (!mEndOfAudioVideoSignal.get()) {
- postListener.onError(
- ERROR_RECORDING_IN_PROGRESS, "It is still in video recording!",
- null);
- return;
- }
-
- try {
- // audioRecord start
- // Begin Signal Custom Code Block
- if (mAudioRecorder != null) {
- mAudioRecorder.startRecording();
- } else {
- Log.w(TAG, "Missing audio recorder in start()!");
- }
- // End Signal Custom Code Block
- } catch (IllegalStateException e) {
- postListener.onError(ERROR_ENCODER, "AudioRecorder start fail", e);
- return;
- }
-
- CameraInternal boundCamera = getBoundCamera();
- String cameraId = getBoundCameraId();
- Size resolution = getAttachedSurfaceResolution(cameraId);
- try {
- // video encoder start
- Log.i(TAG, "videoEncoder start");
- mVideoEncoder.start();
- // audio encoder start
- Log.i(TAG, "audioEncoder start");
- mAudioEncoder.start();
-
- } catch (IllegalStateException e) {
- setupEncoder(cameraId, resolution);
- postListener.onError(ERROR_ENCODER, "Audio/Video encoder start fail",
- e);
- return;
- }
-
- CameraInfoInternal cameraInfoInternal = boundCamera.getCameraInfoInternal();
- int relativeRotation = cameraInfoInternal.getSensorRotationDegrees(
- ((ImageOutputConfig) getUseCaseConfig()).getTargetRotation(Surface.ROTATION_0));
-
- try {
- synchronized (mMuxerLock) {
- mMuxer =
- 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());
- }
- }
- } catch (IOException e) {
- setupEncoder(cameraId, resolution);
- postListener.onError(ERROR_MUXER, "MediaMuxer creation failed!", e);
- return;
- }
-
- mEndOfVideoStreamSignal.set(false);
- mEndOfAudioStreamSignal.set(false);
- mEndOfAudioVideoSignal.set(false);
- mIsRecording = true;
-
- notifyActive();
- mAudioHandler.post(
- new Runnable() {
- @Override
- public void run() {
- VideoCapture.this.audioEncode(postListener);
- }
- });
-
- mVideoHandler.post(
- new Runnable() {
- @Override
- public void run() {
- boolean errorOccurred = VideoCapture.this.videoEncode(postListener,
- cameraId, resolution);
- if (!errorOccurred) {
- postListener.onVideoSaved(saveLocation);
- }
- }
- });
- }
-
- /**
- * Stops recording video, this must be called after {@link
- * VideoCapture#startRecording(File, Metadata, Executor, OnVideoSavedCallback)} is called.
- *
- * stopRecording() is asynchronous API. User need to check if {@link
- * OnVideoSavedCallback#onVideoSaved(File)} or
- * {@link OnVideoSavedCallback#onError(int, String, Throwable)} be called
- * before startRecording.
- */
- public void stopRecording() {
- Log.i(TAG, "stopRecording");
- notifyInactive();
- if (!mEndOfAudioVideoSignal.get() && mIsRecording) {
- // stop audio encoder thread, and wait video encoder and muxer stop.
- mEndOfAudioStreamSignal.set(true);
- }
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- public void clear() {
- mVideoHandlerThread.quitSafely();
-
- // audio encoder release
- mAudioHandlerThread.quitSafely();
- if (mAudioEncoder != null) {
- mAudioEncoder.release();
- mAudioEncoder = null;
- }
-
- if (mAudioRecorder != null) {
- mAudioRecorder.release();
- mAudioRecorder = null;
- }
-
- if (mCameraSurface != null) {
- releaseCameraSurface(true);
- }
-
- super.clear();
- }
-
- private void releaseCameraSurface(final boolean releaseVideoEncoder) {
- if (mDeferrableSurface == null) {
- return;
- }
-
- final MediaCodec videoEncoder = mVideoEncoder;
-
- // Calling close should allow termination future to complete and close the surface with
- // the listener that was added after constructing the DeferrableSurface.
- mDeferrableSurface.close();
- mDeferrableSurface.getTerminationFuture().addListener(
- () -> {
- if (releaseVideoEncoder && videoEncoder != null) {
- videoEncoder.release();
- }
- }, CameraXExecutors.mainThreadExecutor());
-
- if (releaseVideoEncoder) {
- mVideoEncoder = null;
- }
- mCameraSurface = null;
- mDeferrableSurface = null;
- }
-
-
- /**
- * Sets the desired rotation of the output video.
- *
- * In most cases this should be set to the current rotation returned by {@link
- * Display#getRotation()}.
- *
- * @param rotation Desired rotation of the output video.
- */
- public void setTargetRotation(@RotationValue int rotation) {
- VideoCaptureConfig oldConfig = (VideoCaptureConfig) getUseCaseConfig();
- VideoCaptureConfig.Builder builder = VideoCaptureConfig.Builder.fromConfig(oldConfig);
- int oldRotation = oldConfig.getTargetRotation(ImageOutputConfig.INVALID_ROTATION);
- if (oldRotation == ImageOutputConfig.INVALID_ROTATION || oldRotation != rotation) {
- UseCaseConfigUtil.updateTargetRotationAndRelatedConfigs(builder, rotation);
- updateUseCaseConfig(builder.getUseCaseConfig());
-
- // TODO(b/122846516): Update session configuration and possibly reconfigure session.
- }
- }
-
- /**
- * Setup the {@link MediaCodec} for encoding video from a camera {@link Surface} and encoding
- * audio from selected audio source.
- */
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
- void setupEncoder(@NonNull String cameraId, @NonNull Size resolution) {
- VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
-
- // video encoder setup
- mVideoEncoder.reset();
- mVideoEncoder.configure(
- createMediaFormat(config, resolution), /*surface*/
- null, /*crypto*/
- null,
- MediaCodec.CONFIGURE_FLAG_ENCODE);
- if (mCameraSurface != null) {
- releaseCameraSurface(false);
- }
- Surface cameraSurface = mVideoEncoder.createInputSurface();
- mCameraSurface = cameraSurface;
-
- SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
-
- if (mDeferrableSurface != null) {
- mDeferrableSurface.close();
- }
- mDeferrableSurface = new ImmediateSurface(mCameraSurface);
- mDeferrableSurface.getTerminationFuture().addListener(
- cameraSurface::release, CameraXExecutors.mainThreadExecutor()
- );
-
- sessionConfigBuilder.addSurface(mDeferrableSurface);
-
- sessionConfigBuilder.addErrorListener(new SessionConfig.ErrorListener() {
- @Override
- public void onError(@NonNull SessionConfig sessionConfig,
- @NonNull SessionConfig.SessionError error) {
- // Ensure the bound camera has not changed before calling setupEncoder.
- // TODO(b/143915543): Ensure this never gets called by a camera that is not bound
- // to this use case so we don't need to do this check.
- if (isCurrentlyBoundCamera(cameraId)) {
- // Only reset the pipeline when the bound camera is the same.
- setupEncoder(cameraId, resolution);
- }
- }
- });
-
- attachToCamera(cameraId, sessionConfigBuilder.build());
-
- // audio encoder setup
- setAudioParametersByCamcorderProfile(resolution, cameraId);
- mAudioEncoder.reset();
- mAudioEncoder.configure(
- createAudioMediaFormat(), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
-
- if (mAudioRecorder != null) {
- mAudioRecorder.release();
- }
- mAudioRecorder = autoConfigAudioRecordSource(config);
- // check mAudioRecorder
- if (mAudioRecorder == null) {
- Log.e(TAG, "AudioRecord object cannot initialized correctly!");
- }
-
- mVideoTrackIndex = -1;
- mAudioTrackIndex = -1;
- mIsRecording = false;
- }
-
- /**
- * Write a buffer that has been encoded to file.
- *
- * @param bufferIndex the index of the buffer in the videoEncoder that has available data
- * @return returns true if this buffer is the end of the stream
- */
- private boolean writeVideoEncodedBuffer(int bufferIndex) {
- if (bufferIndex < 0) {
- Log.e(TAG, "Output buffer should not have negative index: " + bufferIndex);
- return false;
- }
- // Get data from buffer
- ByteBuffer outputBuffer = mVideoEncoder.getOutputBuffer(bufferIndex);
-
- // Check if buffer is valid, if not then return
- if (outputBuffer == null) {
- Log.d(TAG, "OutputBuffer was null.");
- return false;
- }
-
- // Write data to mMuxer if available
- if (mAudioTrackIndex >= 0 && mVideoTrackIndex >= 0 && mVideoBufferInfo.size > 0) {
- outputBuffer.position(mVideoBufferInfo.offset);
- outputBuffer.limit(mVideoBufferInfo.offset + mVideoBufferInfo.size);
- mVideoBufferInfo.presentationTimeUs = (System.nanoTime() / 1000);
-
- synchronized (mMuxerLock) {
- if (!mIsFirstVideoSampleWrite.get()) {
- Log.i(TAG, "First video sample written.");
- mIsFirstVideoSampleWrite.set(true);
- }
- mMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, mVideoBufferInfo);
- }
- }
-
- // Release data
- mVideoEncoder.releaseOutputBuffer(bufferIndex, false);
-
- // Return true if EOS is set
- return (mVideoBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
- }
-
- private boolean writeAudioEncodedBuffer(int bufferIndex) {
- ByteBuffer buffer = getOutputBuffer(mAudioEncoder, bufferIndex);
- buffer.position(mAudioBufferInfo.offset);
- if (mAudioTrackIndex >= 0
- && mVideoTrackIndex >= 0
- && mAudioBufferInfo.size > 0
- && mAudioBufferInfo.presentationTimeUs > 0) {
- try {
- synchronized (mMuxerLock) {
- if (!mIsFirstAudioSampleWrite.get()) {
- Log.i(TAG, "First audio sample written.");
- mIsFirstAudioSampleWrite.set(true);
- }
- mMuxer.writeSampleData(mAudioTrackIndex, buffer, mAudioBufferInfo);
- }
- } catch (Exception e) {
- Log.e(
- TAG,
- "audio error:size="
- + mAudioBufferInfo.size
- + "/offset="
- + mAudioBufferInfo.offset
- + "/timeUs="
- + mAudioBufferInfo.presentationTimeUs);
- e.printStackTrace();
- }
- }
- mAudioEncoder.releaseOutputBuffer(bufferIndex, false);
- return (mAudioBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
- }
-
- /**
- * Encoding which runs indefinitely until end of stream is signaled. This should not run on the
- * main thread otherwise it will cause the application to block.
- *
- * @return returns {@code true} if an error condition occurred, otherwise returns {@code false}
- */
- boolean videoEncode(@NonNull OnVideoSavedCallback videoSavedCallback, @NonNull String cameraId,
- @NonNull Size resolution) {
- // Main encoding loop. Exits on end of stream.
- boolean errorOccurred = false;
- boolean videoEos = false;
- while (!videoEos && !errorOccurred) {
- // Check for end of stream from main thread
- if (mEndOfVideoStreamSignal.get()) {
- mVideoEncoder.signalEndOfInputStream();
- mEndOfVideoStreamSignal.set(false);
- }
-
- // Deque buffer to check for processing step
- int outputBufferId =
- mVideoEncoder.dequeueOutputBuffer(mVideoBufferInfo, DEQUE_TIMEOUT_USEC);
- switch (outputBufferId) {
- case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
- if (mMuxerStarted) {
- videoSavedCallback.onError(
- ERROR_ENCODER,
- "Unexpected change in video encoding format.",
- null);
- errorOccurred = true;
- }
-
- synchronized (mMuxerLock) {
- mVideoTrackIndex = mMuxer.addTrack(mVideoEncoder.getOutputFormat());
- if (mAudioTrackIndex >= 0 && mVideoTrackIndex >= 0) {
- mMuxerStarted = true;
- Log.i(TAG, "media mMuxer start");
- mMuxer.start();
- }
- }
- break;
- case MediaCodec.INFO_TRY_AGAIN_LATER:
- // Timed out. Just wait until next attempt to deque.
- default:
- videoEos = writeVideoEncodedBuffer(outputBufferId);
- }
- }
-
- try {
- Log.i(TAG, "videoEncoder stop");
- mVideoEncoder.stop();
- } catch (IllegalStateException e) {
- videoSavedCallback.onError(ERROR_ENCODER,
- "Video encoder stop failed!", e);
- errorOccurred = true;
- }
-
- try {
- // new MediaMuxer instance required for each new file written, and release current one.
- synchronized (mMuxerLock) {
- if (mMuxer != null) {
- if (mMuxerStarted) {
- mMuxer.stop();
- }
- mMuxer.release();
- mMuxer = null;
- }
- }
- } catch (IllegalStateException e) {
- videoSavedCallback.onError(ERROR_MUXER, "Muxer stop failed!", e);
- errorOccurred = true;
- }
-
- mMuxerStarted = false;
- // Do the setup of the videoEncoder at the end of video recording instead of at the start of
- // recording because it requires attaching a new Surface. This causes a glitch so we don't
- // want that to incur latency at the start of capture.
- setupEncoder(cameraId, resolution);
- notifyReset();
-
- // notify the UI thread that the video recording has finished
- mEndOfAudioVideoSignal.set(true);
-
- Log.i(TAG, "Video encode thread end.");
- return errorOccurred;
- }
-
- boolean audioEncode(OnVideoSavedCallback videoSavedCallback) {
- // Audio encoding loop. Exits on end of stream.
- boolean audioEos = false;
- int outIndex;
- while (!audioEos && mIsRecording) {
- // Check for end of stream from main thread
- if (mEndOfAudioStreamSignal.get()) {
- mEndOfAudioStreamSignal.set(false);
- mIsRecording = false;
- }
-
- // get audio deque input buffer
- if (mAudioEncoder != null && mAudioRecorder != null) {
- int index = mAudioEncoder.dequeueInputBuffer(-1);
- if (index >= 0) {
- final ByteBuffer buffer = getInputBuffer(mAudioEncoder, index);
- buffer.clear();
- 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);
- }
- }
-
- // start to dequeue audio output buffer
- do {
- outIndex = mAudioEncoder.dequeueOutputBuffer(mAudioBufferInfo, 0);
- switch (outIndex) {
- case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
- synchronized (mMuxerLock) {
- mAudioTrackIndex = mMuxer.addTrack(mAudioEncoder.getOutputFormat());
- if (mAudioTrackIndex >= 0 && mVideoTrackIndex >= 0) {
- mMuxerStarted = true;
- mMuxer.start();
- }
- }
- break;
- case MediaCodec.INFO_TRY_AGAIN_LATER:
- break;
- default:
- audioEos = writeAudioEncodedBuffer(outIndex);
- }
- } while (outIndex >= 0 && !audioEos); // end of dequeue output buffer
- }
- } // end of while loop
-
- // Audio Stop
- try {
- Log.i(TAG, "audioRecorder stop");
- // Begin Signal Custom Code Block
- if (mAudioRecorder != null) {
- mAudioRecorder.stop();
- } else {
- Log.w(TAG, "Missing audio recorder in stop()!");
- }
- // End Signal Custom Code Block
- } catch (IllegalStateException e) {
- videoSavedCallback.onError(
- ERROR_ENCODER, "Audio recorder stop failed!", e);
- }
-
- try {
- mAudioEncoder.stop();
- } catch (IllegalStateException e) {
- videoSavedCallback.onError(ERROR_ENCODER,
- "Audio encoder stop failed!", e);
- }
-
- Log.i(TAG, "Audio encode thread end");
- // Use AtomicBoolean to signal because MediaCodec.signalEndOfInputStream() is not thread
- // safe
- mEndOfVideoStreamSignal.set(true);
-
- return false;
- }
-
- private ByteBuffer getInputBuffer(MediaCodec codec, int index) {
- return codec.getInputBuffer(index);
- }
-
- private ByteBuffer getOutputBuffer(MediaCodec codec, int index) {
- return codec.getOutputBuffer(index);
- }
-
- /** Creates a {@link MediaFormat} using parameters for audio from the configuration */
- private MediaFormat createAudioMediaFormat() {
- MediaFormat format =
- MediaFormat.createAudioFormat(AUDIO_MIME_TYPE, mAudioSampleRate,
- mAudioChannelCount);
- format.setInteger(
- MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
- format.setInteger(MediaFormat.KEY_BIT_RATE, mAudioBitRate);
-
- return format;
- }
-
- /** Create a AudioRecord object to get raw data */
- private AudioRecord autoConfigAudioRecordSource(VideoCaptureConfig config) {
- for (short audioFormat : sAudioEncoding) {
-
- // Use channel count to determine stereo vs mono
- int channelConfig =
- mAudioChannelCount == 1
- ? AudioFormat.CHANNEL_IN_MONO
- : AudioFormat.CHANNEL_IN_STEREO;
- int source = config.getAudioRecordSource();
-
- try {
- int bufferSize =
- AudioRecord.getMinBufferSize(mAudioSampleRate, channelConfig, audioFormat);
-
- if (bufferSize <= 0) {
- bufferSize = config.getAudioMinBufferSize();
- }
-
- AudioRecord recorder =
- 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);
- return recorder;
- }
- } catch (Exception e) {
- Log.e(TAG, "Exception, keep trying.", e);
- }
- }
-
- return null;
- }
-
- /** Set audio record parameters by CamcorderProfile */
- private void setAudioParametersByCamcorderProfile(Size currentResolution, String cameraId) {
- CamcorderProfile profile;
- boolean isCamcorderProfileFound = false;
-
- for (int quality : CamcorderQuality) {
- if (CamcorderProfile.hasProfile(Integer.parseInt(cameraId), quality)) {
- profile = CamcorderProfile.get(Integer.parseInt(cameraId), quality);
- if (currentResolution.getWidth() == profile.videoFrameWidth
- && currentResolution.getHeight() == profile.videoFrameHeight) {
- mAudioChannelCount = profile.audioChannels;
- mAudioSampleRate = profile.audioSampleRate;
- mAudioBitRate = profile.audioBitRate;
- isCamcorderProfileFound = true;
- break;
- }
- }
- }
-
- // In case no corresponding camcorder profile can be founded, * get default value from
- // VideoCaptureConfig.
- if (!isCamcorderProfileFound) {
- VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
- mAudioChannelCount = config.getAudioChannelCount();
- mAudioSampleRate = config.getAudioSampleRate();
- mAudioBitRate = config.getAudioBitRate();
- }
- }
-
- /**
- * 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.OnVideoSavedCallback#onError(int, String, Throwable)}.
- *
- * See message parameter in onError callback or log for more details.
- *
- * @hide
- */
- @IntDef({ERROR_UNKNOWN, ERROR_ENCODER, ERROR_MUXER, ERROR_RECORDING_IN_PROGRESS})
- @Retention(RetentionPolicy.SOURCE)
- @RestrictTo(Scope.LIBRARY_GROUP)
- public @interface VideoCaptureError {
- }
-
- /** Listener containing callbacks for video file I/O events. */
- public interface OnVideoSavedCallback {
- /** Called when the video has been successfully saved. */
- // TODO: Should remove file argument to match ImageCapture.OnImageSavedCallback
- // #onImageSaved()
- // Begin Signal Custom Code Block
- void onVideoSaved(@NonNull FileDescriptor file);
- // End Signal Custom Code Block
-
- /** Called when an error occurs while attempting to save the video. */
- void onError(@VideoCaptureError int videoCaptureError, @NonNull String message,
- @Nullable Throwable cause);
- }
-
- /**
- * Provides a base static default configuration for the VideoCapture
- *
- * These values may be overridden by the implementation. They only provide a minimum set of
- * defaults that are implementation independent.
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- public static final class Defaults
- implements ConfigProviderSURFACE_VIEW
if a {@link android.view.SurfaceView}
- * should be used to display the camera feed, or
- * TEXTURE_VIEW
to use a {@link android.view.TextureView}
- */
- public void setImplementationMode(@NonNull final ImplementationMode implementationMode) {
- mImplementationMode = implementationMode;
- setUp();
- }
-
- /**
- * Returns the implementation mode of the {@link PreviewView}.
- *
- * @return SURFACE_VIEW
if the {@link PreviewView} is internally using a
- * {@link android.view.SurfaceView} to display the camera feed, or TEXTURE_VIEW
- * if a {@link android.view.TextureView} is being used.
- */
- @NonNull
- public ImplementationMode getImplementationMode() {
- return mImplementationMode;
- }
-
- /**
- * Gets the {@link Preview.SurfaceProvider} to be used with
- * {@link Preview#setSurfaceProvider(Executor, Preview.SurfaceProvider)}.
- */
- @NonNull
- public Preview.SurfaceProvider getPreviewSurfaceProvider() {
- return mImplementation.getSurfaceProvider();
- }
-
- /**
- * Implements this interface to create PreviewView implementation.
- */
- interface Implementation {
-
- /**
- * Initializes the parent view with sub views.
- *
- * @param parent the containing parent {@link FrameLayout}.
- */
- void init(@NonNull FrameLayout parent);
-
- /**
- * Gets the {@link Preview.SurfaceProvider} to be used with {@link Preview}.
- */
- @NonNull
- Preview.SurfaceProvider getSurfaceProvider();
-
- /**
- * Notifies that the display properties have changed.
- *
- *