1 /* 2 * Copyright 2016 The WebRTC project authors. All Rights Reserved. 3 * 4 * Use of this source code is governed by a BSD-style license 5 * that can be found in the LICENSE file in the root of the source 6 * tree. An additional intellectual property rights grant can be found 7 * in the file PATENTS. All contributing project authors may 8 * be found in the AUTHORS file in the root of the source tree. 9 */ 10 11 package org.webrtc; 12 13 import android.content.Context; 14 import android.os.Handler; 15 import android.os.Looper; 16 import androidx.annotation.Nullable; 17 import java.util.Arrays; 18 import java.util.List; 19 20 @SuppressWarnings("deprecation") 21 abstract class CameraCapturer implements CameraVideoCapturer { 22 enum SwitchState { 23 IDLE, // No switch requested. 24 PENDING, // Waiting for previous capture session to open. 25 IN_PROGRESS, // Waiting for new switched capture session to start. 26 } 27 28 private static final String TAG = "CameraCapturer"; 29 private final static int MAX_OPEN_CAMERA_ATTEMPTS = 3; 30 private final static int OPEN_CAMERA_DELAY_MS = 500; 31 private final static int OPEN_CAMERA_TIMEOUT = 10000; 32 33 private final CameraEnumerator cameraEnumerator; 34 private final CameraEventsHandler eventsHandler; 35 private final Handler uiThreadHandler; 36 37 @Nullable 38 private final CameraSession.CreateSessionCallback createSessionCallback = 39 new CameraSession.CreateSessionCallback() { 40 @Override 41 public void onDone(CameraSession session) { 42 checkIsOnCameraThread(); 43 Logging.d(TAG, "Create session done. Switch state: " + switchState); 44 uiThreadHandler.removeCallbacks(openCameraTimeoutRunnable); 45 synchronized (stateLock) { 46 capturerObserver.onCapturerStarted(true /* success */); 47 sessionOpening = false; 48 currentSession = session; 49 cameraStatistics = new CameraStatistics(surfaceHelper, eventsHandler); 50 firstFrameObserved = false; 51 stateLock.notifyAll(); 52 53 if (switchState == SwitchState.IN_PROGRESS) { 54 switchState = SwitchState.IDLE; 55 if (switchEventsHandler != null) { 56 switchEventsHandler.onCameraSwitchDone(cameraEnumerator.isFrontFacing(cameraName)); 57 switchEventsHandler = null; 58 } 59 } else if (switchState == SwitchState.PENDING) { 60 String selectedCameraName = pendingCameraName; 61 pendingCameraName = null; 62 switchState = SwitchState.IDLE; 63 switchCameraInternal(switchEventsHandler, selectedCameraName); 64 } 65 } 66 } 67 68 @Override 69 public void onFailure(CameraSession.FailureType failureType, String error) { 70 checkIsOnCameraThread(); 71 uiThreadHandler.removeCallbacks(openCameraTimeoutRunnable); 72 synchronized (stateLock) { 73 capturerObserver.onCapturerStarted(false /* success */); 74 openAttemptsRemaining--; 75 76 if (openAttemptsRemaining <= 0) { 77 Logging.w(TAG, "Opening camera failed, passing: " + error); 78 sessionOpening = false; 79 stateLock.notifyAll(); 80 81 if (switchState != SwitchState.IDLE) { 82 if (switchEventsHandler != null) { 83 switchEventsHandler.onCameraSwitchError(error); 84 switchEventsHandler = null; 85 } 86 switchState = SwitchState.IDLE; 87 } 88 89 if (failureType == CameraSession.FailureType.DISCONNECTED) { 90 eventsHandler.onCameraDisconnected(); 91 } else { 92 eventsHandler.onCameraError(error); 93 } 94 } else { 95 Logging.w(TAG, "Opening camera failed, retry: " + error); 96 createSessionInternal(OPEN_CAMERA_DELAY_MS); 97 } 98 } 99 } 100 }; 101 102 @Nullable 103 private final CameraSession.Events cameraSessionEventsHandler = new CameraSession.Events() { 104 @Override 105 public void onCameraOpening() { 106 checkIsOnCameraThread(); 107 synchronized (stateLock) { 108 if (currentSession != null) { 109 Logging.w(TAG, "onCameraOpening while session was open."); 110 return; 111 } 112 eventsHandler.onCameraOpening(cameraName); 113 } 114 } 115 116 @Override 117 public void onCameraError(CameraSession session, String error) { 118 checkIsOnCameraThread(); 119 synchronized (stateLock) { 120 if (session != currentSession) { 121 Logging.w(TAG, "onCameraError from another session: " + error); 122 return; 123 } 124 eventsHandler.onCameraError(error); 125 stopCapture(); 126 } 127 } 128 129 @Override 130 public void onCameraDisconnected(CameraSession session) { 131 checkIsOnCameraThread(); 132 synchronized (stateLock) { 133 if (session != currentSession) { 134 Logging.w(TAG, "onCameraDisconnected from another session."); 135 return; 136 } 137 eventsHandler.onCameraDisconnected(); 138 stopCapture(); 139 } 140 } 141 142 @Override 143 public void onCameraClosed(CameraSession session) { 144 checkIsOnCameraThread(); 145 synchronized (stateLock) { 146 if (session != currentSession && currentSession != null) { 147 Logging.d(TAG, "onCameraClosed from another session."); 148 return; 149 } 150 eventsHandler.onCameraClosed(); 151 } 152 } 153 154 @Override 155 public void onFrameCaptured(CameraSession session, VideoFrame frame) { 156 checkIsOnCameraThread(); 157 synchronized (stateLock) { 158 if (session != currentSession) { 159 Logging.w(TAG, "onFrameCaptured from another session."); 160 return; 161 } 162 if (!firstFrameObserved) { 163 eventsHandler.onFirstFrameAvailable(); 164 firstFrameObserved = true; 165 } 166 cameraStatistics.addFrame(); 167 capturerObserver.onFrameCaptured(frame); 168 } 169 } 170 }; 171 172 private final Runnable openCameraTimeoutRunnable = new Runnable() { 173 @Override 174 public void run() { 175 eventsHandler.onCameraError("Camera failed to start within timeout."); 176 } 177 }; 178 179 // Initialized on initialize 180 // ------------------------- 181 private Handler cameraThreadHandler; 182 private Context applicationContext; 183 private org.webrtc.CapturerObserver capturerObserver; 184 private SurfaceTextureHelper surfaceHelper; 185 186 private final Object stateLock = new Object(); 187 private boolean sessionOpening; /* guarded by stateLock */ 188 @Nullable private CameraSession currentSession; /* guarded by stateLock */ 189 private String cameraName; /* guarded by stateLock */ 190 private String pendingCameraName; /* guarded by stateLock */ 191 private int width; /* guarded by stateLock */ 192 private int height; /* guarded by stateLock */ 193 private int framerate; /* guarded by stateLock */ 194 private int openAttemptsRemaining; /* guarded by stateLock */ 195 private SwitchState switchState = SwitchState.IDLE; /* guarded by stateLock */ 196 @Nullable private CameraSwitchHandler switchEventsHandler; /* guarded by stateLock */ 197 // Valid from onDone call until stopCapture, otherwise null. 198 @Nullable private CameraStatistics cameraStatistics; /* guarded by stateLock */ 199 private boolean firstFrameObserved; /* guarded by stateLock */ 200 CameraCapturer(String cameraName, @Nullable CameraEventsHandler eventsHandler, CameraEnumerator cameraEnumerator)201 public CameraCapturer(String cameraName, @Nullable CameraEventsHandler eventsHandler, 202 CameraEnumerator cameraEnumerator) { 203 if (eventsHandler == null) { 204 eventsHandler = new CameraEventsHandler() { 205 @Override 206 public void onCameraError(String errorDescription) {} 207 @Override 208 public void onCameraDisconnected() {} 209 @Override 210 public void onCameraFreezed(String errorDescription) {} 211 @Override 212 public void onCameraOpening(String cameraName) {} 213 @Override 214 public void onFirstFrameAvailable() {} 215 @Override 216 public void onCameraClosed() {} 217 }; 218 } 219 220 this.eventsHandler = eventsHandler; 221 this.cameraEnumerator = cameraEnumerator; 222 this.cameraName = cameraName; 223 List<String> deviceNames = Arrays.asList(cameraEnumerator.getDeviceNames()); 224 uiThreadHandler = new Handler(Looper.getMainLooper()); 225 226 if (deviceNames.isEmpty()) { 227 throw new RuntimeException("No cameras attached."); 228 } 229 if (!deviceNames.contains(this.cameraName)) { 230 throw new IllegalArgumentException( 231 "Camera name " + this.cameraName + " does not match any known camera device."); 232 } 233 } 234 235 @Override initialize(SurfaceTextureHelper surfaceTextureHelper, Context applicationContext, org.webrtc.CapturerObserver capturerObserver)236 public void initialize(SurfaceTextureHelper surfaceTextureHelper, Context applicationContext, 237 org.webrtc.CapturerObserver capturerObserver) { 238 this.applicationContext = applicationContext; 239 this.capturerObserver = capturerObserver; 240 this.surfaceHelper = surfaceTextureHelper; 241 this.cameraThreadHandler = surfaceTextureHelper.getHandler(); 242 } 243 244 @Override startCapture(int width, int height, int framerate)245 public void startCapture(int width, int height, int framerate) { 246 Logging.d(TAG, "startCapture: " + width + "x" + height + "@" + framerate); 247 if (applicationContext == null) { 248 throw new RuntimeException("CameraCapturer must be initialized before calling startCapture."); 249 } 250 251 synchronized (stateLock) { 252 if (sessionOpening || currentSession != null) { 253 Logging.w(TAG, "Session already open"); 254 return; 255 } 256 257 this.width = width; 258 this.height = height; 259 this.framerate = framerate; 260 261 sessionOpening = true; 262 openAttemptsRemaining = MAX_OPEN_CAMERA_ATTEMPTS; 263 createSessionInternal(0); 264 } 265 } 266 createSessionInternal(int delayMs)267 private void createSessionInternal(int delayMs) { 268 uiThreadHandler.postDelayed(openCameraTimeoutRunnable, delayMs + OPEN_CAMERA_TIMEOUT); 269 cameraThreadHandler.postDelayed(new Runnable() { 270 @Override 271 public void run() { 272 createCameraSession(createSessionCallback, cameraSessionEventsHandler, applicationContext, 273 surfaceHelper, cameraName, width, height, framerate); 274 } 275 }, delayMs); 276 } 277 278 @Override stopCapture()279 public void stopCapture() { 280 Logging.d(TAG, "Stop capture"); 281 282 synchronized (stateLock) { 283 while (sessionOpening) { 284 Logging.d(TAG, "Stop capture: Waiting for session to open"); 285 try { 286 stateLock.wait(); 287 } catch (InterruptedException e) { 288 Logging.w(TAG, "Stop capture interrupted while waiting for the session to open."); 289 Thread.currentThread().interrupt(); 290 return; 291 } 292 } 293 294 if (currentSession != null) { 295 Logging.d(TAG, "Stop capture: Nulling session"); 296 cameraStatistics.release(); 297 cameraStatistics = null; 298 final CameraSession oldSession = currentSession; 299 cameraThreadHandler.post(new Runnable() { 300 @Override 301 public void run() { 302 oldSession.stop(); 303 } 304 }); 305 currentSession = null; 306 capturerObserver.onCapturerStopped(); 307 } else { 308 Logging.d(TAG, "Stop capture: No session open"); 309 } 310 } 311 312 Logging.d(TAG, "Stop capture done"); 313 } 314 315 @Override changeCaptureFormat(int width, int height, int framerate)316 public void changeCaptureFormat(int width, int height, int framerate) { 317 Logging.d(TAG, "changeCaptureFormat: " + width + "x" + height + "@" + framerate); 318 synchronized (stateLock) { 319 stopCapture(); 320 startCapture(width, height, framerate); 321 } 322 } 323 324 @Override dispose()325 public void dispose() { 326 Logging.d(TAG, "dispose"); 327 stopCapture(); 328 } 329 330 @Override switchCamera(final CameraSwitchHandler switchEventsHandler)331 public void switchCamera(final CameraSwitchHandler switchEventsHandler) { 332 Logging.d(TAG, "switchCamera"); 333 cameraThreadHandler.post(new Runnable() { 334 @Override 335 public void run() { 336 List<String> deviceNames = Arrays.asList(cameraEnumerator.getDeviceNames()); 337 338 if (deviceNames.size() < 2) { 339 reportCameraSwitchError("No camera to switch to.", switchEventsHandler); 340 return; 341 } 342 343 int cameraNameIndex = deviceNames.indexOf(cameraName); 344 String cameraName = deviceNames.get((cameraNameIndex + 1) % deviceNames.size()); 345 switchCameraInternal(switchEventsHandler, cameraName); 346 } 347 }); 348 } 349 350 @Override switchCamera(final CameraSwitchHandler switchEventsHandler, final String cameraName)351 public void switchCamera(final CameraSwitchHandler switchEventsHandler, final String cameraName) { 352 Logging.d(TAG, "switchCamera"); 353 cameraThreadHandler.post(new Runnable() { 354 @Override 355 public void run() { 356 switchCameraInternal(switchEventsHandler, cameraName); 357 } 358 }); 359 } 360 361 @Override isScreencast()362 public boolean isScreencast() { 363 return false; 364 } 365 printStackTrace()366 public void printStackTrace() { 367 Thread cameraThread = null; 368 if (cameraThreadHandler != null) { 369 cameraThread = cameraThreadHandler.getLooper().getThread(); 370 } 371 if (cameraThread != null) { 372 StackTraceElement[] cameraStackTrace = cameraThread.getStackTrace(); 373 if (cameraStackTrace.length > 0) { 374 Logging.d(TAG, "CameraCapturer stack trace:"); 375 for (StackTraceElement traceElem : cameraStackTrace) { 376 Logging.d(TAG, traceElem.toString()); 377 } 378 } 379 } 380 } 381 reportCameraSwitchError( String error, @Nullable CameraSwitchHandler switchEventsHandler)382 private void reportCameraSwitchError( 383 String error, @Nullable CameraSwitchHandler switchEventsHandler) { 384 Logging.e(TAG, error); 385 if (switchEventsHandler != null) { 386 switchEventsHandler.onCameraSwitchError(error); 387 } 388 } 389 switchCameraInternal( @ullable final CameraSwitchHandler switchEventsHandler, final String selectedCameraName)390 private void switchCameraInternal( 391 @Nullable final CameraSwitchHandler switchEventsHandler, final String selectedCameraName) { 392 Logging.d(TAG, "switchCamera internal"); 393 List<String> deviceNames = Arrays.asList(cameraEnumerator.getDeviceNames()); 394 395 if (!deviceNames.contains(selectedCameraName)) { 396 reportCameraSwitchError("Attempted to switch to unknown camera device " + selectedCameraName, 397 switchEventsHandler); 398 return; 399 } 400 401 synchronized (stateLock) { 402 if (switchState != SwitchState.IDLE) { 403 reportCameraSwitchError("Camera switch already in progress.", switchEventsHandler); 404 return; 405 } 406 if (!sessionOpening && currentSession == null) { 407 reportCameraSwitchError("switchCamera: camera is not running.", switchEventsHandler); 408 return; 409 } 410 411 this.switchEventsHandler = switchEventsHandler; 412 if (sessionOpening) { 413 switchState = SwitchState.PENDING; 414 pendingCameraName = selectedCameraName; 415 return; 416 } else { 417 switchState = SwitchState.IN_PROGRESS; 418 } 419 420 Logging.d(TAG, "switchCamera: Stopping session"); 421 cameraStatistics.release(); 422 cameraStatistics = null; 423 final CameraSession oldSession = currentSession; 424 cameraThreadHandler.post(new Runnable() { 425 @Override 426 public void run() { 427 oldSession.stop(); 428 } 429 }); 430 currentSession = null; 431 432 cameraName = selectedCameraName; 433 434 sessionOpening = true; 435 openAttemptsRemaining = 1; 436 createSessionInternal(0); 437 } 438 Logging.d(TAG, "switchCamera done"); 439 } 440 checkIsOnCameraThread()441 private void checkIsOnCameraThread() { 442 if (Thread.currentThread() != cameraThreadHandler.getLooper().getThread()) { 443 Logging.e(TAG, "Check is on camera thread failed."); 444 throw new RuntimeException("Not on camera thread."); 445 } 446 } 447 getCameraName()448 protected String getCameraName() { 449 synchronized (stateLock) { 450 return cameraName; 451 } 452 } 453 createCameraSession( CameraSession.CreateSessionCallback createSessionCallback, CameraSession.Events events, Context applicationContext, SurfaceTextureHelper surfaceTextureHelper, String cameraName, int width, int height, int framerate)454 abstract protected void createCameraSession( 455 CameraSession.CreateSessionCallback createSessionCallback, CameraSession.Events events, 456 Context applicationContext, SurfaceTextureHelper surfaceTextureHelper, String cameraName, 457 int width, int height, int framerate); 458 } 459