mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-09 02:18:34 +00:00
Update CameraX to Alpha06 and bring in new View / Module code.
This commit is contained in:
parent
46ebff3659
commit
c2da4fcd7d
@ -78,8 +78,8 @@ dependencies {
|
|||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
|
||||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
|
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
|
||||||
implementation "androidx.camera:camera-core:1.0.0-alpha04"
|
implementation "androidx.camera:camera-core:1.0.0-alpha06"
|
||||||
implementation "androidx.camera:camera-camera2:1.0.0-alpha04"
|
implementation "androidx.camera:camera-camera2:1.0.0-alpha06"
|
||||||
|
|
||||||
implementation('com.google.firebase:firebase-messaging:17.3.4') {
|
implementation('com.google.firebase:firebase-messaging:17.3.4') {
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||||
|
@ -340,4 +340,27 @@
|
|||||||
<attr name="recordSize" format="dimension" />
|
<attr name="recordSize" format="dimension" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
|
<declare-styleable name="CameraXView">
|
||||||
|
<attr format="enum" name="scaleType">
|
||||||
|
<enum name="centerCrop" value="0"/>
|
||||||
|
<enum name="centerInside" value="1"/>
|
||||||
|
</attr>
|
||||||
|
<attr format="enum" name="lensFacing">
|
||||||
|
<enum name="none" value="0"/>
|
||||||
|
<enum name="front" value="1"/>
|
||||||
|
<enum name="back" value="2"/>
|
||||||
|
</attr>
|
||||||
|
<attr format="enum" name="captureMode">
|
||||||
|
<enum name="image" value="0"/>
|
||||||
|
<enum name="video" value="1"/>
|
||||||
|
<enum name="mixed" value="2"/>
|
||||||
|
</attr>
|
||||||
|
<attr format="enum" name="flash">
|
||||||
|
<enum name="auto" value="1"/>
|
||||||
|
<enum name="on" value="2"/>
|
||||||
|
<enum name="off" value="4"/>
|
||||||
|
</attr>
|
||||||
|
|
||||||
|
<attr format="boolean" name="pinchToZoomEnabled"/>
|
||||||
|
</declare-styleable>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.mediasend;
|
package org.thoughtcrime.securesms.mediasend;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.ActivityInfo;
|
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.GestureDetector;
|
import android.view.GestureDetector;
|
||||||
@ -26,11 +24,13 @@ import androidx.annotation.RequiresApi;
|
|||||||
import androidx.camera.core.CameraX;
|
import androidx.camera.core.CameraX;
|
||||||
import androidx.camera.core.ImageCapture;
|
import androidx.camera.core.ImageCapture;
|
||||||
import androidx.camera.core.ImageProxy;
|
import androidx.camera.core.ImageProxy;
|
||||||
|
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.util.Executors;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||||
@ -325,7 +325,7 @@ public class CameraXFragment extends Fragment implements CameraFragment {
|
|||||||
selfieFlash
|
selfieFlash
|
||||||
);
|
);
|
||||||
|
|
||||||
camera.takePicture(new ImageCapture.OnImageCapturedListener() {
|
camera.takePicture(Executors.mainThreadExecutor(), new ImageCapture.OnImageCapturedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
|
public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
|
||||||
flashHelper.endFlash();
|
flashHelper.endFlash();
|
||||||
@ -352,7 +352,7 @@ public class CameraXFragment extends Fragment implements CameraFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ImageCapture.UseCaseError useCaseError, String message, @Nullable Throwable cause) {
|
public void onError(ImageCapture.ImageCaptureError useCaseError, String message, @Nullable Throwable cause) {
|
||||||
flashHelper.endFlash();
|
flashHelper.endFlash();
|
||||||
controller.onCameraError();
|
controller.onCameraError();
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import com.bumptech.glide.util.Executors;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||||
@ -119,7 +121,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
|
|||||||
this.camera.setZoomLevel(0f);
|
this.camera.setZoomLevel(0f);
|
||||||
callback.onVideoRecordStarted();
|
callback.onVideoRecordStarted();
|
||||||
shrinkCaptureArea();
|
shrinkCaptureArea();
|
||||||
camera.startRecording(memoryFileDescriptor.getFileDescriptor(), videoSavedListener);
|
camera.startRecording(memoryFileDescriptor.getFileDescriptor(), Executors.mainThreadExecutor(), videoSavedListener);
|
||||||
updateProgressAnimator.start();
|
updateProgressAnimator.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
package org.thoughtcrime.securesms.mediasend.camerax;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 The Android Open Source Project
|
* Copyright (C) 2019 The Android Open Source Project
|
||||||
*
|
*
|
||||||
@ -16,6 +14,8 @@ package org.thoughtcrime.securesms.mediasend.camerax;
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||||
|
|
||||||
import android.Manifest.permission;
|
import android.Manifest.permission;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -25,23 +25,24 @@ import android.graphics.SurfaceTexture;
|
|||||||
import android.hardware.camera2.CameraAccessException;
|
import android.hardware.camera2.CameraAccessException;
|
||||||
import android.hardware.camera2.CameraCharacteristics;
|
import android.hardware.camera2.CameraCharacteristics;
|
||||||
import android.hardware.camera2.CameraManager;
|
import android.hardware.camera2.CameraManager;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import android.util.Log;
|
||||||
import android.util.Rational;
|
import android.util.Rational;
|
||||||
import android.util.Size;
|
import android.util.Size;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.annotation.RequiresPermission;
|
import androidx.annotation.RequiresPermission;
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
|
import androidx.camera.core.AspectRatio;
|
||||||
import androidx.camera.core.CameraInfo;
|
import androidx.camera.core.CameraInfo;
|
||||||
import androidx.camera.core.CameraInfoUnavailableException;
|
import androidx.camera.core.CameraInfoUnavailableException;
|
||||||
import androidx.camera.core.CameraOrientationUtil;
|
import androidx.camera.core.CameraOrientationUtil;
|
||||||
import androidx.camera.core.CameraX;
|
import androidx.camera.core.CameraX;
|
||||||
import androidx.camera.core.CameraX.LensFacing;
|
|
||||||
import androidx.camera.core.FlashMode;
|
import androidx.camera.core.FlashMode;
|
||||||
import androidx.camera.core.ImageCapture;
|
import androidx.camera.core.ImageCapture;
|
||||||
import androidx.camera.core.ImageCapture.OnImageCapturedListener;
|
|
||||||
import androidx.camera.core.ImageCapture.OnImageSavedListener;
|
|
||||||
import androidx.camera.core.ImageCaptureConfig;
|
import androidx.camera.core.ImageCaptureConfig;
|
||||||
import androidx.camera.core.Preview;
|
import androidx.camera.core.Preview;
|
||||||
import androidx.camera.core.PreviewConfig;
|
import androidx.camera.core.PreviewConfig;
|
||||||
@ -49,12 +50,10 @@ import androidx.camera.core.VideoCaptureConfig;
|
|||||||
import androidx.lifecycle.Lifecycle;
|
import androidx.lifecycle.Lifecycle;
|
||||||
import androidx.lifecycle.LifecycleObserver;
|
import androidx.lifecycle.LifecycleObserver;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.OnLifecycleEvent;
|
import androidx.lifecycle.OnLifecycleEvent;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXView.CaptureMode;
|
|
||||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
|
||||||
import org.thoughtcrime.securesms.video.VideoUtil;
|
import org.thoughtcrime.securesms.video.VideoUtil;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -62,11 +61,11 @@ import java.io.FileDescriptor;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/** CameraX use case operation built on @{link androidx.camera.core}. */
|
/** CameraX use case operation built on @{link androidx.camera.core}. */
|
||||||
@RequiresApi(21)
|
@RequiresApi(21)
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
final class CameraXModule {
|
final class CameraXModule {
|
||||||
public static final String TAG = "CameraXModule";
|
public static final String TAG = "CameraXModule";
|
||||||
|
|
||||||
@ -82,9 +81,9 @@ final class CameraXModule {
|
|||||||
private final PreviewConfig.Builder mPreviewConfigBuilder;
|
private final PreviewConfig.Builder mPreviewConfigBuilder;
|
||||||
private final VideoCaptureConfig.Builder mVideoCaptureConfigBuilder;
|
private final VideoCaptureConfig.Builder mVideoCaptureConfigBuilder;
|
||||||
private final ImageCaptureConfig.Builder mImageCaptureConfigBuilder;
|
private final ImageCaptureConfig.Builder mImageCaptureConfigBuilder;
|
||||||
private final CameraXView mCameraXView;
|
private final CameraXView mCameraView;
|
||||||
final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false);
|
final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false);
|
||||||
private CaptureMode mCaptureMode = CaptureMode.IMAGE;
|
private CameraXView.CaptureMode mCaptureMode = CameraXView.CaptureMode.IMAGE;
|
||||||
private long mMaxVideoDuration = CameraXView.INDEFINITE_VIDEO_DURATION;
|
private long mMaxVideoDuration = CameraXView.INDEFINITE_VIDEO_DURATION;
|
||||||
private long mMaxVideoSize = CameraXView.INDEFINITE_VIDEO_SIZE;
|
private long mMaxVideoSize = CameraXView.INDEFINITE_VIDEO_SIZE;
|
||||||
private FlashMode mFlash = FlashMode.OFF;
|
private FlashMode mFlash = FlashMode.OFF;
|
||||||
@ -112,10 +111,10 @@ final class CameraXModule {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private Rect mCropRegion;
|
private Rect mCropRegion;
|
||||||
@Nullable
|
@Nullable
|
||||||
private CameraX.LensFacing mCameraLensFacing = LensFacing.BACK;
|
private CameraX.LensFacing mCameraLensFacing = CameraX.LensFacing.BACK;
|
||||||
|
|
||||||
CameraXModule(CameraXView view) {
|
CameraXModule(CameraXView view) {
|
||||||
this.mCameraXView = view;
|
this.mCameraView = view;
|
||||||
|
|
||||||
mCameraManager = (CameraManager) view.getContext().getSystemService(Context.CAMERA_SERVICE);
|
mCameraManager = (CameraManager) view.getContext().getSystemService(Context.CAMERA_SERVICE);
|
||||||
|
|
||||||
@ -126,11 +125,10 @@ final class CameraXModule {
|
|||||||
|
|
||||||
// Begin Signal Custom Code Block
|
// Begin Signal Custom Code Block
|
||||||
mVideoCaptureConfigBuilder =
|
mVideoCaptureConfigBuilder =
|
||||||
new VideoCaptureConfig.Builder()
|
new VideoCaptureConfig.Builder().setTargetName("VideoCapture")
|
||||||
.setAudioBitRate(VideoUtil.AUDIO_BIT_RATE)
|
.setAudioBitRate(VideoUtil.AUDIO_BIT_RATE)
|
||||||
.setVideoFrameRate(VideoUtil.VIDEO_FRAME_RATE)
|
.setVideoFrameRate(VideoUtil.VIDEO_FRAME_RATE)
|
||||||
.setBitRate(VideoUtil.VIDEO_BIT_RATE)
|
.setBitRate(VideoUtil.VIDEO_BIT_RATE);
|
||||||
.setTargetName("VideoCapture");
|
|
||||||
// End Signal Custom Code Block
|
// End Signal Custom Code Block
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,8 +190,7 @@ final class CameraXModule {
|
|||||||
|
|
||||||
final int cameraOrientation;
|
final int cameraOrientation;
|
||||||
try {
|
try {
|
||||||
String cameraId;
|
Set<CameraX.LensFacing> available = getAvailableCameraLensFacing();
|
||||||
Set<LensFacing> available = getAvailableCameraLensFacing();
|
|
||||||
|
|
||||||
if (available.isEmpty()) {
|
if (available.isEmpty()) {
|
||||||
Log.w(TAG, "Unable to bindToLifeCycle since no cameras available");
|
Log.w(TAG, "Unable to bindToLifeCycle since no cameras available");
|
||||||
@ -217,41 +214,32 @@ final class CameraXModule {
|
|||||||
if (mCameraLensFacing == null) {
|
if (mCameraLensFacing == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
CameraInfo cameraInfo = CameraX.getCameraInfo(getLensFacing());
|
||||||
cameraId = CameraX.getCameraWithLensFacing(mCameraLensFacing);
|
|
||||||
if (cameraId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
|
|
||||||
cameraOrientation = cameraInfo.getSensorRotationDegrees();
|
cameraOrientation = cameraInfo.getSensorRotationDegrees();
|
||||||
|
} catch (CameraInfoUnavailableException e) {
|
||||||
|
throw new IllegalStateException("Unable to get Camera Info.", e);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new IllegalStateException("Unable to bind to lifecycle.", e);
|
throw new IllegalStateException("Unable to bind to lifecycle.", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the preferred aspect ratio as 4:3 if it is IMAGE only mode. Set the preferred aspect
|
// Set the preferred aspect ratio as 4:3 if it is IMAGE only mode. Set the preferred aspect
|
||||||
// ratio as 16:9 if it is VIDEO or MIXED mode. Then, it will be WYSIWYG when the view finder
|
// ratio as 16:9 if it is VIDEO or MIXED mode. Then, it will be WYSIWYG when the view finder
|
||||||
// is
|
// is in CENTER_INSIDE mode.
|
||||||
// in CENTER_INSIDE mode.
|
|
||||||
|
|
||||||
boolean isDisplayPortrait = getDisplayRotationDegrees() == 0
|
boolean isDisplayPortrait = getDisplayRotationDegrees() == 0
|
||||||
|| getDisplayRotationDegrees() == 180;
|
|| getDisplayRotationDegrees() == 180;
|
||||||
|
|
||||||
if (getCaptureMode() == CaptureMode.IMAGE) {
|
Rational targetAspectRatio;
|
||||||
mImageCaptureConfigBuilder.setTargetAspectRatio(
|
if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) {
|
||||||
isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3);
|
mImageCaptureConfigBuilder.setTargetAspectRatio(AspectRatio.RATIO_4_3);
|
||||||
mPreviewConfigBuilder.setTargetAspectRatio(
|
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3;
|
||||||
isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3);
|
|
||||||
} else {
|
} else {
|
||||||
mImageCaptureConfigBuilder.setTargetAspectRatio(
|
mImageCaptureConfigBuilder.setTargetAspectRatio(AspectRatio.RATIO_16_9);
|
||||||
isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9);
|
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
|
||||||
mPreviewConfigBuilder.setTargetAspectRatio(
|
|
||||||
isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mImageCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation());
|
mImageCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation());
|
||||||
mImageCaptureConfigBuilder.setLensFacing(mCameraLensFacing);
|
mImageCaptureConfigBuilder.setLensFacing(mCameraLensFacing);
|
||||||
mImageCaptureConfigBuilder.setCaptureMode(CameraXUtil.getOptimalCaptureMode());
|
|
||||||
mImageCaptureConfigBuilder.setTargetResolution(new Size(1920, 1920));
|
|
||||||
mImageCapture = new ImageCapture(mImageCaptureConfigBuilder.build());
|
mImageCapture = new ImageCapture(mImageCaptureConfigBuilder.build());
|
||||||
|
|
||||||
// Begin Signal Custom Code Block
|
// Begin Signal Custom Code Block
|
||||||
@ -267,25 +255,17 @@ final class CameraXModule {
|
|||||||
if (MediaConstraints.isVideoTranscodeAvailable()) {
|
if (MediaConstraints.isVideoTranscodeAvailable()) {
|
||||||
mVideoCapture = new VideoCapture(mVideoCaptureConfigBuilder.build());
|
mVideoCapture = new VideoCapture(mVideoCaptureConfigBuilder.build());
|
||||||
}
|
}
|
||||||
// End Signal Custom Code Block
|
|
||||||
|
|
||||||
mPreviewConfigBuilder.setLensFacing(mCameraLensFacing);
|
mPreviewConfigBuilder.setLensFacing(mCameraLensFacing);
|
||||||
|
|
||||||
int relativeCameraOrientation = getRelativeCameraOrientation(false);
|
// Adjusts the preview resolution according to the view size and the target aspect ratio.
|
||||||
|
int height = (int) (getMeasuredWidth() / targetAspectRatio.floatValue());
|
||||||
if (relativeCameraOrientation == 90 || relativeCameraOrientation == 270) {
|
mPreviewConfigBuilder.setTargetResolution(new Size(getMeasuredWidth(), height));
|
||||||
mPreviewConfigBuilder.setTargetResolution(
|
|
||||||
new Size(getMeasuredHeight(), getMeasuredWidth()));
|
|
||||||
} else {
|
|
||||||
mPreviewConfigBuilder.setTargetResolution(
|
|
||||||
new Size(getMeasuredWidth(), getMeasuredHeight()));
|
|
||||||
}
|
|
||||||
|
|
||||||
mPreview = new Preview(mPreviewConfigBuilder.build());
|
mPreview = new Preview(mPreviewConfigBuilder.build());
|
||||||
mPreview.setOnPreviewOutputUpdateListener(
|
mPreview.setOnPreviewOutputUpdateListener(
|
||||||
new Preview.OnPreviewOutputUpdateListener() {
|
new Preview.OnPreviewOutputUpdateListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onUpdated(Preview.PreviewOutput output) {
|
public void onUpdated(@NonNull Preview.PreviewOutput output) {
|
||||||
boolean needReverse = cameraOrientation != 0 && cameraOrientation != 180;
|
boolean needReverse = cameraOrientation != 0 && cameraOrientation != 180;
|
||||||
int textureWidth =
|
int textureWidth =
|
||||||
needReverse
|
needReverse
|
||||||
@ -301,9 +281,9 @@ final class CameraXModule {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (getCaptureMode() == CaptureMode.IMAGE) {
|
if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) {
|
||||||
CameraX.bindToLifecycle(mCurrentLifecycle, mImageCapture, mPreview);
|
CameraX.bindToLifecycle(mCurrentLifecycle, mImageCapture, mPreview);
|
||||||
} else if (getCaptureMode() == CaptureMode.VIDEO) {
|
} else if (getCaptureMode() == CameraXView.CaptureMode.VIDEO) {
|
||||||
CameraX.bindToLifecycle(mCurrentLifecycle, mVideoCapture, mPreview);
|
CameraX.bindToLifecycle(mCurrentLifecycle, mVideoCapture, mPreview);
|
||||||
} else {
|
} else {
|
||||||
CameraX.bindToLifecycle(mCurrentLifecycle, mImageCapture, mVideoCapture, mPreview);
|
CameraX.bindToLifecycle(mCurrentLifecycle, mImageCapture, mVideoCapture, mPreview);
|
||||||
@ -324,18 +304,12 @@ final class CameraXModule {
|
|||||||
"Explicit open/close of camera not yet supported. Use bindtoLifecycle() instead.");
|
"Explicit open/close of camera not yet supported. Use bindtoLifecycle() instead.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stopPreview() {
|
public void takePicture(Executor executor, ImageCapture.OnImageCapturedListener listener) {
|
||||||
if (mPreview != null) {
|
|
||||||
mPreview.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void takePicture(OnImageCapturedListener listener) {
|
|
||||||
if (mImageCapture == null) {
|
if (mImageCapture == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getCaptureMode() == CaptureMode.VIDEO) {
|
if (getCaptureMode() == CameraXView.CaptureMode.VIDEO) {
|
||||||
throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
|
throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,15 +317,15 @@ final class CameraXModule {
|
|||||||
throw new IllegalArgumentException("OnImageCapturedListener should not be empty");
|
throw new IllegalArgumentException("OnImageCapturedListener should not be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
mImageCapture.takePicture(listener);
|
mImageCapture.takePicture(executor, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void takePicture(File saveLocation, OnImageSavedListener listener) {
|
public void takePicture(File saveLocation, Executor executor, ImageCapture.OnImageSavedListener listener) {
|
||||||
if (mImageCapture == null) {
|
if (mImageCapture == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getCaptureMode() == CaptureMode.VIDEO) {
|
if (getCaptureMode() == CameraXView.CaptureMode.VIDEO) {
|
||||||
throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
|
throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,19 +334,19 @@ final class CameraXModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImageCapture.Metadata metadata = new ImageCapture.Metadata();
|
ImageCapture.Metadata metadata = new ImageCapture.Metadata();
|
||||||
metadata.isReversedHorizontal = mCameraLensFacing == LensFacing.FRONT;
|
metadata.isReversedHorizontal = mCameraLensFacing == CameraX.LensFacing.FRONT;
|
||||||
mImageCapture.takePicture(saveLocation, listener, metadata);
|
mImageCapture.takePicture(saveLocation, metadata, executor, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin Signal Custom Code Block
|
// Begin Signal Custom Code Block
|
||||||
@RequiresApi(26)
|
@RequiresApi(26)
|
||||||
|
public void startRecording(FileDescriptor file, Executor executor, final VideoCapture.OnVideoSavedListener listener) {
|
||||||
// End Signal Custom Code Block
|
// End Signal Custom Code Block
|
||||||
public void startRecording(FileDescriptor file, final VideoCapture.OnVideoSavedListener listener) {
|
|
||||||
if (mVideoCapture == null) {
|
if (mVideoCapture == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getCaptureMode() == CaptureMode.IMAGE) {
|
if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) {
|
||||||
throw new IllegalStateException("Can not record video under IMAGE capture mode.");
|
throw new IllegalStateException("Can not record video under IMAGE capture mode.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,25 +357,24 @@ final class CameraXModule {
|
|||||||
mVideoIsRecording.set(true);
|
mVideoIsRecording.set(true);
|
||||||
mVideoCapture.startRecording(
|
mVideoCapture.startRecording(
|
||||||
file,
|
file,
|
||||||
|
executor,
|
||||||
new VideoCapture.OnVideoSavedListener() {
|
new VideoCapture.OnVideoSavedListener() {
|
||||||
@Override
|
@Override
|
||||||
// Begin Signal Custom Code Block
|
// Begin Signal Custom Code block
|
||||||
public void onVideoSaved(FileDescriptor savedFileDescriptor) {
|
public void onVideoSaved(@NonNull FileDescriptor savedFile) {
|
||||||
// End Signal Custom Code Block
|
// End Signal Custom Code Block
|
||||||
mVideoIsRecording.set(false);
|
mVideoIsRecording.set(false);
|
||||||
// Begin Signal Custom Code Block
|
listener.onVideoSaved(savedFile);
|
||||||
listener.onVideoSaved(savedFileDescriptor);
|
|
||||||
// End Signal Custom Code Block
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(
|
public void onError(
|
||||||
VideoCapture.VideoCaptureError useCaseError,
|
@NonNull VideoCapture.VideoCaptureError videoCaptureError,
|
||||||
String message,
|
@NonNull String message,
|
||||||
@Nullable Throwable cause) {
|
@Nullable Throwable cause) {
|
||||||
mVideoIsRecording.set(false);
|
mVideoIsRecording.set(false);
|
||||||
Log.e(TAG, message, cause);
|
Log.e(TAG, message, cause);
|
||||||
listener.onError(useCaseError, message, cause);
|
listener.onError(videoCaptureError, message, cause);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -423,7 +396,7 @@ final class CameraXModule {
|
|||||||
|
|
||||||
// TODO(b/124269166): Rethink how we can handle permissions here.
|
// TODO(b/124269166): Rethink how we can handle permissions here.
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
public void setCameraLensFacing(@Nullable LensFacing lensFacing) {
|
public void setCameraLensFacing(@Nullable CameraX.LensFacing lensFacing) {
|
||||||
// Setting same lens facing is a no-op, so check for that first
|
// Setting same lens facing is a no-op, so check for that first
|
||||||
if (mCameraLensFacing != lensFacing) {
|
if (mCameraLensFacing != lensFacing) {
|
||||||
// If we're not bound to a lifecycle, just update the camera that will be opened when we
|
// If we're not bound to a lifecycle, just update the camera that will be opened when we
|
||||||
@ -438,7 +411,7 @@ final class CameraXModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequiresPermission(permission.CAMERA)
|
@RequiresPermission(permission.CAMERA)
|
||||||
public boolean hasCameraWithLensFacing(LensFacing lensFacing) {
|
public boolean hasCameraWithLensFacing(CameraX.LensFacing lensFacing) {
|
||||||
String cameraId;
|
String cameraId;
|
||||||
try {
|
try {
|
||||||
cameraId = CameraX.getCameraWithLensFacing(lensFacing);
|
cameraId = CameraX.getCameraWithLensFacing(lensFacing);
|
||||||
@ -450,14 +423,14 @@ final class CameraXModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public LensFacing getLensFacing() {
|
public CameraX.LensFacing getLensFacing() {
|
||||||
return mCameraLensFacing;
|
return mCameraLensFacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void toggleCamera() {
|
public void toggleCamera() {
|
||||||
// TODO(b/124269166): Rethink how we can handle permissions here.
|
// TODO(b/124269166): Rethink how we can handle permissions here.
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
Set<LensFacing> availableCameraLensFacing = getAvailableCameraLensFacing();
|
Set<CameraX.LensFacing> availableCameraLensFacing = getAvailableCameraLensFacing();
|
||||||
|
|
||||||
if (availableCameraLensFacing.isEmpty()) {
|
if (availableCameraLensFacing.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
@ -468,44 +441,19 @@ final class CameraXModule {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mCameraLensFacing == LensFacing.BACK
|
if (mCameraLensFacing == CameraX.LensFacing.BACK
|
||||||
&& availableCameraLensFacing.contains(LensFacing.FRONT)) {
|
&& availableCameraLensFacing.contains(CameraX.LensFacing.FRONT)) {
|
||||||
setCameraLensFacing(LensFacing.FRONT);
|
setCameraLensFacing(CameraX.LensFacing.FRONT);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mCameraLensFacing == LensFacing.FRONT
|
if (mCameraLensFacing == CameraX.LensFacing.FRONT
|
||||||
&& availableCameraLensFacing.contains(LensFacing.BACK)) {
|
&& availableCameraLensFacing.contains(CameraX.LensFacing.BACK)) {
|
||||||
setCameraLensFacing(LensFacing.BACK);
|
setCameraLensFacing(CameraX.LensFacing.BACK);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void focus(Rect focus, Rect metering) {
|
|
||||||
if (mPreview == null) {
|
|
||||||
// Nothing to focus on since we don't yet have a preview
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Rect rescaledFocus;
|
|
||||||
Rect rescaledMetering;
|
|
||||||
try {
|
|
||||||
Rect sensorRegion;
|
|
||||||
if (mCropRegion != null) {
|
|
||||||
sensorRegion = mCropRegion;
|
|
||||||
} else {
|
|
||||||
sensorRegion = getSensorSize(getActiveCamera());
|
|
||||||
}
|
|
||||||
rescaledFocus = rescaleViewRectToSensorRect(focus, sensorRegion);
|
|
||||||
rescaledMetering = rescaleViewRectToSensorRect(metering, sensorRegion);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Failed to rescale the focus and metering rectangles.", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mPreview.focus(rescaledFocus, rescaledMetering);
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getZoomLevel() {
|
public float getZoomLevel() {
|
||||||
return mZoomLevel;
|
return mZoomLevel;
|
||||||
}
|
}
|
||||||
@ -604,17 +552,17 @@ final class CameraXModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int getRelativeCameraOrientation(boolean compensateForMirroring) {
|
int getRelativeCameraOrientation(boolean compensateForMirroring) {
|
||||||
int rotationDegrees;
|
int rotationDegrees = 0;
|
||||||
try {
|
try {
|
||||||
String cameraId = CameraX.getCameraWithLensFacing(getLensFacing());
|
CameraInfo cameraInfo = CameraX.getCameraInfo(getLensFacing());
|
||||||
CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
|
|
||||||
rotationDegrees = cameraInfo.getSensorRotationDegrees(getDisplaySurfaceRotation());
|
rotationDegrees = cameraInfo.getSensorRotationDegrees(getDisplaySurfaceRotation());
|
||||||
if (compensateForMirroring) {
|
if (compensateForMirroring) {
|
||||||
rotationDegrees = (360 - rotationDegrees) % 360;
|
rotationDegrees = (360 - rotationDegrees) % 360;
|
||||||
}
|
}
|
||||||
|
} catch (CameraInfoUnavailableException e) {
|
||||||
|
Log.e(TAG, "Failed to get CameraInfo", e);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Failed to query camera", e);
|
Log.e(TAG, "Failed to query camera", e);
|
||||||
rotationDegrees = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rotationDegrees;
|
return rotationDegrees;
|
||||||
@ -678,30 +626,28 @@ final class CameraXModule {
|
|||||||
// Update view related information used in use cases
|
// Update view related information used in use cases
|
||||||
private void updateViewInfo() {
|
private void updateViewInfo() {
|
||||||
if (mImageCapture != null) {
|
if (mImageCapture != null) {
|
||||||
mImageCapture.setTargetAspectRatio(new Rational(getWidth(), getHeight()));
|
mImageCapture.setTargetAspectRatioCustom(new Rational(getWidth(), getHeight()));
|
||||||
mImageCapture.setTargetRotation(getDisplaySurfaceRotation());
|
mImageCapture.setTargetRotation(getDisplaySurfaceRotation());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin Signal Custom Code Block
|
if (mVideoCapture != null && MediaConstraints.isVideoTranscodeAvailable()) {
|
||||||
if (mImageCapture != null && MediaConstraints.isVideoTranscodeAvailable()) {
|
|
||||||
// End Signal Custom Code Block
|
|
||||||
mVideoCapture.setTargetRotation(getDisplaySurfaceRotation());
|
mVideoCapture.setTargetRotation(getDisplaySurfaceRotation());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresPermission(permission.CAMERA)
|
@RequiresPermission(permission.CAMERA)
|
||||||
private Set<LensFacing> getAvailableCameraLensFacing() {
|
private Set<CameraX.LensFacing> getAvailableCameraLensFacing() {
|
||||||
// Start with all camera directions
|
// Start with all camera directions
|
||||||
Set<LensFacing> available = new LinkedHashSet<>(Arrays.asList(LensFacing.values()));
|
Set<CameraX.LensFacing> available = new LinkedHashSet<>(Arrays.asList(CameraX.LensFacing.values()));
|
||||||
|
|
||||||
// If we're bound to a lifecycle, remove unavailable cameras
|
// If we're bound to a lifecycle, remove unavailable cameras
|
||||||
if (mCurrentLifecycle != null) {
|
if (mCurrentLifecycle != null) {
|
||||||
if (!hasCameraWithLensFacing(LensFacing.BACK)) {
|
if (!hasCameraWithLensFacing(CameraX.LensFacing.BACK)) {
|
||||||
available.remove(LensFacing.BACK);
|
available.remove(CameraX.LensFacing.BACK);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasCameraWithLensFacing(LensFacing.FRONT)) {
|
if (!hasCameraWithLensFacing(CameraX.LensFacing.FRONT)) {
|
||||||
available.remove(LensFacing.FRONT);
|
available.remove(CameraX.LensFacing.FRONT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -723,16 +669,6 @@ final class CameraXModule {
|
|||||||
mImageCapture.setFlashMode(flash);
|
mImageCapture.setFlashMode(flash);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasFlash() {
|
|
||||||
try {
|
|
||||||
Boolean flashInfoAvailable = mCameraManager.getCameraCharacteristics(getActiveCamera())
|
|
||||||
.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
|
|
||||||
return flashInfoAvailable == Boolean.TRUE;
|
|
||||||
} catch (CameraInfoUnavailableException | CameraAccessException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void enableTorch(boolean torch) {
|
public void enableTorch(boolean torch) {
|
||||||
if (mPreview == null) {
|
if (mPreview == null) {
|
||||||
return;
|
return;
|
||||||
@ -748,48 +684,59 @@ final class CameraXModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Context getContext() {
|
public Context getContext() {
|
||||||
return mCameraXView.getContext();
|
return mCameraView.getContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getWidth() {
|
public int getWidth() {
|
||||||
return mCameraXView.getWidth();
|
return mCameraView.getWidth();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getHeight() {
|
public int getHeight() {
|
||||||
return mCameraXView.getHeight();
|
return mCameraView.getHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getDisplayRotationDegrees() {
|
public int getDisplayRotationDegrees() {
|
||||||
return CameraOrientationUtil.surfaceRotationToDegrees(getDisplaySurfaceRotation());
|
return CameraOrientationUtil.surfaceRotationToDegrees(getDisplaySurfaceRotation());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Begin Signal Custom Code Block
|
||||||
|
public boolean hasFlash() {
|
||||||
|
try {
|
||||||
|
LiveData<Boolean> isFlashAvailable = CameraX.getCameraInfo(getLensFacing()).isFlashAvailable();
|
||||||
|
return isFlashAvailable.getValue() == Boolean.TRUE;
|
||||||
|
} catch (CameraInfoUnavailableException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// End Signal Custom Code Block
|
||||||
|
|
||||||
protected int getDisplaySurfaceRotation() {
|
protected int getDisplaySurfaceRotation() {
|
||||||
return mCameraXView.getDisplaySurfaceRotation();
|
return mCameraView.getDisplaySurfaceRotation();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSurfaceTexture(SurfaceTexture st) {
|
public void setSurfaceTexture(SurfaceTexture st) {
|
||||||
mCameraXView.setSurfaceTexture(st);
|
mCameraView.setSurfaceTexture(st);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getPreviewWidth() {
|
private int getPreviewWidth() {
|
||||||
return mCameraXView.getPreviewWidth();
|
return mCameraView.getPreviewWidth();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getPreviewHeight() {
|
private int getPreviewHeight() {
|
||||||
return mCameraXView.getPreviewHeight();
|
return mCameraView.getPreviewHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getMeasuredWidth() {
|
private int getMeasuredWidth() {
|
||||||
return mCameraXView.getMeasuredWidth();
|
return mCameraView.getMeasuredWidth();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getMeasuredHeight() {
|
private int getMeasuredHeight() {
|
||||||
return mCameraXView.getMeasuredHeight();
|
return mCameraView.getMeasuredHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setTransform(final Matrix matrix) {
|
void setTransform(final Matrix matrix) {
|
||||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||||
mCameraXView.post(
|
mCameraView.post(
|
||||||
new Runnable() {
|
new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@ -797,7 +744,7 @@ final class CameraXModule {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
mCameraXView.setTransform(matrix);
|
mCameraView.setTransform(matrix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -810,14 +757,14 @@ final class CameraXModule {
|
|||||||
* @param height height of camera source buffers.
|
* @param height height of camera source buffers.
|
||||||
*/
|
*/
|
||||||
void onPreviewSourceDimensUpdated(int width, int height) {
|
void onPreviewSourceDimensUpdated(int width, int height) {
|
||||||
mCameraXView.onPreviewSourceDimensUpdated(width, height);
|
mCameraView.onPreviewSourceDimensUpdated(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CaptureMode getCaptureMode() {
|
public CameraXView.CaptureMode getCaptureMode() {
|
||||||
return mCaptureMode;
|
return mCaptureMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCaptureMode(CaptureMode captureMode) {
|
public void setCaptureMode(CameraXView.CaptureMode captureMode) {
|
||||||
this.mCaptureMode = captureMode;
|
this.mCaptureMode = captureMode;
|
||||||
rebindToLifecycle();
|
rebindToLifecycle();
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
package org.thoughtcrime.securesms.mediasend.camerax;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 The Android Open Source Project
|
* Copyright (C) 2019 The Android Open Source Project
|
||||||
*
|
*
|
||||||
@ -16,9 +14,12 @@ package org.thoughtcrime.securesms.mediasend.camerax;
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||||
|
|
||||||
import android.Manifest.permission;
|
import android.Manifest.permission;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
import android.graphics.Matrix;
|
import android.graphics.Matrix;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
@ -31,6 +32,7 @@ import android.os.Looper;
|
|||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Log;
|
||||||
import android.util.Size;
|
import android.util.Size;
|
||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
@ -40,31 +42,35 @@ import android.view.TextureView;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewConfiguration;
|
import android.view.ViewConfiguration;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.view.animation.BaseInterpolator;
|
||||||
import android.view.animation.DecelerateInterpolator;
|
import android.view.animation.DecelerateInterpolator;
|
||||||
import android.view.animation.Interpolator;
|
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.annotation.RequiresPermission;
|
import androidx.annotation.RequiresPermission;
|
||||||
import androidx.annotation.RestrictTo;
|
import androidx.annotation.RestrictTo;
|
||||||
import androidx.annotation.RestrictTo.Scope;
|
import androidx.annotation.RestrictTo.Scope;
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
import androidx.camera.core.CameraX.LensFacing;
|
import androidx.camera.core.CameraInfoUnavailableException;
|
||||||
|
import androidx.camera.core.CameraX;
|
||||||
import androidx.camera.core.FlashMode;
|
import androidx.camera.core.FlashMode;
|
||||||
import androidx.camera.core.ImageCapture.OnImageCapturedListener;
|
import androidx.camera.core.FocusMeteringAction;
|
||||||
import androidx.camera.core.ImageCapture.OnImageSavedListener;
|
import androidx.camera.core.ImageCapture;
|
||||||
import androidx.camera.core.ImageProxy;
|
import androidx.camera.core.MeteringPoint;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link View} that displays a preview of the camera with methods {@link
|
* A {@link View} that displays a preview of the camera with methods {@link
|
||||||
* #takePicture(OnImageCapturedListener)}, {@link #takePicture(File, OnImageSavedListener)}, {@link
|
* #takePicture(Executor, OnImageCapturedListener)},
|
||||||
* #startRecording(File, OnVideoSavedListener)} and {@link #stopRecording()}.
|
* {@link #takePicture(File, Executor, OnImageSavedListener)},
|
||||||
|
* {@link #startRecording(File, Executor, OnVideoSavedListener)} and {@link #stopRecording()}.
|
||||||
*
|
*
|
||||||
* <p>Because the Camera is a limited resource and consumes a high amount of power, CameraView must
|
* <p>Because the Camera is a limited resource and consumes a high amount of power, CameraView must
|
||||||
* be opened/closed. CameraView will handle opening/closing automatically through use of a {@link
|
* be opened/closed. CameraView will handle opening/closing automatically through use of a {@link
|
||||||
@ -88,8 +94,12 @@ public final class CameraXView extends ViewGroup {
|
|||||||
private static final String EXTRA_CAMERA_DIRECTION = "camera_direction";
|
private static final String EXTRA_CAMERA_DIRECTION = "camera_direction";
|
||||||
private static final String EXTRA_CAPTURE_MODE = "captureMode";
|
private static final String EXTRA_CAPTURE_MODE = "captureMode";
|
||||||
|
|
||||||
private final Rect mFocusingRect = new Rect();
|
private static final int LENS_FACING_NONE = 0;
|
||||||
private final Rect mMeteringRect = new Rect();
|
private static final int LENS_FACING_FRONT = 1;
|
||||||
|
private static final int LENS_FACING_BACK = 2;
|
||||||
|
private static final int FLASH_MODE_AUTO = 1;
|
||||||
|
private static final int FLASH_MODE_ON = 2;
|
||||||
|
private static final int FLASH_MODE_OFF = 4;
|
||||||
// For tap-to-focus
|
// For tap-to-focus
|
||||||
private long mDownEventTimestamp;
|
private long mDownEventTimestamp;
|
||||||
// For pinch-to-zoom
|
// For pinch-to-zoom
|
||||||
@ -116,8 +126,7 @@ public final class CameraXView extends ViewGroup {
|
|||||||
private ScaleType mScaleType = ScaleType.CENTER_CROP;
|
private ScaleType mScaleType = ScaleType.CENTER_CROP;
|
||||||
// For accessibility event
|
// For accessibility event
|
||||||
private MotionEvent mUpEvent;
|
private MotionEvent mUpEvent;
|
||||||
private @Nullable
|
private @Nullable Paint mLayerPaint;
|
||||||
Paint mLayerPaint;
|
|
||||||
|
|
||||||
public CameraXView(Context context) {
|
public CameraXView(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
@ -188,11 +197,52 @@ public final class CameraXView extends ViewGroup {
|
|||||||
onPreviewSourceDimensUpdated(640, 480);
|
onPreviewSourceDimensUpdated(640, 480);
|
||||||
}
|
}
|
||||||
|
|
||||||
setScaleType(ScaleType.CENTER_CROP);
|
if (attrs != null) {
|
||||||
setPinchToZoomEnabled(true);
|
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView);
|
||||||
setCaptureMode(CaptureMode.IMAGE);
|
setScaleType(
|
||||||
setCameraLensFacing(LensFacing.FRONT);
|
ScaleType.fromId(
|
||||||
setFlash(FlashMode.OFF);
|
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) {
|
if (getBackground() == null) {
|
||||||
setBackgroundColor(0xFF111111);
|
setBackgroundColor(0xFF111111);
|
||||||
@ -245,7 +295,7 @@ public final class CameraXView extends ViewGroup {
|
|||||||
setCameraLensFacing(
|
setCameraLensFacing(
|
||||||
TextUtils.isEmpty(lensFacingString)
|
TextUtils.isEmpty(lensFacingString)
|
||||||
? null
|
? null
|
||||||
: LensFacing.valueOf(lensFacingString));
|
: CameraX.LensFacing.valueOf(lensFacingString));
|
||||||
setCaptureMode(CaptureMode.fromId(state.getInt(EXTRA_CAPTURE_MODE)));
|
setCaptureMode(CaptureMode.fromId(state.getInt(EXTRA_CAPTURE_MODE)));
|
||||||
} else {
|
} else {
|
||||||
super.onRestoreInstanceState(savedState);
|
super.onRestoreInstanceState(savedState);
|
||||||
@ -578,33 +628,42 @@ public final class CameraXView extends ViewGroup {
|
|||||||
* Takes a picture, and calls {@link OnImageCapturedListener#onCaptureSuccess(ImageProxy, int)}
|
* Takes a picture, and calls {@link OnImageCapturedListener#onCaptureSuccess(ImageProxy, int)}
|
||||||
* once when done.
|
* once when done.
|
||||||
*
|
*
|
||||||
|
* @param executor The executor in which the listener callback methods will be run.
|
||||||
* @param listener Listener which will receive success or failure callbacks.
|
* @param listener Listener which will receive success or failure callbacks.
|
||||||
*/
|
*/
|
||||||
public void takePicture(OnImageCapturedListener listener) {
|
@SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
|
||||||
mCameraModule.takePicture(listener);
|
public void takePicture(@NonNull Executor executor, @NonNull ImageCapture.OnImageCapturedListener listener) {
|
||||||
|
mCameraModule.takePicture(executor, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a picture and calls {@link OnImageSavedListener#onImageSaved(File)} when done.
|
* Takes a picture and calls {@link OnImageSavedListener#onImageSaved(File)} when done.
|
||||||
*
|
*
|
||||||
* @param file The destination.
|
* @param file The destination.
|
||||||
|
* @param executor The executor in which the listener callback methods will be run.
|
||||||
* @param listener Listener which will receive success or failure callbacks.
|
* @param listener Listener which will receive success or failure callbacks.
|
||||||
*/
|
*/
|
||||||
public void takePicture(File file, OnImageSavedListener listener) {
|
@SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
|
||||||
mCameraModule.takePicture(file, listener);
|
public void takePicture(@NonNull File file, @NonNull Executor executor,
|
||||||
|
@NonNull ImageCapture.OnImageSavedListener listener) {
|
||||||
|
mCameraModule.takePicture(file, executor, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin Signal Custom Code Block
|
|
||||||
/**
|
/**
|
||||||
* Takes a video and calls the OnVideoSavedListener when done.
|
* Takes a video and calls the OnVideoSavedListener when done.
|
||||||
*
|
*
|
||||||
* @param fileDescriptor The destination.
|
* @param file The destination.
|
||||||
|
* @param executor The executor in which the listener callback methods will be run.
|
||||||
|
* @param listener Listener which will receive success or failure callbacks.
|
||||||
*/
|
*/
|
||||||
|
// Begin Signal Custom Code Block
|
||||||
@RequiresApi(26)
|
@RequiresApi(26)
|
||||||
public void startRecording(FileDescriptor fileDescriptor, VideoCapture.OnVideoSavedListener listener) {
|
@SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
|
||||||
mCameraModule.startRecording(fileDescriptor, listener);
|
public void startRecording(@NonNull FileDescriptor file, @NonNull Executor executor,
|
||||||
}
|
|
||||||
// End Signal Custom Code Block
|
// End Signal Custom Code Block
|
||||||
|
@NonNull VideoCapture.OnVideoSavedListener listener) {
|
||||||
|
mCameraModule.startRecording(file, executor, listener);
|
||||||
|
}
|
||||||
|
|
||||||
/** Stops an in progress video. */
|
/** Stops an in progress video. */
|
||||||
// Begin Signal Custom Code Block
|
// Begin Signal Custom Code Block
|
||||||
@ -626,7 +685,7 @@ public final class CameraXView extends ViewGroup {
|
|||||||
* @throws IllegalStateException if the CAMERA permission is not currently granted.
|
* @throws IllegalStateException if the CAMERA permission is not currently granted.
|
||||||
*/
|
*/
|
||||||
@RequiresPermission(permission.CAMERA)
|
@RequiresPermission(permission.CAMERA)
|
||||||
public boolean hasCameraWithLensFacing(LensFacing lensFacing) {
|
public boolean hasCameraWithLensFacing(CameraX.LensFacing lensFacing) {
|
||||||
return mCameraModule.hasCameraWithLensFacing(lensFacing);
|
return mCameraModule.hasCameraWithLensFacing(lensFacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -655,29 +714,21 @@ public final class CameraXView extends ViewGroup {
|
|||||||
*
|
*
|
||||||
* @param lensFacing The desired camera lensFacing.
|
* @param lensFacing The desired camera lensFacing.
|
||||||
*/
|
*/
|
||||||
public void setCameraLensFacing(@Nullable LensFacing lensFacing) {
|
public void setCameraLensFacing(@Nullable CameraX.LensFacing lensFacing) {
|
||||||
mCameraModule.setCameraLensFacing(lensFacing);
|
mCameraModule.setCameraLensFacing(lensFacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the currently selected {@link LensFacing}. */
|
/** Returns the currently selected {@link LensFacing}. */
|
||||||
@Nullable
|
@Nullable
|
||||||
public LensFacing getCameraLensFacing() {
|
public CameraX.LensFacing getCameraLensFacing() {
|
||||||
return mCameraModule.getLensFacing();
|
return mCameraModule.getLensFacing();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Begin Signal Custom Code Block
|
||||||
* Focuses the camera on the given area.
|
public boolean hasFlash() {
|
||||||
*
|
return mCameraModule.hasFlash();
|
||||||
* <p>Sets the focus and exposure metering rectangles. Coordinates for both X and Y dimensions
|
|
||||||
* are Limited from -1000 to 1000, where (0, 0) is the center of the image and the width/height
|
|
||||||
* represent the values from -1000 to 1000.
|
|
||||||
*
|
|
||||||
* @param focus Area used to focus the camera.
|
|
||||||
* @param metering Area used for exposure metering.
|
|
||||||
*/
|
|
||||||
public void focus(Rect focus, Rect metering) {
|
|
||||||
mCameraModule.focus(focus, metering);
|
|
||||||
}
|
}
|
||||||
|
// End Signal Custom Code Block
|
||||||
|
|
||||||
/** Gets the active flash strategy. */
|
/** Gets the active flash strategy. */
|
||||||
public FlashMode getFlash() {
|
public FlashMode getFlash() {
|
||||||
@ -685,14 +736,10 @@ public final class CameraXView extends ViewGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the active flash strategy. */
|
/** Sets the active flash strategy. */
|
||||||
public void setFlash(FlashMode flashMode) {
|
public void setFlash(@NonNull FlashMode flashMode) {
|
||||||
mCameraModule.setFlash(flashMode);
|
mCameraModule.setFlash(flashMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasFlash() {
|
|
||||||
return mCameraModule.hasFlash();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getRelativeCameraOrientation(boolean compensateForMirroring) {
|
private int getRelativeCameraOrientation(boolean compensateForMirroring) {
|
||||||
return mCameraModule.getRelativeCameraOrientation(compensateForMirroring);
|
return mCameraModule.getRelativeCameraOrientation(compensateForMirroring);
|
||||||
}
|
}
|
||||||
@ -702,7 +749,7 @@ public final class CameraXView extends ViewGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouchEvent(MotionEvent event) {
|
public boolean onTouchEvent(@NonNull MotionEvent event) {
|
||||||
// Disable pinch-to-zoom and tap-to-focus while the camera module is paused.
|
// Disable pinch-to-zoom and tap-to-focus while the camera module is paused.
|
||||||
if (mCameraModule.isPaused()) {
|
if (mCameraModule.isPaused()) {
|
||||||
return false;
|
return false;
|
||||||
@ -745,10 +792,21 @@ public final class CameraXView extends ViewGroup {
|
|||||||
final float x = (mUpEvent != null) ? mUpEvent.getX() : getX() + getWidth() / 2f;
|
final float x = (mUpEvent != null) ? mUpEvent.getX() : getX() + getWidth() / 2f;
|
||||||
final float y = (mUpEvent != null) ? mUpEvent.getY() : getY() + getHeight() / 2f;
|
final float y = (mUpEvent != null) ? mUpEvent.getY() : getY() + getHeight() / 2f;
|
||||||
mUpEvent = null;
|
mUpEvent = null;
|
||||||
calculateTapArea(mFocusingRect, x, y, 1f);
|
|
||||||
calculateTapArea(mMeteringRect, x, y, 1.5f);
|
TextureViewMeteringPointFactory pointFactory = new TextureViewMeteringPointFactory(
|
||||||
if (area(mFocusingRect) > 0 && area(mMeteringRect) > 0) {
|
mCameraTextureView);
|
||||||
focus(mFocusingRect, mMeteringRect);
|
float afPointWidth = 1.0f / 6.0f; // 1/6 total area
|
||||||
|
float aePointWidth = afPointWidth * 1.5f;
|
||||||
|
MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth, 1.0f);
|
||||||
|
MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth, 1.0f);
|
||||||
|
|
||||||
|
try {
|
||||||
|
CameraX.getCameraControl(getCameraLensFacing()).startFocusAndMetering(
|
||||||
|
FocusMeteringAction.Builder.from(afPoint, FocusMeteringAction.MeteringMode.AF_ONLY)
|
||||||
|
.addPoint(aePoint, FocusMeteringAction.MeteringMode.AE_ONLY)
|
||||||
|
.build());
|
||||||
|
} catch (CameraInfoUnavailableException e) {
|
||||||
|
Log.d(TAG, "cannot access camera", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -759,80 +817,6 @@ public final class CameraXView extends ViewGroup {
|
|||||||
return rect.width() * rect.height();
|
return rect.width() * rect.height();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The area must be between -1000,-1000 and 1000,1000 */
|
|
||||||
private void calculateTapArea(Rect rect, float x, float y, float coefficient) {
|
|
||||||
int max = 1000;
|
|
||||||
int min = -1000;
|
|
||||||
|
|
||||||
// Default to 300 (1/6th the total area) and scale by the coefficient
|
|
||||||
int areaSize = (int) (300 * coefficient);
|
|
||||||
|
|
||||||
// Rotate the coordinates if the camera orientation is different
|
|
||||||
int width = getWidth();
|
|
||||||
int height = getHeight();
|
|
||||||
|
|
||||||
// Compensate orientation as it's mirrored on preview for forward facing cameras
|
|
||||||
boolean compensateForMirroring = (getCameraLensFacing() == LensFacing.FRONT);
|
|
||||||
int relativeCameraOrientation = getRelativeCameraOrientation(compensateForMirroring);
|
|
||||||
int temp;
|
|
||||||
float tempf;
|
|
||||||
switch (relativeCameraOrientation) {
|
|
||||||
case 90:
|
|
||||||
// Fall-through
|
|
||||||
case 270:
|
|
||||||
// We're horizontal. Swap width/height. Swap x/y.
|
|
||||||
temp = width;
|
|
||||||
//noinspection SuspiciousNameCombination
|
|
||||||
width = height;
|
|
||||||
height = temp;
|
|
||||||
|
|
||||||
tempf = x;
|
|
||||||
//noinspection SuspiciousNameCombination
|
|
||||||
x = y;
|
|
||||||
y = tempf;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (relativeCameraOrientation) {
|
|
||||||
// Map to correct coordinates according to relativeCameraOrientation
|
|
||||||
case 90:
|
|
||||||
y = height - y;
|
|
||||||
break;
|
|
||||||
case 180:
|
|
||||||
x = width - x;
|
|
||||||
y = height - y;
|
|
||||||
break;
|
|
||||||
case 270:
|
|
||||||
x = width - x;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swap x if it's a mirrored preview
|
|
||||||
if (compensateForMirroring) {
|
|
||||||
x = width - x;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab the x, y position from within the View and normalize it to -1000 to 1000
|
|
||||||
x = min + distance(max, min) * (x / width);
|
|
||||||
y = min + distance(max, min) * (y / height);
|
|
||||||
|
|
||||||
// Modify the rect to the bounding area
|
|
||||||
rect.top = (int) y - areaSize / 2;
|
|
||||||
rect.left = (int) x - areaSize / 2;
|
|
||||||
rect.bottom = rect.top + areaSize;
|
|
||||||
rect.right = rect.left + areaSize;
|
|
||||||
|
|
||||||
// Cap at -1000 to 1000
|
|
||||||
rect.top = rangeLimit(rect.top, max, min);
|
|
||||||
rect.left = rangeLimit(rect.left, max, min);
|
|
||||||
rect.bottom = rangeLimit(rect.bottom, max, min);
|
|
||||||
rect.right = rangeLimit(rect.right, max, min);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int rangeLimit(int val, int max, int min) {
|
private int rangeLimit(int val, int max, int min) {
|
||||||
return Math.min(Math.max(val, min), max);
|
return Math.min(Math.max(val, min), max);
|
||||||
}
|
}
|
||||||
@ -975,7 +959,7 @@ public final class CameraXView extends ViewGroup {
|
|||||||
* The capture mode used by CameraView.
|
* The capture mode used by CameraView.
|
||||||
*
|
*
|
||||||
* <p>This enum can be used to determine which capture mode will be enabled for {@link
|
* <p>This enum can be used to determine which capture mode will be enabled for {@link
|
||||||
* CameraXView}.
|
* CameraView}.
|
||||||
*/
|
*/
|
||||||
public enum CaptureMode {
|
public enum CaptureMode {
|
||||||
/** A mode where image capture is enabled. */
|
/** A mode where image capture is enabled. */
|
||||||
@ -1024,7 +1008,7 @@ public final class CameraXView extends ViewGroup {
|
|||||||
private class PinchToZoomGestureDetector extends ScaleGestureDetector
|
private class PinchToZoomGestureDetector extends ScaleGestureDetector
|
||||||
implements ScaleGestureDetector.OnScaleGestureListener {
|
implements ScaleGestureDetector.OnScaleGestureListener {
|
||||||
private static final float SCALE_MULTIPIER = 0.75f;
|
private static final float SCALE_MULTIPIER = 0.75f;
|
||||||
private final Interpolator mInterpolator = new DecelerateInterpolator(2f);
|
private final BaseInterpolator mInterpolator = new DecelerateInterpolator(2f);
|
||||||
private float mNormalizedScaleFactor = 0;
|
private float mNormalizedScaleFactor = 0;
|
||||||
|
|
||||||
PinchToZoomGestureDetector(Context context) {
|
PinchToZoomGestureDetector(Context context) {
|
||||||
|
@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||||
|
|
||||||
|
import android.graphics.Matrix;
|
||||||
|
import android.graphics.PointF;
|
||||||
|
import android.graphics.SurfaceTexture;
|
||||||
|
import android.view.TextureView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.camera.core.MeteringPoint;
|
||||||
|
import androidx.camera.core.MeteringPointFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link MeteringPointFactory} for creating a {@link MeteringPoint} by {@link TextureView} and
|
||||||
|
* (x,y).
|
||||||
|
*
|
||||||
|
* <p>SurfaceTexture in TextureView could be cropped, scaled or rotated by
|
||||||
|
* {@link TextureView#getTransform(Matrix)}. This factory translates the (x, y) into the sensor
|
||||||
|
* crop region normalized (x,y) by this transform. {@link SurfaceTexture#getTransformMatrix} is
|
||||||
|
* also used during the translation. No lens facing information is required because
|
||||||
|
* {@link SurfaceTexture#getTransformMatrix} contains the necessary transformation corresponding
|
||||||
|
* to the lens face of current camera ouput.
|
||||||
|
*/
|
||||||
|
public class TextureViewMeteringPointFactory extends MeteringPointFactory {
|
||||||
|
private final TextureView mTextureView;
|
||||||
|
|
||||||
|
public TextureViewMeteringPointFactory(@NonNull TextureView textureView) {
|
||||||
|
mTextureView = textureView;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates a (x,y) from TextureView.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
protected PointF translatePoint(float x, float y) {
|
||||||
|
Matrix transform = new Matrix();
|
||||||
|
mTextureView.getTransform(transform);
|
||||||
|
|
||||||
|
// applying reverse of TextureView#getTransform
|
||||||
|
Matrix inverse = new Matrix();
|
||||||
|
transform.invert(inverse);
|
||||||
|
float[] pt = new float[]{x, y};
|
||||||
|
inverse.mapPoints(pt);
|
||||||
|
|
||||||
|
// get SurfaceTexture#getTransformMatrix
|
||||||
|
float[] surfaceTextureMat = new float[16];
|
||||||
|
mTextureView.getSurfaceTexture().getTransformMatrix(surfaceTextureMat);
|
||||||
|
|
||||||
|
// convert SurfaceTexture#getTransformMatrix(4x4 column major 3D matrix) to
|
||||||
|
// android.graphics.Matrix(3x3 row major 2D matrix)
|
||||||
|
Matrix surfaceTextureTransform = glMatrixToGraphicsMatrix(surfaceTextureMat);
|
||||||
|
|
||||||
|
float[] pt2 = new float[2];
|
||||||
|
// convert to texture coordinates first.
|
||||||
|
pt2[0] = pt[0] / mTextureView.getWidth();
|
||||||
|
pt2[1] = (mTextureView.getHeight() - pt[1]) / mTextureView.getHeight();
|
||||||
|
surfaceTextureTransform.mapPoints(pt2);
|
||||||
|
|
||||||
|
return new PointF(pt2[0], pt2[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Matrix glMatrixToGraphicsMatrix(float[] glMatrix) {
|
||||||
|
float[] convert = new float[9];
|
||||||
|
convert[0] = glMatrix[0];
|
||||||
|
convert[1] = glMatrix[4];
|
||||||
|
convert[2] = glMatrix[12];
|
||||||
|
convert[3] = glMatrix[1];
|
||||||
|
convert[4] = glMatrix[5];
|
||||||
|
convert[5] = glMatrix[13];
|
||||||
|
convert[6] = glMatrix[3];
|
||||||
|
convert[7] = glMatrix[7];
|
||||||
|
convert[8] = glMatrix[15];
|
||||||
|
Matrix graphicsMatrix = new Matrix();
|
||||||
|
graphicsMatrix.setValues(convert);
|
||||||
|
return graphicsMatrix;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,3 @@
|
|||||||
package org.thoughtcrime.securesms.mediasend.camerax;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 The Android Open Source Project
|
* Copyright (C) 2019 The Android Open Source Project
|
||||||
*
|
*
|
||||||
@ -16,18 +14,24 @@ package org.thoughtcrime.securesms.mediasend.camerax;
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.location.Location;
|
import android.location.Location;
|
||||||
import android.media.AudioFormat;
|
import android.media.AudioFormat;
|
||||||
import android.media.AudioRecord;
|
import android.media.AudioRecord;
|
||||||
import android.media.CamcorderProfile;
|
import android.media.CamcorderProfile;
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
|
import android.media.MediaCodec.BufferInfo;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
|
import android.media.MediaCodecInfo.CodecCapabilities;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
import android.media.MediaMuxer;
|
import android.media.MediaMuxer;
|
||||||
import android.media.MediaRecorder;
|
import android.media.MediaRecorder.AudioSource;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.HandlerThread;
|
import android.os.HandlerThread;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import android.util.Log;
|
||||||
import android.util.Size;
|
import android.util.Size;
|
||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
@ -37,13 +41,16 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.annotation.RestrictTo;
|
import androidx.annotation.RestrictTo;
|
||||||
import androidx.camera.core.CameraInfo;
|
import androidx.annotation.RestrictTo.Scope;
|
||||||
|
import androidx.camera.core.CameraInfoInternal;
|
||||||
import androidx.camera.core.CameraInfoUnavailableException;
|
import androidx.camera.core.CameraInfoUnavailableException;
|
||||||
import androidx.camera.core.CameraX;
|
import androidx.camera.core.CameraX;
|
||||||
|
import androidx.camera.core.CameraX.LensFacing;
|
||||||
import androidx.camera.core.CameraXThreads;
|
import androidx.camera.core.CameraXThreads;
|
||||||
import androidx.camera.core.ConfigProvider;
|
import androidx.camera.core.ConfigProvider;
|
||||||
import androidx.camera.core.DeferrableSurface;
|
import androidx.camera.core.DeferrableSurface;
|
||||||
import androidx.camera.core.ImageOutputConfig;
|
import androidx.camera.core.ImageOutputConfig;
|
||||||
|
import androidx.camera.core.ImageOutputConfig.RotationValue;
|
||||||
import androidx.camera.core.ImmediateSurface;
|
import androidx.camera.core.ImmediateSurface;
|
||||||
import androidx.camera.core.SessionConfig;
|
import androidx.camera.core.SessionConfig;
|
||||||
import androidx.camera.core.UseCase;
|
import androidx.camera.core.UseCase;
|
||||||
@ -51,7 +58,6 @@ import androidx.camera.core.UseCaseConfig;
|
|||||||
import androidx.camera.core.VideoCaptureConfig;
|
import androidx.camera.core.VideoCaptureConfig;
|
||||||
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.video.VideoUtil;
|
import org.thoughtcrime.securesms.video.VideoUtil;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -59,6 +65,8 @@ import java.io.FileDescriptor;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -70,6 +78,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
* @hide In the earlier stage, the VideoCapture is deprioritized.
|
* @hide In the earlier stage, the VideoCapture is deprioritized.
|
||||||
*/
|
*/
|
||||||
@RequiresApi(26)
|
@RequiresApi(26)
|
||||||
|
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||||
public class VideoCapture extends UseCase {
|
public class VideoCapture extends UseCase {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,9 +86,9 @@ public class VideoCapture extends UseCase {
|
|||||||
*
|
*
|
||||||
* @hide
|
* @hide
|
||||||
*/
|
*/
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
|
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||||
public static final VideoCapture.Defaults DEFAULT_CONFIG = new VideoCapture.Defaults();
|
public static final Defaults DEFAULT_CONFIG = new Defaults();
|
||||||
private static final VideoCapture.Metadata EMPTY_METADATA = new VideoCapture.Metadata();
|
private static final Metadata EMPTY_METADATA = new Metadata();
|
||||||
private static final String TAG = "VideoCapture";
|
private static final String TAG = "VideoCapture";
|
||||||
/** Amount of time to wait for dequeuing a buffer from the videoEncoder. */
|
/** Amount of time to wait for dequeuing a buffer from the videoEncoder. */
|
||||||
private static final int DEQUE_TIMEOUT_USEC = 10000;
|
private static final int DEQUE_TIMEOUT_USEC = 10000;
|
||||||
@ -90,10 +99,10 @@ public class VideoCapture extends UseCase {
|
|||||||
// End Signal Custom Code Block
|
// End Signal Custom Code Block
|
||||||
/** Camcorder profiles quality list */
|
/** Camcorder profiles quality list */
|
||||||
private static final int[] CamcorderQuality = {
|
private static final int[] CamcorderQuality = {
|
||||||
CamcorderProfile.QUALITY_2160P,
|
CamcorderProfile.QUALITY_2160P,
|
||||||
CamcorderProfile.QUALITY_1080P,
|
CamcorderProfile.QUALITY_1080P,
|
||||||
CamcorderProfile.QUALITY_720P,
|
CamcorderProfile.QUALITY_720P,
|
||||||
CamcorderProfile.QUALITY_480P
|
CamcorderProfile.QUALITY_480P
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Audio encoding
|
* Audio encoding
|
||||||
@ -101,28 +110,29 @@ public class VideoCapture extends UseCase {
|
|||||||
* <p>the result of PCM_8BIT and PCM_FLOAT are not good. Set PCM_16BIT as the first option.
|
* <p>the result of PCM_8BIT and PCM_FLOAT are not good. Set PCM_16BIT as the first option.
|
||||||
*/
|
*/
|
||||||
private static final short[] sAudioEncoding = {
|
private static final short[] sAudioEncoding = {
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
AudioFormat.ENCODING_PCM_8BIT,
|
AudioFormat.ENCODING_PCM_8BIT,
|
||||||
AudioFormat.ENCODING_PCM_FLOAT
|
AudioFormat.ENCODING_PCM_FLOAT
|
||||||
};
|
};
|
||||||
private final MediaCodec.BufferInfo mVideoBufferInfo = new MediaCodec.BufferInfo();
|
private final BufferInfo mVideoBufferInfo = new BufferInfo();
|
||||||
private final Object mMuxerLock = new Object();
|
private final Object mMuxerLock = new Object();
|
||||||
/** Thread on which all encoding occurs. */
|
/** Thread on which all encoding occurs. */
|
||||||
private final HandlerThread mVideoHandlerThread =
|
private final HandlerThread mVideoHandlerThread =
|
||||||
new HandlerThread(CameraXThreads.TAG + "video encoding thread");
|
new HandlerThread(CameraXThreads.TAG + "video encoding thread");
|
||||||
private final Handler mVideoHandler;
|
private final Handler mVideoHandler;
|
||||||
/** Thread on which audio encoding occurs. */
|
/** Thread on which audio encoding occurs. */
|
||||||
private final HandlerThread mAudioHandlerThread =
|
private final HandlerThread mAudioHandlerThread =
|
||||||
new HandlerThread(CameraXThreads.TAG + "audio encoding thread");
|
new HandlerThread(CameraXThreads.TAG + "audio encoding thread");
|
||||||
private final Handler mAudioHandler;
|
private final Handler mAudioHandler;
|
||||||
private final AtomicBoolean mEndOfVideoStreamSignal = new AtomicBoolean(true);
|
private final AtomicBoolean mEndOfVideoStreamSignal = new AtomicBoolean(true);
|
||||||
private final AtomicBoolean mEndOfAudioStreamSignal = new AtomicBoolean(true);
|
private final AtomicBoolean mEndOfAudioStreamSignal = new AtomicBoolean(true);
|
||||||
private final AtomicBoolean mEndOfAudioVideoSignal = new AtomicBoolean(true);
|
private final AtomicBoolean mEndOfAudioVideoSignal = new AtomicBoolean(true);
|
||||||
private final MediaCodec.BufferInfo mAudioBufferInfo = new MediaCodec.BufferInfo();
|
private final BufferInfo mAudioBufferInfo = new BufferInfo();
|
||||||
/** For record the first sample written time. */
|
/** For record the first sample written time. */
|
||||||
private final AtomicBoolean mIsFirstVideoSampleWrite = new AtomicBoolean(false);
|
private final AtomicBoolean mIsFirstVideoSampleWrite = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean mIsFirstAudioSampleWrite = new AtomicBoolean(false);
|
private final AtomicBoolean mIsFirstAudioSampleWrite = new AtomicBoolean(false);
|
||||||
private final VideoCaptureConfig.Builder mUseCaseConfigBuilder;
|
private final VideoCaptureConfig.Builder mUseCaseConfigBuilder;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
MediaCodec mVideoEncoder;
|
MediaCodec mVideoEncoder;
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -138,6 +148,7 @@ public class VideoCapture extends UseCase {
|
|||||||
/** Surface the camera writes to, which the videoEncoder uses as input. */
|
/** Surface the camera writes to, which the videoEncoder uses as input. */
|
||||||
Surface mCameraSurface;
|
Surface mCameraSurface;
|
||||||
/** audio raw data */
|
/** audio raw data */
|
||||||
|
@NonNull
|
||||||
private AudioRecord mAudioRecorder;
|
private AudioRecord mAudioRecorder;
|
||||||
private int mAudioBufferSize;
|
private int mAudioBufferSize;
|
||||||
private boolean mIsRecording = false;
|
private boolean mIsRecording = false;
|
||||||
@ -167,9 +178,9 @@ public class VideoCapture extends UseCase {
|
|||||||
/** Creates a {@link MediaFormat} using parameters from the configuration */
|
/** Creates a {@link MediaFormat} using parameters from the configuration */
|
||||||
private static MediaFormat createMediaFormat(VideoCaptureConfig config, Size resolution) {
|
private static MediaFormat createMediaFormat(VideoCaptureConfig config, Size resolution) {
|
||||||
MediaFormat format =
|
MediaFormat format =
|
||||||
MediaFormat.createVideoFormat(
|
MediaFormat.createVideoFormat(
|
||||||
VIDEO_MIME_TYPE, resolution.getWidth(), resolution.getHeight());
|
VIDEO_MIME_TYPE, resolution.getWidth(), resolution.getHeight());
|
||||||
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
|
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface);
|
||||||
format.setInteger(MediaFormat.KEY_BIT_RATE, config.getBitRate());
|
format.setInteger(MediaFormat.KEY_BIT_RATE, config.getBitRate());
|
||||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, config.getVideoFrameRate());
|
format.setInteger(MediaFormat.KEY_FRAME_RATE, config.getVideoFrameRate());
|
||||||
// Begin Signal Custom Code Block
|
// Begin Signal Custom Code Block
|
||||||
@ -188,10 +199,10 @@ public class VideoCapture extends UseCase {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
|
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||||
protected UseCaseConfig.Builder<?, ?, ?> getDefaultBuilder(CameraX.LensFacing lensFacing) {
|
protected UseCaseConfig.Builder<?, ?, ?> getDefaultBuilder(LensFacing lensFacing) {
|
||||||
VideoCaptureConfig defaults = CameraX.getDefaultUseCaseConfig(
|
VideoCaptureConfig defaults = CameraX.getDefaultUseCaseConfig(
|
||||||
VideoCaptureConfig.class, lensFacing);
|
VideoCaptureConfig.class, lensFacing);
|
||||||
if (defaults != null) {
|
if (defaults != null) {
|
||||||
return VideoCaptureConfig.Builder.fromConfig(defaults);
|
return VideoCaptureConfig.Builder.fromConfig(defaults);
|
||||||
}
|
}
|
||||||
@ -205,9 +216,9 @@ public class VideoCapture extends UseCase {
|
|||||||
* @hide
|
* @hide
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
|
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||||
protected Map<String, Size> onSuggestedResolutionUpdated(
|
protected Map<String, Size> onSuggestedResolutionUpdated(
|
||||||
Map<String, Size> suggestedResolutionMap) {
|
Map<String, Size> suggestedResolutionMap) {
|
||||||
VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
|
VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
|
||||||
if (mCameraSurface != null) {
|
if (mCameraSurface != null) {
|
||||||
mVideoEncoder.stop();
|
mVideoEncoder.stop();
|
||||||
@ -228,7 +239,7 @@ public class VideoCapture extends UseCase {
|
|||||||
Size resolution = suggestedResolutionMap.get(cameraId);
|
Size resolution = suggestedResolutionMap.get(cameraId);
|
||||||
if (resolution == null) {
|
if (resolution == null) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Suggested resolution map missing resolution for camera " + cameraId);
|
"Suggested resolution map missing resolution for camera " + cameraId);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupEncoder(resolution);
|
setupEncoder(resolution);
|
||||||
@ -240,17 +251,20 @@ public class VideoCapture extends UseCase {
|
|||||||
* called.
|
* called.
|
||||||
*
|
*
|
||||||
* <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the
|
* <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the
|
||||||
* {@link VideoCapture.OnVideoSavedListener#onError(VideoCapture.VideoCaptureError, String, Throwable)}.
|
* {@link OnVideoSavedListener#onError(VideoCaptureError, String, Throwable)}.
|
||||||
*
|
*
|
||||||
* @param saveLocation Location to save the video capture
|
* @param saveLocation Location to save the video capture
|
||||||
|
* @param executor The executor in which the listener callback methods will be run.
|
||||||
* @param listener Listener to call for the recorded video
|
* @param listener Listener to call for the recorded video
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
|
||||||
// Begin Signal Custom Code Block
|
// Begin Signal Custom Code Block
|
||||||
public void startRecording(FileDescriptor saveLocation, VideoCapture.OnVideoSavedListener listener) {
|
public void startRecording(@NonNull FileDescriptor saveLocation,
|
||||||
|
@NonNull Executor executor, @NonNull OnVideoSavedListener listener) {
|
||||||
// End Signal Custom Code Block
|
// End Signal Custom Code Block
|
||||||
mIsFirstVideoSampleWrite.set(false);
|
mIsFirstVideoSampleWrite.set(false);
|
||||||
mIsFirstAudioSampleWrite.set(false);
|
mIsFirstAudioSampleWrite.set(false);
|
||||||
startRecording(saveLocation, listener, EMPTY_METADATA);
|
startRecording(saveLocation, EMPTY_METADATA, executor, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -258,38 +272,37 @@ public class VideoCapture extends UseCase {
|
|||||||
* called.
|
* called.
|
||||||
*
|
*
|
||||||
* <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the
|
* <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the
|
||||||
* {@link VideoCapture.OnVideoSavedListener#onError(VideoCapture.VideoCaptureError, String, Throwable)}.
|
* {@link OnVideoSavedListener#onError(VideoCaptureError, String, Throwable)}.
|
||||||
*
|
*
|
||||||
* @param saveLocation Location to save the video capture
|
* @param saveLocation Location to save the video capture
|
||||||
* @param listener Listener to call for the recorded video
|
|
||||||
* @param metadata Metadata to save with the recorded video
|
* @param metadata Metadata to save with the recorded video
|
||||||
|
* @param executor The executor in which the listener callback methods will be run.
|
||||||
|
* @param listener Listener to call for the recorded video
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
|
||||||
// Begin Signal Custom Code Block
|
// Begin Signal Custom Code Block
|
||||||
public void startRecording(
|
public void startRecording(
|
||||||
final FileDescriptor saveLocation, final VideoCapture.OnVideoSavedListener listener, VideoCapture.Metadata metadata) {
|
@NonNull FileDescriptor saveLocation, @NonNull Metadata metadata,
|
||||||
|
@NonNull Executor executor,
|
||||||
|
@NonNull OnVideoSavedListener listener) {
|
||||||
// End Signal Custom Code Block
|
// End Signal Custom Code Block
|
||||||
Log.i(TAG, "startRecording");
|
Log.i(TAG, "startRecording");
|
||||||
|
OnVideoSavedListener postListener = new VideoSavedListenerWrapper(executor, listener);
|
||||||
|
|
||||||
if (!mEndOfAudioVideoSignal.get()) {
|
if (!mEndOfAudioVideoSignal.get()) {
|
||||||
listener.onError(
|
postListener.onError(
|
||||||
VideoCapture.VideoCaptureError.RECORDING_IN_PROGRESS, "It is still in video recording!",
|
VideoCaptureError.RECORDING_IN_PROGRESS, "It is still in video recording!",
|
||||||
null);
|
null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin Signal Custom Code Block
|
try {
|
||||||
if (mAudioRecorder != null) {
|
// audioRecord start
|
||||||
try {
|
mAudioRecorder.startRecording();
|
||||||
// audioRecord start
|
} catch (IllegalStateException e) {
|
||||||
mAudioRecorder.startRecording();
|
postListener.onError(VideoCaptureError.ENCODER_ERROR, "AudioRecorder start fail", e);
|
||||||
} catch (IllegalStateException e) {
|
return;
|
||||||
listener.onError(VideoCapture.VideoCaptureError.ENCODER_ERROR, "AudioRecorder start fail", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Audio recorder was not initialized! Can't record audio.");
|
|
||||||
}
|
}
|
||||||
// End Signal Custom Code Block
|
|
||||||
|
|
||||||
VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
|
VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
|
||||||
String cameraId = getCameraIdUnchecked(config);
|
String cameraId = getCameraIdUnchecked(config);
|
||||||
@ -303,18 +316,19 @@ public class VideoCapture extends UseCase {
|
|||||||
|
|
||||||
} catch (IllegalStateException e) {
|
} catch (IllegalStateException e) {
|
||||||
setupEncoder(getAttachedSurfaceResolution(cameraId));
|
setupEncoder(getAttachedSurfaceResolution(cameraId));
|
||||||
listener.onError(VideoCapture.VideoCaptureError.ENCODER_ERROR, "Audio/Video encoder start fail", e);
|
postListener.onError(VideoCaptureError.ENCODER_ERROR, "Audio/Video encoder start fail",
|
||||||
|
e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the relative rotation or default to 0 if the camera info is unavailable
|
// Get the relative rotation or default to 0 if the camera info is unavailable
|
||||||
int relativeRotation = 0;
|
int relativeRotation = 0;
|
||||||
try {
|
try {
|
||||||
CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
|
CameraInfoInternal cameraInfoInternal = CameraX.getCameraInfo(cameraId);
|
||||||
relativeRotation =
|
relativeRotation =
|
||||||
cameraInfo.getSensorRotationDegrees(
|
cameraInfoInternal.getSensorRotationDegrees(
|
||||||
((ImageOutputConfig) getUseCaseConfig())
|
((ImageOutputConfig) getUseCaseConfig())
|
||||||
.getTargetRotation(Surface.ROTATION_0));
|
.getTargetRotation(Surface.ROTATION_0));
|
||||||
} catch (CameraInfoUnavailableException e) {
|
} catch (CameraInfoUnavailableException e) {
|
||||||
Log.e(TAG, "Unable to retrieve camera sensor orientation.", e);
|
Log.e(TAG, "Unable to retrieve camera sensor orientation.", e);
|
||||||
}
|
}
|
||||||
@ -322,22 +336,22 @@ public class VideoCapture extends UseCase {
|
|||||||
try {
|
try {
|
||||||
synchronized (mMuxerLock) {
|
synchronized (mMuxerLock) {
|
||||||
mMuxer =
|
mMuxer =
|
||||||
new MediaMuxer(
|
new MediaMuxer(
|
||||||
// Begin Signal Custom Code Block
|
// Begin Signal Custom Code Block
|
||||||
saveLocation,
|
saveLocation,
|
||||||
// End Signal Custom Code Block
|
// End Signal Custom Code Block
|
||||||
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
|
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
|
||||||
|
|
||||||
mMuxer.setOrientationHint(relativeRotation);
|
mMuxer.setOrientationHint(relativeRotation);
|
||||||
if (metadata.location != null) {
|
if (metadata.location != null) {
|
||||||
mMuxer.setLocation(
|
mMuxer.setLocation(
|
||||||
(float) metadata.location.getLatitude(),
|
(float) metadata.location.getLatitude(),
|
||||||
(float) metadata.location.getLongitude());
|
(float) metadata.location.getLongitude());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
setupEncoder(getAttachedSurfaceResolution(cameraId));
|
setupEncoder(getAttachedSurfaceResolution(cameraId));
|
||||||
listener.onError(VideoCapture.VideoCaptureError.MUXER_ERROR, "MediaMuxer creation failed!", e);
|
postListener.onError(VideoCaptureError.MUXER_ERROR, "MediaMuxer creation failed!", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,32 +362,32 @@ public class VideoCapture extends UseCase {
|
|||||||
|
|
||||||
notifyActive();
|
notifyActive();
|
||||||
mAudioHandler.post(
|
mAudioHandler.post(
|
||||||
new Runnable() {
|
new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
VideoCapture.this.audioEncode(listener);
|
VideoCapture.this.audioEncode(postListener);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mVideoHandler.post(
|
mVideoHandler.post(
|
||||||
new Runnable() {
|
new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
boolean errorOccurred = VideoCapture.this.videoEncode(listener);
|
boolean errorOccurred = VideoCapture.this.videoEncode(postListener);
|
||||||
if (!errorOccurred) {
|
if (!errorOccurred) {
|
||||||
listener.onVideoSaved(saveLocation);
|
postListener.onVideoSaved(saveLocation);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops recording video, this must be called after {@link
|
* Stops recording video, this must be called after {@link
|
||||||
* VideoCapture#startRecording(File, VideoCapture.OnVideoSavedListener, VideoCapture.Metadata)} is called.
|
* VideoCapture#startRecording(File, Metadata, Executor, OnVideoSavedListener)} is called.
|
||||||
*
|
*
|
||||||
* <p>stopRecording() is asynchronous API. User need to check if {@link
|
* <p>stopRecording() is asynchronous API. User need to check if {@link
|
||||||
* VideoCapture.OnVideoSavedListener#onVideoSaved(File)} or
|
* OnVideoSavedListener#onVideoSaved(File)} or
|
||||||
* {@link VideoCapture.OnVideoSavedListener#onError(VideoCapture.VideoCaptureError, String, Throwable)} be called
|
* {@link OnVideoSavedListener#onError(VideoCaptureError, String, Throwable)} be called
|
||||||
* before startRecording.
|
* before startRecording.
|
||||||
*/
|
*/
|
||||||
public void stopRecording() {
|
public void stopRecording() {
|
||||||
@ -390,7 +404,7 @@ public class VideoCapture extends UseCase {
|
|||||||
*
|
*
|
||||||
* @hide
|
* @hide
|
||||||
*/
|
*/
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
|
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||||
@Override
|
@Override
|
||||||
public void clear() {
|
public void clear() {
|
||||||
mVideoHandlerThread.quitSafely();
|
mVideoHandlerThread.quitSafely();
|
||||||
@ -423,19 +437,19 @@ public class VideoCapture extends UseCase {
|
|||||||
final MediaCodec videoEncoder = mVideoEncoder;
|
final MediaCodec videoEncoder = mVideoEncoder;
|
||||||
|
|
||||||
mDeferrableSurface.setOnSurfaceDetachedListener(
|
mDeferrableSurface.setOnSurfaceDetachedListener(
|
||||||
CameraXExecutors.mainThreadExecutor(),
|
CameraXExecutors.mainThreadExecutor(),
|
||||||
new DeferrableSurface.OnSurfaceDetachedListener() {
|
new DeferrableSurface.OnSurfaceDetachedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onSurfaceDetached() {
|
public void onSurfaceDetached() {
|
||||||
if (releaseVideoEncoder && videoEncoder != null) {
|
if (releaseVideoEncoder && videoEncoder != null) {
|
||||||
videoEncoder.release();
|
videoEncoder.release();
|
||||||
}
|
|
||||||
|
|
||||||
if (surface != null) {
|
|
||||||
surface.release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
if (surface != null) {
|
||||||
|
surface.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (releaseVideoEncoder) {
|
if (releaseVideoEncoder) {
|
||||||
mVideoEncoder = null;
|
mVideoEncoder = null;
|
||||||
@ -453,7 +467,7 @@ public class VideoCapture extends UseCase {
|
|||||||
*
|
*
|
||||||
* @param rotation Desired rotation of the output video.
|
* @param rotation Desired rotation of the output video.
|
||||||
*/
|
*/
|
||||||
public void setTargetRotation(@ImageOutputConfig.RotationValue int rotation) {
|
public void setTargetRotation(@RotationValue int rotation) {
|
||||||
ImageOutputConfig oldConfig = (ImageOutputConfig) getUseCaseConfig();
|
ImageOutputConfig oldConfig = (ImageOutputConfig) getUseCaseConfig();
|
||||||
int oldRotation = oldConfig.getTargetRotation(ImageOutputConfig.INVALID_ROTATION);
|
int oldRotation = oldConfig.getTargetRotation(ImageOutputConfig.INVALID_ROTATION);
|
||||||
if (oldRotation == ImageOutputConfig.INVALID_ROTATION || oldRotation != rotation) {
|
if (oldRotation == ImageOutputConfig.INVALID_ROTATION || oldRotation != rotation) {
|
||||||
@ -468,35 +482,45 @@ public class VideoCapture extends UseCase {
|
|||||||
* Setup the {@link MediaCodec} for encoding video from a camera {@link Surface} and encoding
|
* Setup the {@link MediaCodec} for encoding video from a camera {@link Surface} and encoding
|
||||||
* audio from selected audio source.
|
* audio from selected audio source.
|
||||||
*/
|
*/
|
||||||
private void setupEncoder(Size resolution) {
|
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
|
||||||
|
void setupEncoder(Size resolution) {
|
||||||
VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
|
VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
|
||||||
|
|
||||||
// video encoder setup
|
// video encoder setup
|
||||||
mVideoEncoder.reset();
|
mVideoEncoder.reset();
|
||||||
mVideoEncoder.configure(
|
mVideoEncoder.configure(
|
||||||
createMediaFormat(config, resolution), /*surface*/
|
createMediaFormat(config, resolution), /*surface*/
|
||||||
null, /*crypto*/
|
null, /*crypto*/
|
||||||
null,
|
null,
|
||||||
MediaCodec.CONFIGURE_FLAG_ENCODE);
|
MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||||
if (mCameraSurface != null) {
|
if (mCameraSurface != null) {
|
||||||
releaseCameraSurface(false);
|
releaseCameraSurface(false);
|
||||||
}
|
}
|
||||||
mCameraSurface = mVideoEncoder.createInputSurface();
|
mCameraSurface = mVideoEncoder.createInputSurface();
|
||||||
|
|
||||||
SessionConfig.Builder builder = SessionConfig.Builder.createFrom(config);
|
SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
|
||||||
|
|
||||||
mDeferrableSurface = new ImmediateSurface(mCameraSurface);
|
mDeferrableSurface = new ImmediateSurface(mCameraSurface);
|
||||||
|
|
||||||
builder.addSurface(mDeferrableSurface);
|
sessionConfigBuilder.addSurface(mDeferrableSurface);
|
||||||
|
|
||||||
String cameraId = getCameraIdUnchecked(config);
|
String cameraId = getCameraIdUnchecked(config);
|
||||||
attachToCamera(cameraId, builder.build());
|
|
||||||
|
sessionConfigBuilder.addErrorListener(new SessionConfig.ErrorListener() {
|
||||||
|
@Override
|
||||||
|
public void onError(@NonNull SessionConfig sessionConfig,
|
||||||
|
@NonNull SessionConfig.SessionError error) {
|
||||||
|
setupEncoder(resolution);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
attachToCamera(cameraId, sessionConfigBuilder.build());
|
||||||
|
|
||||||
// audio encoder setup
|
// audio encoder setup
|
||||||
setAudioParametersByCamcorderProfile(resolution, cameraId);
|
setAudioParametersByCamcorderProfile(resolution, cameraId);
|
||||||
mAudioEncoder.reset();
|
mAudioEncoder.reset();
|
||||||
mAudioEncoder.configure(
|
mAudioEncoder.configure(
|
||||||
createAudioMediaFormat(), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
createAudioMediaFormat(), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||||
|
|
||||||
if (mAudioRecorder != null) {
|
if (mAudioRecorder != null) {
|
||||||
mAudioRecorder.release();
|
mAudioRecorder.release();
|
||||||
@ -558,9 +582,9 @@ public class VideoCapture extends UseCase {
|
|||||||
ByteBuffer buffer = getOutputBuffer(mAudioEncoder, bufferIndex);
|
ByteBuffer buffer = getOutputBuffer(mAudioEncoder, bufferIndex);
|
||||||
buffer.position(mAudioBufferInfo.offset);
|
buffer.position(mAudioBufferInfo.offset);
|
||||||
if (mAudioTrackIndex >= 0
|
if (mAudioTrackIndex >= 0
|
||||||
&& mVideoTrackIndex >= 0
|
&& mVideoTrackIndex >= 0
|
||||||
&& mAudioBufferInfo.size > 0
|
&& mAudioBufferInfo.size > 0
|
||||||
&& mAudioBufferInfo.presentationTimeUs > 0) {
|
&& mAudioBufferInfo.presentationTimeUs > 0) {
|
||||||
try {
|
try {
|
||||||
synchronized (mMuxerLock) {
|
synchronized (mMuxerLock) {
|
||||||
if (!mIsFirstAudioSampleWrite.get()) {
|
if (!mIsFirstAudioSampleWrite.get()) {
|
||||||
@ -571,13 +595,13 @@ public class VideoCapture extends UseCase {
|
|||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(
|
Log.e(
|
||||||
TAG,
|
TAG,
|
||||||
"audio error:size="
|
"audio error:size="
|
||||||
+ mAudioBufferInfo.size
|
+ mAudioBufferInfo.size
|
||||||
+ "/offset="
|
+ "/offset="
|
||||||
+ mAudioBufferInfo.offset
|
+ mAudioBufferInfo.offset
|
||||||
+ "/timeUs="
|
+ "/timeUs="
|
||||||
+ mAudioBufferInfo.presentationTimeUs);
|
+ mAudioBufferInfo.presentationTimeUs);
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -591,7 +615,7 @@ public class VideoCapture extends UseCase {
|
|||||||
*
|
*
|
||||||
* @return returns {@code true} if an error condition occurred, otherwise returns {@code false}
|
* @return returns {@code true} if an error condition occurred, otherwise returns {@code false}
|
||||||
*/
|
*/
|
||||||
boolean videoEncode(VideoCapture.OnVideoSavedListener videoSavedListener) {
|
boolean videoEncode(OnVideoSavedListener videoSavedListener) {
|
||||||
VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
|
VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
|
||||||
// Main encoding loop. Exits on end of stream.
|
// Main encoding loop. Exits on end of stream.
|
||||||
boolean errorOccurred = false;
|
boolean errorOccurred = false;
|
||||||
@ -605,14 +629,14 @@ public class VideoCapture extends UseCase {
|
|||||||
|
|
||||||
// Deque buffer to check for processing step
|
// Deque buffer to check for processing step
|
||||||
int outputBufferId =
|
int outputBufferId =
|
||||||
mVideoEncoder.dequeueOutputBuffer(mVideoBufferInfo, DEQUE_TIMEOUT_USEC);
|
mVideoEncoder.dequeueOutputBuffer(mVideoBufferInfo, DEQUE_TIMEOUT_USEC);
|
||||||
switch (outputBufferId) {
|
switch (outputBufferId) {
|
||||||
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
||||||
if (mMuxerStarted) {
|
if (mMuxerStarted) {
|
||||||
videoSavedListener.onError(
|
videoSavedListener.onError(
|
||||||
VideoCapture.VideoCaptureError.ENCODER_ERROR,
|
VideoCaptureError.ENCODER_ERROR,
|
||||||
"Unexpected change in video encoding format.",
|
"Unexpected change in video encoding format.",
|
||||||
null);
|
null);
|
||||||
errorOccurred = true;
|
errorOccurred = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -640,8 +664,8 @@ public class VideoCapture extends UseCase {
|
|||||||
Log.i(TAG, "videoEncoder stop");
|
Log.i(TAG, "videoEncoder stop");
|
||||||
mVideoEncoder.stop();
|
mVideoEncoder.stop();
|
||||||
} catch (IllegalStateException e) {
|
} catch (IllegalStateException e) {
|
||||||
videoSavedListener.onError(VideoCapture.VideoCaptureError.ENCODER_ERROR,
|
videoSavedListener.onError(VideoCaptureError.ENCODER_ERROR,
|
||||||
"Video encoder stop failed!", e);
|
"Video encoder stop failed!", e);
|
||||||
errorOccurred = true;
|
errorOccurred = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -657,7 +681,7 @@ public class VideoCapture extends UseCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IllegalStateException e) {
|
} catch (IllegalStateException e) {
|
||||||
videoSavedListener.onError(VideoCapture.VideoCaptureError.MUXER_ERROR, "Muxer stop failed!", e);
|
videoSavedListener.onError(VideoCaptureError.MUXER_ERROR, "Muxer stop failed!", e);
|
||||||
errorOccurred = true;
|
errorOccurred = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -676,7 +700,7 @@ public class VideoCapture extends UseCase {
|
|||||||
return errorOccurred;
|
return errorOccurred;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean audioEncode(VideoCapture.OnVideoSavedListener videoSavedListener) {
|
boolean audioEncode(OnVideoSavedListener videoSavedListener) {
|
||||||
// Audio encoding loop. Exits on end of stream.
|
// Audio encoding loop. Exits on end of stream.
|
||||||
boolean audioEos = false;
|
boolean audioEos = false;
|
||||||
int outIndex;
|
int outIndex;
|
||||||
@ -696,11 +720,11 @@ public class VideoCapture extends UseCase {
|
|||||||
int length = mAudioRecorder.read(buffer, mAudioBufferSize);
|
int length = mAudioRecorder.read(buffer, mAudioBufferSize);
|
||||||
if (length > 0) {
|
if (length > 0) {
|
||||||
mAudioEncoder.queueInputBuffer(
|
mAudioEncoder.queueInputBuffer(
|
||||||
index,
|
index,
|
||||||
0,
|
0,
|
||||||
length,
|
length,
|
||||||
(System.nanoTime() / 1000),
|
(System.nanoTime() / 1000),
|
||||||
mIsRecording ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
mIsRecording ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -729,25 +753,17 @@ public class VideoCapture extends UseCase {
|
|||||||
// Audio Stop
|
// Audio Stop
|
||||||
try {
|
try {
|
||||||
Log.i(TAG, "audioRecorder stop");
|
Log.i(TAG, "audioRecorder stop");
|
||||||
// Begin Signal Custom Code Block
|
mAudioRecorder.stop();
|
||||||
if (mAudioRecorder != null) {
|
|
||||||
mAudioRecorder.stop();
|
|
||||||
}
|
|
||||||
// End Signal Custom Code Block
|
|
||||||
} catch (IllegalStateException e) {
|
} catch (IllegalStateException e) {
|
||||||
videoSavedListener.onError(
|
videoSavedListener.onError(
|
||||||
VideoCapture.VideoCaptureError.ENCODER_ERROR, "Audio recorder stop failed!", e);
|
VideoCaptureError.ENCODER_ERROR, "Audio recorder stop failed!", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Begin Signal Custom Code Block
|
mAudioEncoder.stop();
|
||||||
if (mAudioRecorder != null) {
|
|
||||||
mAudioEncoder.stop();
|
|
||||||
}
|
|
||||||
// End Signal Custom Code Block
|
|
||||||
} catch (IllegalStateException e) {
|
} catch (IllegalStateException e) {
|
||||||
videoSavedListener.onError(VideoCapture.VideoCaptureError.ENCODER_ERROR,
|
videoSavedListener.onError(VideoCaptureError.ENCODER_ERROR,
|
||||||
"Audio encoder stop failed!", e);
|
"Audio encoder stop failed!", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Audio encode thread end");
|
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 */
|
/** Creates a {@link MediaFormat} using parameters for audio from the configuration */
|
||||||
private MediaFormat createAudioMediaFormat() {
|
private MediaFormat createAudioMediaFormat() {
|
||||||
MediaFormat format =
|
MediaFormat format =
|
||||||
MediaFormat.createAudioFormat(AUDIO_MIME_TYPE, mAudioSampleRate,
|
MediaFormat.createAudioFormat(AUDIO_MIME_TYPE, mAudioSampleRate,
|
||||||
mAudioChannelCount);
|
mAudioChannelCount);
|
||||||
format.setInteger(
|
format.setInteger(
|
||||||
MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
|
MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
|
||||||
format.setInteger(MediaFormat.KEY_BIT_RATE, mAudioBitRate);
|
format.setInteger(MediaFormat.KEY_BIT_RATE, mAudioBitRate);
|
||||||
|
|
||||||
return format;
|
return format;
|
||||||
@ -784,41 +800,41 @@ public class VideoCapture extends UseCase {
|
|||||||
|
|
||||||
// Use channel count to determine stereo vs mono
|
// Use channel count to determine stereo vs mono
|
||||||
int channelConfig =
|
int channelConfig =
|
||||||
mAudioChannelCount == 1
|
mAudioChannelCount == 1
|
||||||
? AudioFormat.CHANNEL_IN_MONO
|
? AudioFormat.CHANNEL_IN_MONO
|
||||||
: AudioFormat.CHANNEL_IN_STEREO;
|
: AudioFormat.CHANNEL_IN_STEREO;
|
||||||
int source = config.getAudioRecordSource();
|
int source = config.getAudioRecordSource();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
int bufferSize =
|
int bufferSize =
|
||||||
AudioRecord.getMinBufferSize(mAudioSampleRate, channelConfig, audioFormat);
|
AudioRecord.getMinBufferSize(mAudioSampleRate, channelConfig, audioFormat);
|
||||||
|
|
||||||
if (bufferSize <= 0) {
|
if (bufferSize <= 0) {
|
||||||
bufferSize = config.getAudioMinBufferSize();
|
bufferSize = config.getAudioMinBufferSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioRecord recorder =
|
AudioRecord recorder =
|
||||||
new AudioRecord(
|
new AudioRecord(
|
||||||
source,
|
source,
|
||||||
mAudioSampleRate,
|
mAudioSampleRate,
|
||||||
channelConfig,
|
channelConfig,
|
||||||
audioFormat,
|
audioFormat,
|
||||||
bufferSize * 2);
|
bufferSize * 2);
|
||||||
|
|
||||||
if (recorder.getState() == AudioRecord.STATE_INITIALIZED) {
|
if (recorder.getState() == AudioRecord.STATE_INITIALIZED) {
|
||||||
mAudioBufferSize = bufferSize;
|
mAudioBufferSize = bufferSize;
|
||||||
Log.i(
|
Log.i(
|
||||||
TAG,
|
TAG,
|
||||||
"source: "
|
"source: "
|
||||||
+ source
|
+ source
|
||||||
+ " audioSampleRate: "
|
+ " audioSampleRate: "
|
||||||
+ mAudioSampleRate
|
+ mAudioSampleRate
|
||||||
+ " channelConfig: "
|
+ " channelConfig: "
|
||||||
+ channelConfig
|
+ channelConfig
|
||||||
+ " audioFormat: "
|
+ " audioFormat: "
|
||||||
+ audioFormat
|
+ audioFormat
|
||||||
+ " bufferSize: "
|
+ " bufferSize: "
|
||||||
+ bufferSize);
|
+ bufferSize);
|
||||||
return recorder;
|
return recorder;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -838,7 +854,7 @@ public class VideoCapture extends UseCase {
|
|||||||
if (CamcorderProfile.hasProfile(Integer.parseInt(cameraId), quality)) {
|
if (CamcorderProfile.hasProfile(Integer.parseInt(cameraId), quality)) {
|
||||||
profile = CamcorderProfile.get(Integer.parseInt(cameraId), quality);
|
profile = CamcorderProfile.get(Integer.parseInt(cameraId), quality);
|
||||||
if (currentResolution.getWidth() == profile.videoFrameWidth
|
if (currentResolution.getWidth() == profile.videoFrameWidth
|
||||||
&& currentResolution.getHeight() == profile.videoFrameHeight) {
|
&& currentResolution.getHeight() == profile.videoFrameHeight) {
|
||||||
mAudioChannelCount = profile.audioChannels;
|
mAudioChannelCount = profile.audioChannels;
|
||||||
mAudioSampleRate = profile.audioSampleRate;
|
mAudioSampleRate = profile.audioSampleRate;
|
||||||
mAudioBitRate = profile.audioBitRate;
|
mAudioBitRate = profile.audioBitRate;
|
||||||
@ -862,7 +878,7 @@ public class VideoCapture extends UseCase {
|
|||||||
* Describes the error that occurred during video capture operations.
|
* Describes the error that occurred during video capture operations.
|
||||||
*
|
*
|
||||||
* <p>This is a parameter sent to the error callback functions set in listeners such as {@link
|
* <p>This is a parameter sent to the error callback functions set in listeners such as {@link
|
||||||
* .VideoCapture.OnVideoSavedListener#onError(VideoCapture.VideoCaptureError, String, Throwable)}.
|
* VideoCapture.OnVideoSavedListener#onError(VideoCaptureError, String, Throwable)}.
|
||||||
*
|
*
|
||||||
* <p>See message parameter in onError callback or log for more details.
|
* <p>See message parameter in onError callback or log for more details.
|
||||||
*/
|
*/
|
||||||
@ -890,11 +906,11 @@ public class VideoCapture extends UseCase {
|
|||||||
public interface OnVideoSavedListener {
|
public interface OnVideoSavedListener {
|
||||||
/** Called when the video has been successfully saved. */
|
/** Called when the video has been successfully saved. */
|
||||||
// Begin Signal Custom Code Block
|
// Begin Signal Custom Code Block
|
||||||
void onVideoSaved(@NonNull FileDescriptor fileDescriptor);
|
void onVideoSaved(@NonNull FileDescriptor file);
|
||||||
// End Signal Custom Code Block
|
// End Signal Custom Code Block
|
||||||
|
|
||||||
/** Called when an error occurs while attempting to save the video. */
|
/** Called when an error occurs while attempting to save the video. */
|
||||||
void onError(@NonNull VideoCapture.VideoCaptureError videoCaptureError, @NonNull String message,
|
void onError(@NonNull VideoCaptureError videoCaptureError, @NonNull String message,
|
||||||
@Nullable Throwable cause);
|
@Nullable Throwable cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -906,9 +922,9 @@ public class VideoCapture extends UseCase {
|
|||||||
*
|
*
|
||||||
* @hide
|
* @hide
|
||||||
*/
|
*/
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
|
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||||
public static final class Defaults
|
public static final class Defaults
|
||||||
implements ConfigProvider<VideoCaptureConfig> {
|
implements ConfigProvider<VideoCaptureConfig> {
|
||||||
private static final Handler DEFAULT_HANDLER = new Handler(Looper.getMainLooper());
|
private static final Handler DEFAULT_HANDLER = new Handler(Looper.getMainLooper());
|
||||||
private static final int DEFAULT_VIDEO_FRAME_RATE = 30;
|
private static final int DEFAULT_VIDEO_FRAME_RATE = 30;
|
||||||
/** 8Mb/s the recommend rate for 30fps 1080p */
|
/** 8Mb/s the recommend rate for 30fps 1080p */
|
||||||
@ -922,7 +938,7 @@ public class VideoCapture extends UseCase {
|
|||||||
/** audio channel count */
|
/** audio channel count */
|
||||||
private static final int DEFAULT_AUDIO_CHANNEL_COUNT = 1;
|
private static final int DEFAULT_AUDIO_CHANNEL_COUNT = 1;
|
||||||
/** audio record source */
|
/** audio record source */
|
||||||
private static final int DEFAULT_AUDIO_RECORD_SOURCE = MediaRecorder.AudioSource.MIC;
|
private static final int DEFAULT_AUDIO_RECORD_SOURCE = AudioSource.MIC;
|
||||||
/** audio default minimum buffer size */
|
/** audio default minimum buffer size */
|
||||||
private static final int DEFAULT_AUDIO_MIN_BUFFER_SIZE = 1024;
|
private static final int DEFAULT_AUDIO_MIN_BUFFER_SIZE = 1024;
|
||||||
/** Current max resolution of VideoCapture is set as FHD */
|
/** Current max resolution of VideoCapture is set as FHD */
|
||||||
@ -934,24 +950,23 @@ public class VideoCapture extends UseCase {
|
|||||||
|
|
||||||
static {
|
static {
|
||||||
VideoCaptureConfig.Builder builder =
|
VideoCaptureConfig.Builder builder =
|
||||||
new VideoCaptureConfig.Builder()
|
new VideoCaptureConfig.Builder()
|
||||||
.setCallbackHandler(DEFAULT_HANDLER)
|
.setVideoFrameRate(DEFAULT_VIDEO_FRAME_RATE)
|
||||||
.setVideoFrameRate(DEFAULT_VIDEO_FRAME_RATE)
|
.setBitRate(DEFAULT_BIT_RATE)
|
||||||
.setBitRate(DEFAULT_BIT_RATE)
|
.setIFrameInterval(DEFAULT_INTRA_FRAME_INTERVAL)
|
||||||
.setIFrameInterval(DEFAULT_INTRA_FRAME_INTERVAL)
|
.setAudioBitRate(DEFAULT_AUDIO_BIT_RATE)
|
||||||
.setAudioBitRate(DEFAULT_AUDIO_BIT_RATE)
|
.setAudioSampleRate(DEFAULT_AUDIO_SAMPLE_RATE)
|
||||||
.setAudioSampleRate(DEFAULT_AUDIO_SAMPLE_RATE)
|
.setAudioChannelCount(DEFAULT_AUDIO_CHANNEL_COUNT)
|
||||||
.setAudioChannelCount(DEFAULT_AUDIO_CHANNEL_COUNT)
|
.setAudioRecordSource(DEFAULT_AUDIO_RECORD_SOURCE)
|
||||||
.setAudioRecordSource(DEFAULT_AUDIO_RECORD_SOURCE)
|
.setAudioMinBufferSize(DEFAULT_AUDIO_MIN_BUFFER_SIZE)
|
||||||
.setAudioMinBufferSize(DEFAULT_AUDIO_MIN_BUFFER_SIZE)
|
.setMaxResolution(DEFAULT_MAX_RESOLUTION)
|
||||||
.setMaxResolution(DEFAULT_MAX_RESOLUTION)
|
.setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
|
||||||
.setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = builder.build();
|
DEFAULT_CONFIG = builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public VideoCaptureConfig getConfig(CameraX.LensFacing lensFacing) {
|
public VideoCaptureConfig getConfig(LensFacing lensFacing) {
|
||||||
return DEFAULT_CONFIG;
|
return DEFAULT_CONFIG;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -962,4 +977,39 @@ public class VideoCapture extends UseCase {
|
|||||||
@Nullable
|
@Nullable
|
||||||
public Location location;
|
public Location location;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class VideoSavedListenerWrapper implements OnVideoSavedListener {
|
||||||
|
|
||||||
|
@NonNull Executor mExecutor;
|
||||||
|
@NonNull OnVideoSavedListener mOnVideoSavedListener;
|
||||||
|
|
||||||
|
VideoSavedListenerWrapper(@NonNull Executor executor,
|
||||||
|
@NonNull OnVideoSavedListener onVideoSavedListener) {
|
||||||
|
mExecutor = executor;
|
||||||
|
mOnVideoSavedListener = onVideoSavedListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
// Begin Signal Custom Code Block
|
||||||
|
public void onVideoSaved(@NonNull FileDescriptor file) {
|
||||||
|
// End Signal Custom Code Block
|
||||||
|
try {
|
||||||
|
mExecutor.execute(() -> mOnVideoSavedListener.onVideoSaved(file));
|
||||||
|
} catch (RejectedExecutionException e) {
|
||||||
|
Log.e(TAG, "Unable to post to the supplied executor.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(@NonNull VideoCaptureError videoCaptureError, @NonNull String message,
|
||||||
|
@Nullable Throwable cause) {
|
||||||
|
try {
|
||||||
|
mExecutor.execute(
|
||||||
|
() -> mOnVideoSavedListener.onError(videoCaptureError, message, cause));
|
||||||
|
} catch (RejectedExecutionException e) {
|
||||||
|
Log.e(TAG, "Unable to post to the supplied executor.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,11 +24,11 @@ dependencyVerification {
|
|||||||
['androidx.asynclayoutinflater:asynclayoutinflater:1.0.0',
|
['androidx.asynclayoutinflater:asynclayoutinflater:1.0.0',
|
||||||
'f7eab60c57addd94bb06275832fe7600611beaaae1a1ec597c231956faf96c8b'],
|
'f7eab60c57addd94bb06275832fe7600611beaaae1a1ec597c231956faf96c8b'],
|
||||||
|
|
||||||
['androidx.camera:camera-camera2:1.0.0-alpha04',
|
['androidx.camera:camera-camera2:1.0.0-alpha06',
|
||||||
'b7897230aec96365d675712c92f5edcb8b464badfd61788c8f956ec2d6e49bfe'],
|
'e50f20deb950ffebcd4d1de5408ef7a5404bec80ec77119e05663c890739b903'],
|
||||||
|
|
||||||
['androidx.camera:camera-core:1.0.0-alpha04',
|
['androidx.camera:camera-core:1.0.0-alpha06',
|
||||||
'e1c70de55600a0caf826eb4f8a75c96c5ff8f0b626bf08413d31e80ffa55f8ba'],
|
'0096cabe539d9b4288f406acfb44264b137ebd600e38e33504ff425c979016c9'],
|
||||||
|
|
||||||
['androidx.cardview:cardview:1.0.0',
|
['androidx.cardview:cardview:1.0.0',
|
||||||
'1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7'],
|
'1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7'],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user