kopia lustrzana https://github.com/ryukoposting/Signal-Android
230 wiersze
8.9 KiB
Java
230 wiersze
8.9 KiB
Java
package org.thoughtcrime.securesms.mediasend;
|
|
|
|
import android.Manifest;
|
|
import android.animation.ValueAnimator;
|
|
import android.annotation.SuppressLint;
|
|
import android.content.Context;
|
|
import android.util.DisplayMetrics;
|
|
import android.util.Size;
|
|
import android.view.ViewGroup;
|
|
import android.view.animation.LinearInterpolator;
|
|
import android.widget.Toast;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.RequiresApi;
|
|
import androidx.camera.core.ZoomState;
|
|
import androidx.camera.view.CameraController;
|
|
import androidx.camera.view.PreviewView;
|
|
import androidx.camera.view.video.ExperimentalVideo;
|
|
import androidx.camera.view.video.OnVideoSavedCallback;
|
|
import androidx.camera.view.video.OutputFileOptions;
|
|
import androidx.camera.view.video.OutputFileResults;
|
|
import androidx.fragment.app.Fragment;
|
|
|
|
import com.bumptech.glide.util.Executors;
|
|
|
|
import org.signal.core.util.logging.Log;
|
|
import org.thoughtcrime.securesms.R;
|
|
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModePolicy;
|
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
|
import org.thoughtcrime.securesms.util.Debouncer;
|
|
import org.thoughtcrime.securesms.util.MemoryFileDescriptor;
|
|
import org.thoughtcrime.securesms.video.VideoUtil;
|
|
|
|
import java.io.FileDescriptor;
|
|
import java.io.IOException;
|
|
import java.util.Objects;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
@RequiresApi(26)
|
|
@ExperimentalVideo
|
|
class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener {
|
|
|
|
private static final String TAG = CameraXVideoCaptureHelper.class.getName();
|
|
private static final String VIDEO_DEBUG_LABEL = "video-capture";
|
|
private static final long VIDEO_SIZE = 10 * 1024 * 1024;
|
|
|
|
private final @NonNull Fragment fragment;
|
|
private final @NonNull PreviewView previewView;
|
|
private final @NonNull CameraController cameraController;
|
|
private final @NonNull Callback callback;
|
|
private final @NonNull MemoryFileDescriptor memoryFileDescriptor;
|
|
private final @NonNull ValueAnimator updateProgressAnimator;
|
|
private final @NonNull Debouncer debouncer;
|
|
private final @NonNull CameraXModePolicy cameraXModePolicy;
|
|
|
|
private ValueAnimator cameraMetricsAnimator;
|
|
|
|
private final OnVideoSavedCallback videoSavedListener = new OnVideoSavedCallback() {
|
|
@SuppressLint("RestrictedApi")
|
|
@Override
|
|
public void onVideoSaved(@NonNull OutputFileResults outputFileResults) {
|
|
try {
|
|
debouncer.clear();
|
|
cameraController.setZoomRatio(Objects.requireNonNull(cameraController.getZoomState().getValue()).getMinZoomRatio());
|
|
memoryFileDescriptor.seek(0);
|
|
callback.onVideoSaved(memoryFileDescriptor.getFileDescriptor());
|
|
} catch (IOException e) {
|
|
callback.onVideoError(e);
|
|
}
|
|
}
|
|
|
|
@SuppressLint("RestrictedApi")
|
|
@Override
|
|
public void onError(int videoCaptureError, @NonNull String message, @Nullable Throwable cause) {
|
|
debouncer.clear();
|
|
callback.onVideoError(cause);
|
|
}
|
|
};
|
|
|
|
CameraXVideoCaptureHelper(@NonNull Fragment fragment,
|
|
@NonNull CameraButtonView captureButton,
|
|
@NonNull CameraController cameraController,
|
|
@NonNull PreviewView previewView,
|
|
@NonNull MemoryFileDescriptor memoryFileDescriptor,
|
|
@NonNull CameraXModePolicy cameraXModePolicy,
|
|
int maxVideoDurationSec,
|
|
@NonNull Callback callback)
|
|
{
|
|
this.fragment = fragment;
|
|
this.cameraController = cameraController;
|
|
this.previewView = previewView;
|
|
this.memoryFileDescriptor = memoryFileDescriptor;
|
|
this.callback = callback;
|
|
this.updateProgressAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(TimeUnit.SECONDS.toMillis(maxVideoDurationSec));
|
|
this.debouncer = new Debouncer(TimeUnit.SECONDS.toMillis(maxVideoDurationSec));
|
|
this.cameraXModePolicy = cameraXModePolicy;
|
|
|
|
updateProgressAnimator.setInterpolator(new LinearInterpolator());
|
|
updateProgressAnimator.addUpdateListener(anim -> captureButton.setProgress(anim.getAnimatedFraction()));
|
|
}
|
|
|
|
@Override
|
|
public void onVideoCaptureStarted() {
|
|
Log.d(TAG, "onVideoCaptureStarted");
|
|
|
|
if (canRecordAudio()) {
|
|
beginCameraRecording();
|
|
} else {
|
|
displayAudioRecordingPermissionsDialog();
|
|
}
|
|
}
|
|
|
|
private boolean canRecordAudio() {
|
|
return Permissions.hasAll(fragment.requireContext(), Manifest.permission.RECORD_AUDIO);
|
|
}
|
|
|
|
private void displayAudioRecordingPermissionsDialog() {
|
|
Permissions.with(fragment)
|
|
.request(Manifest.permission.RECORD_AUDIO)
|
|
.ifNecessary()
|
|
.withRationaleDialog(fragment.getString(R.string.ConversationActivity_enable_the_microphone_permission_to_capture_videos_with_sound), R.drawable.ic_mic_solid_24)
|
|
.withPermanentDenialDialog(fragment.getString(R.string.ConversationActivity_signal_needs_the_recording_permissions_to_capture_video))
|
|
.onAnyDenied(() -> Toast.makeText(fragment.requireContext(), R.string.ConversationActivity_signal_needs_recording_permissions_to_capture_video, Toast.LENGTH_LONG).show())
|
|
.execute();
|
|
}
|
|
|
|
@SuppressLint("RestrictedApi")
|
|
private void beginCameraRecording() {
|
|
cameraXModePolicy.setToVideo(cameraController);
|
|
this.cameraController.setZoomRatio(Objects.requireNonNull(this.cameraController.getZoomState().getValue()).getMinZoomRatio());
|
|
callback.onVideoRecordStarted();
|
|
shrinkCaptureArea();
|
|
|
|
OutputFileOptions options = OutputFileOptions.builder(memoryFileDescriptor.getParcelFileDescriptor()).build();
|
|
|
|
cameraController.startRecording(options, Executors.mainThreadExecutor(), videoSavedListener);
|
|
updateProgressAnimator.start();
|
|
debouncer.publish(this::onVideoCaptureComplete);
|
|
}
|
|
|
|
private void shrinkCaptureArea() {
|
|
Size screenSize = getScreenSize();
|
|
Size videoRecordingSize = VideoUtil.getVideoRecordingSize();
|
|
float scale = getSurfaceScaleForRecording();
|
|
float targetWidthForAnimation = videoRecordingSize.getWidth() * scale;
|
|
float scaleX = targetWidthForAnimation / screenSize.getWidth();
|
|
|
|
if (scaleX == 1f) {
|
|
float targetHeightForAnimation = videoRecordingSize.getHeight() * scale;
|
|
|
|
if (screenSize.getHeight() == targetHeightForAnimation) {
|
|
return;
|
|
}
|
|
|
|
cameraMetricsAnimator = ValueAnimator.ofFloat(screenSize.getHeight(), targetHeightForAnimation);
|
|
} else {
|
|
|
|
if (screenSize.getWidth() == targetWidthForAnimation) {
|
|
return;
|
|
}
|
|
|
|
cameraMetricsAnimator = ValueAnimator.ofFloat(screenSize.getWidth(), targetWidthForAnimation);
|
|
}
|
|
|
|
ViewGroup.LayoutParams params = previewView.getLayoutParams();
|
|
cameraMetricsAnimator.setInterpolator(new LinearInterpolator());
|
|
cameraMetricsAnimator.setDuration(200);
|
|
cameraMetricsAnimator.addUpdateListener(animation -> {
|
|
if (scaleX == 1f) {
|
|
params.height = Math.round((float) animation.getAnimatedValue());
|
|
} else {
|
|
params.width = Math.round((float) animation.getAnimatedValue());
|
|
}
|
|
previewView.setLayoutParams(params);
|
|
});
|
|
cameraMetricsAnimator.start();
|
|
}
|
|
|
|
private Size getScreenSize() {
|
|
DisplayMetrics metrics = previewView.getResources().getDisplayMetrics();
|
|
return new Size(metrics.widthPixels, metrics.heightPixels);
|
|
}
|
|
|
|
private float getSurfaceScaleForRecording() {
|
|
Size videoRecordingSize = VideoUtil.getVideoRecordingSize();
|
|
Size screenSize = getScreenSize();
|
|
return Math.min(screenSize.getHeight(), screenSize.getWidth()) / (float) Math.min(videoRecordingSize.getHeight(), videoRecordingSize.getWidth());
|
|
}
|
|
|
|
@Override
|
|
public void onVideoCaptureComplete() {
|
|
if (!canRecordAudio()) return;
|
|
|
|
Log.d(TAG, "onVideoCaptureComplete");
|
|
cameraController.stopRecording();
|
|
|
|
if (cameraMetricsAnimator != null && cameraMetricsAnimator.isRunning()) {
|
|
cameraMetricsAnimator.reverse();
|
|
}
|
|
|
|
updateProgressAnimator.cancel();
|
|
debouncer.clear();
|
|
cameraXModePolicy.setToImage(cameraController);
|
|
}
|
|
|
|
@Override
|
|
public void onZoomIncremented(float increment) {
|
|
ZoomState zoomState = Objects.requireNonNull(cameraController.getZoomState().getValue());
|
|
float range = zoomState.getMaxZoomRatio() - zoomState.getMinZoomRatio();
|
|
cameraController.setZoomRatio((range * increment) + zoomState.getMinZoomRatio());
|
|
}
|
|
|
|
static MemoryFileDescriptor createFileDescriptor(@NonNull Context context) throws MemoryFileDescriptor.MemoryFileException {
|
|
return MemoryFileDescriptor.newMemoryFileDescriptor(
|
|
context,
|
|
VIDEO_DEBUG_LABEL,
|
|
VIDEO_SIZE
|
|
);
|
|
}
|
|
|
|
interface Callback {
|
|
void onVideoRecordStarted();
|
|
|
|
void onVideoSaved(@NonNull FileDescriptor fd);
|
|
|
|
void onVideoError(@Nullable Throwable cause);
|
|
}
|
|
}
|