• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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