package com.lassi.presentation.cameraview.controls; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.graphics.ImageFormat; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.hardware.Camera; import android.location.Location; import android.os.Build; import android.view.SurfaceHolder; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import com.lassi.presentation.cameraview.audio.Audio; import com.lassi.presentation.cameraview.audio.Facing; import com.lassi.presentation.cameraview.audio.Flash; import com.lassi.presentation.cameraview.audio.Gesture; import com.lassi.presentation.cameraview.audio.Hdr; import com.lassi.presentation.cameraview.audio.Mode; import com.lassi.presentation.cameraview.audio.WhiteBalance; import com.lassi.presentation.cameraview.preview.GlCameraPreview; import com.lassi.presentation.cameraview.utils.CameraLogger; import com.lassi.presentation.cameraview.utils.CropHelper; import com.lassi.presentation.cameraview.utils.Task; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; @SuppressWarnings("deprecation") public class Camera1 extends CameraController implements Camera.PreviewCallback, Camera.ErrorCallback, VideoRecorder.VideoResultListener, PictureRecorder.PictureResultListener { private static final String TAG = Camera1.class.getSimpleName(); private static final CameraLogger LOG = CameraLogger.create(TAG); private Camera mCamera; private boolean mIsBound = false; private Runnable mPostFocusResetRunnable = new Runnable() { @Override public void run() { if (!isCameraAvailable()) return; mCamera.cancelAutoFocus(); Camera.Parameters params = mCamera.getParameters(); int maxAF = params.getMaxNumFocusAreas(); int maxAE = params.getMaxNumMeteringAreas(); if (maxAF > 0) params.setFocusAreas(null); if (maxAE > 0) params.setMeteringAreas(null); applyDefaultFocus(params); // Revert to internal focus. mCamera.setParameters(params); } }; Camera1(@NonNull CameraView.CameraCallbacks callback) { super(callback); mMapper = new CameraMapper(); } @NonNull @WorkerThread private static List<Camera.Area> computeMeteringAreas(double viewClickX, double viewClickY, int viewWidth, int viewHeight, int sensorToDisplay) { // Event came in view coordinates. We must rotate to sensor coordinates. // First, rescale to the -1000 ... 1000 range. int displayToSensor = -sensorToDisplay; viewClickX = -1000d + (viewClickX / (double) viewWidth) * 2000d; viewClickY = -1000d + (viewClickY / (double) viewHeight) * 2000d; // Apply rotation to this point. // https://academo.org/demos/rotation-about-point/ double theta = ((double) displayToSensor) * Math.PI / 180; double sensorClickX = viewClickX * Math.cos(theta) - viewClickY * Math.sin(theta); double sensorClickY = viewClickX * Math.sin(theta) + viewClickY * Math.cos(theta); LOG.i("focus:", "viewClickX:", viewClickX, "viewClickY:", viewClickY); LOG.i("focus:", "sensorClickX:", sensorClickX, "sensorClickY:", sensorClickY); // Compute the rect bounds. Rect rect1 = computeMeteringArea(sensorClickX, sensorClickY, 150d); int weight1 = 1000; // 150 * 150 * 1000 = more than 10.000.000 Rect rect2 = computeMeteringArea(sensorClickX, sensorClickY, 300d); int weight2 = 100; // 300 * 300 * 100 = 9.000.000 List<Camera.Area> list = new ArrayList<>(2); list.add(new Camera.Area(rect1, weight1)); list.add(new Camera.Area(rect2, weight2)); return list; } @NonNull private static Rect computeMeteringArea(double centerX, double centerY, double size) { double delta = size / 2d; int top = (int) Math.max(centerY - delta, -1000); int bottom = (int) Math.min(centerY + delta, 1000); int left = (int) Math.max(centerX - delta, -1000); int right = (int) Math.min(centerX + delta, 1000); LOG.i("focus:", "computeMeteringArea:", "top:", top, "left:", left, "bottom:", bottom, "right:", right); return new Rect(left, top, right, bottom); } private void schedule(@Nullable final Task<Void> task, final boolean ensureAvailable, final Runnable action) { mHandler.post(new Runnable() { @Override public void run() { if (ensureAvailable && !isCameraAvailable()) { if (task != null) task.end(null); } else { action.run(); if (task != null) task.end(null); } } }); } /** * Preview surface is now available. If camera is open, set up. * At this point we are sure that mPreview is not null. */ @Override public void onSurfaceAvailable() { LOG.i("onSurfaceAvailable:", "Size is", getPreviewSurfaceSize(REF_VIEW)); schedule(null, false, new Runnable() { @Override public void run() { LOG.i("onSurfaceAvailable:", "Inside handler. About to bind."); if (shouldBindToSurface()) bindToSurface(); if (shouldStartPreview()) startPreview("onSurfaceAvailable"); } }); } /** * Preview surface did change its size. Compute a new preview size. * This requires stopping and restarting the preview. * At this point we are sure that mPreview is not null. */ @Override public void onSurfaceChanged() { LOG.i("onSurfaceChanged, size is", getPreviewSurfaceSize(REF_VIEW)); schedule(null, true, new Runnable() { @Override public void run() { if (!mIsBound) return; // Compute a new camera preview size. Size newSize = computePreviewStreamSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes())); if (newSize.equals(mPreviewStreamSize)) return; // Apply. LOG.i("onSurfaceChanged:", "Computed a new preview size. Going on."); mPreviewStreamSize = newSize; stopPreview(); startPreview("onSurfaceChanged:"); } }); } @Override public void onSurfaceDestroyed() { LOG.i("onSurfaceDestroyed"); schedule(null, true, new Runnable() { @Override public void run() { stopPreview(); if (mIsBound) unbindFromSurface(); } }); } private boolean shouldBindToSurface() { return isCameraAvailable() && mPreview != null && mPreview.hasSurface() && !mIsBound; } /** * The act of binding an "open" camera to a "ready" preview. * These can happen at different times but we want to end up here. * At this point we are sure that mPreview is not null. */ @WorkerThread private void bindToSurface() { LOG.i("bindToSurface:", "Started"); Object output = mPreview.getOutput(); try { if (output instanceof SurfaceHolder) { mCamera.setPreviewDisplay((SurfaceHolder) output); } else if (output instanceof SurfaceTexture) { mCamera.setPreviewTexture((SurfaceTexture) output); } else { throw new RuntimeException("Unknown CameraPreview output class."); } } catch (IOException e) { LOG.e("bindToSurface:", "Failed to bind.", e); throw new CameraException(e, CameraException.REASON_FAILED_TO_START_PREVIEW); } mCaptureSize = computeCaptureSize(); mPreviewStreamSize = computePreviewStreamSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes())); mIsBound = true; } @WorkerThread private void unbindFromSurface() { mIsBound = false; mPreviewStreamSize = null; mCaptureSize = null; try { if (mPreview.getOutputClass() == SurfaceHolder.class) { mCamera.setPreviewDisplay(null); } else if (mPreview.getOutputClass() == SurfaceTexture.class) { mCamera.setPreviewTexture(null); } else { throw new RuntimeException("Unknown CameraPreview output class."); } } catch (IOException e) { LOG.e("unbindFromSurface", "Could not release surface", e); } } private boolean shouldStartPreview() { return isCameraAvailable() && mIsBound; } // To be called when the preview size is setup or changed. private void startPreview(String log) { LOG.i(log, "Dispatching onCameraPreviewStreamSizeChanged."); mCameraCallbacks.onCameraPreviewStreamSizeChanged(); Size previewSize = getPreviewStreamSize(REF_VIEW); boolean wasFlipped = flip(REF_SENSOR, REF_VIEW); mPreview.setStreamSize(previewSize.getWidth(), previewSize.getHeight(), wasFlipped); Camera.Parameters params = mCamera.getParameters(); mPreviewFormat = params.getPreviewFormat(); params.setPreviewSize(mPreviewStreamSize.getWidth(), mPreviewStreamSize.getHeight()); // <- not allowed during preview if (mMode == Mode.PICTURE) { params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // <- allowed } else { // mCaptureSize in this case is a video size. The available video sizes are not necessarily // a subset of the picture sizes, so we can't use the mCaptureSize value: it might crash. // However, the setPictureSize() passed here is useless : we don't allow HQ pictures in video mode. // While this might be lifted in the future, for now, just use a picture capture size. Size pictureSize = computeCaptureSize(Mode.PICTURE); params.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight()); } mCamera.setParameters(params); mCamera.setPreviewCallbackWithBuffer(null); // Release anything left mCamera.setPreviewCallbackWithBuffer(this); // Add ourselves mFrameManager.allocate(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewStreamSize); LOG.i(log, "Starting preview with startPreview()."); try { mCamera.startPreview(); } catch (Exception e) { LOG.e(log, "Failed to start preview.", e); throw new CameraException(e, CameraException.REASON_FAILED_TO_START_PREVIEW); } LOG.i(log, "Started preview."); } private void stopPreview() { mPreviewFormat = 0; mFrameManager.release(); mCamera.setPreviewCallbackWithBuffer(null); // Release anything left try { mCamera.stopPreview(); } catch (Exception e) { LOG.e("stopPreview", "Could not stop preview", e); } } private void createCamera() { try { mCamera = Camera.open(mCameraId); } catch (Exception e) { LOG.e("createCamera:", "Failed to connect. Maybe in use by another app?"); throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT); } mCamera.setErrorCallback(this); // Set parameters that might have been set before the camera was opened. LOG.i("createCamera:", "Applying default parameters."); Camera.Parameters params = mCamera.getParameters(); mCameraOptions = new CameraOptions(params, flip(REF_SENSOR, REF_VIEW)); applyDefaultFocus(params); applyFlash(params, Flash.DEFAULT); applyLocation(params, null); applyWhiteBalance(params, WhiteBalance.DEFAULT); applyHdr(params, Hdr.DEFAULT); applyPlaySounds(mPlaySounds); params.setRecordingHint(mMode == Mode.VIDEO); mCamera.setParameters(params); mCamera.setDisplayOrientation(offset(REF_SENSOR, REF_VIEW)); // <- not allowed during preview } private void destroyCamera() { try { LOG.i("destroyCamera:", "Clean up.", "Releasing camera."); mCamera.release(); LOG.i("destroyCamera:", "Clean up.", "Released camera."); } catch (Exception e) { LOG.w("destroyCamera:", "Clean up.", "Exception while releasing camera.", e); } mCamera = null; mCameraOptions = null; } @WorkerThread @Override void onStart() { if (isCameraAvailable()) { LOG.w("onStart:", "Camera not available. Should not happen."); onStop(); // Should not happen. } if (collectCameraId()) { createCamera(); if (shouldBindToSurface()) bindToSurface(); if (shouldStartPreview()) startPreview("onStart"); LOG.i("onStart:", "Ended"); } else { LOG.e("onStart:", "No camera available for facing", mFacing); throw new CameraException(CameraException.REASON_NO_CAMERA); } } @WorkerThread @Override void onStop() { LOG.i("onStop:", "About to clean up."); mHandler.get().removeCallbacks(mPostFocusResetRunnable); if (mVideoRecorder != null) { mVideoRecorder.stop(); mVideoRecorder = null; } if (mCamera != null) { stopPreview(); if (mIsBound) unbindFromSurface(); destroyCamera(); } mCameraOptions = null; mCamera = null; mPreviewStreamSize = null; mCaptureSize = null; mIsBound = false; LOG.w("onStop:", "Clean up.", "Returning."); // We were saving a reference to the exception here and throwing to the user. // I don't think it's correct. We are closing and have already done our best // to clean up resources. No need to throw. // if (error != null) throw new CameraException(error); } private boolean collectCameraId() { int internalFacing = mMapper.map(mFacing); LOG.i("collectCameraId", "Facing:", mFacing, "Internal:", internalFacing, "Cameras:", Camera.getNumberOfCameras()); Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) { Camera.getCameraInfo(i, cameraInfo); if (cameraInfo.facing == internalFacing) { mSensorOffset = cameraInfo.orientation; mCameraId = i; return true; } } return false; } @Override public void onBufferAvailable(@NonNull byte[] buffer) { // TODO: sync with handler? if (isCameraAvailable()) { mCamera.addCallbackBuffer(buffer); } } @Override public void onError(int error, Camera camera) { if (error == Camera.CAMERA_ERROR_SERVER_DIED) { // Looks like this is recoverable. LOG.w("Recoverable error inside the onError callback.", "CAMERA_ERROR_SERVER_DIED"); stopImmediately(); start(); return; } LOG.e("Internal Camera1 error.", error); Exception runtime = new RuntimeException(CameraLogger.lastMessage); int reason; switch (error) { case Camera.CAMERA_ERROR_EVICTED: reason = CameraException.REASON_DISCONNECTED; break; case Camera.CAMERA_ERROR_UNKNOWN: reason = CameraException.REASON_UNKNOWN; break; default: reason = CameraException.REASON_UNKNOWN; } throw new CameraException(runtime, reason); } @Override void setMode(@NonNull Mode mode) { if (mode != mMode) { mMode = mode; schedule(null, true, new Runnable() { @Override public void run() { restart(); } }); } } @Override void setLocation(@Nullable Location location) { final Location oldLocation = mLocation; mLocation = location; schedule(mLocationTask, true, new Runnable() { @Override public void run() { Camera.Parameters params = mCamera.getParameters(); if (applyLocation(params, oldLocation)) mCamera.setParameters(params); } }); } private boolean applyLocation(@NonNull Camera.Parameters params, @Nullable Location oldLocation) { if (mLocation != null) { params.setGpsLatitude(mLocation.getLatitude()); params.setGpsLongitude(mLocation.getLongitude()); params.setGpsAltitude(mLocation.getAltitude()); params.setGpsTimestamp(mLocation.getTime()); params.setGpsProcessingMethod(mLocation.getProvider()); } return true; } @Override void setFacing(@NonNull Facing facing) { final Facing old = mFacing; if (facing != old) { mFacing = facing; schedule(null, true, new Runnable() { @Override public void run() { if (collectCameraId()) { restart(); } else { mFacing = old; } } }); } } @Override void setWhiteBalance(@NonNull WhiteBalance whiteBalance) { final WhiteBalance old = mWhiteBalance; mWhiteBalance = whiteBalance; schedule(mWhiteBalanceTask, true, new Runnable() { @Override public void run() { Camera.Parameters params = mCamera.getParameters(); if (applyWhiteBalance(params, old)) mCamera.setParameters(params); } }); } private boolean applyWhiteBalance(@NonNull Camera.Parameters params, @NonNull WhiteBalance oldWhiteBalance) { if (mCameraOptions.supports(mWhiteBalance)) { params.setWhiteBalance((String) mMapper.map(mWhiteBalance)); return true; } mWhiteBalance = oldWhiteBalance; return false; } @Override void setHdr(@NonNull Hdr hdr) { final Hdr old = mHdr; mHdr = hdr; schedule(mHdrTask, true, new Runnable() { @Override public void run() { Camera.Parameters params = mCamera.getParameters(); if (applyHdr(params, old)) mCamera.setParameters(params); } }); } private boolean applyHdr(@NonNull Camera.Parameters params, @NonNull Hdr oldHdr) { if (mCameraOptions.supports(mHdr)) { params.setSceneMode((String) mMapper.map(mHdr)); return true; } mHdr = oldHdr; return false; } @TargetApi(17) private boolean applyPlaySounds(boolean oldPlaySound) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { Camera.CameraInfo info = new Camera.CameraInfo(); Camera.getCameraInfo(mCameraId, info); if (info.canDisableShutterSound) { try { // this method is documented to throw on some occasions. #377 return mCamera.enableShutterSound(mPlaySounds); } catch (RuntimeException exception) { return false; } } } if (mPlaySounds) { return true; } mPlaySounds = oldPlaySound; return false; } @Override void setAudio(@NonNull Audio audio) { if (mAudio != audio) { if (isTakingVideo()) { LOG.w("Audio setting was changed while recording. " + "Changes will take place starting from next video"); } mAudio = audio; } } @Override void setFlash(@NonNull Flash flash) { final Flash old = mFlash; mFlash = flash; schedule(mFlashTask, true, new Runnable() { @Override public void run() { Camera.Parameters params = mCamera.getParameters(); if (applyFlash(params, old)) mCamera.setParameters(params); } }); } // ----------------- // Picture recording stuff. private boolean applyFlash(@NonNull Camera.Parameters params, @NonNull Flash oldFlash) { if (mCameraOptions.supports(mFlash)) { params.setFlashMode((String) mMapper.map(mFlash)); return true; } mFlash = oldFlash; return false; } // Choose the best default focus, based on session type. private void applyDefaultFocus(@NonNull Camera.Parameters params) { List<String> modes = params.getSupportedFocusModes(); if (mMode == Mode.VIDEO && modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); return; } if (modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); return; } if (modes.contains(Camera.Parameters.FOCUS_MODE_INFINITY)) { params.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY); return; } if (modes.contains(Camera.Parameters.FOCUS_MODE_FIXED)) { params.setFocusMode(Camera.Parameters.FOCUS_MODE_FIXED); //noinspection UnnecessaryReturnStatement return; } } @Override public void onPictureShutter(boolean didPlaySound) { mCameraCallbacks.onShutter(!didPlaySound); } @Override public void onPictureResult(@Nullable PictureResult result) { mPictureRecorder = null; if (result != null) { mCameraCallbacks.dispatchOnPictureTaken(result); } else { // Something went wrong. mCameraCallbacks.dispatchError(new CameraException(CameraException.REASON_PICTURE_FAILED)); LOG.e("onPictureResult", "result is null: something went wrong."); } } @Override void takePicture() { LOG.v("takePicture: scheduling"); schedule(null, true, new Runnable() { @Override public void run() { if (mMode == Mode.VIDEO) { // Could redirect to takePictureSnapshot, but it's better if people know // what they are doing. throw new IllegalStateException("Can't take hq pictures while in VIDEO mode"); } LOG.v("takePicture: performing.", isTakingPicture()); if (isTakingPicture()) return; PictureResult result = new PictureResult(); result.isSnapshot = false; result.location = mLocation; result.rotation = offset(REF_SENSOR, REF_OUTPUT); result.size = getPictureSize(REF_OUTPUT); result.facing = mFacing; mPictureRecorder = new FullPictureRecorder(result, Camera1.this, mCamera); mPictureRecorder.take(); } }); } /** * Just a note about the snapshot size - it is the PreviewStreamSize, cropped with the view ratio. * * @param viewAspectRatio the view aspect ratio */ @Override void takePictureSnapshot(@NonNull final AspectRatio viewAspectRatio) { LOG.v("takePictureSnapshot: scheduling"); schedule(null, true, new Runnable() { @Override public void run() { LOG.v("takePictureSnapshot: performing.", isTakingPicture()); if (isTakingPicture()) return; PictureResult result = new PictureResult(); result.location = mLocation; result.isSnapshot = true; result.facing = mFacing; result.size = getUncroppedSnapshotSize(REF_OUTPUT); // Not the real size: it will be cropped to match the view ratio result.rotation = offset(REF_SENSOR, REF_OUTPUT); // Actually it will be rotated and set to 0. AspectRatio outputRatio = flip(REF_OUTPUT, REF_VIEW) ? viewAspectRatio.inverse() : viewAspectRatio; // LOG.e("ROTBUG_pic", "aspectRatio (REF_VIEW):", viewAspectRatio); // LOG.e("ROTBUG_pic", "aspectRatio (REF_OUTPUT):", outputRatio); // LOG.e("ROTBUG_pic", "sizeUncropped (REF_OUTPUT):", result.size); // LOG.e("ROTBUG_pic", "rotation:", result.rotation); LOG.v("Rotations", "SV", offset(REF_SENSOR, REF_VIEW), "VS", offset(REF_VIEW, REF_SENSOR)); LOG.v("Rotations", "SO", offset(REF_SENSOR, REF_OUTPUT), "OS", offset(REF_OUTPUT, REF_SENSOR)); LOG.v("Rotations", "VO", offset(REF_VIEW, REF_OUTPUT), "OV", offset(REF_OUTPUT, REF_VIEW)); mPictureRecorder = new SnapshotPictureRecorder(result, Camera1.this, mCamera, outputRatio); mPictureRecorder.take(); } }); } // ----------------- // Video recording stuff. @Override public void onPreviewFrame(@NonNull byte[] data, Camera camera) { Frame frame = mFrameManager.getFrame(data, System.currentTimeMillis(), offset(REF_SENSOR, REF_OUTPUT), mPreviewStreamSize, mPreviewFormat); mCameraCallbacks.dispatchFrame(frame); } private boolean isCameraAvailable() { switch (mState) { // If we are stopped, don't. case STATE_STOPPED: return false; // If we are going to be closed, don't act on camera. // Even if mCamera != null, it might have been released. case STATE_STOPPING: return false; // If we are started, mCamera should never be null. case STATE_STARTED: return true; // If we are starting, theoretically we could act. // Just check that camera is available. case STATE_STARTING: return mCamera != null; } return false; } @Override public void onVideoResult(@Nullable VideoResult result, @Nullable Exception exception) { mVideoRecorder = null; if (result != null) { mCameraCallbacks.dispatchOnVideoTaken(result); } else { // Something went wrong, lock the camera again. mCameraCallbacks.dispatchError(new CameraException(exception, CameraException.REASON_VIDEO_FAILED)); mCamera.lock(); } } @Override void takeVideo(@NonNull final File videoFile) { schedule(mStartVideoTask, true, new Runnable() { @Override public void run() { if (mMode == Mode.PICTURE) { throw new IllegalStateException("Can't record video while in PICTURE mode"); } if (isTakingVideo()) return; // Create the video result stub VideoResult videoResult = new VideoResult(); videoResult.file = videoFile; videoResult.isSnapshot = false; videoResult.codec = mVideoCodec; videoResult.location = mLocation; videoResult.facing = mFacing; videoResult.rotation = offset(REF_SENSOR, REF_OUTPUT); videoResult.size = flip(REF_SENSOR, REF_OUTPUT) ? mCaptureSize.flip() : mCaptureSize; videoResult.audio = mAudio; videoResult.maxSize = mVideoMaxSize; videoResult.maxDuration = mVideoMaxDuration; videoResult.videoBitRate = mVideoBitRate; videoResult.audioBitRate = mAudioBitRate; // Unlock the camera and start recording. try { mCamera.unlock(); } catch (Exception e) { // If this failed, we are unlikely able to record the video. // Dispatch an error. onVideoResult(null, e); return; } mVideoRecorder = new FullVideoRecorder(videoResult, Camera1.this, Camera1.this, mCamera, mCameraId); mVideoRecorder.start(); } }); } // ----------------- // Zoom and simpler stuff. /** * @param file the output file * @param viewAspectRatio the view aspect ratio */ @SuppressLint("NewApi") @Override void takeVideoSnapshot(@NonNull final File file, @NonNull final AspectRatio viewAspectRatio) { if (!(mPreview instanceof GlCameraPreview)) { throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview."); } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { throw new IllegalStateException("Video snapshots are only supported starting from API 18."); } schedule(mStartVideoTask, true, new Runnable() { @Override public void run() { if (isTakingVideo()) return; // Create the video result stub VideoResult videoResult = new VideoResult(); videoResult.file = file; videoResult.isSnapshot = true; videoResult.codec = mVideoCodec; videoResult.location = mLocation; videoResult.facing = mFacing; videoResult.videoBitRate = mVideoBitRate; videoResult.audioBitRate = mAudioBitRate; videoResult.audio = mAudio; videoResult.maxSize = mVideoMaxSize; videoResult.maxDuration = mVideoMaxDuration; // Size and rotation turned out to be extremely tricky. In case of SnapshotPictureRecorder // we use the preview size in REF_OUTPUT (cropped) and offset(REF_SENSOR, REF_OUTPUT) as rotation. // These values mean that we expect input to be in the REF_SENSOR system. // Here everything seems different. We would expect a difference because the two snapshot // recorders have different mechanics (the picture one uses a SurfaceTexture with setBufferSize, // the video one here uses the MediaCodec input surface which we can't control). // The strangest thing is the fact that the correct angle seems to be the same for FRONT and // BACK sensor, which means that our sensor correction actually screws things up. For this reason // facing value is temporarily set to BACK. Facing realFacing = mFacing; mFacing = Facing.BACK; // These are the angles that make it work on a Nexus5X, compared to the offset() results. // For instance, SV means offset(REF_SENSOR, REF_VIEW). The rest should be clear. // CONFIG | WANTED | SV | VS | VO | OV | SO | OS | // ------------|--------|--------|--------|--------|--------|--------|--------| // Vertical | 0 | 270 | 90 | 0 | 0 | 270 | 90 | // Left | 270 | 270 | 90 | 270 | 90 | 180 | 180 | // Right | 90 | 270 | 90 | 90 | 270 | 0 | 0 | // Upside down | 180 | 270 | 90 | 180 | 180 | 90 | 270 | // The VO is the only correct value. Things change when using FRONT camera, in which case, // no value is actually correct, and the needed values are the same of BACK! // CONFIG | WANTED | SV | VS | VO | OV | SO | OS | // ------------|--------|--------|--------|--------|--------|--------|--------| // Vertical | 0 | 90 | 270 | 180 | 180 | 270 | 90 | // Left | 270 | 90 | 270 | 270 | 90 | 0 | 0 | // Right | 90 | 90 | 270 | 90 | 270 | 180 | 180 | // Upside down | 180 | 90 | 270 | 0 | 0 | 90 | 270 | // Based on this we will use VO for everything. See if we get issues about distortion // and maybe we can improve. The reason why this happen is beyond my understanding. Size outputSize = getUncroppedSnapshotSize(REF_OUTPUT); AspectRatio outputRatio = flip(REF_OUTPUT, REF_VIEW) ? viewAspectRatio.inverse() : viewAspectRatio; Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio); outputSize = new Size(outputCrop.width(), outputCrop.height()); videoResult.size = outputSize; videoResult.rotation = offset(REF_VIEW, REF_OUTPUT); // LOG.e("ROTBUG_video", "aspectRatio (REF_VIEW):", viewAspectRatio); // LOG.e("ROTBUG_video", "aspectRatio (REF_OUTPUT):", outputRatio); // LOG.e("ROTBUG_video", "sizeUncropped (REF_OUTPUT):", outputSize); // LOG.e("ROTBUG_video", "sizeCropped (REF_OUTPUT):", videoResult.size); // LOG.e("ROTBUG_video", "rotation:", videoResult.rotation); // Reset facing and start. mFacing = realFacing; GlCameraPreview cameraPreview = (GlCameraPreview) mPreview; mVideoRecorder = new SnapshotVideoRecorder(videoResult, Camera1.this, cameraPreview); mVideoRecorder.start(); } }); } @Override void stopVideo() { schedule(null, false, new Runnable() { @Override public void run() { LOG.i("stopVideo", "mVideoRecorder is null?", mVideoRecorder == null); if (mVideoRecorder != null) { mVideoRecorder.stop(); mVideoRecorder = null; } } }); } // ----------------- // Tap to focus stuff. @Override void setZoom(final float zoom, @Nullable final PointF[] points, final boolean notify) { schedule(mZoomTask, true, new Runnable() { @Override public void run() { if (!mCameraOptions.isZoomSupported()) return; mZoomValue = zoom; Camera.Parameters params = mCamera.getParameters(); float max = params.getMaxZoom(); params.setZoom((int) (zoom * max)); mCamera.setParameters(params); if (notify) { mCameraCallbacks.dispatchOnZoomChanged(zoom, points); } } }); } @Override void setExposureCorrection(final float EVvalue, @NonNull final float[] bounds, @Nullable final PointF[] points, final boolean notify) { schedule(mExposureCorrectionTask, true, new Runnable() { @Override public void run() { if (!mCameraOptions.isExposureCorrectionSupported()) return; float value = EVvalue; float max = mCameraOptions.getExposureCorrectionMaxValue(); float min = mCameraOptions.getExposureCorrectionMinValue(); value = value < min ? min : value > max ? max : value; // cap mExposureCorrectionValue = value; Camera.Parameters params = mCamera.getParameters(); int indexValue = (int) (value / params.getExposureCompensationStep()); params.setExposureCompensation(indexValue); mCamera.setParameters(params); if (notify) { mCameraCallbacks.dispatchOnExposureCorrectionChanged(value, bounds, points); } } }); } @Override void startAutoFocus(@Nullable final Gesture gesture, @NonNull final PointF point) { // Must get width and height from the UI thread. int viewWidth = 0, viewHeight = 0; if (mPreview != null && mPreview.hasSurface()) { viewWidth = mPreview.getView().getWidth(); viewHeight = mPreview.getView().getHeight(); } final int viewWidthF = viewWidth; final int viewHeightF = viewHeight; // Schedule. schedule(null, true, new Runnable() { @Override public void run() { if (!mCameraOptions.isAutoFocusSupported()) return; final PointF p = new PointF(point.x, point.y); // copy. List<Camera.Area> meteringAreas2 = computeMeteringAreas(p.x, p.y, viewWidthF, viewHeightF, offset(REF_SENSOR, REF_VIEW)); List<Camera.Area> meteringAreas1 = meteringAreas2.subList(0, 1); // At this point we are sure that camera supports auto focus... right? Look at CameraView.onTouchEvent(). Camera.Parameters params = mCamera.getParameters(); int maxAF = params.getMaxNumFocusAreas(); int maxAE = params.getMaxNumMeteringAreas(); if (maxAF > 0) params.setFocusAreas(maxAF > 1 ? meteringAreas2 : meteringAreas1); if (maxAE > 0) params.setMeteringAreas(maxAE > 1 ? meteringAreas2 : meteringAreas1); params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); mCamera.setParameters(params); mCameraCallbacks.dispatchOnFocusStart(gesture, p); // TODO this is not guaranteed to be called... Fix. try { mCamera.autoFocus(new Camera.AutoFocusCallback() { @Override public void onAutoFocus(boolean success, Camera camera) { // TODO lock auto exposure and white balance for a while mCameraCallbacks.dispatchOnFocusEnd(gesture, success, p); mHandler.get().removeCallbacks(mPostFocusResetRunnable); if (shouldResetAutoFocus()) { mHandler.get().postDelayed(mPostFocusResetRunnable, getAutoFocusResetDelay()); } } }); } catch (RuntimeException e) { // Handling random auto-focus exception on some devices // See https://github.com/natario1/CameraView/issues/181 LOG.e("startAutoFocus:", "Error calling autoFocus", e); mCameraCallbacks.dispatchOnFocusEnd(gesture, false, p); } } }); } // ----------------- // Size stuff. @Nullable private List<Size> sizesFromList(@Nullable List<Camera.Size> sizes) { if (sizes == null) return null; List<Size> result = new ArrayList<>(sizes.size()); for (Camera.Size size : sizes) { Size add = new Size(size.width, size.height); if (!result.contains(add)) result.add(add); } LOG.i("size:", "sizesFromList:", result); return result; } @Override void setPlaySounds(boolean playSounds) { final boolean old = mPlaySounds; mPlaySounds = playSounds; schedule(mPlaySoundsTask, true, new Runnable() { @Override public void run() { applyPlaySounds(old); } }); } }