| /* | |
| * Copyright 2016 The WebRTC project authors. All Rights Reserved. | |
| * | |
| * Use of this source code is governed by a BSD-style license | |
| * that can be found in the LICENSE file in the root of the source | |
| * tree. An additional intellectual property rights grant can be found | |
| * in the file PATENTS. All contributing project authors may | |
| * be found in the AUTHORS file in the root of the source tree. | |
| */ | |
| package org.webrtc; | |
| import android.content.Context; | |
| import android.hardware.camera2.CameraAccessException; | |
| import android.hardware.camera2.CameraCaptureSession; | |
| import android.hardware.camera2.CameraCharacteristics; | |
| import android.hardware.camera2.CameraDevice; | |
| import android.hardware.camera2.CameraManager; | |
| import android.hardware.camera2.CameraMetadata; | |
| import android.hardware.camera2.CaptureFailure; | |
| import android.hardware.camera2.CaptureRequest; | |
| import android.os.Handler; | |
| import android.util.Range; | |
| import android.view.Surface; | |
| import androidx.annotation.Nullable; | |
| import java.util.Arrays; | |
| import java.util.List; | |
| import java.util.concurrent.TimeUnit; | |
| import org.webrtc.CameraEnumerationAndroid.CaptureFormat; | |
| class Camera2Session implements CameraSession { | |
| private static final String TAG = "Camera2Session"; | |
| private static final Histogram camera2StartTimeMsHistogram = | |
| Histogram.createCounts("WebRTC.Android.Camera2.StartTimeMs", 1, 10000, 50); | |
| private static final Histogram camera2StopTimeMsHistogram = | |
| Histogram.createCounts("WebRTC.Android.Camera2.StopTimeMs", 1, 10000, 50); | |
| private static final Histogram camera2ResolutionHistogram = Histogram.createEnumeration( | |
| "WebRTC.Android.Camera2.Resolution", CameraEnumerationAndroid.COMMON_RESOLUTIONS.size()); | |
| private static enum SessionState { RUNNING, STOPPED } | |
| private final Handler cameraThreadHandler; | |
| private final CreateSessionCallback callback; | |
| private final Events events; | |
| private final Context applicationContext; | |
| private final CameraManager cameraManager; | |
| private final SurfaceTextureHelper surfaceTextureHelper; | |
| private final String cameraId; | |
| private final int width; | |
| private final int height; | |
| private final int framerate; | |
| // Initialized at start | |
| private CameraCharacteristics cameraCharacteristics; | |
| private int cameraOrientation; | |
| private boolean isCameraFrontFacing; | |
| private int fpsUnitFactor; | |
| private CaptureFormat captureFormat; | |
| // Initialized when camera opens | |
| @Nullable private CameraDevice cameraDevice; | |
| @Nullable private Surface surface; | |
| // Initialized when capture session is created | |
| @Nullable private CameraCaptureSession captureSession; | |
| // State | |
| private SessionState state = SessionState.RUNNING; | |
| private boolean firstFrameReported; | |
| // Used only for stats. Only used on the camera thread. | |
| private final long constructionTimeNs; // Construction time of this class. | |
| private class CameraStateCallback extends CameraDevice.StateCallback { | |
| private String getErrorDescription(int errorCode) { | |
| switch (errorCode) { | |
| case CameraDevice.StateCallback.ERROR_CAMERA_DEVICE: | |
| return "Camera device has encountered a fatal error."; | |
| case CameraDevice.StateCallback.ERROR_CAMERA_DISABLED: | |
| return "Camera device could not be opened due to a device policy."; | |
| case CameraDevice.StateCallback.ERROR_CAMERA_IN_USE: | |
| return "Camera device is in use already."; | |
| case CameraDevice.StateCallback.ERROR_CAMERA_SERVICE: | |
| return "Camera service has encountered a fatal error."; | |
| case CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE: | |
| return "Camera device could not be opened because" | |
| + " there are too many other open camera devices."; | |
| default: | |
| return "Unknown camera error: " + errorCode; | |
| } | |
| } | |
| @Override | |
| public void onDisconnected(CameraDevice camera) { | |
| checkIsOnCameraThread(); | |
| final boolean startFailure = (captureSession == null) && (state != SessionState.STOPPED); | |
| state = SessionState.STOPPED; | |
| stopInternal(); | |
| if (startFailure) { | |
| callback.onFailure(FailureType.DISCONNECTED, "Camera disconnected / evicted."); | |
| } else { | |
| events.onCameraDisconnected(Camera2Session.this); | |
| } | |
| } | |
| @Override | |
| public void onError(CameraDevice camera, int errorCode) { | |
| checkIsOnCameraThread(); | |
| reportError(getErrorDescription(errorCode)); | |
| } | |
| @Override | |
| public void onOpened(CameraDevice camera) { | |
| checkIsOnCameraThread(); | |
| Logging.d(TAG, "Camera opened."); | |
| cameraDevice = camera; | |
| surfaceTextureHelper.setTextureSize(captureFormat.width, captureFormat.height); | |
| surface = new Surface(surfaceTextureHelper.getSurfaceTexture()); | |
| try { | |
| camera.createCaptureSession( | |
| Arrays.asList(surface), new CaptureSessionCallback(), cameraThreadHandler); | |
| } catch (CameraAccessException e) { | |
| reportError("Failed to create capture session. " + e); | |
| return; | |
| } | |
| } | |
| @Override | |
| public void onClosed(CameraDevice camera) { | |
| checkIsOnCameraThread(); | |
| Logging.d(TAG, "Camera device closed."); | |
| events.onCameraClosed(Camera2Session.this); | |
| } | |
| } | |
| private class CaptureSessionCallback extends CameraCaptureSession.StateCallback { | |
| @Override | |
| public void onConfigureFailed(CameraCaptureSession session) { | |
| checkIsOnCameraThread(); | |
| session.close(); | |
| reportError("Failed to configure capture session."); | |
| } | |
| @Override | |
| public void onConfigured(CameraCaptureSession session) { | |
| checkIsOnCameraThread(); | |
| Logging.d(TAG, "Camera capture session configured."); | |
| captureSession = session; | |
| try { | |
| /* | |
| * The viable options for video capture requests are: | |
| * TEMPLATE_PREVIEW: High frame rate is given priority over the highest-quality | |
| * post-processing. | |
| * TEMPLATE_RECORD: Stable frame rate is used, and post-processing is set for recording | |
| * quality. | |
| */ | |
| final CaptureRequest.Builder captureRequestBuilder = | |
| cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); | |
| // Set auto exposure fps range. | |
| captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, | |
| new Range<Integer>(captureFormat.framerate.min / fpsUnitFactor, | |
| captureFormat.framerate.max / fpsUnitFactor)); | |
| captureRequestBuilder.set( | |
| CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); | |
| captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); | |
| chooseStabilizationMode(captureRequestBuilder); | |
| chooseFocusMode(captureRequestBuilder); | |
| captureRequestBuilder.addTarget(surface); | |
| session.setRepeatingRequest( | |
| captureRequestBuilder.build(), new CameraCaptureCallback(), cameraThreadHandler); | |
| } catch (CameraAccessException e) { | |
| reportError("Failed to start capture request. " + e); | |
| return; | |
| } | |
| surfaceTextureHelper.startListening((VideoFrame frame) -> { | |
| checkIsOnCameraThread(); | |
| if (state != SessionState.RUNNING) { | |
| Logging.d(TAG, "Texture frame captured but camera is no longer running."); | |
| return; | |
| } | |
| if (!firstFrameReported) { | |
| firstFrameReported = true; | |
| final int startTimeMs = | |
| (int) TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - constructionTimeNs); | |
| camera2StartTimeMsHistogram.addSample(startTimeMs); | |
| } | |
| // Undo the mirror that the OS "helps" us with. | |
| // http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int) | |
| // Also, undo camera orientation, we report it as rotation instead. | |
| final VideoFrame modifiedFrame = | |
| new VideoFrame(CameraSession.createTextureBufferWithModifiedTransformMatrix( | |
| (TextureBufferImpl) frame.getBuffer(), | |
| /* mirror= */ isCameraFrontFacing, | |
| /* rotation= */ -cameraOrientation), | |
| /* rotation= */ getFrameOrientation(), frame.getTimestampNs()); | |
| events.onFrameCaptured(Camera2Session.this, modifiedFrame); | |
| modifiedFrame.release(); | |
| }); | |
| Logging.d(TAG, "Camera device successfully started."); | |
| callback.onDone(Camera2Session.this); | |
| } | |
| // Prefers optical stabilization over software stabilization if available. Only enables one of | |
| // the stabilization modes at a time because having both enabled can cause strange results. | |
| private void chooseStabilizationMode(CaptureRequest.Builder captureRequestBuilder) { | |
| final int[] availableOpticalStabilization = cameraCharacteristics.get( | |
| CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION); | |
| if (availableOpticalStabilization != null) { | |
| for (int mode : availableOpticalStabilization) { | |
| if (mode == CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON) { | |
| captureRequestBuilder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, | |
| CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON); | |
| captureRequestBuilder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, | |
| CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF); | |
| Logging.d(TAG, "Using optical stabilization."); | |
| return; | |
| } | |
| } | |
| } | |
| // If no optical mode is available, try software. | |
| final int[] availableVideoStabilization = cameraCharacteristics.get( | |
| CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES); | |
| if (availableVideoStabilization != null) { | |
| for (int mode : availableVideoStabilization) { | |
| if (mode == CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON) { | |
| captureRequestBuilder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, | |
| CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON); | |
| captureRequestBuilder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, | |
| CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_OFF); | |
| Logging.d(TAG, "Using video stabilization."); | |
| return; | |
| } | |
| } | |
| } | |
| Logging.d(TAG, "Stabilization not available."); | |
| } | |
| private void chooseFocusMode(CaptureRequest.Builder captureRequestBuilder) { | |
| final int[] availableFocusModes = | |
| cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); | |
| for (int mode : availableFocusModes) { | |
| if (mode == CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO) { | |
| captureRequestBuilder.set( | |
| CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO); | |
| Logging.d(TAG, "Using continuous video auto-focus."); | |
| return; | |
| } | |
| } | |
| Logging.d(TAG, "Auto-focus is not available."); | |
| } | |
| } | |
| private static class CameraCaptureCallback extends CameraCaptureSession.CaptureCallback { | |
| @Override | |
| public void onCaptureFailed( | |
| CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) { | |
| Logging.d(TAG, "Capture failed: " + failure); | |
| } | |
| } | |
| public static void create(CreateSessionCallback callback, Events events, | |
| Context applicationContext, CameraManager cameraManager, | |
| SurfaceTextureHelper surfaceTextureHelper, String cameraId, int width, int height, | |
| int framerate) { | |
| new Camera2Session(callback, events, applicationContext, cameraManager, surfaceTextureHelper, | |
| cameraId, width, height, framerate); | |
| } | |
| private Camera2Session(CreateSessionCallback callback, Events events, Context applicationContext, | |
| CameraManager cameraManager, SurfaceTextureHelper surfaceTextureHelper, String cameraId, | |
| int width, int height, int framerate) { | |
| Logging.d(TAG, "Create new camera2 session on camera " + cameraId); | |
| constructionTimeNs = System.nanoTime(); | |
| this.cameraThreadHandler = new Handler(); | |
| this.callback = callback; | |
| this.events = events; | |
| this.applicationContext = applicationContext; | |
| this.cameraManager = cameraManager; | |
| this.surfaceTextureHelper = surfaceTextureHelper; | |
| this.cameraId = cameraId; | |
| this.width = width; | |
| this.height = height; | |
| this.framerate = framerate; | |
| start(); | |
| } | |
| private void start() { | |
| checkIsOnCameraThread(); | |
| Logging.d(TAG, "start"); | |
| try { | |
| cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId); | |
| } catch (CameraAccessException | IllegalArgumentException e) { | |
| reportError("getCameraCharacteristics(): " + e.getMessage()); | |
| return; | |
| } | |
| cameraOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); | |
| isCameraFrontFacing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) | |
| == CameraMetadata.LENS_FACING_FRONT; | |
| findCaptureFormat(); | |
| if (captureFormat == null) { | |
| // findCaptureFormat reports an error already. | |
| return; | |
| } | |
| openCamera(); | |
| } | |
| private void findCaptureFormat() { | |
| checkIsOnCameraThread(); | |
| Range<Integer>[] fpsRanges = | |
| cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); | |
| fpsUnitFactor = Camera2Enumerator.getFpsUnitFactor(fpsRanges); | |
| List<CaptureFormat.FramerateRange> framerateRanges = | |
| Camera2Enumerator.convertFramerates(fpsRanges, fpsUnitFactor); | |
| List<Size> sizes = Camera2Enumerator.getSupportedSizes(cameraCharacteristics); | |
| Logging.d(TAG, "Available preview sizes: " + sizes); | |
| Logging.d(TAG, "Available fps ranges: " + framerateRanges); | |
| if (framerateRanges.isEmpty() || sizes.isEmpty()) { | |
| reportError("No supported capture formats."); | |
| return; | |
| } | |
| final CaptureFormat.FramerateRange bestFpsRange = | |
| CameraEnumerationAndroid.getClosestSupportedFramerateRange(framerateRanges, framerate); | |
| final Size bestSize = CameraEnumerationAndroid.getClosestSupportedSize(sizes, width, height); | |
| CameraEnumerationAndroid.reportCameraResolution(camera2ResolutionHistogram, bestSize); | |
| captureFormat = new CaptureFormat(bestSize.width, bestSize.height, bestFpsRange); | |
| Logging.d(TAG, "Using capture format: " + captureFormat); | |
| } | |
| private void openCamera() { | |
| checkIsOnCameraThread(); | |
| Logging.d(TAG, "Opening camera " + cameraId); | |
| events.onCameraOpening(); | |
| try { | |
| cameraManager.openCamera(cameraId, new CameraStateCallback(), cameraThreadHandler); | |
| } catch (CameraAccessException | IllegalArgumentException | SecurityException e) { | |
| reportError("Failed to open camera: " + e); | |
| return; | |
| } | |
| } | |
| @Override | |
| public void stop() { | |
| Logging.d(TAG, "Stop camera2 session on camera " + cameraId); | |
| checkIsOnCameraThread(); | |
| if (state != SessionState.STOPPED) { | |
| final long stopStartTime = System.nanoTime(); | |
| state = SessionState.STOPPED; | |
| stopInternal(); | |
| final int stopTimeMs = (int) TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - stopStartTime); | |
| camera2StopTimeMsHistogram.addSample(stopTimeMs); | |
| } | |
| } | |
| private void stopInternal() { | |
| Logging.d(TAG, "Stop internal"); | |
| checkIsOnCameraThread(); | |
| surfaceTextureHelper.stopListening(); | |
| if (captureSession != null) { | |
| captureSession.close(); | |
| captureSession = null; | |
| } | |
| if (surface != null) { | |
| surface.release(); | |
| surface = null; | |
| } | |
| if (cameraDevice != null) { | |
| cameraDevice.close(); | |
| cameraDevice = null; | |
| } | |
| Logging.d(TAG, "Stop done"); | |
| } | |
| private void reportError(String error) { | |
| checkIsOnCameraThread(); | |
| Logging.e(TAG, "Error: " + error); | |
| final boolean startFailure = (captureSession == null) && (state != SessionState.STOPPED); | |
| state = SessionState.STOPPED; | |
| stopInternal(); | |
| if (startFailure) { | |
| callback.onFailure(FailureType.ERROR, error); | |
| } else { | |
| events.onCameraError(this, error); | |
| } | |
| } | |
| private int getFrameOrientation() { | |
| int rotation = CameraSession.getDeviceOrientation(applicationContext); | |
| if (!isCameraFrontFacing) { | |
| rotation = 360 - rotation; | |
| } | |
| return (cameraOrientation + rotation) % 360; | |
| } | |
| private void checkIsOnCameraThread() { | |
| if (Thread.currentThread() != cameraThreadHandler.getLooper().getThread()) { | |
| throw new IllegalStateException("Wrong thread"); | |
| } | |
| } | |
| } |