| /* |
| * Copyright 2015 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.content.Context; |
| import android.os.Handler; |
| import android.os.SystemClock; |
| import android.view.Surface; |
| import android.view.WindowManager; |
| |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| |
| // Android specific implementation of VideoCapturer. |
| // An instance of this class can be created by an application using |
| // VideoCapturerAndroid.create(); |
| // This class extends VideoCapturer with a method to easily switch between the |
| // front and back camera. It also provides methods for enumerating valid device |
| // names. |
| // |
| // Threading notes: this class is called from C++ code, Android Camera callbacks, and possibly |
| // arbitrary Java threads. All public entry points are thread safe, and delegate the work to the |
| // camera thread. The internal *OnCameraThread() methods must check |camera| for null to check if |
| // the camera has been stopped. |
| // TODO(magjed): This class name is now confusing - rename to Camera1VideoCapturer. |
| @SuppressWarnings("deprecation") |
| public class VideoCapturerAndroid implements |
| CameraVideoCapturer, |
| android.hardware.Camera.PreviewCallback, |
| SurfaceTextureHelper.OnTextureFrameAvailableListener { |
| private static final String TAG = "VideoCapturerAndroid"; |
| private static final int CAMERA_STOP_TIMEOUT_MS = 7000; |
| |
| private android.hardware.Camera camera; // Only non-null while capturing. |
| private final AtomicBoolean isCameraRunning = new AtomicBoolean(); |
| // Use maybePostOnCameraThread() instead of posting directly to the handler - this way all |
| // callbacks with a specifed token can be removed at once. |
| private volatile Handler cameraThreadHandler; |
| private Context applicationContext; |
| // Synchronization lock for |id|. |
| private final Object cameraIdLock = new Object(); |
| private int id; |
| private android.hardware.Camera.CameraInfo info; |
| private CameraStatistics cameraStatistics; |
| // Remember the requested format in case we want to switch cameras. |
| private int requestedWidth; |
| private int requestedHeight; |
| private int requestedFramerate; |
| // The capture format will be the closest supported format to the requested format. |
| private CaptureFormat captureFormat; |
| private final Object pendingCameraSwitchLock = new Object(); |
| private volatile boolean pendingCameraSwitch; |
| private CapturerObserver frameObserver = null; |
| private final CameraEventsHandler eventsHandler; |
| private boolean firstFrameReported; |
| // Arbitrary queue depth. Higher number means more memory allocated & held, |
| // lower number means more sensitivity to processing time in the client (and |
| // potentially stalling the capturer if it runs out of buffers to write to). |
| private static final int NUMBER_OF_CAPTURE_BUFFERS = 3; |
| private final Set<byte[]> queuedBuffers = new HashSet<byte[]>(); |
| private final boolean isCapturingToTexture; |
| private SurfaceTextureHelper surfaceHelper; |
| private final static int MAX_OPEN_CAMERA_ATTEMPTS = 3; |
| private final static int OPEN_CAMERA_DELAY_MS = 500; |
| private int openCameraAttempts; |
| |
| // Camera error callback. |
| private final android.hardware.Camera.ErrorCallback cameraErrorCallback = |
| new android.hardware.Camera.ErrorCallback() { |
| @Override |
| public void onError(int error, android.hardware.Camera camera) { |
| String errorMessage; |
| if (error == android.hardware.Camera.CAMERA_ERROR_SERVER_DIED) { |
| errorMessage = "Camera server died!"; |
| } else { |
| errorMessage = "Camera error: " + error; |
| } |
| Logging.e(TAG, errorMessage); |
| if (eventsHandler != null) { |
| eventsHandler.onCameraError(errorMessage); |
| } |
| } |
| }; |
| |
| public static VideoCapturerAndroid create(String name, |
| CameraEventsHandler eventsHandler) { |
| return VideoCapturerAndroid.create(name, eventsHandler, false /* captureToTexture */); |
| } |
| |
| // Use ctor directly instead. |
| @Deprecated |
| public static VideoCapturerAndroid create(String name, |
| CameraEventsHandler eventsHandler, boolean captureToTexture) { |
| try { |
| return new VideoCapturerAndroid(name, eventsHandler, captureToTexture); |
| } catch (RuntimeException e) { |
| Logging.e(TAG, "Couldn't create camera.", e); |
| return null; |
| } |
| } |
| |
| public void printStackTrace() { |
| Thread cameraThread = null; |
| if (cameraThreadHandler != null) { |
| cameraThread = cameraThreadHandler.getLooper().getThread(); |
| } |
| if (cameraThread != null) { |
| StackTraceElement[] cameraStackTraces = cameraThread.getStackTrace(); |
| if (cameraStackTraces.length > 0) { |
| Logging.d(TAG, "VideoCapturerAndroid stacks trace:"); |
| for (StackTraceElement stackTrace : cameraStackTraces) { |
| Logging.d(TAG, stackTrace.toString()); |
| } |
| } |
| } |
| } |
| |
| // Switch camera to the next valid camera id. This can only be called while |
| // the camera is running. |
| @Override |
| public void switchCamera(final CameraSwitchHandler switchEventsHandler) { |
| if (android.hardware.Camera.getNumberOfCameras() < 2) { |
| if (switchEventsHandler != null) { |
| switchEventsHandler.onCameraSwitchError("No camera to switch to."); |
| } |
| return; |
| } |
| synchronized (pendingCameraSwitchLock) { |
| if (pendingCameraSwitch) { |
| // Do not handle multiple camera switch request to avoid blocking |
| // camera thread by handling too many switch request from a queue. |
| Logging.w(TAG, "Ignoring camera switch request."); |
| if (switchEventsHandler != null) { |
| switchEventsHandler.onCameraSwitchError("Pending camera switch already in progress."); |
| } |
| return; |
| } |
| pendingCameraSwitch = true; |
| } |
| final boolean didPost = maybePostOnCameraThread(new Runnable() { |
| @Override |
| public void run() { |
| switchCameraOnCameraThread(); |
| synchronized (pendingCameraSwitchLock) { |
| pendingCameraSwitch = false; |
| } |
| if (switchEventsHandler != null) { |
| switchEventsHandler.onCameraSwitchDone( |
| info.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT); |
| } |
| } |
| }); |
| if (!didPost && switchEventsHandler != null) { |
| switchEventsHandler.onCameraSwitchError("Camera is stopped."); |
| } |
| } |
| |
| // Requests a new output format from the video capturer. Captured frames |
| // by the camera will be scaled/or dropped by the video capturer. |
| // It does not matter if width and height are flipped. I.E, |width| = 640, |height| = 480 produce |
| // the same result as |width| = 480, |height| = 640. |
| // TODO(magjed/perkj): Document what this function does. Change name? |
| @Override |
| public void onOutputFormatRequest(final int width, final int height, final int framerate) { |
| maybePostOnCameraThread(new Runnable() { |
| @Override public void run() { |
| onOutputFormatRequestOnCameraThread(width, height, framerate); |
| } |
| }); |
| } |
| |
| // Reconfigure the camera to capture in a new format. This should only be called while the camera |
| // is running. |
| @Override |
| public void changeCaptureFormat(final int width, final int height, final int framerate) { |
| maybePostOnCameraThread(new Runnable() { |
| @Override public void run() { |
| startPreviewOnCameraThread(width, height, framerate); |
| } |
| }); |
| } |
| |
| // Helper function to retrieve the current camera id synchronously. Note that the camera id might |
| // change at any point by switchCamera() calls. |
| private int getCurrentCameraId() { |
| synchronized (cameraIdLock) { |
| return id; |
| } |
| } |
| |
| @Override |
| public List<CaptureFormat> getSupportedFormats() { |
| return Camera1Enumerator.getSupportedFormats(getCurrentCameraId()); |
| } |
| |
| // Returns true if this VideoCapturer is setup to capture video frames to a SurfaceTexture. |
| public boolean isCapturingToTexture() { |
| return isCapturingToTexture; |
| } |
| |
| public VideoCapturerAndroid(String cameraName, CameraEventsHandler eventsHandler, |
| boolean captureToTexture) { |
| if (android.hardware.Camera.getNumberOfCameras() == 0) { |
| throw new RuntimeException("No cameras available"); |
| } |
| if (cameraName == null || cameraName.equals("")) { |
| this.id = 0; |
| } else { |
| this.id = Camera1Enumerator.getCameraIndex(cameraName); |
| } |
| this.eventsHandler = eventsHandler; |
| isCapturingToTexture = captureToTexture; |
| Logging.d(TAG, "VideoCapturerAndroid isCapturingToTexture : " + isCapturingToTexture); |
| } |
| |
| private void checkIsOnCameraThread() { |
| if (cameraThreadHandler == null) { |
| Logging.e(TAG, "Camera is not initialized - can't check thread."); |
| } else if (Thread.currentThread() != cameraThreadHandler.getLooper().getThread()) { |
| throw new IllegalStateException("Wrong thread"); |
| } |
| } |
| |
| private boolean maybePostOnCameraThread(Runnable runnable) { |
| return maybePostDelayedOnCameraThread(0 /* delayMs */, runnable); |
| } |
| |
| private boolean maybePostDelayedOnCameraThread(int delayMs, Runnable runnable) { |
| return cameraThreadHandler != null && isCameraRunning.get() |
| && cameraThreadHandler.postAtTime( |
| runnable, this /* token */, SystemClock.uptimeMillis() + delayMs); |
| } |
| |
| @Override |
| public void dispose() { |
| Logging.d(TAG, "dispose"); |
| } |
| |
| private boolean isInitialized() { |
| return applicationContext != null && frameObserver != null; |
| } |
| |
| @Override |
| public void initialize(SurfaceTextureHelper surfaceTextureHelper, Context applicationContext, |
| CapturerObserver frameObserver) { |
| Logging.d(TAG, "initialize"); |
| if (applicationContext == null) { |
| throw new IllegalArgumentException("applicationContext not set."); |
| } |
| if (frameObserver == null) { |
| throw new IllegalArgumentException("frameObserver not set."); |
| } |
| if (isInitialized()) { |
| throw new IllegalStateException("Already initialized"); |
| } |
| this.applicationContext = applicationContext; |
| this.frameObserver = frameObserver; |
| this.surfaceHelper = surfaceTextureHelper; |
| this.cameraThreadHandler = |
| surfaceTextureHelper == null ? null : surfaceTextureHelper.getHandler(); |
| } |
| |
| // Note that this actually opens the camera, and Camera callbacks run on the |
| // thread that calls open(), so this is done on the CameraThread. |
| @Override |
| public void startCapture(final int width, final int height, final int framerate) { |
| Logging.d(TAG, "startCapture requested: " + width + "x" + height + "@" + framerate); |
| if (!isInitialized()) { |
| throw new IllegalStateException("startCapture called in uninitialized state"); |
| } |
| if (surfaceHelper == null) { |
| frameObserver.onCapturerStarted(false /* success */); |
| if (eventsHandler != null) { |
| eventsHandler.onCameraError("No SurfaceTexture created."); |
| } |
| return; |
| } |
| if (isCameraRunning.getAndSet(true)) { |
| Logging.e(TAG, "Camera has already been started."); |
| return; |
| } |
| final boolean didPost = maybePostOnCameraThread(new Runnable() { |
| @Override |
| public void run() { |
| openCameraAttempts = 0; |
| startCaptureOnCameraThread(width, height, framerate); |
| } |
| }); |
| if (!didPost) { |
| frameObserver.onCapturerStarted(false); |
| if (eventsHandler != null) { |
| eventsHandler.onCameraError("Could not post task to camera thread."); |
| } |
| isCameraRunning.set(false); |
| } |
| } |
| |
| private void startCaptureOnCameraThread(final int width, final int height, final int framerate) { |
| checkIsOnCameraThread(); |
| if (!isCameraRunning.get()) { |
| Logging.e(TAG, "startCaptureOnCameraThread: Camera is stopped"); |
| return; |
| } |
| if (camera != null) { |
| Logging.e(TAG, "startCaptureOnCameraThread: Camera has already been started."); |
| return; |
| } |
| this.firstFrameReported = false; |
| |
| try { |
| try { |
| synchronized (cameraIdLock) { |
| Logging.d(TAG, "Opening camera " + id); |
| if (eventsHandler != null) { |
| eventsHandler.onCameraOpening(id); |
| } |
| camera = android.hardware.Camera.open(id); |
| info = new android.hardware.Camera.CameraInfo(); |
| android.hardware.Camera.getCameraInfo(id, info); |
| } |
| } catch (RuntimeException e) { |
| openCameraAttempts++; |
| if (openCameraAttempts < MAX_OPEN_CAMERA_ATTEMPTS) { |
| Logging.e(TAG, "Camera.open failed, retrying", e); |
| maybePostDelayedOnCameraThread(OPEN_CAMERA_DELAY_MS, new Runnable() { |
| @Override |
| public void run() { |
| startCaptureOnCameraThread(width, height, framerate); |
| } |
| }); |
| return; |
| } |
| throw e; |
| } |
| |
| camera.setPreviewTexture(surfaceHelper.getSurfaceTexture()); |
| |
| Logging.d(TAG, "Camera orientation: " + info.orientation + |
| " .Device orientation: " + getDeviceOrientation()); |
| camera.setErrorCallback(cameraErrorCallback); |
| startPreviewOnCameraThread(width, height, framerate); |
| frameObserver.onCapturerStarted(true); |
| if (isCapturingToTexture) { |
| surfaceHelper.startListening(this); |
| } |
| |
| // Start camera observer. |
| cameraStatistics = new CameraStatistics(surfaceHelper, eventsHandler); |
| } catch (IOException|RuntimeException e) { |
| Logging.e(TAG, "startCapture failed", e); |
| // Make sure the camera is released. |
| stopCaptureOnCameraThread(true /* stopHandler */); |
| frameObserver.onCapturerStarted(false); |
| if (eventsHandler != null) { |
| eventsHandler.onCameraError("Camera can not be started."); |
| } |
| } |
| } |
| |
| // (Re)start preview with the closest supported format to |width| x |height| @ |framerate|. |
| private void startPreviewOnCameraThread(int width, int height, int framerate) { |
| checkIsOnCameraThread(); |
| if (!isCameraRunning.get() || camera == null) { |
| Logging.e(TAG, "startPreviewOnCameraThread: Camera is stopped"); |
| return; |
| } |
| Logging.d( |
| TAG, "startPreviewOnCameraThread requested: " + width + "x" + height + "@" + framerate); |
| |
| requestedWidth = width; |
| requestedHeight = height; |
| requestedFramerate = framerate; |
| |
| // Find closest supported format for |width| x |height| @ |framerate|. |
| final android.hardware.Camera.Parameters parameters = camera.getParameters(); |
| final List<CaptureFormat.FramerateRange> supportedFramerates = |
| Camera1Enumerator.convertFramerates(parameters.getSupportedPreviewFpsRange()); |
| Logging.d(TAG, "Available fps ranges: " + supportedFramerates); |
| |
| final CaptureFormat.FramerateRange fpsRange = |
| CameraEnumerationAndroid.getClosestSupportedFramerateRange(supportedFramerates, framerate); |
| |
| final Size previewSize = CameraEnumerationAndroid.getClosestSupportedSize( |
| Camera1Enumerator.convertSizes(parameters.getSupportedPreviewSizes()), width, height); |
| |
| final CaptureFormat captureFormat = |
| new CaptureFormat(previewSize.width, previewSize.height, fpsRange); |
| |
| // Check if we are already using this capture format, then we don't need to do anything. |
| if (captureFormat.equals(this.captureFormat)) { |
| return; |
| } |
| |
| // Update camera parameters. |
| Logging.d(TAG, "isVideoStabilizationSupported: " + |
| parameters.isVideoStabilizationSupported()); |
| if (parameters.isVideoStabilizationSupported()) { |
| parameters.setVideoStabilization(true); |
| } |
| // Note: setRecordingHint(true) actually decrease frame rate on N5. |
| // parameters.setRecordingHint(true); |
| if (captureFormat.framerate.max > 0) { |
| parameters.setPreviewFpsRange(captureFormat.framerate.min, captureFormat.framerate.max); |
| } |
| parameters.setPreviewSize(previewSize.width, previewSize.height); |
| |
| if (!isCapturingToTexture) { |
| parameters.setPreviewFormat(captureFormat.imageFormat); |
| } |
| // Picture size is for taking pictures and not for preview/video, but we need to set it anyway |
| // as a workaround for an aspect ratio problem on Nexus 7. |
| final Size pictureSize = CameraEnumerationAndroid.getClosestSupportedSize( |
| Camera1Enumerator.convertSizes(parameters.getSupportedPictureSizes()), width, height); |
| parameters.setPictureSize(pictureSize.width, pictureSize.height); |
| |
| // Temporarily stop preview if it's already running. |
| if (this.captureFormat != null) { |
| camera.stopPreview(); |
| // Calling |setPreviewCallbackWithBuffer| with null should clear the internal camera buffer |
| // queue, but sometimes we receive a frame with the old resolution after this call anyway. |
| camera.setPreviewCallbackWithBuffer(null); |
| } |
| |
| // (Re)start preview. |
| Logging.d(TAG, "Start capturing: " + captureFormat); |
| this.captureFormat = captureFormat; |
| |
| List<String> focusModes = parameters.getSupportedFocusModes(); |
| if (focusModes.contains(android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { |
| parameters.setFocusMode(android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); |
| } |
| |
| camera.setParameters(parameters); |
| // Calculate orientation manually and send it as CVO instead. |
| camera.setDisplayOrientation(0 /* degrees */); |
| if (!isCapturingToTexture) { |
| queuedBuffers.clear(); |
| final int frameSize = captureFormat.frameSize(); |
| for (int i = 0; i < NUMBER_OF_CAPTURE_BUFFERS; ++i) { |
| final ByteBuffer buffer = ByteBuffer.allocateDirect(frameSize); |
| queuedBuffers.add(buffer.array()); |
| camera.addCallbackBuffer(buffer.array()); |
| } |
| camera.setPreviewCallbackWithBuffer(this); |
| } |
| camera.startPreview(); |
| } |
| |
| // Blocks until camera is known to be stopped. |
| @Override |
| public void stopCapture() throws InterruptedException { |
| Logging.d(TAG, "stopCapture"); |
| final CountDownLatch barrier = new CountDownLatch(1); |
| final boolean didPost = maybePostOnCameraThread(new Runnable() { |
| @Override public void run() { |
| stopCaptureOnCameraThread(true /* stopHandler */); |
| barrier.countDown(); |
| } |
| }); |
| if (!didPost) { |
| Logging.e(TAG, "Calling stopCapture() for already stopped camera."); |
| return; |
| } |
| if (!barrier.await(CAMERA_STOP_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { |
| Logging.e(TAG, "Camera stop timeout"); |
| printStackTrace(); |
| if (eventsHandler != null) { |
| eventsHandler.onCameraError("Camera stop timeout"); |
| } |
| } |
| frameObserver.onCapturerStopped(); |
| Logging.d(TAG, "stopCapture done"); |
| } |
| |
| private void stopCaptureOnCameraThread(boolean stopHandler) { |
| checkIsOnCameraThread(); |
| Logging.d(TAG, "stopCaptureOnCameraThread"); |
| // Note that the camera might still not be started here if startCaptureOnCameraThread failed |
| // and we posted a retry. |
| |
| // Make sure onTextureFrameAvailable() is not called anymore. |
| if (surfaceHelper != null) { |
| surfaceHelper.stopListening(); |
| } |
| if (stopHandler) { |
| // Clear the cameraThreadHandler first, in case stopPreview or |
| // other driver code deadlocks. Deadlock in |
| // android.hardware.Camera._stopPreview(Native Method) has |
| // been observed on Nexus 5 (hammerhead), OS version LMY48I. |
| // The camera might post another one or two preview frames |
| // before stopped, so we have to check |isCameraRunning|. |
| // Remove all pending Runnables posted from |this|. |
| isCameraRunning.set(false); |
| cameraThreadHandler.removeCallbacksAndMessages(this /* token */); |
| } |
| if (cameraStatistics != null) { |
| cameraStatistics.release(); |
| cameraStatistics = null; |
| } |
| Logging.d(TAG, "Stop preview."); |
| if (camera != null) { |
| camera.stopPreview(); |
| camera.setPreviewCallbackWithBuffer(null); |
| } |
| queuedBuffers.clear(); |
| captureFormat = null; |
| |
| Logging.d(TAG, "Release camera."); |
| if (camera != null) { |
| camera.release(); |
| camera = null; |
| } |
| if (eventsHandler != null) { |
| eventsHandler.onCameraClosed(); |
| } |
| Logging.d(TAG, "stopCaptureOnCameraThread done"); |
| } |
| |
| private void switchCameraOnCameraThread() { |
| checkIsOnCameraThread(); |
| if (!isCameraRunning.get()) { |
| Logging.e(TAG, "switchCameraOnCameraThread: Camera is stopped"); |
| return; |
| } |
| Logging.d(TAG, "switchCameraOnCameraThread"); |
| stopCaptureOnCameraThread(false /* stopHandler */); |
| synchronized (cameraIdLock) { |
| id = (id + 1) % android.hardware.Camera.getNumberOfCameras(); |
| } |
| startCaptureOnCameraThread(requestedWidth, requestedHeight, requestedFramerate); |
| Logging.d(TAG, "switchCameraOnCameraThread done"); |
| } |
| |
| private void onOutputFormatRequestOnCameraThread(int width, int height, int framerate) { |
| checkIsOnCameraThread(); |
| Logging.d(TAG, "onOutputFormatRequestOnCameraThread: " + width + "x" + height + |
| "@" + framerate); |
| frameObserver.onOutputFormatRequest(width, height, framerate); |
| } |
| |
| 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 (info.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK) { |
| rotation = 360 - rotation; |
| } |
| return (info.orientation + rotation) % 360; |
| } |
| |
| // Called on cameraThread so must not "synchronized". |
| @Override |
| public void onPreviewFrame(byte[] data, android.hardware.Camera callbackCamera) { |
| checkIsOnCameraThread(); |
| if (!isCameraRunning.get()) { |
| Logging.e(TAG, "onPreviewFrame: Camera is stopped"); |
| return; |
| } |
| if (!queuedBuffers.contains(data)) { |
| // |data| is an old invalid buffer. |
| return; |
| } |
| if (camera != callbackCamera) { |
| throw new RuntimeException("Unexpected camera in callback!"); |
| } |
| |
| final long captureTimeNs = |
| TimeUnit.MILLISECONDS.toNanos(SystemClock.elapsedRealtime()); |
| |
| if (eventsHandler != null && !firstFrameReported) { |
| eventsHandler.onFirstFrameAvailable(); |
| firstFrameReported = true; |
| } |
| |
| cameraStatistics.addFrame(); |
| frameObserver.onByteBufferFrameCaptured(data, captureFormat.width, captureFormat.height, |
| getFrameOrientation(), captureTimeNs); |
| camera.addCallbackBuffer(data); |
| } |
| |
| @Override |
| public void onTextureFrameAvailable( |
| int oesTextureId, float[] transformMatrix, long timestampNs) { |
| checkIsOnCameraThread(); |
| if (!isCameraRunning.get()) { |
| Logging.e(TAG, "onTextureFrameAvailable: Camera is stopped"); |
| surfaceHelper.returnTextureFrame(); |
| return; |
| } |
| if (eventsHandler != null && !firstFrameReported) { |
| eventsHandler.onFirstFrameAvailable(); |
| firstFrameReported = true; |
| } |
| |
| int rotation = getFrameOrientation(); |
| if (info.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT) { |
| // 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()); |
| } |
| cameraStatistics.addFrame(); |
| frameObserver.onTextureFrameCaptured(captureFormat.width, captureFormat.height, oesTextureId, |
| transformMatrix, rotation, timestampNs); |
| } |
| } |