/* * 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.os.Handler; import android.os.Looper; import androidx.annotation.Nullable; import java.util.Arrays; import java.util.List; @SuppressWarnings("deprecation") abstract class CameraCapturer implements CameraVideoCapturer { enum SwitchState { IDLE, // No switch requested. PENDING, // Waiting for previous capture session to open. IN_PROGRESS, // Waiting for new switched capture session to start. } private static final String TAG = "CameraCapturer"; private final static int MAX_OPEN_CAMERA_ATTEMPTS = 3; private final static int OPEN_CAMERA_DELAY_MS = 500; private final static int OPEN_CAMERA_TIMEOUT = 10000; private final CameraEnumerator cameraEnumerator; private final CameraEventsHandler eventsHandler; private final Handler uiThreadHandler; @Nullable private final CameraSession.CreateSessionCallback createSessionCallback = new CameraSession.CreateSessionCallback() { @Override public void onDone(CameraSession session) { checkIsOnCameraThread(); Logging.d(TAG, "Create session done. Switch state: " + switchState); uiThreadHandler.removeCallbacks(openCameraTimeoutRunnable); synchronized (stateLock) { capturerObserver.onCapturerStarted(true /* success */); sessionOpening = false; currentSession = session; cameraStatistics = new CameraStatistics(surfaceHelper, eventsHandler); firstFrameObserved = false; stateLock.notifyAll(); if (switchState == SwitchState.IN_PROGRESS) { switchState = SwitchState.IDLE; if (switchEventsHandler != null) { switchEventsHandler.onCameraSwitchDone(cameraEnumerator.isFrontFacing(cameraName)); switchEventsHandler = null; } } else if (switchState == SwitchState.PENDING) { String selectedCameraName = pendingCameraName; pendingCameraName = null; switchState = SwitchState.IDLE; switchCameraInternal(switchEventsHandler, selectedCameraName); } } } @Override public void onFailure(CameraSession.FailureType failureType, String error) { checkIsOnCameraThread(); uiThreadHandler.removeCallbacks(openCameraTimeoutRunnable); synchronized (stateLock) { capturerObserver.onCapturerStarted(false /* success */); openAttemptsRemaining--; if (openAttemptsRemaining <= 0) { Logging.w(TAG, "Opening camera failed, passing: " + error); sessionOpening = false; stateLock.notifyAll(); if (switchState != SwitchState.IDLE) { if (switchEventsHandler != null) { switchEventsHandler.onCameraSwitchError(error); switchEventsHandler = null; } switchState = SwitchState.IDLE; } if (failureType == CameraSession.FailureType.DISCONNECTED) { eventsHandler.onCameraDisconnected(); } else { eventsHandler.onCameraError(error); } } else { Logging.w(TAG, "Opening camera failed, retry: " + error); createSessionInternal(OPEN_CAMERA_DELAY_MS); } } } }; @Nullable private final CameraSession.Events cameraSessionEventsHandler = new CameraSession.Events() { @Override public void onCameraOpening() { checkIsOnCameraThread(); synchronized (stateLock) { if (currentSession != null) { Logging.w(TAG, "onCameraOpening while session was open."); return; } eventsHandler.onCameraOpening(cameraName); } } @Override public void onCameraError(CameraSession session, String error) { checkIsOnCameraThread(); synchronized (stateLock) { if (session != currentSession) { Logging.w(TAG, "onCameraError from another session: " + error); return; } eventsHandler.onCameraError(error); stopCapture(); } } @Override public void onCameraDisconnected(CameraSession session) { checkIsOnCameraThread(); synchronized (stateLock) { if (session != currentSession) { Logging.w(TAG, "onCameraDisconnected from another session."); return; } eventsHandler.onCameraDisconnected(); stopCapture(); } } @Override public void onCameraClosed(CameraSession session) { checkIsOnCameraThread(); synchronized (stateLock) { if (session != currentSession && currentSession != null) { Logging.d(TAG, "onCameraClosed from another session."); return; } eventsHandler.onCameraClosed(); } } @Override public void onFrameCaptured(CameraSession session, VideoFrame frame) { checkIsOnCameraThread(); synchronized (stateLock) { if (session != currentSession) { Logging.w(TAG, "onFrameCaptured from another session."); return; } if (!firstFrameObserved) { eventsHandler.onFirstFrameAvailable(); firstFrameObserved = true; } cameraStatistics.addFrame(); capturerObserver.onFrameCaptured(frame); } } }; private final Runnable openCameraTimeoutRunnable = new Runnable() { @Override public void run() { eventsHandler.onCameraError("Camera failed to start within timeout."); } }; // Initialized on initialize // ------------------------- private Handler cameraThreadHandler; private Context applicationContext; private org.webrtc.CapturerObserver capturerObserver; private SurfaceTextureHelper surfaceHelper; private final Object stateLock = new Object(); private boolean sessionOpening; /* guarded by stateLock */ @Nullable private CameraSession currentSession; /* guarded by stateLock */ private String cameraName; /* guarded by stateLock */ private String pendingCameraName; /* guarded by stateLock */ private int width; /* guarded by stateLock */ private int height; /* guarded by stateLock */ private int framerate; /* guarded by stateLock */ private int openAttemptsRemaining; /* guarded by stateLock */ private SwitchState switchState = SwitchState.IDLE; /* guarded by stateLock */ @Nullable private CameraSwitchHandler switchEventsHandler; /* guarded by stateLock */ // Valid from onDone call until stopCapture, otherwise null. @Nullable private CameraStatistics cameraStatistics; /* guarded by stateLock */ private boolean firstFrameObserved; /* guarded by stateLock */ public CameraCapturer(String cameraName, @Nullable CameraEventsHandler eventsHandler, CameraEnumerator cameraEnumerator) { if (eventsHandler == null) { eventsHandler = new CameraEventsHandler() { @Override public void onCameraError(String errorDescription) {} @Override public void onCameraDisconnected() {} @Override public void onCameraFreezed(String errorDescription) {} @Override public void onCameraOpening(String cameraName) {} @Override public void onFirstFrameAvailable() {} @Override public void onCameraClosed() {} }; } this.eventsHandler = eventsHandler; this.cameraEnumerator = cameraEnumerator; this.cameraName = cameraName; List deviceNames = Arrays.asList(cameraEnumerator.getDeviceNames()); uiThreadHandler = new Handler(Looper.getMainLooper()); if (deviceNames.isEmpty()) { throw new RuntimeException("No cameras attached."); } if (!deviceNames.contains(this.cameraName)) { throw new IllegalArgumentException( "Camera name " + this.cameraName + " does not match any known camera device."); } } @Override public void initialize(SurfaceTextureHelper surfaceTextureHelper, Context applicationContext, org.webrtc.CapturerObserver capturerObserver) { this.applicationContext = applicationContext; this.capturerObserver = capturerObserver; this.surfaceHelper = surfaceTextureHelper; this.cameraThreadHandler = surfaceTextureHelper.getHandler(); } @Override public void startCapture(int width, int height, int framerate) { Logging.d(TAG, "startCapture: " + width + "x" + height + "@" + framerate); if (applicationContext == null) { throw new RuntimeException("CameraCapturer must be initialized before calling startCapture."); } synchronized (stateLock) { if (sessionOpening || currentSession != null) { Logging.w(TAG, "Session already open"); return; } this.width = width; this.height = height; this.framerate = framerate; sessionOpening = true; openAttemptsRemaining = MAX_OPEN_CAMERA_ATTEMPTS; createSessionInternal(0); } } private void createSessionInternal(int delayMs) { uiThreadHandler.postDelayed(openCameraTimeoutRunnable, delayMs + OPEN_CAMERA_TIMEOUT); cameraThreadHandler.postDelayed(new Runnable() { @Override public void run() { createCameraSession(createSessionCallback, cameraSessionEventsHandler, applicationContext, surfaceHelper, cameraName, width, height, framerate); } }, delayMs); } @Override public void stopCapture() { Logging.d(TAG, "Stop capture"); synchronized (stateLock) { while (sessionOpening) { Logging.d(TAG, "Stop capture: Waiting for session to open"); try { stateLock.wait(); } catch (InterruptedException e) { Logging.w(TAG, "Stop capture interrupted while waiting for the session to open."); Thread.currentThread().interrupt(); return; } } if (currentSession != null) { Logging.d(TAG, "Stop capture: Nulling session"); cameraStatistics.release(); cameraStatistics = null; final CameraSession oldSession = currentSession; cameraThreadHandler.post(new Runnable() { @Override public void run() { oldSession.stop(); } }); currentSession = null; capturerObserver.onCapturerStopped(); } else { Logging.d(TAG, "Stop capture: No session open"); } } Logging.d(TAG, "Stop capture done"); } @Override public void changeCaptureFormat(int width, int height, int framerate) { Logging.d(TAG, "changeCaptureFormat: " + width + "x" + height + "@" + framerate); synchronized (stateLock) { stopCapture(); startCapture(width, height, framerate); } } @Override public void dispose() { Logging.d(TAG, "dispose"); stopCapture(); } @Override public void switchCamera(final CameraSwitchHandler switchEventsHandler) { Logging.d(TAG, "switchCamera"); cameraThreadHandler.post(new Runnable() { @Override public void run() { List deviceNames = Arrays.asList(cameraEnumerator.getDeviceNames()); if (deviceNames.size() < 2) { reportCameraSwitchError("No camera to switch to.", switchEventsHandler); return; } int cameraNameIndex = deviceNames.indexOf(cameraName); String cameraName = deviceNames.get((cameraNameIndex + 1) % deviceNames.size()); switchCameraInternal(switchEventsHandler, cameraName); } }); } @Override public void switchCamera(final CameraSwitchHandler switchEventsHandler, final String cameraName) { Logging.d(TAG, "switchCamera"); cameraThreadHandler.post(new Runnable() { @Override public void run() { switchCameraInternal(switchEventsHandler, cameraName); } }); } @Override public boolean isScreencast() { return false; } public void printStackTrace() { Thread cameraThread = null; if (cameraThreadHandler != null) { cameraThread = cameraThreadHandler.getLooper().getThread(); } if (cameraThread != null) { StackTraceElement[] cameraStackTrace = cameraThread.getStackTrace(); if (cameraStackTrace.length > 0) { Logging.d(TAG, "CameraCapturer stack trace:"); for (StackTraceElement traceElem : cameraStackTrace) { Logging.d(TAG, traceElem.toString()); } } } } private void reportCameraSwitchError( String error, @Nullable CameraSwitchHandler switchEventsHandler) { Logging.e(TAG, error); if (switchEventsHandler != null) { switchEventsHandler.onCameraSwitchError(error); } } private void switchCameraInternal( @Nullable final CameraSwitchHandler switchEventsHandler, final String selectedCameraName) { Logging.d(TAG, "switchCamera internal"); List deviceNames = Arrays.asList(cameraEnumerator.getDeviceNames()); if (!deviceNames.contains(selectedCameraName)) { reportCameraSwitchError("Attempted to switch to unknown camera device " + selectedCameraName, switchEventsHandler); return; } synchronized (stateLock) { if (switchState != SwitchState.IDLE) { reportCameraSwitchError("Camera switch already in progress.", switchEventsHandler); return; } if (!sessionOpening && currentSession == null) { reportCameraSwitchError("switchCamera: camera is not running.", switchEventsHandler); return; } this.switchEventsHandler = switchEventsHandler; if (sessionOpening) { switchState = SwitchState.PENDING; pendingCameraName = selectedCameraName; return; } else { switchState = SwitchState.IN_PROGRESS; } Logging.d(TAG, "switchCamera: Stopping session"); cameraStatistics.release(); cameraStatistics = null; final CameraSession oldSession = currentSession; cameraThreadHandler.post(new Runnable() { @Override public void run() { oldSession.stop(); } }); currentSession = null; cameraName = selectedCameraName; sessionOpening = true; openAttemptsRemaining = 1; createSessionInternal(0); } Logging.d(TAG, "switchCamera done"); } private void checkIsOnCameraThread() { if (Thread.currentThread() != cameraThreadHandler.getLooper().getThread()) { Logging.e(TAG, "Check is on camera thread failed."); throw new RuntimeException("Not on camera thread."); } } protected String getCameraName() { synchronized (stateLock) { return cameraName; } } abstract protected void createCameraSession( CameraSession.CreateSessionCallback createSessionCallback, CameraSession.Events events, Context applicationContext, SurfaceTextureHelper surfaceTextureHelper, String cameraName, int width, int height, int framerate); }