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 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