| /* |
| * 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 org.webrtc.CameraEnumerationAndroid.CaptureFormat; |
| |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.graphics.SurfaceTexture; |
| 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 android.view.WindowManager; |
| |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.concurrent.CountDownLatch; |
| |
| @TargetApi(21) |
| public class Camera2Session implements CameraSession { |
| private static final String TAG = "Camera2Session"; |
| |
| private static enum SessionState { RUNNING, STOPPED }; |
| |
| private final Handler cameraThreadHandler; |
| private final CameraManager cameraManager; |
| private final CreateSessionCallback callback; |
| private final CameraVideoCapturer.CameraEventsHandler eventsHandler; |
| private final Context applicationContext; |
| private final CameraVideoCapturer.CapturerObserver capturerObserver; |
| 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 |
| private CameraDevice cameraDevice; |
| private Surface surface; |
| |
| // Initialized when capture session is created |
| private CameraCaptureSession captureSession; |
| private CameraVideoCapturer.CameraStatistics cameraStatistics; |
| |
| // State |
| private SessionState state = SessionState.RUNNING; |
| private boolean firstFrameReported = false; |
| |
| 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(); |
| reportError("Camera disconnected."); |
| } |
| |
| @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; |
| |
| final SurfaceTexture surfaceTexture = surfaceTextureHelper.getSurfaceTexture(); |
| surfaceTexture.setDefaultBufferSize(captureFormat.width, captureFormat.height); |
| surface = new Surface(surfaceTexture); |
| 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."); |
| eventsHandler.onCameraClosed(); |
| } |
| } |
| |
| 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); |
| |
| captureRequestBuilder.addTarget(surface); |
| session.setRepeatingRequest( |
| captureRequestBuilder.build(), new CameraCaptureCallback(), cameraThreadHandler); |
| } catch (CameraAccessException e) { |
| reportError("Failed to start capture request. " + e); |
| return; |
| } |
| |
| surfaceTextureHelper.startListening( |
| new SurfaceTextureHelper.OnTextureFrameAvailableListener() { |
| @Override |
| public void onTextureFrameAvailable( |
| int oesTextureId, float[] transformMatrix, long timestampNs) { |
| checkIsOnCameraThread(); |
| |
| if (state != SessionState.RUNNING) { |
| Logging.d(TAG, "Texture frame captured but camera is no longer running."); |
| return; |
| } |
| |
| if (!firstFrameReported) { |
| eventsHandler.onFirstFrameAvailable(); |
| firstFrameReported = true; |
| } |
| |
| int rotation = getFrameOrientation(); |
| if (isCameraFrontFacing) { |
| // Undo the mirror that the OS "helps" us with. |
| // http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int) |
| transformMatrix = RendererCommon.multiplyMatrices( |
| transformMatrix, RendererCommon.horizontalFlipMatrix()); |
| } |
| |
| // Undo camera orientation - we report it as rotation instead. |
| transformMatrix = RendererCommon.rotateTextureMatrix( |
| transformMatrix, -cameraOrientation); |
| |
| cameraStatistics.addFrame(); |
| capturerObserver.onTextureFrameCaptured(captureFormat.width, captureFormat.height, |
| oesTextureId, transformMatrix, rotation, timestampNs); |
| } |
| }); |
| capturerObserver.onCapturerStarted(true /* success */); |
| cameraStatistics = new CameraVideoCapturer.CameraStatistics( |
| surfaceTextureHelper, eventsHandler); |
| Logging.d(TAG, "Camera device successfully started."); |
| callback.onDone(Camera2Session.this); |
| } |
| } |
| |
| private 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( |
| CameraManager cameraManager, CreateSessionCallback callback, |
| CameraVideoCapturer.CameraEventsHandler eventsHandler, Context applicationContext, |
| CameraVideoCapturer.CapturerObserver capturerObserver, |
| SurfaceTextureHelper surfaceTextureHelper, |
| String cameraId, int width, int height, int framerate) { |
| new Camera2Session( |
| cameraManager, callback, |
| eventsHandler, applicationContext, |
| capturerObserver, |
| surfaceTextureHelper, |
| cameraId, width, height, framerate); |
| } |
| |
| private Camera2Session( |
| CameraManager cameraManager, CreateSessionCallback callback, |
| CameraVideoCapturer.CameraEventsHandler eventsHandler, Context applicationContext, |
| CameraVideoCapturer.CapturerObserver capturerObserver, |
| SurfaceTextureHelper surfaceTextureHelper, |
| String cameraId, int width, int height, int framerate) { |
| Logging.d(TAG, "Create new camera2 session on camera " + cameraId); |
| |
| this.cameraThreadHandler = new Handler(); |
| this.cameraManager = cameraManager; |
| this.callback = callback; |
| this.eventsHandler = eventsHandler; |
| this.applicationContext = applicationContext; |
| this.capturerObserver = capturerObserver; |
| 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 (final CameraAccessException e) { |
| reportError("getCameraCharacteristics(): " + e.getMessage()); |
| } |
| cameraOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); |
| isCameraFrontFacing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) |
| == CameraMetadata.LENS_FACING_FRONT; |
| |
| findCaptureFormat(); |
| 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); |
| |
| if (framerateRanges.isEmpty() || sizes.isEmpty()) { |
| reportError("No supported capture formats."); |
| } |
| |
| final CaptureFormat.FramerateRange bestFpsRange = |
| CameraEnumerationAndroid.getClosestSupportedFramerateRange( |
| framerateRanges, framerate); |
| |
| final Size bestSize = CameraEnumerationAndroid.getClosestSupportedSize( |
| sizes, width, height); |
| |
| 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); |
| int cameraIndex = -1; |
| try { |
| cameraIndex = Integer.parseInt(cameraId); |
| } catch (NumberFormatException e) { |
| Logging.d(TAG, "External camera with non-int identifier: " + cameraId); |
| } |
| eventsHandler.onCameraOpening(cameraIndex); |
| |
| try { |
| cameraManager.openCamera(cameraId, new CameraStateCallback(), cameraThreadHandler); |
| } catch (CameraAccessException e) { |
| reportError("Failed to open camera: " + e); |
| } |
| } |
| |
| @Override |
| public void stop() { |
| Logging.d(TAG, "Stop camera2 session on camera " + cameraId); |
| if (Thread.currentThread() == cameraThreadHandler.getLooper().getThread()) { |
| if (state != SessionState.STOPPED) { |
| state = SessionState.STOPPED; |
| capturerObserver.onCapturerStopped(); |
| // Post the stopInternal to return earlier. |
| cameraThreadHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| stopInternal(); |
| } |
| }); |
| } |
| } else { |
| final CountDownLatch stopLatch = new CountDownLatch(1); |
| |
| cameraThreadHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| if (state != SessionState.STOPPED) { |
| state = SessionState.STOPPED; |
| capturerObserver.onCapturerStopped(); |
| stopLatch.countDown(); |
| stopInternal(); |
| } |
| } |
| }); |
| |
| ThreadUtils.awaitUninterruptibly(stopLatch); |
| } |
| } |
| |
| private void stopInternal() { |
| Logging.d(TAG, "Stop internal"); |
| checkIsOnCameraThread(); |
| |
| surfaceTextureHelper.stopListening(); |
| cameraStatistics.release(); |
| |
| captureSession.close(); |
| captureSession = null; |
| surface.release(); |
| surface = null; |
| cameraDevice.close(); |
| cameraDevice = null; |
| |
| Logging.d(TAG, "Stop done"); |
| } |
| |
| private void reportError(String error) { |
| checkIsOnCameraThread(); |
| Logging.e(TAG, "Error: " + error); |
| |
| if (captureSession == null) { |
| if (cameraDevice != null) { |
| cameraDevice.close(); |
| cameraDevice = null; |
| } |
| |
| state = SessionState.STOPPED; |
| callback.onFailure(error); |
| capturerObserver.onCapturerStarted(false /* success */); |
| } else { |
| eventsHandler.onCameraError(error); |
| } |
| } |
| |
| private int getDeviceOrientation() { |
| int orientation = 0; |
| |
| WindowManager wm = (WindowManager) applicationContext.getSystemService( |
| Context.WINDOW_SERVICE); |
| switch(wm.getDefaultDisplay().getRotation()) { |
| case Surface.ROTATION_90: |
| orientation = 90; |
| break; |
| case Surface.ROTATION_180: |
| orientation = 180; |
| break; |
| case Surface.ROTATION_270: |
| orientation = 270; |
| break; |
| case Surface.ROTATION_0: |
| default: |
| orientation = 0; |
| break; |
| } |
| return orientation; |
| } |
| |
| private int getFrameOrientation() { |
| int rotation = getDeviceOrientation(); |
| if (!isCameraFrontFacing) { |
| rotation = 360 - rotation; |
| } |
| return (cameraOrientation + rotation) % 360; |
| } |
| |
| private void checkIsOnCameraThread() { |
| if (Thread.currentThread() != cameraThreadHandler.getLooper().getThread()) { |
| throw new IllegalStateException("Wrong thread"); |
| } |
| } |
| } |