OLD | NEW |
1 /* | 1 /* |
2 * libjingle | 2 * libjingle |
3 * Copyright 2015 Google Inc. | 3 * Copyright 2015 Google Inc. |
4 * | 4 * |
5 * Redistribution and use in source and binary forms, with or without | 5 * Redistribution and use in source and binary forms, with or without |
6 * modification, are permitted provided that the following conditions are met: | 6 * modification, are permitted provided that the following conditions are met: |
7 * | 7 * |
8 * 1. Redistributions of source code must retain the above copyright notice, | 8 * 1. Redistributions of source code must retain the above copyright notice, |
9 * this list of conditions and the following disclaimer. | 9 * this list of conditions and the following disclaimer. |
10 * 2. Redistributions in binary form must reproduce the above copyright notice, | 10 * 2. Redistributions in binary form must reproduce the above copyright notice, |
(...skipping 16 matching lines...) Expand all Loading... |
27 | 27 |
28 package org.webrtc; | 28 package org.webrtc; |
29 | 29 |
30 import android.content.Context; | 30 import android.content.Context; |
31 import android.graphics.SurfaceTexture; | 31 import android.graphics.SurfaceTexture; |
32 import android.hardware.Camera; | 32 import android.hardware.Camera; |
33 import android.hardware.Camera.PreviewCallback; | 33 import android.hardware.Camera.PreviewCallback; |
34 import android.opengl.GLES11Ext; | 34 import android.opengl.GLES11Ext; |
35 import android.opengl.GLES20; | 35 import android.opengl.GLES20; |
36 import android.os.Handler; | 36 import android.os.Handler; |
37 import android.os.Looper; | 37 import android.os.HandlerThread; |
38 import android.os.SystemClock; | 38 import android.os.SystemClock; |
39 import android.view.Surface; | 39 import android.view.Surface; |
40 import android.view.WindowManager; | 40 import android.view.WindowManager; |
41 | 41 |
42 import org.json.JSONException; | 42 import org.json.JSONException; |
43 import org.webrtc.CameraEnumerationAndroid.CaptureFormat; | 43 import org.webrtc.CameraEnumerationAndroid.CaptureFormat; |
44 import org.webrtc.Logging; | 44 import org.webrtc.Logging; |
45 | 45 |
46 import java.io.IOException; | 46 import java.io.IOException; |
47 import java.nio.ByteBuffer; | 47 import java.nio.ByteBuffer; |
48 import java.util.ArrayList; | 48 import java.util.ArrayList; |
49 import java.util.HashMap; | 49 import java.util.HashMap; |
50 import java.util.IdentityHashMap; | 50 import java.util.IdentityHashMap; |
51 import java.util.List; | 51 import java.util.List; |
52 import java.util.Map; | 52 import java.util.Map; |
53 import java.util.concurrent.Exchanger; | 53 import java.util.concurrent.CountDownLatch; |
54 import java.util.concurrent.TimeUnit; | 54 import java.util.concurrent.TimeUnit; |
55 | 55 |
56 // Android specific implementation of VideoCapturer. | 56 // Android specific implementation of VideoCapturer. |
57 // An instance of this class can be created by an application using | 57 // An instance of this class can be created by an application using |
58 // VideoCapturerAndroid.create(); | 58 // VideoCapturerAndroid.create(); |
59 // This class extends VideoCapturer with a method to easily switch between the | 59 // This class extends VideoCapturer with a method to easily switch between the |
60 // front and back camera. It also provides methods for enumerating valid device | 60 // front and back camera. It also provides methods for enumerating valid device |
61 // names. | 61 // names. |
62 // | 62 // |
63 // Threading notes: this class is called from C++ code, and from Camera | 63 // Threading notes: this class is called from C++ code, Android Camera callbacks
, and possibly |
64 // Java callbacks. Since these calls happen on different threads, | 64 // arbitrary Java threads. All public entry points are thread safe, and delegate
the work to the |
65 // the entry points to this class are all synchronized. This shouldn't present | 65 // camera thread. The internal *OnCameraThread() methods must check |camera| for
null to check if |
66 // a performance bottleneck because only onPreviewFrame() is called more than | 66 // the camera has been stopped. |
67 // once (and is called serially on a single thread), so the lock should be | |
68 // uncontended. Note that each of these synchronized methods must check | |
69 // |camera| for null to account for having possibly waited for stopCapture() to | |
70 // complete. | |
71 @SuppressWarnings("deprecation") | 67 @SuppressWarnings("deprecation") |
72 public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
ck { | 68 public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
ck { |
73 private final static String TAG = "VideoCapturerAndroid"; | 69 private final static String TAG = "VideoCapturerAndroid"; |
74 private final static int CAMERA_OBSERVER_PERIOD_MS = 5000; | 70 private final static int CAMERA_OBSERVER_PERIOD_MS = 5000; |
75 | 71 |
76 private Camera camera; // Only non-null while capturing. | 72 private Camera camera; // Only non-null while capturing. |
77 private CameraThread cameraThread; | 73 private HandlerThread cameraThread; |
78 private Handler cameraThreadHandler; | 74 private final Handler cameraThreadHandler; |
79 // |cameraSurfaceTexture| is used with setPreviewTexture. Must be a member, se
e issue webrtc:5021. | 75 // |cameraSurfaceTexture| is used with setPreviewTexture. Must be a member, se
e issue webrtc:5021. |
80 private SurfaceTexture cameraSurfaceTexture; | 76 private SurfaceTexture cameraSurfaceTexture; |
81 private Context applicationContext; | 77 private Context applicationContext; |
| 78 // Synchronization lock for |id|. |
| 79 private final Object cameraIdLock = new Object(); |
82 private int id; | 80 private int id; |
83 private Camera.CameraInfo info; | 81 private Camera.CameraInfo info; |
84 private int cameraGlTexture = 0; | 82 private int cameraGlTexture = 0; |
85 private final FramePool videoBuffers = new FramePool(); | 83 private final FramePool videoBuffers; |
86 // Remember the requested format in case we want to switch cameras. | 84 // Remember the requested format in case we want to switch cameras. |
87 private int requestedWidth; | 85 private int requestedWidth; |
88 private int requestedHeight; | 86 private int requestedHeight; |
89 private int requestedFramerate; | 87 private int requestedFramerate; |
90 // The capture format will be the closest supported format to the requested fo
rmat. | 88 // The capture format will be the closest supported format to the requested fo
rmat. |
91 private CaptureFormat captureFormat; | 89 private CaptureFormat captureFormat; |
92 private int cameraFramesCount; | 90 private int cameraFramesCount; |
93 private int captureBuffersCount; | 91 private int captureBuffersCount; |
| 92 private final Object pendingCameraSwitchLock = new Object(); |
94 private volatile boolean pendingCameraSwitch; | 93 private volatile boolean pendingCameraSwitch; |
95 private CapturerObserver frameObserver = null; | 94 private CapturerObserver frameObserver = null; |
96 private CameraErrorHandler errorHandler = null; | 95 private CameraErrorHandler errorHandler = null; |
97 | 96 |
98 // Camera error callback. | 97 // Camera error callback. |
99 private final Camera.ErrorCallback cameraErrorCallback = | 98 private final Camera.ErrorCallback cameraErrorCallback = |
100 new Camera.ErrorCallback() { | 99 new Camera.ErrorCallback() { |
101 @Override | 100 @Override |
102 public void onError(int error, Camera camera) { | 101 public void onError(int error, Camera camera) { |
103 String errorMessage; | 102 String errorMessage; |
(...skipping 25 matching lines...) Expand all Loading... |
129 String.format("%.1f", averageCaptureBuffersCount) + | 128 String.format("%.1f", averageCaptureBuffersCount) + |
130 ". Pending buffers: " + videoBuffers.pendingFramesTimeStamps()); | 129 ". Pending buffers: " + videoBuffers.pendingFramesTimeStamps()); |
131 if (cameraFramesCount == 0) { | 130 if (cameraFramesCount == 0) { |
132 Logging.e(TAG, "Camera freezed."); | 131 Logging.e(TAG, "Camera freezed."); |
133 if (errorHandler != null) { | 132 if (errorHandler != null) { |
134 errorHandler.onCameraError("Camera failure."); | 133 errorHandler.onCameraError("Camera failure."); |
135 } | 134 } |
136 } else { | 135 } else { |
137 cameraFramesCount = 0; | 136 cameraFramesCount = 0; |
138 captureBuffersCount = 0; | 137 captureBuffersCount = 0; |
139 if (cameraThreadHandler != null) { | 138 cameraThreadHandler.postDelayed(this, CAMERA_OBSERVER_PERIOD_MS); |
140 cameraThreadHandler.postDelayed(this, CAMERA_OBSERVER_PERIOD_MS); | |
141 } | |
142 } | 139 } |
143 } | 140 } |
144 }; | 141 }; |
145 | 142 |
146 // Camera error handler - invoked when camera stops receiving frames | 143 // Camera error handler - invoked when camera stops receiving frames |
147 // or any camera exception happens on camera thread. | 144 // or any camera exception happens on camera thread. |
148 public static interface CameraErrorHandler { | 145 public static interface CameraErrorHandler { |
149 public void onCameraError(String errorDescription); | 146 public void onCameraError(String errorDescription); |
150 } | 147 } |
151 | 148 |
| 149 // Camera switch handler - one of these functions are invoked with the result
of switchCamera(). |
| 150 // The callback may be called on an arbitrary thread. |
| 151 public interface CameraSwitchHandler { |
| 152 // Invoked on success. |isFrontCamera| is true if the new camera is front fa
cing. |
| 153 void onCameraSwitchDone(boolean isFrontCamera); |
| 154 // Invoked on failure, e.g. camera is stopped or only one camera available. |
| 155 void onCameraSwitchError(String errorDescription); |
| 156 } |
| 157 |
152 public static VideoCapturerAndroid create(String name, | 158 public static VideoCapturerAndroid create(String name, |
153 CameraErrorHandler errorHandler) { | 159 CameraErrorHandler errorHandler) { |
154 VideoCapturer capturer = VideoCapturer.create(name); | 160 VideoCapturer capturer = VideoCapturer.create(name); |
155 if (capturer != null) { | 161 if (capturer != null) { |
156 VideoCapturerAndroid capturerAndroid = (VideoCapturerAndroid) capturer; | 162 VideoCapturerAndroid capturerAndroid = (VideoCapturerAndroid) capturer; |
157 capturerAndroid.errorHandler = errorHandler; | 163 capturerAndroid.errorHandler = errorHandler; |
158 return capturerAndroid; | 164 return capturerAndroid; |
159 } | 165 } |
160 return null; | 166 return null; |
161 } | 167 } |
162 | 168 |
163 // Switch camera to the next valid camera id. This can only be called while | 169 // Switch camera to the next valid camera id. This can only be called while |
164 // the camera is running. | 170 // the camera is running. |
165 // Returns true on success. False if the next camera does not support the | 171 public void switchCamera(final CameraSwitchHandler handler) { |
166 // current resolution. | 172 if (Camera.getNumberOfCameras() < 2) { |
167 public synchronized boolean switchCamera(final Runnable switchDoneEvent) { | 173 if (handler != null) { |
168 if (Camera.getNumberOfCameras() < 2 ) | 174 handler.onCameraSwitchError("No camera to switch to."); |
169 return false; | 175 } |
170 | 176 return; |
171 if (cameraThreadHandler == null) { | |
172 Logging.e(TAG, "Calling switchCamera() for stopped camera."); | |
173 return false; | |
174 } | 177 } |
175 if (pendingCameraSwitch) { | 178 synchronized (pendingCameraSwitchLock) { |
176 // Do not handle multiple camera switch request to avoid blocking | 179 if (pendingCameraSwitch) { |
177 // camera thread by handling too many switch request from a queue. | 180 // Do not handle multiple camera switch request to avoid blocking |
178 Logging.w(TAG, "Ignoring camera switch request."); | 181 // camera thread by handling too many switch request from a queue. |
179 return false; | 182 Logging.w(TAG, "Ignoring camera switch request."); |
| 183 if (handler != null) { |
| 184 handler.onCameraSwitchError("Pending camera switch already in progress
."); |
| 185 } |
| 186 return; |
| 187 } |
| 188 pendingCameraSwitch = true; |
180 } | 189 } |
181 | |
182 pendingCameraSwitch = true; | |
183 id = (id + 1) % Camera.getNumberOfCameras(); | |
184 cameraThreadHandler.post(new Runnable() { | 190 cameraThreadHandler.post(new Runnable() { |
185 @Override public void run() { | 191 @Override public void run() { |
186 switchCameraOnCameraThread(switchDoneEvent); | 192 if (camera == null) { |
| 193 if (handler != null) { |
| 194 handler.onCameraSwitchError("Camera is stopped."); |
| 195 } |
| 196 return; |
| 197 } |
| 198 switchCameraOnCameraThread(); |
| 199 synchronized (pendingCameraSwitchLock) { |
| 200 pendingCameraSwitch = false; |
| 201 } |
| 202 if (handler != null) { |
| 203 handler.onCameraSwitchDone(info.facing == Camera.CameraInfo.CAMERA_FAC
ING_FRONT); |
| 204 } |
187 } | 205 } |
188 }); | 206 }); |
189 return true; | |
190 } | 207 } |
191 | 208 |
192 // Requests a new output format from the video capturer. Captured frames | 209 // Requests a new output format from the video capturer. Captured frames |
193 // by the camera will be scaled/or dropped by the video capturer. | 210 // by the camera will be scaled/or dropped by the video capturer. |
194 public synchronized void onOutputFormatRequest( | 211 // TODO(magjed/perkj): Document what this function does. Change name? |
195 final int width, final int height, final int fps) { | 212 public void onOutputFormatRequest(final int width, final int height, final int
fps) { |
196 if (cameraThreadHandler == null) { | |
197 Logging.e(TAG, "Calling onOutputFormatRequest() for already stopped camera
."); | |
198 return; | |
199 } | |
200 cameraThreadHandler.post(new Runnable() { | 213 cameraThreadHandler.post(new Runnable() { |
201 @Override public void run() { | 214 @Override public void run() { |
202 onOutputFormatRequestOnCameraThread(width, height, fps); | 215 onOutputFormatRequestOnCameraThread(width, height, fps); |
203 } | 216 } |
204 }); | 217 }); |
205 } | 218 } |
206 | 219 |
207 // Reconfigure the camera to capture in a new format. This should only be call
ed while the camera | 220 // Reconfigure the camera to capture in a new format. This should only be call
ed while the camera |
208 // is running. | 221 // is running. |
209 public synchronized void changeCaptureFormat( | 222 public void changeCaptureFormat(final int width, final int height, final int f
ramerate) { |
210 final int width, final int height, final int framerate) { | |
211 if (cameraThreadHandler == null) { | |
212 Logging.e(TAG, "Calling changeCaptureFormat() for already stopped camera."
); | |
213 return; | |
214 } | |
215 cameraThreadHandler.post(new Runnable() { | 223 cameraThreadHandler.post(new Runnable() { |
216 @Override public void run() { | 224 @Override public void run() { |
217 startPreviewOnCameraThread(width, height, framerate); | 225 startPreviewOnCameraThread(width, height, framerate); |
218 } | 226 } |
219 }); | 227 }); |
220 } | 228 } |
221 | 229 |
222 public synchronized List<CaptureFormat> getSupportedFormats() { | 230 // Helper function to retrieve the current camera id synchronously. Note that
the camera id might |
223 return CameraEnumerationAndroid.getSupportedFormats(id); | 231 // change at any point by switchCamera() calls. |
| 232 private int getCurrentCameraId() { |
| 233 synchronized (cameraIdLock) { |
| 234 return id; |
| 235 } |
224 } | 236 } |
225 | 237 |
226 // Return a list of timestamps for the frames that have been sent out, but not
returned yet. | 238 public List<CaptureFormat> getSupportedFormats() { |
227 // Useful for logging and testing. | 239 return CameraEnumerationAndroid.getSupportedFormats(getCurrentCameraId()); |
228 public String pendingFramesTimeStamps() { | 240 } |
229 return videoBuffers.pendingFramesTimeStamps(); | 241 |
| 242 // Called from native code. |
| 243 private String getSupportedFormatsAsJson() throws JSONException { |
| 244 return CameraEnumerationAndroid.getSupportedFormatsAsJson(getCurrentCameraId
()); |
230 } | 245 } |
231 | 246 |
232 private VideoCapturerAndroid() { | 247 private VideoCapturerAndroid() { |
233 Logging.d(TAG, "VideoCapturerAndroid"); | 248 Logging.d(TAG, "VideoCapturerAndroid"); |
| 249 cameraThread = new HandlerThread(TAG); |
| 250 cameraThread.start(); |
| 251 cameraThreadHandler = new Handler(cameraThread.getLooper()); |
| 252 videoBuffers = new FramePool(cameraThread); |
| 253 } |
| 254 |
| 255 private void checkIsOnCameraThread() { |
| 256 if (Thread.currentThread() != cameraThread) { |
| 257 throw new IllegalStateException("Wrong thread"); |
| 258 } |
234 } | 259 } |
235 | 260 |
236 // Called by native code. | 261 // Called by native code. |
237 // Initializes local variables for the camera named |deviceName|. If |deviceNa
me| is empty, the | 262 // Initializes local variables for the camera named |deviceName|. If |deviceNa
me| is empty, the |
238 // first available device is used in order to be compatible with the generic V
ideoCapturer class. | 263 // first available device is used in order to be compatible with the generic V
ideoCapturer class. |
239 synchronized boolean init(String deviceName) { | 264 boolean init(String deviceName) { |
240 Logging.d(TAG, "init: " + deviceName); | 265 Logging.d(TAG, "init: " + deviceName); |
241 if (deviceName == null) | 266 if (deviceName == null) |
242 return false; | 267 return false; |
243 | 268 |
244 boolean foundDevice = false; | |
245 if (deviceName.isEmpty()) { | 269 if (deviceName.isEmpty()) { |
246 this.id = 0; | 270 synchronized (cameraIdLock) { |
247 foundDevice = true; | 271 this.id = 0; |
| 272 } |
| 273 return true; |
248 } else { | 274 } else { |
249 for (int i = 0; i < Camera.getNumberOfCameras(); ++i) { | 275 for (int i = 0; i < Camera.getNumberOfCameras(); ++i) { |
250 String existing_device = CameraEnumerationAndroid.getDeviceName(i); | 276 if (deviceName.equals(CameraEnumerationAndroid.getDeviceName(i))) { |
251 if (existing_device != null && deviceName.equals(existing_device)) { | 277 synchronized (cameraIdLock) { |
252 this.id = i; | 278 this.id = i; |
253 foundDevice = true; | 279 } |
| 280 return true; |
254 } | 281 } |
255 } | 282 } |
256 } | 283 } |
257 return foundDevice; | 284 return false; |
258 } | 285 } |
259 | 286 |
260 String getSupportedFormatsAsJson() throws JSONException { | 287 // Called by native code to quit the camera thread. This needs to be done manu
ally, otherwise the |
261 return CameraEnumerationAndroid.getSupportedFormatsAsJson(id); | 288 // thread and handler will not be garbage collected. |
| 289 private void release() { |
| 290 if (isReleased()) { |
| 291 throw new IllegalStateException("Already released"); |
| 292 } |
| 293 cameraThreadHandler.post(new Runnable() { |
| 294 @Override |
| 295 public void run() { |
| 296 if (camera != null) { |
| 297 throw new IllegalStateException("Release called while camera is runnin
g"); |
| 298 } |
| 299 if (videoBuffers.pendingFramesCount() != 0) { |
| 300 throw new IllegalStateException("Release called with pending frames le
ft"); |
| 301 } |
| 302 } |
| 303 }); |
| 304 cameraThread.quitSafely(); |
| 305 ThreadUtils.joinUninterruptibly(cameraThread); |
| 306 cameraThread = null; |
262 } | 307 } |
263 | 308 |
264 private class CameraThread extends Thread { | 309 // Used for testing purposes to check if release() has been called. |
265 private Exchanger<Handler> handlerExchanger; | 310 public boolean isReleased() { |
266 public CameraThread(Exchanger<Handler> handlerExchanger) { | 311 return (cameraThread == null); |
267 this.handlerExchanger = handlerExchanger; | |
268 } | |
269 | |
270 @Override public void run() { | |
271 Looper.prepare(); | |
272 exchange(handlerExchanger, new Handler()); | |
273 Looper.loop(); | |
274 } | |
275 } | 312 } |
276 | 313 |
277 // Called by native code. Returns true if capturer is started. | 314 // Called by native code. |
278 // | 315 // |
279 // Note that this actually opens the camera, and Camera callbacks run on the | 316 // Note that this actually opens the camera, and Camera callbacks run on the |
280 // thread that calls open(), so this is done on the CameraThread. | 317 // thread that calls open(), so this is done on the CameraThread. |
281 synchronized void startCapture( | 318 void startCapture( |
282 final int width, final int height, final int framerate, | 319 final int width, final int height, final int framerate, |
283 final Context applicationContext, final CapturerObserver frameObserver) { | 320 final Context applicationContext, final CapturerObserver frameObserver) { |
284 Logging.d(TAG, "startCapture requested: " + width + "x" + height | 321 Logging.d(TAG, "startCapture requested: " + width + "x" + height |
285 + "@" + framerate); | 322 + "@" + framerate); |
286 if (applicationContext == null) { | 323 if (applicationContext == null) { |
287 throw new RuntimeException("applicationContext not set."); | 324 throw new RuntimeException("applicationContext not set."); |
288 } | 325 } |
289 if (frameObserver == null) { | 326 if (frameObserver == null) { |
290 throw new RuntimeException("frameObserver not set."); | 327 throw new RuntimeException("frameObserver not set."); |
291 } | 328 } |
292 if (cameraThreadHandler != null) { | |
293 throw new RuntimeException("Camera has already been started."); | |
294 } | |
295 | |
296 Exchanger<Handler> handlerExchanger = new Exchanger<Handler>(); | |
297 cameraThread = new CameraThread(handlerExchanger); | |
298 cameraThread.start(); | |
299 cameraThreadHandler = exchange(handlerExchanger, null); | |
300 cameraThreadHandler.post(new Runnable() { | 329 cameraThreadHandler.post(new Runnable() { |
301 @Override public void run() { | 330 @Override public void run() { |
302 startCaptureOnCameraThread(width, height, framerate, frameObserver, | 331 startCaptureOnCameraThread(width, height, framerate, frameObserver, |
303 applicationContext); | 332 applicationContext); |
304 } | 333 } |
305 }); | 334 }); |
306 } | 335 } |
307 | 336 |
308 private void startCaptureOnCameraThread( | 337 private void startCaptureOnCameraThread( |
309 int width, int height, int framerate, CapturerObserver frameObserver, | 338 int width, int height, int framerate, CapturerObserver frameObserver, |
310 Context applicationContext) { | 339 Context applicationContext) { |
311 Throwable error = null; | 340 Throwable error = null; |
| 341 checkIsOnCameraThread(); |
| 342 if (camera != null) { |
| 343 throw new RuntimeException("Camera has already been started."); |
| 344 } |
312 this.applicationContext = applicationContext; | 345 this.applicationContext = applicationContext; |
313 this.frameObserver = frameObserver; | 346 this.frameObserver = frameObserver; |
314 try { | 347 try { |
315 Logging.d(TAG, "Opening camera " + id); | 348 synchronized (cameraIdLock) { |
316 camera = Camera.open(id); | 349 Logging.d(TAG, "Opening camera " + id); |
317 info = new Camera.CameraInfo(); | 350 camera = Camera.open(id); |
318 Camera.getCameraInfo(id, info); | 351 info = new Camera.CameraInfo(); |
| 352 Camera.getCameraInfo(id, info); |
| 353 } |
319 // No local renderer (we only care about onPreviewFrame() buffers, not a | 354 // No local renderer (we only care about onPreviewFrame() buffers, not a |
320 // directly-displayed UI element). Camera won't capture without | 355 // directly-displayed UI element). Camera won't capture without |
321 // setPreview{Texture,Display}, so we create a SurfaceTexture and hand | 356 // setPreview{Texture,Display}, so we create a SurfaceTexture and hand |
322 // it over to Camera, but never listen for frame-ready callbacks, | 357 // it over to Camera, but never listen for frame-ready callbacks, |
323 // and never call updateTexImage on it. | 358 // and never call updateTexImage on it. |
324 try { | 359 try { |
325 cameraGlTexture = GlUtil.generateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_O
ES); | 360 cameraGlTexture = GlUtil.generateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_O
ES); |
326 cameraSurfaceTexture = new SurfaceTexture(cameraGlTexture); | 361 cameraSurfaceTexture = new SurfaceTexture(cameraGlTexture); |
327 cameraSurfaceTexture.setOnFrameAvailableListener(null); | 362 cameraSurfaceTexture.setOnFrameAvailableListener(null); |
328 camera.setPreviewTexture(cameraSurfaceTexture); | 363 camera.setPreviewTexture(cameraSurfaceTexture); |
(...skipping 11 matching lines...) Expand all Loading... |
340 // Start camera observer. | 375 // Start camera observer. |
341 cameraFramesCount = 0; | 376 cameraFramesCount = 0; |
342 captureBuffersCount = 0; | 377 captureBuffersCount = 0; |
343 cameraThreadHandler.postDelayed(cameraObserver, CAMERA_OBSERVER_PERIOD_MS)
; | 378 cameraThreadHandler.postDelayed(cameraObserver, CAMERA_OBSERVER_PERIOD_MS)
; |
344 return; | 379 return; |
345 } catch (RuntimeException e) { | 380 } catch (RuntimeException e) { |
346 error = e; | 381 error = e; |
347 } | 382 } |
348 Logging.e(TAG, "startCapture failed", error); | 383 Logging.e(TAG, "startCapture failed", error); |
349 stopCaptureOnCameraThread(); | 384 stopCaptureOnCameraThread(); |
350 cameraThreadHandler = null; | |
351 frameObserver.OnCapturerStarted(false); | 385 frameObserver.OnCapturerStarted(false); |
352 if (errorHandler != null) { | 386 if (errorHandler != null) { |
353 errorHandler.onCameraError("Camera can not be started."); | 387 errorHandler.onCameraError("Camera can not be started."); |
354 } | 388 } |
355 return; | 389 return; |
356 } | 390 } |
357 | 391 |
358 // (Re)start preview with the closest supported format to |width| x |height| @
|framerate|. | 392 // (Re)start preview with the closest supported format to |width| x |height| @
|framerate|. |
359 private void startPreviewOnCameraThread(int width, int height, int framerate)
{ | 393 private void startPreviewOnCameraThread(int width, int height, int framerate)
{ |
| 394 checkIsOnCameraThread(); |
360 Logging.d( | 395 Logging.d( |
361 TAG, "startPreviewOnCameraThread requested: " + width + "x" + height + "
@" + framerate); | 396 TAG, "startPreviewOnCameraThread requested: " + width + "x" + height + "
@" + framerate); |
362 if (camera == null) { | 397 if (camera == null) { |
363 Logging.e(TAG, "Calling startPreviewOnCameraThread on stopped camera."); | 398 Logging.e(TAG, "Calling startPreviewOnCameraThread on stopped camera."); |
364 return; | 399 return; |
365 } | 400 } |
366 | 401 |
367 requestedWidth = width; | 402 requestedWidth = width; |
368 requestedHeight = height; | 403 requestedHeight = height; |
369 requestedFramerate = framerate; | 404 requestedFramerate = framerate; |
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
413 // (Re)start preview. | 448 // (Re)start preview. |
414 Logging.d(TAG, "Start capturing: " + captureFormat); | 449 Logging.d(TAG, "Start capturing: " + captureFormat); |
415 this.captureFormat = captureFormat; | 450 this.captureFormat = captureFormat; |
416 camera.setParameters(parameters); | 451 camera.setParameters(parameters); |
417 videoBuffers.queueCameraBuffers(captureFormat.frameSize(), camera); | 452 videoBuffers.queueCameraBuffers(captureFormat.frameSize(), camera); |
418 camera.setPreviewCallbackWithBuffer(this); | 453 camera.setPreviewCallbackWithBuffer(this); |
419 camera.startPreview(); | 454 camera.startPreview(); |
420 } | 455 } |
421 | 456 |
422 // Called by native code. Returns true when camera is known to be stopped. | 457 // Called by native code. Returns true when camera is known to be stopped. |
423 synchronized void stopCapture() throws InterruptedException { | 458 void stopCapture() throws InterruptedException { |
424 if (cameraThreadHandler == null) { | |
425 Logging.e(TAG, "Calling stopCapture() for already stopped camera."); | |
426 return; | |
427 } | |
428 Logging.d(TAG, "stopCapture"); | 459 Logging.d(TAG, "stopCapture"); |
| 460 final CountDownLatch barrier = new CountDownLatch(1); |
429 cameraThreadHandler.post(new Runnable() { | 461 cameraThreadHandler.post(new Runnable() { |
430 @Override public void run() { | 462 @Override public void run() { |
431 stopCaptureOnCameraThread(); | 463 stopCaptureOnCameraThread(); |
| 464 barrier.countDown(); |
432 } | 465 } |
433 }); | 466 }); |
434 cameraThread.join(); | 467 barrier.await(); |
435 cameraThreadHandler = null; | |
436 Logging.d(TAG, "stopCapture done"); | 468 Logging.d(TAG, "stopCapture done"); |
437 } | 469 } |
438 | 470 |
439 private void stopCaptureOnCameraThread() { | 471 private void stopCaptureOnCameraThread() { |
440 doStopCaptureOnCameraThread(); | 472 checkIsOnCameraThread(); |
441 Looper.myLooper().quit(); | |
442 return; | |
443 } | |
444 | |
445 private void doStopCaptureOnCameraThread() { | |
446 Logging.d(TAG, "stopCaptureOnCameraThread"); | 473 Logging.d(TAG, "stopCaptureOnCameraThread"); |
447 if (camera == null) { | 474 if (camera == null) { |
| 475 Logging.e(TAG, "Calling stopCapture() for already stopped camera."); |
448 return; | 476 return; |
449 } | 477 } |
450 | 478 |
451 cameraThreadHandler.removeCallbacks(cameraObserver); | 479 cameraThreadHandler.removeCallbacks(cameraObserver); |
452 Logging.d(TAG, "Stop preview."); | 480 Logging.d(TAG, "Stop preview."); |
453 camera.stopPreview(); | 481 camera.stopPreview(); |
454 camera.setPreviewCallbackWithBuffer(null); | 482 camera.setPreviewCallbackWithBuffer(null); |
455 videoBuffers.stopReturnBuffersToCamera(); | 483 videoBuffers.stopReturnBuffersToCamera(); |
456 captureFormat = null; | 484 captureFormat = null; |
457 | 485 |
458 if (cameraGlTexture != 0) { | 486 if (cameraGlTexture != 0) { |
459 GLES20.glDeleteTextures(1, new int[] {cameraGlTexture}, 0); | 487 GLES20.glDeleteTextures(1, new int[] {cameraGlTexture}, 0); |
460 cameraGlTexture = 0; | 488 cameraGlTexture = 0; |
461 } | 489 } |
462 Logging.d(TAG, "Release camera."); | 490 Logging.d(TAG, "Release camera."); |
463 camera.release(); | 491 camera.release(); |
464 camera = null; | 492 camera = null; |
| 493 cameraSurfaceTexture.release(); |
465 cameraSurfaceTexture = null; | 494 cameraSurfaceTexture = null; |
466 } | 495 } |
467 | 496 |
468 private void switchCameraOnCameraThread(Runnable switchDoneEvent) { | 497 private void switchCameraOnCameraThread() { |
| 498 checkIsOnCameraThread(); |
469 Logging.d(TAG, "switchCameraOnCameraThread"); | 499 Logging.d(TAG, "switchCameraOnCameraThread"); |
470 | 500 stopCaptureOnCameraThread(); |
471 doStopCaptureOnCameraThread(); | 501 synchronized (cameraIdLock) { |
| 502 id = (id + 1) % Camera.getNumberOfCameras(); |
| 503 } |
472 startCaptureOnCameraThread(requestedWidth, requestedHeight, requestedFramera
te, frameObserver, | 504 startCaptureOnCameraThread(requestedWidth, requestedHeight, requestedFramera
te, frameObserver, |
473 applicationContext); | 505 applicationContext); |
474 pendingCameraSwitch = false; | |
475 Logging.d(TAG, "switchCameraOnCameraThread done"); | 506 Logging.d(TAG, "switchCameraOnCameraThread done"); |
476 if (switchDoneEvent != null) { | |
477 switchDoneEvent.run(); | |
478 } | |
479 } | 507 } |
480 | 508 |
481 private void onOutputFormatRequestOnCameraThread( | 509 private void onOutputFormatRequestOnCameraThread(int width, int height, int fp
s) { |
482 int width, int height, int fps) { | 510 checkIsOnCameraThread(); |
483 if (camera == null) { | 511 if (camera == null) { |
| 512 Logging.e(TAG, "Calling onOutputFormatRequest() on stopped camera."); |
484 return; | 513 return; |
485 } | 514 } |
486 Logging.d(TAG, "onOutputFormatRequestOnCameraThread: " + width + "x" + heigh
t + | 515 Logging.d(TAG, "onOutputFormatRequestOnCameraThread: " + width + "x" + heigh
t + |
487 "@" + fps); | 516 "@" + fps); |
488 frameObserver.OnOutputFormatRequest(width, height, fps); | 517 frameObserver.OnOutputFormatRequest(width, height, fps); |
489 } | 518 } |
490 | 519 |
491 void returnBuffer(long timeStamp) { | 520 public void returnBuffer(final long timeStamp) { |
492 videoBuffers.returnBuffer(timeStamp); | 521 cameraThreadHandler.post(new Runnable() { |
| 522 @Override public void run() { |
| 523 videoBuffers.returnBuffer(timeStamp); |
| 524 } |
| 525 }); |
493 } | 526 } |
494 | 527 |
495 private int getDeviceOrientation() { | 528 private int getDeviceOrientation() { |
496 int orientation = 0; | 529 int orientation = 0; |
497 | 530 |
498 WindowManager wm = (WindowManager) applicationContext.getSystemService( | 531 WindowManager wm = (WindowManager) applicationContext.getSystemService( |
499 Context.WINDOW_SERVICE); | 532 Context.WINDOW_SERVICE); |
500 switch(wm.getDefaultDisplay().getRotation()) { | 533 switch(wm.getDefaultDisplay().getRotation()) { |
501 case Surface.ROTATION_90: | 534 case Surface.ROTATION_90: |
502 orientation = 90; | 535 orientation = 90; |
503 break; | 536 break; |
504 case Surface.ROTATION_180: | 537 case Surface.ROTATION_180: |
505 orientation = 180; | 538 orientation = 180; |
506 break; | 539 break; |
507 case Surface.ROTATION_270: | 540 case Surface.ROTATION_270: |
508 orientation = 270; | 541 orientation = 270; |
509 break; | 542 break; |
510 case Surface.ROTATION_0: | 543 case Surface.ROTATION_0: |
511 default: | 544 default: |
512 orientation = 0; | 545 orientation = 0; |
513 break; | 546 break; |
514 } | 547 } |
515 return orientation; | 548 return orientation; |
516 } | 549 } |
517 | 550 |
518 // Called on cameraThread so must not "synchronized". | 551 // Called on cameraThread so must not "synchronized". |
519 @Override | 552 @Override |
520 public void onPreviewFrame(byte[] data, Camera callbackCamera) { | 553 public void onPreviewFrame(byte[] data, Camera callbackCamera) { |
521 if (Thread.currentThread() != cameraThread) { | 554 checkIsOnCameraThread(); |
522 throw new RuntimeException("Camera callback not on camera thread?!?"); | |
523 } | |
524 if (camera == null) { | 555 if (camera == null) { |
525 return; | 556 return; |
526 } | 557 } |
527 if (camera != callbackCamera) { | 558 if (camera != callbackCamera) { |
528 throw new RuntimeException("Unexpected camera in callback!"); | 559 throw new RuntimeException("Unexpected camera in callback!"); |
529 } | 560 } |
530 | 561 |
531 final long captureTimeNs = | 562 final long captureTimeNs = |
532 TimeUnit.MILLISECONDS.toNanos(SystemClock.elapsedRealtime()); | 563 TimeUnit.MILLISECONDS.toNanos(SystemClock.elapsedRealtime()); |
533 | 564 |
534 captureBuffersCount += videoBuffers.numCaptureBuffersAvailable(); | 565 captureBuffersCount += videoBuffers.numCaptureBuffersAvailable(); |
535 int rotation = getDeviceOrientation(); | 566 int rotation = getDeviceOrientation(); |
536 if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) { | 567 if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) { |
537 rotation = 360 - rotation; | 568 rotation = 360 - rotation; |
538 } | 569 } |
539 rotation = (info.orientation + rotation) % 360; | 570 rotation = (info.orientation + rotation) % 360; |
540 // Mark the frame owning |data| as used. | 571 // Mark the frame owning |data| as used. |
541 // Note that since data is directBuffer, | 572 // Note that since data is directBuffer, |
542 // data.length >= videoBuffers.frameSize. | 573 // data.length >= videoBuffers.frameSize. |
543 if (videoBuffers.reserveByteBuffer(data, captureTimeNs)) { | 574 if (videoBuffers.reserveByteBuffer(data, captureTimeNs)) { |
544 cameraFramesCount++; | 575 cameraFramesCount++; |
545 frameObserver.OnFrameCaptured(data, videoBuffers.frameSize, captureFormat.
width, | 576 frameObserver.OnFrameCaptured(data, videoBuffers.frameSize, captureFormat.
width, |
546 captureFormat.height, rotation, captureTimeNs); | 577 captureFormat.height, rotation, captureTimeNs); |
547 } else { | 578 } else { |
548 Logging.w(TAG, "reserveByteBuffer failed - dropping frame."); | 579 Logging.w(TAG, "reserveByteBuffer failed - dropping frame."); |
549 } | 580 } |
550 } | 581 } |
551 | 582 |
552 // runCameraThreadUntilIdle make sure all posted messages to the cameraThread | |
553 // is processed before returning. It does that by itself posting a message to | |
554 // to the message queue and waits until is has been processed. | |
555 // It is used in tests. | |
556 void runCameraThreadUntilIdle() { | |
557 if (cameraThreadHandler == null) | |
558 return; | |
559 final Exchanger<Boolean> result = new Exchanger<Boolean>(); | |
560 cameraThreadHandler.post(new Runnable() { | |
561 @Override public void run() { | |
562 exchange(result, true); // |true| is a dummy here. | |
563 } | |
564 }); | |
565 exchange(result, false); // |false| is a dummy value here. | |
566 return; | |
567 } | |
568 | |
569 // Exchanges |value| with |exchanger|, converting InterruptedExceptions to | |
570 // RuntimeExceptions (since we expect never to see these). | |
571 private static <T> T exchange(Exchanger<T> exchanger, T value) { | |
572 try { | |
573 return exchanger.exchange(value); | |
574 } catch (InterruptedException e) { | |
575 throw new RuntimeException(e); | |
576 } | |
577 } | |
578 | |
579 // Class used for allocating and bookkeeping video frames. All buffers are | 583 // Class used for allocating and bookkeeping video frames. All buffers are |
580 // direct allocated so that they can be directly used from native code. This c
lass is | 584 // direct allocated so that they can be directly used from native code. This c
lass is |
581 // synchronized and can be called from multiple threads. | 585 // not thread-safe, and enforces single thread use. |
582 private static class FramePool { | 586 private static class FramePool { |
| 587 // Thread that all calls should be made on. |
| 588 private final Thread thread; |
583 // Arbitrary queue depth. Higher number means more memory allocated & held, | 589 // Arbitrary queue depth. Higher number means more memory allocated & held, |
584 // lower number means more sensitivity to processing time in the client (and | 590 // lower number means more sensitivity to processing time in the client (and |
585 // potentially stalling the capturer if it runs out of buffers to write to). | 591 // potentially stalling the capturer if it runs out of buffers to write to). |
586 private static final int numCaptureBuffers = 3; | 592 private static final int numCaptureBuffers = 3; |
587 // This container tracks the buffers added as camera callback buffers. It is
needed for finding | 593 // This container tracks the buffers added as camera callback buffers. It is
needed for finding |
588 // the corresponding ByteBuffer given a byte[]. | 594 // the corresponding ByteBuffer given a byte[]. |
589 private final Map<byte[], ByteBuffer> queuedBuffers = new IdentityHashMap<by
te[], ByteBuffer>(); | 595 private final Map<byte[], ByteBuffer> queuedBuffers = new IdentityHashMap<by
te[], ByteBuffer>(); |
590 // This container tracks the frames that have been sent but not returned. It
is needed for | 596 // This container tracks the frames that have been sent but not returned. It
is needed for |
591 // keeping the buffers alive and for finding the corresponding ByteBuffer gi
ven a timestamp. | 597 // keeping the buffers alive and for finding the corresponding ByteBuffer gi
ven a timestamp. |
592 private final Map<Long, ByteBuffer> pendingBuffers = new HashMap<Long, ByteB
uffer>(); | 598 private final Map<Long, ByteBuffer> pendingBuffers = new HashMap<Long, ByteB
uffer>(); |
593 private int frameSize = 0; | 599 private int frameSize = 0; |
594 private Camera camera; | 600 private Camera camera; |
595 | 601 |
596 synchronized int numCaptureBuffersAvailable() { | 602 public FramePool(Thread thread) { |
| 603 this.thread = thread; |
| 604 } |
| 605 |
| 606 private void checkIsOnValidThread() { |
| 607 if (Thread.currentThread() != thread) { |
| 608 throw new IllegalStateException("Wrong thread"); |
| 609 } |
| 610 } |
| 611 |
| 612 public int numCaptureBuffersAvailable() { |
| 613 checkIsOnValidThread(); |
597 return queuedBuffers.size(); | 614 return queuedBuffers.size(); |
598 } | 615 } |
599 | 616 |
600 // Discards previous queued buffers and adds new callback buffers to camera. | 617 // Discards previous queued buffers and adds new callback buffers to camera. |
601 synchronized void queueCameraBuffers(int frameSize, Camera camera) { | 618 public void queueCameraBuffers(int frameSize, Camera camera) { |
| 619 checkIsOnValidThread(); |
602 this.camera = camera; | 620 this.camera = camera; |
603 this.frameSize = frameSize; | 621 this.frameSize = frameSize; |
604 | 622 |
605 queuedBuffers.clear(); | 623 queuedBuffers.clear(); |
606 for (int i = 0; i < numCaptureBuffers; ++i) { | 624 for (int i = 0; i < numCaptureBuffers; ++i) { |
607 final ByteBuffer buffer = ByteBuffer.allocateDirect(frameSize); | 625 final ByteBuffer buffer = ByteBuffer.allocateDirect(frameSize); |
608 camera.addCallbackBuffer(buffer.array()); | 626 camera.addCallbackBuffer(buffer.array()); |
609 queuedBuffers.put(buffer.array(), buffer); | 627 queuedBuffers.put(buffer.array(), buffer); |
610 } | 628 } |
611 Logging.d(TAG, "queueCameraBuffers enqueued " + numCaptureBuffers | 629 Logging.d(TAG, "queueCameraBuffers enqueued " + numCaptureBuffers |
612 + " buffers of size " + frameSize + "."); | 630 + " buffers of size " + frameSize + "."); |
613 } | 631 } |
614 | 632 |
615 synchronized String pendingFramesTimeStamps() { | 633 // Return number of pending frames that have not been returned. |
| 634 public int pendingFramesCount() { |
| 635 checkIsOnValidThread(); |
| 636 return pendingBuffers.size(); |
| 637 } |
| 638 |
| 639 public String pendingFramesTimeStamps() { |
| 640 checkIsOnValidThread(); |
616 List<Long> timeStampsMs = new ArrayList<Long>(); | 641 List<Long> timeStampsMs = new ArrayList<Long>(); |
617 for (Long timeStampNs : pendingBuffers.keySet()) { | 642 for (Long timeStampNs : pendingBuffers.keySet()) { |
618 timeStampsMs.add(TimeUnit.NANOSECONDS.toMillis(timeStampNs)); | 643 timeStampsMs.add(TimeUnit.NANOSECONDS.toMillis(timeStampNs)); |
619 } | 644 } |
620 return timeStampsMs.toString(); | 645 return timeStampsMs.toString(); |
621 } | 646 } |
622 | 647 |
623 synchronized void stopReturnBuffersToCamera() { | 648 public void stopReturnBuffersToCamera() { |
| 649 checkIsOnValidThread(); |
624 this.camera = null; | 650 this.camera = null; |
625 queuedBuffers.clear(); | 651 queuedBuffers.clear(); |
626 // Frames in |pendingBuffers| need to be kept alive until they are returne
d. | 652 // Frames in |pendingBuffers| need to be kept alive until they are returne
d. |
627 Logging.d(TAG, "stopReturnBuffersToCamera called." | 653 Logging.d(TAG, "stopReturnBuffersToCamera called." |
628 + (pendingBuffers.isEmpty() ? | 654 + (pendingBuffers.isEmpty() ? |
629 " All buffers have been returned." | 655 " All buffers have been returned." |
630 : " Pending buffers: " + pendingFramesTimeStamps() + ".")); | 656 : " Pending buffers: " + pendingFramesTimeStamps() + ".")); |
631 } | 657 } |
632 | 658 |
633 synchronized boolean reserveByteBuffer(byte[] data, long timeStamp) { | 659 public boolean reserveByteBuffer(byte[] data, long timeStamp) { |
| 660 checkIsOnValidThread(); |
634 final ByteBuffer buffer = queuedBuffers.remove(data); | 661 final ByteBuffer buffer = queuedBuffers.remove(data); |
635 if (buffer == null) { | 662 if (buffer == null) { |
636 // Frames might be posted to |onPreviewFrame| with the previous format w
hile changing | 663 // Frames might be posted to |onPreviewFrame| with the previous format w
hile changing |
637 // capture format in |startPreviewOnCameraThread|. Drop these old frames
. | 664 // capture format in |startPreviewOnCameraThread|. Drop these old frames
. |
638 Logging.w(TAG, "Received callback buffer from previous configuration wit
h length: " | 665 Logging.w(TAG, "Received callback buffer from previous configuration wit
h length: " |
639 + (data == null ? "null" : data.length)); | 666 + (data == null ? "null" : data.length)); |
640 return false; | 667 return false; |
641 } | 668 } |
642 if (buffer.capacity() != frameSize) { | 669 if (buffer.capacity() != frameSize) { |
643 throw new IllegalStateException("Callback buffer has unexpected frame si
ze"); | 670 throw new IllegalStateException("Callback buffer has unexpected frame si
ze"); |
644 } | 671 } |
645 if (pendingBuffers.containsKey(timeStamp)) { | 672 if (pendingBuffers.containsKey(timeStamp)) { |
646 Logging.e(TAG, "Timestamp already present in pending buffers - they need
to be unique"); | 673 Logging.e(TAG, "Timestamp already present in pending buffers - they need
to be unique"); |
647 return false; | 674 return false; |
648 } | 675 } |
649 pendingBuffers.put(timeStamp, buffer); | 676 pendingBuffers.put(timeStamp, buffer); |
650 if (queuedBuffers.isEmpty()) { | 677 if (queuedBuffers.isEmpty()) { |
651 Logging.v(TAG, "Camera is running out of capture buffers." | 678 Logging.v(TAG, "Camera is running out of capture buffers." |
652 + " Pending buffers: " + pendingFramesTimeStamps()); | 679 + " Pending buffers: " + pendingFramesTimeStamps()); |
653 } | 680 } |
654 return true; | 681 return true; |
655 } | 682 } |
656 | 683 |
657 synchronized void returnBuffer(long timeStamp) { | 684 public void returnBuffer(long timeStamp) { |
| 685 checkIsOnValidThread(); |
658 final ByteBuffer returnedFrame = pendingBuffers.remove(timeStamp); | 686 final ByteBuffer returnedFrame = pendingBuffers.remove(timeStamp); |
659 if (returnedFrame == null) { | 687 if (returnedFrame == null) { |
660 throw new RuntimeException("unknown data buffer with time stamp " | 688 throw new RuntimeException("unknown data buffer with time stamp " |
661 + timeStamp + "returned?!?"); | 689 + timeStamp + "returned?!?"); |
662 } | 690 } |
663 | 691 |
664 if (camera != null && returnedFrame.capacity() == frameSize) { | 692 if (camera != null && returnedFrame.capacity() == frameSize) { |
665 camera.addCallbackBuffer(returnedFrame.array()); | 693 camera.addCallbackBuffer(returnedFrame.array()); |
666 if (queuedBuffers.isEmpty()) { | 694 if (queuedBuffers.isEmpty()) { |
667 Logging.v(TAG, "Frame returned when camera is running out of capture" | 695 Logging.v(TAG, "Frame returned when camera is running out of capture" |
(...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
729 } | 757 } |
730 | 758 |
731 private native void nativeCapturerStarted(long nativeCapturer, | 759 private native void nativeCapturerStarted(long nativeCapturer, |
732 boolean success); | 760 boolean success); |
733 private native void nativeOnFrameCaptured(long nativeCapturer, | 761 private native void nativeOnFrameCaptured(long nativeCapturer, |
734 byte[] data, int length, int width, int height, int rotation, long timeS
tamp); | 762 byte[] data, int length, int width, int height, int rotation, long timeS
tamp); |
735 private native void nativeOnOutputFormatRequest(long nativeCapturer, | 763 private native void nativeOnOutputFormatRequest(long nativeCapturer, |
736 int width, int height, int fps); | 764 int width, int height, int fps); |
737 } | 765 } |
738 } | 766 } |
OLD | NEW |