Add support to Capture to a texture instead of memory.
This adds support for capturing to a texture in the Java part of VideoCapturerAndroid. After this cl, the C++ also needs modification. https://codereview.webrtc.org/1375953002/ contains the idea and have a working version where textures can be rendered in local preview. BUG=webrtc:4993 R=magjed@webrtc.org Review URL: https://codereview.webrtc.org/1383413002 . Cr-Commit-Position: refs/heads/master@{#10213}
This commit is contained in:
parent
c1cc854d54
commit
3d06eca5e3
@ -7,7 +7,7 @@
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
|
||||
|
||||
<uses-sdk android:minSdkVersion="13" android:targetSdkVersion="21" />
|
||||
<uses-sdk android:minSdkVersion="17" android:targetSdkVersion="21" />
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
@ -26,148 +26,18 @@
|
||||
*/
|
||||
package org.webrtc;
|
||||
|
||||
import android.hardware.Camera;
|
||||
import android.opengl.EGL14;
|
||||
import android.test.ActivityTestCase;
|
||||
import android.test.suitebuilder.annotation.SmallTest;
|
||||
import android.test.suitebuilder.annotation.MediumTest;
|
||||
import android.test.suitebuilder.annotation.SmallTest;
|
||||
import android.util.Size;
|
||||
|
||||
import org.webrtc.CameraEnumerationAndroid.CaptureFormat;
|
||||
import org.webrtc.VideoRenderer.I420Frame;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class VideoCapturerAndroidTest extends ActivityTestCase {
|
||||
static class RendererCallbacks implements VideoRenderer.Callbacks {
|
||||
private int framesRendered = 0;
|
||||
private Object frameLock = 0;
|
||||
|
||||
@Override
|
||||
public void renderFrame(I420Frame frame) {
|
||||
synchronized (frameLock) {
|
||||
++framesRendered;
|
||||
frameLock.notify();
|
||||
}
|
||||
VideoRenderer.renderFrameDone(frame);
|
||||
}
|
||||
|
||||
public int WaitForNextFrameToRender() throws InterruptedException {
|
||||
synchronized (frameLock) {
|
||||
frameLock.wait();
|
||||
return framesRendered;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class FakeAsyncRenderer implements VideoRenderer.Callbacks {
|
||||
private final List<I420Frame> pendingFrames = new ArrayList<I420Frame>();
|
||||
|
||||
@Override
|
||||
public void renderFrame(I420Frame frame) {
|
||||
synchronized (pendingFrames) {
|
||||
pendingFrames.add(frame);
|
||||
pendingFrames.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
// Wait until at least one frame have been received, before returning them.
|
||||
public List<I420Frame> waitForPendingFrames() throws InterruptedException {
|
||||
synchronized (pendingFrames) {
|
||||
while (pendingFrames.isEmpty()) {
|
||||
pendingFrames.wait();
|
||||
}
|
||||
return new ArrayList<I420Frame>(pendingFrames);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class FakeCapturerObserver implements
|
||||
VideoCapturerAndroid.CapturerObserver {
|
||||
private int framesCaptured = 0;
|
||||
private int frameSize = 0;
|
||||
private Object frameLock = 0;
|
||||
private Object capturerStartLock = 0;
|
||||
private boolean captureStartResult = false;
|
||||
private List<Long> timestamps = new ArrayList<Long>();
|
||||
|
||||
@Override
|
||||
public void OnCapturerStarted(boolean success) {
|
||||
synchronized (capturerStartLock) {
|
||||
captureStartResult = success;
|
||||
capturerStartLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void OnFrameCaptured(byte[] frame, int length, int width, int height,
|
||||
int rotation, long timeStamp) {
|
||||
synchronized (frameLock) {
|
||||
++framesCaptured;
|
||||
frameSize = length;
|
||||
timestamps.add(timeStamp);
|
||||
frameLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void OnOutputFormatRequest(int width, int height, int fps) {}
|
||||
|
||||
public boolean WaitForCapturerToStart() throws InterruptedException {
|
||||
synchronized (capturerStartLock) {
|
||||
capturerStartLock.wait();
|
||||
return captureStartResult;
|
||||
}
|
||||
}
|
||||
|
||||
public int WaitForNextCapturedFrame() throws InterruptedException {
|
||||
synchronized (frameLock) {
|
||||
frameLock.wait();
|
||||
return framesCaptured;
|
||||
}
|
||||
}
|
||||
|
||||
int frameSize() {
|
||||
synchronized (frameLock) {
|
||||
return frameSize;
|
||||
}
|
||||
}
|
||||
|
||||
List<Long> getCopyAndResetListOftimeStamps() {
|
||||
synchronized (frameLock) {
|
||||
ArrayList<Long> list = new ArrayList<Long>(timestamps);
|
||||
timestamps.clear();
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return true if the device under test have at least two cameras.
|
||||
@SuppressWarnings("deprecation")
|
||||
boolean HaveTwoCameras() {
|
||||
return (Camera.getNumberOfCameras() >= 2);
|
||||
}
|
||||
|
||||
void startCapturerAndRender(String deviceName) throws InterruptedException {
|
||||
PeerConnectionFactory factory = new PeerConnectionFactory();
|
||||
VideoCapturerAndroid capturer =
|
||||
VideoCapturerAndroid.create(deviceName, null);
|
||||
VideoSource source =
|
||||
factory.createVideoSource(capturer, new MediaConstraints());
|
||||
VideoTrack track = factory.createVideoTrack("dummy", source);
|
||||
RendererCallbacks callbacks = new RendererCallbacks();
|
||||
track.addRenderer(new VideoRenderer(callbacks));
|
||||
assertTrue(callbacks.WaitForNextFrameToRender() > 0);
|
||||
track.dispose();
|
||||
source.dispose();
|
||||
factory.dispose();
|
||||
assertTrue(capturer.isReleased());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUp() {
|
||||
assertTrue(PeerConnectionFactory.initializeAndroidGlobals(
|
||||
@ -206,15 +76,18 @@ public class VideoCapturerAndroidTest extends ActivityTestCase {
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
public void testCreateAndRelease() throws Exception {
|
||||
VideoCapturerAndroid capturer = VideoCapturerAndroid.create("", null);
|
||||
assertNotNull(capturer);
|
||||
capturer.dispose();
|
||||
assertTrue(capturer.isReleased());
|
||||
public void testCreateAndRelease() {
|
||||
VideoCapturerAndroidTestFixtures.release(VideoCapturerAndroid.create("", null));
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
public void testCreateNonExistingCamera() throws Exception {
|
||||
public void testCreateAndReleaseUsingTextures() {
|
||||
VideoCapturerAndroidTestFixtures.release(
|
||||
VideoCapturerAndroid.create("", null, EGL14.EGL_NO_CONTEXT));
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
public void testCreateNonExistingCamera() {
|
||||
VideoCapturerAndroid capturer = VideoCapturerAndroid.create(
|
||||
"non-existing camera", null);
|
||||
assertNull(capturer);
|
||||
@ -224,195 +97,135 @@ public class VideoCapturerAndroidTest extends ActivityTestCase {
|
||||
// This test that the camera can be started and that the frames are forwarded
|
||||
// to a Java video renderer using a "default" capturer.
|
||||
// It tests both the Java and the C++ layer.
|
||||
public void testStartVideoCapturer() throws Exception {
|
||||
startCapturerAndRender("");
|
||||
public void testStartVideoCapturer() throws InterruptedException {
|
||||
VideoCapturerAndroid capturer =
|
||||
VideoCapturerAndroid.create("", null);
|
||||
VideoCapturerAndroidTestFixtures.startCapturerAndRender(capturer);
|
||||
}
|
||||
|
||||
/* TODO(perkj): Enable once VideoCapture to texture support has landed in C++.
|
||||
@SmallTest
|
||||
public void testStartVideoCapturerUsingTextures() throws InterruptedException {
|
||||
VideoCapturerAndroid capturer =
|
||||
VideoCapturerAndroid.create("", null, EGL14.EGL_NO_CONTEXT);
|
||||
VideoCapturerAndroidTestFixtures.startCapturerAndRender(capturer);
|
||||
}*/
|
||||
|
||||
@SmallTest
|
||||
// This test that the camera can be started and that the frames are forwarded
|
||||
// to a Java video renderer using the front facing video capturer.
|
||||
// It tests both the Java and the C++ layer.
|
||||
public void testStartFrontFacingVideoCapturer() throws Exception {
|
||||
startCapturerAndRender(CameraEnumerationAndroid.getNameOfFrontFacingDevice());
|
||||
public void testStartFrontFacingVideoCapturer() throws InterruptedException {
|
||||
String deviceName = CameraEnumerationAndroid.getNameOfFrontFacingDevice();
|
||||
VideoCapturerAndroid capturer =
|
||||
VideoCapturerAndroid.create(deviceName, null);
|
||||
VideoCapturerAndroidTestFixtures.startCapturerAndRender(capturer);
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
// This test that the camera can be started and that the frames are forwarded
|
||||
// to a Java video renderer using the back facing video capturer.
|
||||
// It tests both the Java and the C++ layer.
|
||||
public void testStartBackFacingVideoCapturer() throws Exception {
|
||||
if (!HaveTwoCameras()) {
|
||||
public void testStartBackFacingVideoCapturer() throws InterruptedException {
|
||||
if (!VideoCapturerAndroidTestFixtures.HaveTwoCameras()) {
|
||||
return;
|
||||
}
|
||||
startCapturerAndRender(CameraEnumerationAndroid.getNameOfBackFacingDevice());
|
||||
|
||||
String deviceName = CameraEnumerationAndroid.getNameOfBackFacingDevice();
|
||||
VideoCapturerAndroid capturer =
|
||||
VideoCapturerAndroid.create(deviceName, null);
|
||||
VideoCapturerAndroidTestFixtures.startCapturerAndRender(capturer);
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
// This test that the default camera can be started and that the camera can
|
||||
// later be switched to another camera.
|
||||
// It tests both the Java and the C++ layer.
|
||||
public void testSwitchVideoCapturer() throws Exception {
|
||||
PeerConnectionFactory factory = new PeerConnectionFactory();
|
||||
public void testSwitchVideoCapturer() throws InterruptedException {
|
||||
VideoCapturerAndroid capturer = VideoCapturerAndroid.create("", null);
|
||||
VideoSource source =
|
||||
factory.createVideoSource(capturer, new MediaConstraints());
|
||||
VideoTrack track = factory.createVideoTrack("dummy", source);
|
||||
|
||||
// Array with one element to avoid final problem in nested classes.
|
||||
final boolean[] cameraSwitchSuccessful = new boolean[1];
|
||||
final CountDownLatch barrier = new CountDownLatch(1);
|
||||
capturer.switchCamera(new VideoCapturerAndroid.CameraSwitchHandler() {
|
||||
@Override
|
||||
public void onCameraSwitchDone(boolean isFrontCamera) {
|
||||
cameraSwitchSuccessful[0] = true;
|
||||
barrier.countDown();
|
||||
}
|
||||
@Override
|
||||
public void onCameraSwitchError(String errorDescription) {
|
||||
cameraSwitchSuccessful[0] = false;
|
||||
barrier.countDown();
|
||||
}
|
||||
});
|
||||
// Wait until the camera has been switched.
|
||||
barrier.await();
|
||||
|
||||
// Check result.
|
||||
if (HaveTwoCameras()) {
|
||||
assertTrue(cameraSwitchSuccessful[0]);
|
||||
} else {
|
||||
assertFalse(cameraSwitchSuccessful[0]);
|
||||
}
|
||||
// Ensure that frames are received.
|
||||
RendererCallbacks callbacks = new RendererCallbacks();
|
||||
track.addRenderer(new VideoRenderer(callbacks));
|
||||
assertTrue(callbacks.WaitForNextFrameToRender() > 0);
|
||||
track.dispose();
|
||||
source.dispose();
|
||||
factory.dispose();
|
||||
assertTrue(capturer.isReleased());
|
||||
VideoCapturerAndroidTestFixtures.switchCamera(capturer);
|
||||
}
|
||||
|
||||
/* TODO(perkj): Enable once VideoCapture to texture support has landed in C++.
|
||||
@SmallTest
|
||||
public void testSwitchVideoCapturerUsingTextures() throws InterruptedException {
|
||||
VideoCapturerAndroid capturer = VideoCapturerAndroid.create("", null, EGL14.EGL_NO_CONTEXT);
|
||||
VideoCapturerAndroidTestFixtures.switchCamera(capturer);
|
||||
}*/
|
||||
|
||||
@MediumTest
|
||||
// Test what happens when attempting to call e.g. switchCamera() after camera has been stopped.
|
||||
public void testCameraCallsAfterStop() throws InterruptedException {
|
||||
final String deviceName = CameraEnumerationAndroid.getDeviceName(0);
|
||||
final VideoCapturerAndroid capturer = VideoCapturerAndroid.create(deviceName, null);
|
||||
final List<CaptureFormat> formats = CameraEnumerationAndroid.getSupportedFormats(0);
|
||||
final CameraEnumerationAndroid.CaptureFormat format = formats.get(0);
|
||||
|
||||
final FakeCapturerObserver observer = new FakeCapturerObserver();
|
||||
capturer.startCapture(format.width, format.height, format.maxFramerate,
|
||||
getInstrumentation().getContext(), observer);
|
||||
// Make sure camera is started and then stop it.
|
||||
assertTrue(observer.WaitForCapturerToStart());
|
||||
capturer.stopCapture();
|
||||
for (long timeStamp : observer.getCopyAndResetListOftimeStamps()) {
|
||||
capturer.returnBuffer(timeStamp);
|
||||
}
|
||||
// We can't change |capturer| at this point, but we should not crash.
|
||||
capturer.switchCamera(null);
|
||||
capturer.onOutputFormatRequest(640, 480, 15);
|
||||
capturer.changeCaptureFormat(640, 480, 15);
|
||||
VideoCapturerAndroidTestFixtures.cameraCallsAfterStop(capturer,
|
||||
getInstrumentation().getContext());
|
||||
}
|
||||
|
||||
capturer.dispose();
|
||||
assertTrue(capturer.isReleased());
|
||||
@MediumTest
|
||||
public void testCameraCallsAfterStopUsingTextures() throws InterruptedException {
|
||||
final String deviceName = CameraEnumerationAndroid.getDeviceName(0);
|
||||
final VideoCapturerAndroid capturer = VideoCapturerAndroid.create(deviceName, null,
|
||||
EGL14.EGL_NO_CONTEXT);
|
||||
|
||||
VideoCapturerAndroidTestFixtures.cameraCallsAfterStop(capturer,
|
||||
getInstrumentation().getContext());
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
// This test that the VideoSource that the VideoCapturer is connected to can
|
||||
// be stopped and restarted. It tests both the Java and the C++ layer.
|
||||
public void testStopRestartVideoSource() throws Exception {
|
||||
PeerConnectionFactory factory = new PeerConnectionFactory();
|
||||
public void testStopRestartVideoSource() throws InterruptedException {
|
||||
VideoCapturerAndroid capturer = VideoCapturerAndroid.create("", null);
|
||||
VideoSource source =
|
||||
factory.createVideoSource(capturer, new MediaConstraints());
|
||||
VideoTrack track = factory.createVideoTrack("dummy", source);
|
||||
RendererCallbacks callbacks = new RendererCallbacks();
|
||||
track.addRenderer(new VideoRenderer(callbacks));
|
||||
assertTrue(callbacks.WaitForNextFrameToRender() > 0);
|
||||
assertEquals(MediaSource.State.LIVE, source.state());
|
||||
|
||||
source.stop();
|
||||
assertEquals(MediaSource.State.ENDED, source.state());
|
||||
|
||||
source.restart();
|
||||
assertTrue(callbacks.WaitForNextFrameToRender() > 0);
|
||||
assertEquals(MediaSource.State.LIVE, source.state());
|
||||
track.dispose();
|
||||
source.dispose();
|
||||
factory.dispose();
|
||||
assertTrue(capturer.isReleased());
|
||||
VideoCapturerAndroidTestFixtures.stopRestartVideoSource(capturer);
|
||||
}
|
||||
|
||||
/* TODO(perkj): Enable once VideoCapture to texture support has landed in C++.
|
||||
@SmallTest
|
||||
public void testStopRestartVideoSourceUsingTextures() throws InterruptedException {
|
||||
VideoCapturerAndroid capturer = VideoCapturerAndroid.create("", null, EGL14.EGL_NO_CONTEXT);
|
||||
VideoCapturerAndroidTestFixtures.stopRestartVideoSource(capturer);
|
||||
}*/
|
||||
|
||||
@SmallTest
|
||||
// This test that the camera can be started at different resolutions.
|
||||
// It does not test or use the C++ layer.
|
||||
public void testStartStopWithDifferentResolutions() throws Exception {
|
||||
FakeCapturerObserver observer = new FakeCapturerObserver();
|
||||
|
||||
public void testStartStopWithDifferentResolutions() throws InterruptedException {
|
||||
String deviceName = CameraEnumerationAndroid.getDeviceName(0);
|
||||
List<CaptureFormat> formats = CameraEnumerationAndroid.getSupportedFormats(0);
|
||||
VideoCapturerAndroid capturer =
|
||||
VideoCapturerAndroid.create(deviceName, null);
|
||||
VideoCapturerAndroidTestFixtures.startStopWithDifferentResolutions(capturer,
|
||||
getInstrumentation().getContext());
|
||||
}
|
||||
|
||||
for(int i = 0; i < 3 ; ++i) {
|
||||
CameraEnumerationAndroid.CaptureFormat format = formats.get(i);
|
||||
capturer.startCapture(format.width, format.height, format.maxFramerate,
|
||||
getInstrumentation().getContext(), observer);
|
||||
assertTrue(observer.WaitForCapturerToStart());
|
||||
observer.WaitForNextCapturedFrame();
|
||||
// Check the frame size.
|
||||
assertEquals(format.frameSize(), observer.frameSize());
|
||||
capturer.stopCapture();
|
||||
for (long timestamp : observer.getCopyAndResetListOftimeStamps()) {
|
||||
capturer.returnBuffer(timestamp);
|
||||
}
|
||||
}
|
||||
capturer.dispose();
|
||||
assertTrue(capturer.isReleased());
|
||||
@SmallTest
|
||||
public void testStartStopWithDifferentResolutionsUsingTextures() throws InterruptedException {
|
||||
String deviceName = CameraEnumerationAndroid.getDeviceName(0);
|
||||
VideoCapturerAndroid capturer =
|
||||
VideoCapturerAndroid.create(deviceName, null, EGL14.EGL_NO_CONTEXT);
|
||||
VideoCapturerAndroidTestFixtures.startStopWithDifferentResolutions(capturer,
|
||||
getInstrumentation().getContext());
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
// This test what happens if buffers are returned after the capturer have
|
||||
// been stopped and restarted. It does not test or use the C++ layer.
|
||||
public void testReturnBufferLate() throws Exception {
|
||||
FakeCapturerObserver observer = new FakeCapturerObserver();
|
||||
|
||||
public void testReturnBufferLate() throws InterruptedException {
|
||||
String deviceName = CameraEnumerationAndroid.getDeviceName(0);
|
||||
List<CaptureFormat> formats = CameraEnumerationAndroid.getSupportedFormats(0);
|
||||
VideoCapturerAndroid capturer =
|
||||
VideoCapturerAndroid.create(deviceName, null);
|
||||
VideoCapturerAndroidTestFixtures.returnBufferLate(capturer,
|
||||
getInstrumentation().getContext());
|
||||
}
|
||||
|
||||
CameraEnumerationAndroid.CaptureFormat format = formats.get(0);
|
||||
capturer.startCapture(format.width, format.height, format.maxFramerate,
|
||||
getInstrumentation().getContext(), observer);
|
||||
assertTrue(observer.WaitForCapturerToStart());
|
||||
|
||||
observer.WaitForNextCapturedFrame();
|
||||
capturer.stopCapture();
|
||||
List<Long> listOftimestamps = observer.getCopyAndResetListOftimeStamps();
|
||||
assertTrue(listOftimestamps.size() >= 1);
|
||||
|
||||
format = formats.get(1);
|
||||
capturer.startCapture(format.width, format.height, format.maxFramerate,
|
||||
getInstrumentation().getContext(), observer);
|
||||
observer.WaitForCapturerToStart();
|
||||
observer.WaitForNextCapturedFrame();
|
||||
|
||||
for (Long timeStamp : listOftimestamps) {
|
||||
capturer.returnBuffer(timeStamp);
|
||||
}
|
||||
|
||||
observer.WaitForNextCapturedFrame();
|
||||
capturer.stopCapture();
|
||||
|
||||
listOftimestamps = observer.getCopyAndResetListOftimeStamps();
|
||||
assertTrue(listOftimestamps.size() >= 2);
|
||||
for (Long timeStamp : listOftimestamps) {
|
||||
capturer.returnBuffer(timeStamp);
|
||||
}
|
||||
capturer.dispose();
|
||||
assertTrue(capturer.isReleased());
|
||||
@SmallTest
|
||||
public void testReturnBufferLateUsingTextures() throws InterruptedException {
|
||||
String deviceName = CameraEnumerationAndroid.getDeviceName(0);
|
||||
VideoCapturerAndroid capturer =
|
||||
VideoCapturerAndroid.create(deviceName, null, EGL14.EGL_NO_CONTEXT);
|
||||
VideoCapturerAndroidTestFixtures.returnBufferLate(capturer,
|
||||
getInstrumentation().getContext());
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@ -421,38 +234,14 @@ public class VideoCapturerAndroidTest extends ActivityTestCase {
|
||||
// also test the JNI and C++ AndroidVideoCapturer parts.
|
||||
public void testReturnBufferLateEndToEnd() throws InterruptedException {
|
||||
final VideoCapturerAndroid capturer = VideoCapturerAndroid.create("", null);
|
||||
final PeerConnectionFactory factory = new PeerConnectionFactory();
|
||||
final VideoSource source = factory.createVideoSource(capturer, new MediaConstraints());
|
||||
final VideoTrack track = factory.createVideoTrack("dummy", source);
|
||||
final FakeAsyncRenderer renderer = new FakeAsyncRenderer();
|
||||
track.addRenderer(new VideoRenderer(renderer));
|
||||
// Wait for at least one frame that has not been returned.
|
||||
assertFalse(renderer.waitForPendingFrames().isEmpty());
|
||||
|
||||
capturer.stopCapture();
|
||||
|
||||
// Dispose everything.
|
||||
track.dispose();
|
||||
source.dispose();
|
||||
factory.dispose();
|
||||
|
||||
// The pending frames should keep the JNI parts and |capturer| alive.
|
||||
assertFalse(capturer.isReleased());
|
||||
|
||||
// Return the frame(s), on a different thread out of spite.
|
||||
final List<I420Frame> pendingFrames = renderer.waitForPendingFrames();
|
||||
final Thread returnThread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (I420Frame frame : pendingFrames) {
|
||||
VideoRenderer.renderFrameDone(frame);
|
||||
}
|
||||
}
|
||||
});
|
||||
returnThread.start();
|
||||
returnThread.join();
|
||||
|
||||
// Check that frames have successfully returned. This will cause |capturer| to be released.
|
||||
assertTrue(capturer.isReleased());
|
||||
VideoCapturerAndroidTestFixtures.returnBufferLateEndToEnd(capturer);
|
||||
}
|
||||
|
||||
/* TODO(perkj): Enable once VideoCapture to texture support has landed in C++.
|
||||
@MediumTest
|
||||
public void testReturnBufferLateEndToEndUsingTextures() throws InterruptedException {
|
||||
final VideoCapturerAndroid capturer =
|
||||
VideoCapturerAndroid.create("", null, EGL14.EGL_NO_CONTEXT);
|
||||
VideoCapturerAndroidTestFixtures.returnBufferLateEndToEnd(capturer);
|
||||
}*/
|
||||
}
|
||||
|
||||
@ -0,0 +1,395 @@
|
||||
/*
|
||||
* libjingle
|
||||
* Copyright 2015 Google Inc.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. The name of the author may not be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
||||
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||||
* EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
||||
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package org.webrtc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.Camera;
|
||||
|
||||
import org.webrtc.CameraEnumerationAndroid.CaptureFormat;
|
||||
import org.webrtc.VideoRenderer.I420Frame;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import static junit.framework.Assert.*;
|
||||
|
||||
public class VideoCapturerAndroidTestFixtures {
|
||||
static class RendererCallbacks implements VideoRenderer.Callbacks {
|
||||
private int framesRendered = 0;
|
||||
private Object frameLock = 0;
|
||||
|
||||
@Override
|
||||
public void renderFrame(I420Frame frame) {
|
||||
synchronized (frameLock) {
|
||||
++framesRendered;
|
||||
frameLock.notify();
|
||||
}
|
||||
VideoRenderer.renderFrameDone(frame);
|
||||
}
|
||||
|
||||
public int WaitForNextFrameToRender() throws InterruptedException {
|
||||
synchronized (frameLock) {
|
||||
frameLock.wait();
|
||||
return framesRendered;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class FakeAsyncRenderer implements VideoRenderer.Callbacks {
|
||||
private final List<I420Frame> pendingFrames = new ArrayList<I420Frame>();
|
||||
|
||||
@Override
|
||||
public void renderFrame(I420Frame frame) {
|
||||
synchronized (pendingFrames) {
|
||||
pendingFrames.add(frame);
|
||||
pendingFrames.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
// Wait until at least one frame have been received, before returning them.
|
||||
public List<I420Frame> waitForPendingFrames() throws InterruptedException {
|
||||
synchronized (pendingFrames) {
|
||||
while (pendingFrames.isEmpty()) {
|
||||
pendingFrames.wait();
|
||||
}
|
||||
return new ArrayList<I420Frame>(pendingFrames);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class FakeCapturerObserver implements
|
||||
VideoCapturerAndroid.CapturerObserver {
|
||||
private int framesCaptured = 0;
|
||||
private int frameSize = 0;
|
||||
private int frameWidth = 0;
|
||||
private int frameHeight = 0;
|
||||
private Object frameLock = 0;
|
||||
private Object capturerStartLock = 0;
|
||||
private boolean captureStartResult = false;
|
||||
private List<Long> timestamps = new ArrayList<Long>();
|
||||
|
||||
@Override
|
||||
public void onCapturerStarted(boolean success) {
|
||||
synchronized (capturerStartLock) {
|
||||
captureStartResult = success;
|
||||
capturerStartLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onByteBufferFrameCaptured(byte[] frame, int length, int width, int height,
|
||||
int rotation, long timeStamp) {
|
||||
synchronized (frameLock) {
|
||||
++framesCaptured;
|
||||
frameSize = length;
|
||||
frameWidth = width;
|
||||
frameHeight = height;
|
||||
timestamps.add(timeStamp);
|
||||
frameLock.notify();
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onTextureFrameCaptured(
|
||||
int width, int height, int oesTextureId, float[] transformMatrix, long timeStamp) {
|
||||
synchronized (frameLock) {
|
||||
++framesCaptured;
|
||||
frameWidth = width;
|
||||
frameHeight = height;
|
||||
frameSize = 0;
|
||||
timestamps.add(timeStamp);
|
||||
frameLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputFormatRequest(int width, int height, int fps) {}
|
||||
|
||||
public boolean WaitForCapturerToStart() throws InterruptedException {
|
||||
synchronized (capturerStartLock) {
|
||||
capturerStartLock.wait();
|
||||
return captureStartResult;
|
||||
}
|
||||
}
|
||||
|
||||
public int WaitForNextCapturedFrame() throws InterruptedException {
|
||||
synchronized (frameLock) {
|
||||
frameLock.wait();
|
||||
return framesCaptured;
|
||||
}
|
||||
}
|
||||
|
||||
int frameSize() {
|
||||
synchronized (frameLock) {
|
||||
return frameSize;
|
||||
}
|
||||
}
|
||||
|
||||
int frameWidth() {
|
||||
synchronized (frameLock) {
|
||||
return frameWidth;
|
||||
}
|
||||
}
|
||||
|
||||
int frameHeight() {
|
||||
synchronized (frameLock) {
|
||||
return frameHeight;
|
||||
}
|
||||
}
|
||||
|
||||
List<Long> getCopyAndResetListOftimeStamps() {
|
||||
synchronized (frameLock) {
|
||||
ArrayList<Long> list = new ArrayList<Long>(timestamps);
|
||||
timestamps.clear();
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return true if the device under test have at least two cameras.
|
||||
@SuppressWarnings("deprecation")
|
||||
static public boolean HaveTwoCameras() {
|
||||
return (Camera.getNumberOfCameras() >= 2);
|
||||
}
|
||||
|
||||
static public void release(VideoCapturerAndroid capturer) {
|
||||
assertNotNull(capturer);
|
||||
capturer.dispose();
|
||||
assertTrue(capturer.isReleased());
|
||||
}
|
||||
|
||||
static public void startCapturerAndRender(VideoCapturerAndroid capturer)
|
||||
throws InterruptedException {
|
||||
PeerConnectionFactory factory = new PeerConnectionFactory();
|
||||
VideoSource source =
|
||||
factory.createVideoSource(capturer, new MediaConstraints());
|
||||
VideoTrack track = factory.createVideoTrack("dummy", source);
|
||||
RendererCallbacks callbacks = new RendererCallbacks();
|
||||
track.addRenderer(new VideoRenderer(callbacks));
|
||||
assertTrue(callbacks.WaitForNextFrameToRender() > 0);
|
||||
track.dispose();
|
||||
source.dispose();
|
||||
factory.dispose();
|
||||
assertTrue(capturer.isReleased());
|
||||
}
|
||||
|
||||
static public void switchCamera(VideoCapturerAndroid capturer) throws InterruptedException {
|
||||
PeerConnectionFactory factory = new PeerConnectionFactory();
|
||||
VideoSource source =
|
||||
factory.createVideoSource(capturer, new MediaConstraints());
|
||||
VideoTrack track = factory.createVideoTrack("dummy", source);
|
||||
|
||||
// Array with one element to avoid final problem in nested classes.
|
||||
final boolean[] cameraSwitchSuccessful = new boolean[1];
|
||||
final CountDownLatch barrier = new CountDownLatch(1);
|
||||
capturer.switchCamera(new VideoCapturerAndroid.CameraSwitchHandler() {
|
||||
@Override
|
||||
public void onCameraSwitchDone(boolean isFrontCamera) {
|
||||
cameraSwitchSuccessful[0] = true;
|
||||
barrier.countDown();
|
||||
}
|
||||
@Override
|
||||
public void onCameraSwitchError(String errorDescription) {
|
||||
cameraSwitchSuccessful[0] = false;
|
||||
barrier.countDown();
|
||||
}
|
||||
});
|
||||
// Wait until the camera has been switched.
|
||||
barrier.await();
|
||||
|
||||
// Check result.
|
||||
if (HaveTwoCameras()) {
|
||||
assertTrue(cameraSwitchSuccessful[0]);
|
||||
} else {
|
||||
assertFalse(cameraSwitchSuccessful[0]);
|
||||
}
|
||||
// Ensure that frames are received.
|
||||
RendererCallbacks callbacks = new RendererCallbacks();
|
||||
track.addRenderer(new VideoRenderer(callbacks));
|
||||
assertTrue(callbacks.WaitForNextFrameToRender() > 0);
|
||||
track.dispose();
|
||||
source.dispose();
|
||||
factory.dispose();
|
||||
assertTrue(capturer.isReleased());
|
||||
}
|
||||
|
||||
static public void cameraCallsAfterStop(
|
||||
VideoCapturerAndroid capturer, Context appContext) throws InterruptedException {
|
||||
final List<CaptureFormat> formats = capturer.getSupportedFormats();
|
||||
final CameraEnumerationAndroid.CaptureFormat format = formats.get(0);
|
||||
|
||||
final FakeCapturerObserver observer = new FakeCapturerObserver();
|
||||
capturer.startCapture(format.width, format.height, format.maxFramerate,
|
||||
appContext, observer);
|
||||
// Make sure camera is started and then stop it.
|
||||
assertTrue(observer.WaitForCapturerToStart());
|
||||
capturer.stopCapture();
|
||||
for (long timeStamp : observer.getCopyAndResetListOftimeStamps()) {
|
||||
capturer.returnBuffer(timeStamp);
|
||||
}
|
||||
// We can't change |capturer| at this point, but we should not crash.
|
||||
capturer.switchCamera(null);
|
||||
capturer.onOutputFormatRequest(640, 480, 15);
|
||||
capturer.changeCaptureFormat(640, 480, 15);
|
||||
|
||||
capturer.dispose();
|
||||
assertTrue(capturer.isReleased());
|
||||
}
|
||||
|
||||
static public void stopRestartVideoSource(VideoCapturerAndroid capturer)
|
||||
throws InterruptedException {
|
||||
PeerConnectionFactory factory = new PeerConnectionFactory();
|
||||
VideoSource source =
|
||||
factory.createVideoSource(capturer, new MediaConstraints());
|
||||
VideoTrack track = factory.createVideoTrack("dummy", source);
|
||||
RendererCallbacks callbacks = new RendererCallbacks();
|
||||
track.addRenderer(new VideoRenderer(callbacks));
|
||||
assertTrue(callbacks.WaitForNextFrameToRender() > 0);
|
||||
assertEquals(MediaSource.State.LIVE, source.state());
|
||||
|
||||
source.stop();
|
||||
assertEquals(MediaSource.State.ENDED, source.state());
|
||||
|
||||
source.restart();
|
||||
assertTrue(callbacks.WaitForNextFrameToRender() > 0);
|
||||
assertEquals(MediaSource.State.LIVE, source.state());
|
||||
track.dispose();
|
||||
source.dispose();
|
||||
factory.dispose();
|
||||
assertTrue(capturer.isReleased());
|
||||
}
|
||||
|
||||
static public void startStopWithDifferentResolutions(VideoCapturerAndroid capturer,
|
||||
Context appContext) throws InterruptedException {
|
||||
FakeCapturerObserver observer = new FakeCapturerObserver();
|
||||
List<CaptureFormat> formats = capturer.getSupportedFormats();
|
||||
|
||||
for(int i = 0; i < 3 ; ++i) {
|
||||
CameraEnumerationAndroid.CaptureFormat format = formats.get(i);
|
||||
capturer.startCapture(format.width, format.height, format.maxFramerate,
|
||||
appContext, observer);
|
||||
assertTrue(observer.WaitForCapturerToStart());
|
||||
observer.WaitForNextCapturedFrame();
|
||||
|
||||
// Check the frame size. The actual width and height depend on how the capturer is mounted.
|
||||
final boolean identicalResolution = (observer.frameWidth() == format.width
|
||||
&& observer.frameHeight() == format.height);
|
||||
final boolean flippedResolution = (observer.frameWidth() == format.height
|
||||
&& observer.frameHeight() == format.width);
|
||||
if (!identicalResolution && !flippedResolution) {
|
||||
fail("Wrong resolution, got: " + observer.frameWidth() + "x" + observer.frameHeight()
|
||||
+ " expected: " + format.width + "x" + format.height + " or " + format.height + "x"
|
||||
+ format.width);
|
||||
}
|
||||
|
||||
if (capturer.isCapturingToTexture()) {
|
||||
assertEquals(0, observer.frameSize());
|
||||
} else {
|
||||
assertEquals(format.frameSize(), observer.frameSize());
|
||||
}
|
||||
capturer.stopCapture();
|
||||
for (long timestamp : observer.getCopyAndResetListOftimeStamps()) {
|
||||
capturer.returnBuffer(timestamp);
|
||||
}
|
||||
}
|
||||
capturer.dispose();
|
||||
assertTrue(capturer.isReleased());
|
||||
}
|
||||
|
||||
static public void returnBufferLate(VideoCapturerAndroid capturer,
|
||||
Context appContext) throws InterruptedException {
|
||||
FakeCapturerObserver observer = new FakeCapturerObserver();
|
||||
|
||||
List<CaptureFormat> formats = capturer.getSupportedFormats();
|
||||
CameraEnumerationAndroid.CaptureFormat format = formats.get(0);
|
||||
capturer.startCapture(format.width, format.height, format.maxFramerate,
|
||||
appContext, observer);
|
||||
assertTrue(observer.WaitForCapturerToStart());
|
||||
|
||||
observer.WaitForNextCapturedFrame();
|
||||
capturer.stopCapture();
|
||||
List<Long> listOftimestamps = observer.getCopyAndResetListOftimeStamps();
|
||||
assertTrue(listOftimestamps.size() >= 1);
|
||||
|
||||
format = formats.get(1);
|
||||
capturer.startCapture(format.width, format.height, format.maxFramerate,
|
||||
appContext, observer);
|
||||
observer.WaitForCapturerToStart();
|
||||
|
||||
for (Long timeStamp : listOftimestamps) {
|
||||
capturer.returnBuffer(timeStamp);
|
||||
}
|
||||
|
||||
observer.WaitForNextCapturedFrame();
|
||||
capturer.stopCapture();
|
||||
|
||||
listOftimestamps = observer.getCopyAndResetListOftimeStamps();
|
||||
assertTrue(listOftimestamps.size() >= 1);
|
||||
for (Long timeStamp : listOftimestamps) {
|
||||
capturer.returnBuffer(timeStamp);
|
||||
}
|
||||
capturer.dispose();
|
||||
assertTrue(capturer.isReleased());
|
||||
}
|
||||
|
||||
static public void returnBufferLateEndToEnd(VideoCapturerAndroid capturer)
|
||||
throws InterruptedException {
|
||||
final PeerConnectionFactory factory = new PeerConnectionFactory();
|
||||
final VideoSource source = factory.createVideoSource(capturer, new MediaConstraints());
|
||||
final VideoTrack track = factory.createVideoTrack("dummy", source);
|
||||
final FakeAsyncRenderer renderer = new FakeAsyncRenderer();
|
||||
track.addRenderer(new VideoRenderer(renderer));
|
||||
// Wait for at least one frame that has not been returned.
|
||||
assertFalse(renderer.waitForPendingFrames().isEmpty());
|
||||
|
||||
capturer.stopCapture();
|
||||
|
||||
// Dispose everything.
|
||||
track.dispose();
|
||||
source.dispose();
|
||||
factory.dispose();
|
||||
|
||||
// The pending frames should keep the JNI parts and |capturer| alive.
|
||||
assertFalse(capturer.isReleased());
|
||||
|
||||
// Return the frame(s), on a different thread out of spite.
|
||||
final List<I420Frame> pendingFrames = renderer.waitForPendingFrames();
|
||||
final Thread returnThread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (I420Frame frame : pendingFrames) {
|
||||
VideoRenderer.renderFrameDone(frame);
|
||||
}
|
||||
}
|
||||
});
|
||||
returnThread.start();
|
||||
returnThread.join();
|
||||
|
||||
// Check that frames have successfully returned. This will cause |capturer| to be released.
|
||||
assertTrue(capturer.isReleased());
|
||||
}
|
||||
}
|
||||
@ -76,6 +76,15 @@ public class RendererCommon {
|
||||
0, 1, 0, 1};
|
||||
}
|
||||
|
||||
// Matrix with transform x' = 1 - x.
|
||||
public static final float[] horizontalFlipMatrix() {
|
||||
return new float[] {
|
||||
-1, 0, 0, 0,
|
||||
0, 1, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
1, 0, 0, 1};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns texture matrix that will have the effect of rotating the frame |rotationDegree|
|
||||
* clockwise when rendered.
|
||||
|
||||
@ -28,14 +28,14 @@
|
||||
package org.webrtc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.hardware.Camera;
|
||||
import android.hardware.Camera.PreviewCallback;
|
||||
import android.opengl.GLES11Ext;
|
||||
import android.opengl.GLES20;
|
||||
import android.opengl.EGL14;
|
||||
import android.opengl.EGLContext;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.SystemClock;
|
||||
import android.text.StaticLayout;
|
||||
import android.view.Surface;
|
||||
import android.view.WindowManager;
|
||||
|
||||
@ -47,9 +47,11 @@ import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@ -65,34 +67,36 @@ import java.util.concurrent.TimeUnit;
|
||||
// camera thread. The internal *OnCameraThread() methods must check |camera| for null to check if
|
||||
// the camera has been stopped.
|
||||
@SuppressWarnings("deprecation")
|
||||
public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallback {
|
||||
public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallback,
|
||||
SurfaceTextureHelper.OnTextureFrameAvailableListener {
|
||||
private final static String TAG = "VideoCapturerAndroid";
|
||||
private final static int CAMERA_OBSERVER_PERIOD_MS = 5000;
|
||||
|
||||
private Camera camera; // Only non-null while capturing.
|
||||
private HandlerThread cameraThread;
|
||||
private final Handler cameraThreadHandler;
|
||||
// |cameraSurfaceTexture| is used with setPreviewTexture. Must be a member, see issue webrtc:5021.
|
||||
private SurfaceTexture cameraSurfaceTexture;
|
||||
private Context applicationContext;
|
||||
// Synchronization lock for |id|.
|
||||
private final Object cameraIdLock = new Object();
|
||||
private int id;
|
||||
private Camera.CameraInfo info;
|
||||
private int cameraGlTexture = 0;
|
||||
private final FramePool videoBuffers;
|
||||
private final CameraStatistics cameraStatistics = new CameraStatistics();
|
||||
// Remember the requested format in case we want to switch cameras.
|
||||
private int requestedWidth;
|
||||
private int requestedHeight;
|
||||
private int requestedFramerate;
|
||||
// The capture format will be the closest supported format to the requested format.
|
||||
private CaptureFormat captureFormat;
|
||||
private int cameraFramesCount;
|
||||
private int captureBuffersCount;
|
||||
private final Object pendingCameraSwitchLock = new Object();
|
||||
private volatile boolean pendingCameraSwitch;
|
||||
private CapturerObserver frameObserver = null;
|
||||
private final CameraErrorHandler errorHandler;
|
||||
private final boolean isCapturingToTexture;
|
||||
private final SurfaceTextureHelper surfaceHelper;
|
||||
// The camera API can output one old frame after the camera has been switched or the resolution
|
||||
// has been changed. This flag is used for dropping the first frame after camera restart.
|
||||
private boolean dropNextFrame = false;
|
||||
|
||||
// Camera error callback.
|
||||
private final Camera.ErrorCallback cameraErrorCallback =
|
||||
@ -112,34 +116,74 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
}
|
||||
};
|
||||
|
||||
// Camera observer - monitors camera framerate and amount of available
|
||||
// camera buffers. Observer is excecuted on camera thread.
|
||||
// Camera observer - monitors camera framerate. Observer is executed on camera thread.
|
||||
private final Runnable cameraObserver = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int cameraFramesCount = cameraStatistics.getAndResetFrameCount();
|
||||
int cameraFps = (cameraFramesCount * 1000 + CAMERA_OBSERVER_PERIOD_MS / 2)
|
||||
/ CAMERA_OBSERVER_PERIOD_MS;
|
||||
double averageCaptureBuffersCount = 0;
|
||||
if (cameraFramesCount > 0) {
|
||||
averageCaptureBuffersCount =
|
||||
(double)captureBuffersCount / cameraFramesCount;
|
||||
}
|
||||
Logging.d(TAG, "Camera fps: " + cameraFps + ". CaptureBuffers: " +
|
||||
String.format("%.1f", averageCaptureBuffersCount) +
|
||||
". Pending buffers: " + videoBuffers.pendingFramesTimeStamps());
|
||||
|
||||
Logging.d(TAG, "Camera fps: " + cameraFps +
|
||||
". Pending buffers: " + cameraStatistics.pendingFramesTimeStamps());
|
||||
if (cameraFramesCount == 0) {
|
||||
Logging.e(TAG, "Camera freezed.");
|
||||
if (errorHandler != null) {
|
||||
errorHandler.onCameraError("Camera failure.");
|
||||
}
|
||||
} else {
|
||||
cameraFramesCount = 0;
|
||||
captureBuffersCount = 0;
|
||||
cameraThreadHandler.postDelayed(this, CAMERA_OBSERVER_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private static class CameraStatistics {
|
||||
private int frameCount = 0;
|
||||
private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker();
|
||||
private final Set<Long> timeStampsNs = new HashSet<Long>();
|
||||
|
||||
CameraStatistics() {
|
||||
threadChecker.detachThread();
|
||||
}
|
||||
|
||||
public void addPendingFrame(long timestamp) {
|
||||
threadChecker.checkIsOnValidThread();
|
||||
++frameCount;
|
||||
timeStampsNs.add(timestamp);
|
||||
}
|
||||
|
||||
public void frameReturned(long timestamp) {
|
||||
threadChecker.checkIsOnValidThread();
|
||||
if (!timeStampsNs.contains(timestamp)) {
|
||||
throw new IllegalStateException(
|
||||
"CameraStatistics.frameReturned called with unknown timestamp " + timestamp);
|
||||
}
|
||||
timeStampsNs.remove(timestamp);
|
||||
}
|
||||
|
||||
public int getAndResetFrameCount() {
|
||||
threadChecker.checkIsOnValidThread();
|
||||
int count = frameCount;
|
||||
frameCount = 0;
|
||||
return count;
|
||||
}
|
||||
|
||||
// Return number of pending frames that have not been returned.
|
||||
public int pendingFramesCount() {
|
||||
threadChecker.checkIsOnValidThread();
|
||||
return timeStampsNs.size();
|
||||
}
|
||||
|
||||
public String pendingFramesTimeStamps() {
|
||||
threadChecker.checkIsOnValidThread();
|
||||
List<Long> timeStampsMs = new ArrayList<Long>();
|
||||
for (long ts : timeStampsNs) {
|
||||
timeStampsMs.add(TimeUnit.NANOSECONDS.toMillis(ts));
|
||||
}
|
||||
return timeStampsMs.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Camera error handler - invoked when camera stops receiving frames
|
||||
// or any camera exception happens on camera thread.
|
||||
public static interface CameraErrorHandler {
|
||||
@ -155,12 +199,20 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
void onCameraSwitchError(String errorDescription);
|
||||
}
|
||||
|
||||
public static VideoCapturerAndroid create(String name, CameraErrorHandler errorHandler) {
|
||||
public static VideoCapturerAndroid create(String name,
|
||||
CameraErrorHandler errorHandler) {
|
||||
return VideoCapturerAndroid.create(name, errorHandler, null);
|
||||
}
|
||||
|
||||
public static VideoCapturerAndroid create(String name,
|
||||
CameraErrorHandler errorHandler, EGLContext sharedContext) {
|
||||
final int cameraId = lookupDeviceName(name);
|
||||
if (cameraId == -1) {
|
||||
return null;
|
||||
}
|
||||
final VideoCapturerAndroid capturer = new VideoCapturerAndroid(cameraId, errorHandler);
|
||||
|
||||
final VideoCapturerAndroid capturer = new VideoCapturerAndroid(cameraId, errorHandler,
|
||||
sharedContext);
|
||||
capturer.setNativeCapturer(nativeCreateVideoCapturer(capturer));
|
||||
return capturer;
|
||||
}
|
||||
@ -208,10 +260,10 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
// Requests a new output format from the video capturer. Captured frames
|
||||
// by the camera will be scaled/or dropped by the video capturer.
|
||||
// TODO(magjed/perkj): Document what this function does. Change name?
|
||||
public void onOutputFormatRequest(final int width, final int height, final int fps) {
|
||||
public void onOutputFormatRequest(final int width, final int height, final int framerate) {
|
||||
cameraThreadHandler.post(new Runnable() {
|
||||
@Override public void run() {
|
||||
onOutputFormatRequestOnCameraThread(width, height, fps);
|
||||
onOutputFormatRequestOnCameraThread(width, height, framerate);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -238,6 +290,11 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
return CameraEnumerationAndroid.getSupportedFormats(getCurrentCameraId());
|
||||
}
|
||||
|
||||
// Returns true if this VideoCapturer is setup to capture video frames to a SurfaceTexture.
|
||||
public boolean isCapturingToTexture() {
|
||||
return isCapturingToTexture;
|
||||
}
|
||||
|
||||
// Called from native code.
|
||||
private String getSupportedFormatsAsJson() throws JSONException {
|
||||
return CameraEnumerationAndroid.getSupportedFormatsAsJson(getCurrentCameraId());
|
||||
@ -245,10 +302,11 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
|
||||
// Called from native VideoCapturer_nativeCreateVideoCapturer.
|
||||
private VideoCapturerAndroid(int cameraId) {
|
||||
this(cameraId, null);
|
||||
this(cameraId, null, null);
|
||||
}
|
||||
|
||||
private VideoCapturerAndroid(int cameraId, CameraErrorHandler errorHandler) {
|
||||
private VideoCapturerAndroid(int cameraId, CameraErrorHandler errorHandler,
|
||||
EGLContext sharedContext) {
|
||||
Logging.d(TAG, "VideoCapturerAndroid");
|
||||
this.id = cameraId;
|
||||
this.errorHandler = errorHandler;
|
||||
@ -256,6 +314,13 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
cameraThread.start();
|
||||
cameraThreadHandler = new Handler(cameraThread.getLooper());
|
||||
videoBuffers = new FramePool(cameraThread);
|
||||
surfaceHelper =
|
||||
new SurfaceTextureHelper(sharedContext == null ? EGL14.EGL_NO_CONTEXT : sharedContext,
|
||||
cameraThread);
|
||||
if (sharedContext != null) {
|
||||
surfaceHelper.setListener(this);
|
||||
}
|
||||
isCapturingToTexture = sharedContext != null;
|
||||
}
|
||||
|
||||
private void checkIsOnCameraThread() {
|
||||
@ -285,6 +350,7 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
// Called by native code to quit the camera thread. This needs to be done manually, otherwise the
|
||||
// thread and handler will not be garbage collected.
|
||||
private void release() {
|
||||
Logging.d(TAG, "release");
|
||||
if (isReleased()) {
|
||||
throw new IllegalStateException("Already released");
|
||||
}
|
||||
@ -294,11 +360,13 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
if (camera != null) {
|
||||
throw new IllegalStateException("Release called while camera is running");
|
||||
}
|
||||
if (videoBuffers.pendingFramesCount() != 0) {
|
||||
if (cameraStatistics.pendingFramesCount() != 0) {
|
||||
throw new IllegalStateException("Release called with pending frames left");
|
||||
}
|
||||
}
|
||||
});
|
||||
surfaceHelper.disconnect();
|
||||
|
||||
cameraThread.quitSafely();
|
||||
ThreadUtils.joinUninterruptibly(cameraThread);
|
||||
cameraThread = null;
|
||||
@ -349,16 +417,8 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
info = new Camera.CameraInfo();
|
||||
Camera.getCameraInfo(id, info);
|
||||
}
|
||||
// No local renderer (we only care about onPreviewFrame() buffers, not a
|
||||
// directly-displayed UI element). Camera won't capture without
|
||||
// setPreview{Texture,Display}, so we create a SurfaceTexture and hand
|
||||
// it over to Camera, but never listen for frame-ready callbacks,
|
||||
// and never call updateTexImage on it.
|
||||
try {
|
||||
cameraGlTexture = GlUtil.generateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
|
||||
cameraSurfaceTexture = new SurfaceTexture(cameraGlTexture);
|
||||
cameraSurfaceTexture.setOnFrameAvailableListener(null);
|
||||
camera.setPreviewTexture(cameraSurfaceTexture);
|
||||
camera.setPreviewTexture(surfaceHelper.getSurfaceTexture());
|
||||
} catch (IOException e) {
|
||||
Logging.e(TAG, "setPreviewTexture failed", error);
|
||||
throw new RuntimeException(e);
|
||||
@ -368,11 +428,9 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
" .Device orientation: " + getDeviceOrientation());
|
||||
camera.setErrorCallback(cameraErrorCallback);
|
||||
startPreviewOnCameraThread(width, height, framerate);
|
||||
frameObserver.OnCapturerStarted(true);
|
||||
frameObserver.onCapturerStarted(true);
|
||||
|
||||
// Start camera observer.
|
||||
cameraFramesCount = 0;
|
||||
captureBuffersCount = 0;
|
||||
cameraThreadHandler.postDelayed(cameraObserver, CAMERA_OBSERVER_PERIOD_MS);
|
||||
return;
|
||||
} catch (RuntimeException e) {
|
||||
@ -380,7 +438,7 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
}
|
||||
Logging.e(TAG, "startCapture failed", error);
|
||||
stopCaptureOnCameraThread();
|
||||
frameObserver.OnCapturerStarted(false);
|
||||
frameObserver.onCapturerStarted(false);
|
||||
if (errorHandler != null) {
|
||||
errorHandler.onCameraError("Camera can not be started.");
|
||||
}
|
||||
@ -438,6 +496,7 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
// Temporarily stop preview if it's already running.
|
||||
if (this.captureFormat != null) {
|
||||
camera.stopPreview();
|
||||
dropNextFrame = true;
|
||||
// Calling |setPreviewCallbackWithBuffer| with null should clear the internal camera buffer
|
||||
// queue, but sometimes we receive a frame with the old resolution after this call anyway.
|
||||
camera.setPreviewCallbackWithBuffer(null);
|
||||
@ -453,8 +512,10 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
}
|
||||
|
||||
camera.setParameters(parameters);
|
||||
videoBuffers.queueCameraBuffers(captureFormat.frameSize(), camera);
|
||||
camera.setPreviewCallbackWithBuffer(this);
|
||||
if (!isCapturingToTexture) {
|
||||
videoBuffers.queueCameraBuffers(captureFormat.frameSize(), camera);
|
||||
camera.setPreviewCallbackWithBuffer(this);
|
||||
}
|
||||
camera.startPreview();
|
||||
}
|
||||
|
||||
@ -481,21 +542,22 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
}
|
||||
|
||||
cameraThreadHandler.removeCallbacks(cameraObserver);
|
||||
cameraStatistics.getAndResetFrameCount();
|
||||
Logging.d(TAG, "Stop preview.");
|
||||
camera.stopPreview();
|
||||
camera.setPreviewCallbackWithBuffer(null);
|
||||
videoBuffers.stopReturnBuffersToCamera();
|
||||
if (!isCapturingToTexture()) {
|
||||
videoBuffers.stopReturnBuffersToCamera();
|
||||
Logging.d(TAG, "stopReturnBuffersToCamera called."
|
||||
+ (cameraStatistics.pendingFramesCount() == 0?
|
||||
" All buffers have been returned."
|
||||
: " Pending buffers: " + cameraStatistics.pendingFramesTimeStamps() + "."));
|
||||
}
|
||||
captureFormat = null;
|
||||
|
||||
if (cameraGlTexture != 0) {
|
||||
GLES20.glDeleteTextures(1, new int[] {cameraGlTexture}, 0);
|
||||
cameraGlTexture = 0;
|
||||
}
|
||||
Logging.d(TAG, "Release camera.");
|
||||
camera.release();
|
||||
camera = null;
|
||||
cameraSurfaceTexture.release();
|
||||
cameraSurfaceTexture = null;
|
||||
}
|
||||
|
||||
private void switchCameraOnCameraThread() {
|
||||
@ -505,26 +567,32 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
synchronized (cameraIdLock) {
|
||||
id = (id + 1) % Camera.getNumberOfCameras();
|
||||
}
|
||||
dropNextFrame = true;
|
||||
startCaptureOnCameraThread(requestedWidth, requestedHeight, requestedFramerate, frameObserver,
|
||||
applicationContext);
|
||||
Logging.d(TAG, "switchCameraOnCameraThread done");
|
||||
}
|
||||
|
||||
private void onOutputFormatRequestOnCameraThread(int width, int height, int fps) {
|
||||
private void onOutputFormatRequestOnCameraThread(int width, int height, int framerate) {
|
||||
checkIsOnCameraThread();
|
||||
if (camera == null) {
|
||||
Logging.e(TAG, "Calling onOutputFormatRequest() on stopped camera.");
|
||||
return;
|
||||
}
|
||||
Logging.d(TAG, "onOutputFormatRequestOnCameraThread: " + width + "x" + height +
|
||||
"@" + fps);
|
||||
frameObserver.OnOutputFormatRequest(width, height, fps);
|
||||
"@" + framerate);
|
||||
frameObserver.onOutputFormatRequest(width, height, framerate);
|
||||
}
|
||||
|
||||
public void returnBuffer(final long timeStamp) {
|
||||
cameraThreadHandler.post(new Runnable() {
|
||||
@Override public void run() {
|
||||
videoBuffers.returnBuffer(timeStamp);
|
||||
cameraStatistics.frameReturned(timeStamp);
|
||||
if (isCapturingToTexture) {
|
||||
surfaceHelper.returnTextureFrame();
|
||||
} else {
|
||||
videoBuffers.returnBuffer(timeStamp);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -552,6 +620,14 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
return orientation;
|
||||
}
|
||||
|
||||
private int getFrameOrientation() {
|
||||
int rotation = getDeviceOrientation();
|
||||
if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
|
||||
rotation = 360 - rotation;
|
||||
}
|
||||
return (info.orientation + rotation) % 360;
|
||||
}
|
||||
|
||||
// Called on cameraThread so must not "synchronized".
|
||||
@Override
|
||||
public void onPreviewFrame(byte[] data, Camera callbackCamera) {
|
||||
@ -566,24 +642,50 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
final long captureTimeNs =
|
||||
TimeUnit.MILLISECONDS.toNanos(SystemClock.elapsedRealtime());
|
||||
|
||||
captureBuffersCount += videoBuffers.numCaptureBuffersAvailable();
|
||||
int rotation = getDeviceOrientation();
|
||||
if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
|
||||
rotation = 360 - rotation;
|
||||
}
|
||||
rotation = (info.orientation + rotation) % 360;
|
||||
|
||||
// Mark the frame owning |data| as used.
|
||||
// Note that since data is directBuffer,
|
||||
// data.length >= videoBuffers.frameSize.
|
||||
if (videoBuffers.reserveByteBuffer(data, captureTimeNs)) {
|
||||
cameraFramesCount++;
|
||||
frameObserver.OnFrameCaptured(data, videoBuffers.frameSize, captureFormat.width,
|
||||
captureFormat.height, rotation, captureTimeNs);
|
||||
cameraStatistics.addPendingFrame(captureTimeNs);
|
||||
frameObserver.onByteBufferFrameCaptured(data, videoBuffers.frameSize, captureFormat.width,
|
||||
captureFormat.height, getFrameOrientation(), captureTimeNs);
|
||||
} else {
|
||||
Logging.w(TAG, "reserveByteBuffer failed - dropping frame.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextureFrameAvailable(
|
||||
int oesTextureId, float[] transformMatrix, long timestampNs) {
|
||||
checkIsOnCameraThread();
|
||||
if (camera == null) {
|
||||
// Camera is stopped, we need to return the buffer immediately.
|
||||
surfaceHelper.returnTextureFrame();
|
||||
return;
|
||||
}
|
||||
if (!dropNextFrame) {
|
||||
surfaceHelper.returnTextureFrame();
|
||||
dropNextFrame = true;
|
||||
return;
|
||||
}
|
||||
|
||||
int rotation = getFrameOrientation();
|
||||
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
||||
// Undo the mirror that the OS "helps" us with.
|
||||
// http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)
|
||||
transformMatrix =
|
||||
RendererCommon.multiplyMatrices(transformMatrix, RendererCommon.horizontalFlipMatrix());
|
||||
}
|
||||
transformMatrix = RendererCommon.rotateTextureMatrix(transformMatrix, rotation);
|
||||
|
||||
final int rotatedWidth = (rotation % 180 == 0) ? captureFormat.width : captureFormat.height;
|
||||
final int rotatedHeight = (rotation % 180 == 0) ? captureFormat.height : captureFormat.width;
|
||||
cameraStatistics.addPendingFrame(timestampNs);
|
||||
frameObserver.onTextureFrameCaptured(rotatedWidth, rotatedHeight, oesTextureId,
|
||||
transformMatrix, timestampNs);
|
||||
}
|
||||
|
||||
// Class used for allocating and bookkeeping video frames. All buffers are
|
||||
// direct allocated so that they can be directly used from native code. This class is
|
||||
// not thread-safe, and enforces single thread use.
|
||||
@ -613,11 +715,6 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
}
|
||||
}
|
||||
|
||||
public int numCaptureBuffersAvailable() {
|
||||
checkIsOnValidThread();
|
||||
return queuedBuffers.size();
|
||||
}
|
||||
|
||||
// Discards previous queued buffers and adds new callback buffers to camera.
|
||||
public void queueCameraBuffers(int frameSize, Camera camera) {
|
||||
checkIsOnValidThread();
|
||||
@ -634,30 +731,11 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
+ " buffers of size " + frameSize + ".");
|
||||
}
|
||||
|
||||
// Return number of pending frames that have not been returned.
|
||||
public int pendingFramesCount() {
|
||||
checkIsOnValidThread();
|
||||
return pendingBuffers.size();
|
||||
}
|
||||
|
||||
public String pendingFramesTimeStamps() {
|
||||
checkIsOnValidThread();
|
||||
List<Long> timeStampsMs = new ArrayList<Long>();
|
||||
for (Long timeStampNs : pendingBuffers.keySet()) {
|
||||
timeStampsMs.add(TimeUnit.NANOSECONDS.toMillis(timeStampNs));
|
||||
}
|
||||
return timeStampsMs.toString();
|
||||
}
|
||||
|
||||
public void stopReturnBuffersToCamera() {
|
||||
checkIsOnValidThread();
|
||||
this.camera = null;
|
||||
queuedBuffers.clear();
|
||||
// Frames in |pendingBuffers| need to be kept alive until they are returned.
|
||||
Logging.d(TAG, "stopReturnBuffersToCamera called."
|
||||
+ (pendingBuffers.isEmpty() ?
|
||||
" All buffers have been returned."
|
||||
: " Pending buffers: " + pendingFramesTimeStamps() + "."));
|
||||
}
|
||||
|
||||
public boolean reserveByteBuffer(byte[] data, long timeStamp) {
|
||||
@ -679,8 +757,7 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
}
|
||||
pendingBuffers.put(timeStamp, buffer);
|
||||
if (queuedBuffers.isEmpty()) {
|
||||
Logging.v(TAG, "Camera is running out of capture buffers."
|
||||
+ " Pending buffers: " + pendingFramesTimeStamps());
|
||||
Logging.v(TAG, "Camera is running out of capture buffers.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -722,17 +799,22 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
interface CapturerObserver {
|
||||
// Notify if the camera have been started successfully or not.
|
||||
// Called on a Java thread owned by VideoCapturerAndroid.
|
||||
abstract void OnCapturerStarted(boolean success);
|
||||
abstract void onCapturerStarted(boolean success);
|
||||
|
||||
// Delivers a captured frame. Called on a Java thread owned by
|
||||
// VideoCapturerAndroid.
|
||||
abstract void OnFrameCaptured(byte[] data, int length, int width, int height,
|
||||
abstract void onByteBufferFrameCaptured(byte[] data, int length, int width, int height,
|
||||
int rotation, long timeStamp);
|
||||
|
||||
// Delivers a captured frame in a texture with id |oesTextureId|. Called on a Java thread
|
||||
// owned by VideoCapturerAndroid.
|
||||
abstract void onTextureFrameCaptured(
|
||||
int width, int height, int oesTextureId, float[] transformMatrix, long timestamp);
|
||||
|
||||
// Requests an output format from the video capturer. Captured frames
|
||||
// by the camera will be scaled/or dropped by the video capturer.
|
||||
// Called on a Java thread owned by VideoCapturerAndroid.
|
||||
abstract void OnOutputFormatRequest(int width, int height, int fps);
|
||||
abstract void onOutputFormatRequest(int width, int height, int framerate);
|
||||
}
|
||||
|
||||
// An implementation of CapturerObserver that forwards all calls from
|
||||
@ -745,27 +827,37 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
|
||||
}
|
||||
|
||||
@Override
|
||||
public void OnCapturerStarted(boolean success) {
|
||||
public void onCapturerStarted(boolean success) {
|
||||
nativeCapturerStarted(nativeCapturer, success);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void OnFrameCaptured(byte[] data, int length, int width, int height,
|
||||
public void onByteBufferFrameCaptured(byte[] data, int length, int width, int height,
|
||||
int rotation, long timeStamp) {
|
||||
nativeOnFrameCaptured(nativeCapturer, data, length, width, height, rotation, timeStamp);
|
||||
nativeOnByteBufferFrameCaptured(nativeCapturer, data, length, width, height, rotation,
|
||||
timeStamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void OnOutputFormatRequest(int width, int height, int fps) {
|
||||
nativeOnOutputFormatRequest(nativeCapturer, width, height, fps);
|
||||
public void onTextureFrameCaptured(
|
||||
int width, int height, int oesTextureId, float[] transformMatrix, long timestamp) {
|
||||
nativeOnTextureFrameCaptured(nativeCapturer, width, height, oesTextureId, transformMatrix,
|
||||
timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputFormatRequest(int width, int height, int framerate) {
|
||||
nativeOnOutputFormatRequest(nativeCapturer, width, height, framerate);
|
||||
}
|
||||
|
||||
private native void nativeCapturerStarted(long nativeCapturer,
|
||||
boolean success);
|
||||
private native void nativeOnFrameCaptured(long nativeCapturer,
|
||||
private native void nativeOnByteBufferFrameCaptured(long nativeCapturer,
|
||||
byte[] data, int length, int width, int height, int rotation, long timeStamp);
|
||||
private native void nativeOnTextureFrameCaptured(long nativeCapturer, int width, int height,
|
||||
int oesTextureId, float[] transformMatrix, long timestamp);
|
||||
private native void nativeOnOutputFormatRequest(long nativeCapturer,
|
||||
int width, int height, int fps);
|
||||
int width, int height, int framerate);
|
||||
}
|
||||
|
||||
private static native long nativeCreateVideoCapturer(VideoCapturerAndroid videoCapturer);
|
||||
|
||||
@ -188,7 +188,8 @@ void AndroidVideoCapturerJni::OnOutputFormatRequest(int width,
|
||||
|
||||
JNIEnv* AndroidVideoCapturerJni::jni() { return AttachCurrentThreadIfNeeded(); }
|
||||
|
||||
JOW(void, VideoCapturerAndroid_00024NativeObserver_nativeOnFrameCaptured)
|
||||
JOW(void,
|
||||
VideoCapturerAndroid_00024NativeObserver_nativeOnByteBufferFrameCaptured)
|
||||
(JNIEnv* jni, jclass, jlong j_capturer, jbyteArray j_frame, jint length,
|
||||
jint width, jint height, jint rotation, jlong ts) {
|
||||
jboolean is_copy = true;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user