Schedule everything to the same handler

pull/97/head
Mattia Iavarone 8 years ago
parent bd0a84a65e
commit 90f8325185
  1. 6
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java
  2. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraUtilsTest.java
  3. 82
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java
  4. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/PreviewTest.java
  5. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/WorkerHandlerTest.java
  6. 680
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
  7. 20
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
  8. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java
  9. 4
      cameraview/src/main/utils/com/otaliastudios/cameraview/Task.java

@ -73,8 +73,7 @@ public class CameraCallbacksTest extends BaseTest {
camera.instantiatePreview(); camera.instantiatePreview();
camera.addCameraListener(listener); camera.addCameraListener(listener);
camera.addFrameProcessor(processor); camera.addFrameProcessor(processor);
task = new Task<>(); task = new Task<>(true);
task.listen();
} }
}); });
} }
@ -266,8 +265,7 @@ public class CameraCallbacksTest extends BaseTest {
private int[] testProcessImage(boolean jpeg, boolean crop, int[] viewDim, int[] imageDim) { private int[] testProcessImage(boolean jpeg, boolean crop, int[] viewDim, int[] imageDim) {
// End our task when onPictureTaken is called. Take note of the result. // End our task when onPictureTaken is called. Take note of the result.
final Task<byte[]> jpegTask = new Task<>(); final Task<byte[]> jpegTask = new Task<>(true);
jpegTask.listen();
doAnswer(new Answer() { doAnswer(new Answer() {
@Override @Override
public Object answer(InvocationOnMock invocation) throws Throwable { public Object answer(InvocationOnMock invocation) throws Throwable {

@ -48,8 +48,7 @@ public class CameraUtilsTest extends BaseTest {
source.compress(Bitmap.CompressFormat.PNG, 100, os); source.compress(Bitmap.CompressFormat.PNG, 100, os);
final byte[] data = os.toByteArray(); final byte[] data = os.toByteArray();
final Task<Bitmap> decode = new Task<>(); final Task<Bitmap> decode = new Task<>(true);
decode.listen();
final CameraUtils.BitmapCallback callback = new CameraUtils.BitmapCallback() { final CameraUtils.BitmapCallback callback = new CameraUtils.BitmapCallback() {
@Override @Override
public void onBitmapReady(Bitmap bitmap) { public void onBitmapReady(Bitmap bitmap) {

@ -33,7 +33,7 @@ import static org.junit.Assert.*;
*/ */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@MediumTest @MediumTest
@Ignore // @Ignore
public class IntegrationTest extends BaseTest { public class IntegrationTest extends BaseTest {
@Rule @Rule
@ -77,8 +77,7 @@ public class IntegrationTest extends BaseTest {
} }
private CameraOptions waitForOpen(boolean expectSuccess) { private CameraOptions waitForOpen(boolean expectSuccess) {
final Task<CameraOptions> open = new Task<>(); final Task<CameraOptions> open = new Task<>(true);
open.listen();
doEndTask(open, 0).when(listener).onCameraOpened(any(CameraOptions.class)); doEndTask(open, 0).when(listener).onCameraOpened(any(CameraOptions.class));
CameraOptions result = open.await(4000); CameraOptions result = open.await(4000);
if (expectSuccess) { if (expectSuccess) {
@ -89,9 +88,8 @@ public class IntegrationTest extends BaseTest {
return result; return result;
} }
private Boolean waitForClose(boolean expectSuccess) { private void waitForClose(boolean expectSuccess) {
final Task<Boolean> close = new Task<>(); final Task<Boolean> close = new Task<>(true);
close.listen();
doEndTask(close, true).when(listener).onCameraClosed(); doEndTask(close, true).when(listener).onCameraClosed();
Boolean result = close.await(4000); Boolean result = close.await(4000);
if (expectSuccess) { if (expectSuccess) {
@ -99,12 +97,10 @@ public class IntegrationTest extends BaseTest {
} else { } else {
assertNull("Should not close", result); assertNull("Should not close", result);
} }
return result;
} }
private Boolean waitForVideo(boolean expectSuccess) { private void waitForVideo(boolean expectSuccess) {
final Task<Boolean> video = new Task<>(); final Task<Boolean> video = new Task<>(true);
video.listen();
doEndTask(video, true).when(listener).onVideoTaken(any(File.class)); doEndTask(video, true).when(listener).onVideoTaken(any(File.class));
Boolean result = video.await(2000); Boolean result = video.await(2000);
if (expectSuccess) { if (expectSuccess) {
@ -112,12 +108,10 @@ public class IntegrationTest extends BaseTest {
} else { } else {
assertNull("Should not take video", result); assertNull("Should not take video", result);
} }
return result;
} }
private byte[] waitForPicture(boolean expectSuccess) { private byte[] waitForPicture(boolean expectSuccess) {
final Task<byte[]> pic = new Task<>(); final Task<byte[]> pic = new Task<>(true);
pic.listen();
doEndTask(pic, 0).when(listener).onPictureTaken(any(byte[].class)); doEndTask(pic, 0).when(listener).onPictureTaken(any(byte[].class));
byte[] result = pic.await(5000); byte[] result = pic.await(5000);
if (expectSuccess) { if (expectSuccess) {
@ -130,7 +124,7 @@ public class IntegrationTest extends BaseTest {
//region test open/close //region test open/close
//-@Test @Test
public void testOpenClose() throws Exception { public void testOpenClose() throws Exception {
// Starting and stopping are hard to get since they happen on another thread. // Starting and stopping are hard to get since they happen on another thread.
assertEquals(controller.getState(), CameraController.STATE_STOPPED); assertEquals(controller.getState(), CameraController.STATE_STOPPED);
@ -144,7 +138,7 @@ public class IntegrationTest extends BaseTest {
assertEquals(controller.getState(), CameraController.STATE_STOPPED); assertEquals(controller.getState(), CameraController.STATE_STOPPED);
} }
//-@Test @Test
public void testOpenTwice() { public void testOpenTwice() {
camera.start(); camera.start();
waitForOpen(true); waitForOpen(true);
@ -152,7 +146,7 @@ public class IntegrationTest extends BaseTest {
waitForOpen(false); waitForOpen(false);
} }
//-@Test @Test
public void testCloseTwice() { public void testCloseTwice() {
camera.stop(); camera.stop();
waitForClose(false); waitForClose(false);
@ -236,22 +230,36 @@ public class IntegrationTest extends BaseTest {
public void testSetZoom() { public void testSetZoom() {
camera.start(); camera.start();
CameraOptions options = waitForOpen(true); CameraOptions options = waitForOpen(true);
boolean can = options.isZoomSupported();
controller.mZoomTask.listen();
float oldValue = camera.getZoom(); float oldValue = camera.getZoom();
float newValue = 0.65f; float newValue = 0.65f;
camera.setZoom(newValue); camera.setZoom(newValue);
assertEquals(can ? newValue : oldValue, camera.getZoom(), 0f); controller.mZoomTask.await(500);
if (options.isZoomSupported()) {
assertEquals(newValue, camera.getZoom(), 0f);
} else {
assertEquals(oldValue, camera.getZoom(), 0f);
}
} }
@Test @Test
public void testSetExposureCorrection() { public void testSetExposureCorrection() {
camera.start(); camera.start();
CameraOptions options = waitForOpen(true); CameraOptions options = waitForOpen(true);
boolean can = options.isExposureCorrectionSupported();
controller.mExposureCorrectionTask.listen();
float oldValue = camera.getExposureCorrection(); float oldValue = camera.getExposureCorrection();
float newValue = options.getExposureCorrectionMaxValue(); float newValue = options.getExposureCorrectionMaxValue();
camera.setExposureCorrection(newValue); camera.setExposureCorrection(newValue);
assertEquals(can ? newValue : oldValue, camera.getExposureCorrection(), 0f); controller.mExposureCorrectionTask.await(300);
if (options.isExposureCorrectionSupported()) {
assertEquals(newValue, camera.getExposureCorrection(), 0f);
} else {
assertEquals(oldValue, camera.getExposureCorrection(), 0f);
}
} }
@Test @Test
@ -261,7 +269,9 @@ public class IntegrationTest extends BaseTest {
Flash[] values = Flash.values(); Flash[] values = Flash.values();
Flash oldValue = camera.getFlash(); Flash oldValue = camera.getFlash();
for (Flash value : values) { for (Flash value : values) {
controller.mFlashTask.listen();
camera.setFlash(value); camera.setFlash(value);
controller.mFlashTask.await(300);
if (options.supports(value)) { if (options.supports(value)) {
assertEquals(camera.getFlash(), value); assertEquals(camera.getFlash(), value);
oldValue = value; oldValue = value;
@ -278,7 +288,9 @@ public class IntegrationTest extends BaseTest {
WhiteBalance[] values = WhiteBalance.values(); WhiteBalance[] values = WhiteBalance.values();
WhiteBalance oldValue = camera.getWhiteBalance(); WhiteBalance oldValue = camera.getWhiteBalance();
for (WhiteBalance value : values) { for (WhiteBalance value : values) {
controller.mWhiteBalanceTask.listen();
camera.setWhiteBalance(value); camera.setWhiteBalance(value);
controller.mWhiteBalanceTask.await(300);
if (options.supports(value)) { if (options.supports(value)) {
assertEquals(camera.getWhiteBalance(), value); assertEquals(camera.getWhiteBalance(), value);
oldValue = value; oldValue = value;
@ -295,7 +307,9 @@ public class IntegrationTest extends BaseTest {
Hdr[] values = Hdr.values(); Hdr[] values = Hdr.values();
Hdr oldValue = camera.getHdr(); Hdr oldValue = camera.getHdr();
for (Hdr value : values) { for (Hdr value : values) {
controller.mHdrTask.listen();
camera.setHdr(value); camera.setHdr(value);
controller.mHdrTask.await(300);
if (options.supports(value)) { if (options.supports(value)) {
assertEquals(camera.getHdr(), value); assertEquals(camera.getHdr(), value);
oldValue = value; oldValue = value;
@ -321,7 +335,9 @@ public class IntegrationTest extends BaseTest {
public void testSetLocation() { public void testSetLocation() {
camera.start(); camera.start();
waitForOpen(true); waitForOpen(true);
controller.mLocationTask.listen();
camera.setLocation(10d, 2d); camera.setLocation(10d, 2d);
controller.mLocationTask.await(300);
assertNotNull(camera.getLocation()); assertNotNull(camera.getLocation());
assertEquals(camera.getLocation().getLatitude(), 10d, 0d); assertEquals(camera.getLocation().getLatitude(), 10d, 0d);
assertEquals(camera.getLocation().getLongitude(), 2d, 0d); assertEquals(camera.getLocation().getLongitude(), 2d, 0d);
@ -333,16 +349,18 @@ public class IntegrationTest extends BaseTest {
//region testSetVideoQuality //region testSetVideoQuality
// This can be tricky because can trigger layout changes. // This can be tricky because can trigger layout changes.
// TODO: @Test(expected = IllegalStateException.class) // TODO: the exception is swallowed. @Test(expected = IllegalStateException.class)
// Can't run on Travis, MediaRecorder not supported.
// Error while starting MediaRecorder. java.lang.RuntimeException: start failed.
public void testSetVideoQuality_whileRecording() { public void testSetVideoQuality_whileRecording() {
// Can't run on Travis, MediaRecorder not supported.
// Error while starting MediaRecorder. java.lang.RuntimeException: start failed.
camera.setSessionType(SessionType.VIDEO); camera.setSessionType(SessionType.VIDEO);
camera.setVideoQuality(VideoQuality.HIGHEST); camera.setVideoQuality(VideoQuality.HIGHEST);
camera.start(); camera.start();
waitForOpen(true); waitForOpen(true);
camera.startCapturingVideo(null); camera.startCapturingVideo(null);
controller.mVideoQualityTask.listen();
camera.setVideoQuality(VideoQuality.LOWEST); camera.setVideoQuality(VideoQuality.LOWEST);
controller.mVideoQualityTask.await(300);
} }
@Test @Test
@ -351,20 +369,28 @@ public class IntegrationTest extends BaseTest {
camera.setVideoQuality(VideoQuality.HIGHEST); camera.setVideoQuality(VideoQuality.HIGHEST);
camera.start(); camera.start();
waitForOpen(true); waitForOpen(true);
controller.mVideoQualityTask.listen();
camera.setVideoQuality(VideoQuality.LOWEST); camera.setVideoQuality(VideoQuality.LOWEST);
controller.mVideoQualityTask.await(300);
assertEquals(camera.getVideoQuality(), VideoQuality.LOWEST); assertEquals(camera.getVideoQuality(), VideoQuality.LOWEST);
} }
@Test @Test
public void testSetVideoQuality_whileNotStarted() { public void testSetVideoQuality_whileNotStarted() {
controller.mVideoQualityTask.listen();
camera.setVideoQuality(VideoQuality.HIGHEST); camera.setVideoQuality(VideoQuality.HIGHEST);
controller.mVideoQualityTask.await(300);
assertEquals(camera.getVideoQuality(), VideoQuality.HIGHEST); assertEquals(camera.getVideoQuality(), VideoQuality.HIGHEST);
controller.mVideoQualityTask.listen();
camera.setVideoQuality(VideoQuality.LOWEST); camera.setVideoQuality(VideoQuality.LOWEST);
controller.mVideoQualityTask.await(300);
assertEquals(camera.getVideoQuality(), VideoQuality.LOWEST); assertEquals(camera.getVideoQuality(), VideoQuality.LOWEST);
} }
@Test @Test
public void testSetVideoQuality_shouldRecompute() { public void testSetVideoQuality_shouldRecompute() {
// TODO:
// If video quality changes bring to a new capture size, // If video quality changes bring to a new capture size,
// this might bring to a new aspect ratio, // this might bring to a new aspect ratio,
// which might bring to a new preview size. No idea how to test. // which might bring to a new preview size. No idea how to test.
@ -416,11 +442,17 @@ public class IntegrationTest extends BaseTest {
public void testStartAutoFocus() { public void testStartAutoFocus() {
camera.start(); camera.start();
CameraOptions o = waitForOpen(true); CameraOptions o = waitForOpen(true);
final Task<PointF> focus = new Task<>(true);
doEndTask(focus, 0).when(listener).onFocusStart(any(PointF.class));
camera.startAutoFocus(1, 1); camera.startAutoFocus(1, 1);
PointF point = focus.await(300);
if (o.isAutoFocusSupported()) { if (o.isAutoFocusSupported()) {
verify(listener, times(1)).onFocusStart(new PointF(1, 1)); assertNotNull(point);
assertEquals(point, new PointF(1, 1));
} else { } else {
verify(listener, never()).onFocusStart(any(PointF.class)); assertNull(point);
} }
} }

@ -31,8 +31,7 @@ public abstract class PreviewTest extends BaseTest {
@Before @Before
public void setUp() { public void setUp() {
availability = new Task<>(); availability = new Task<>(true);
availability.listen();
ui(new Runnable() { ui(new Runnable() {
@Override @Override

@ -24,8 +24,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testStaticRun() { public void testStaticRun() {
final Task<Boolean> task = new Task<>(); final Task<Boolean> task = new Task<>(true);
task.listen();
Runnable action = new Runnable() { Runnable action = new Runnable() {
@Override @Override
public void run() { public void run() {

@ -38,98 +38,103 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
private Runnable mPostFocusResetRunnable = new Runnable() { private Runnable mPostFocusResetRunnable = new Runnable() {
@Override @Override
public void run() { public void run() {
synchronized (mLock) { if (!isCameraAvailable()) return;
if (!isCameraAvailable()) return; mCamera.cancelAutoFocus();
mCamera.cancelAutoFocus(); Camera.Parameters params = mCamera.getParameters();
Camera.Parameters params = mCamera.getParameters(); params.setFocusAreas(null);
params.setFocusAreas(null); params.setMeteringAreas(null);
params.setMeteringAreas(null); applyDefaultFocus(params); // Revert to internal focus.
applyDefaultFocus(params); // Revert to internal focus. mCamera.setParameters(params);
mCamera.setParameters(params);
}
} }
}; };
private Mapper mMapper = new Mapper.Mapper1(); private ScheduleRunnable mScheduleRunnable;
private boolean mIsSetup = false; private Mapper mMapper;
private boolean mIsBound = false;
private boolean mIsCapturingImage = false; private boolean mIsCapturingImage = false;
private boolean mIsCapturingVideo = false; private boolean mIsCapturingVideo = false;
Camera1(CameraView.CameraCallbacks callback) { Camera1(CameraView.CameraCallbacks callback) {
super(callback); super(callback);
mScheduleRunnable = new ScheduleRunnable();
mMapper = new Mapper.Mapper1();
} }
/** private class ScheduleRunnable implements Runnable {
* Preview surface is now available. If camera is open, set up. private Task<Void> mTask;
*/ private Runnable mAction;
private boolean mEnsureAvailable;
@Override
public void run() {
if (mEnsureAvailable && !isCameraAvailable()) {
if (mTask != null) mTask.end(null);
} else {
mAction.run();
if (mTask != null) mTask.end(null);
}
}
}
private void schedule(@Nullable Task<Void> task, boolean ensureAvailable, Runnable action) {
mScheduleRunnable.mEnsureAvailable = ensureAvailable;
mScheduleRunnable.mAction = action;
mScheduleRunnable.mTask = task;
mHandler.post(mScheduleRunnable);
}
// Preview surface is now available. If camera is open, set up.
@Override @Override
public void onSurfaceAvailable() { public void onSurfaceAvailable() {
LOG.i("onSurfaceAvailable:", "Size is", mPreview.getSurfaceSize()); LOG.i("onSurfaceAvailable:", "Size is", mPreview.getSurfaceSize());
if (!shouldSetup()) return; schedule(null, false, new Runnable() {
mHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
if (!shouldSetup()) return; if (shouldBindToSurface()) {
LOG.i("onSurfaceAvailable:", "Inside handler. About to bind."); LOG.i("onSurfaceAvailable:", "Inside handler. About to bind.");
try { try {
setup(); bindToSurface();
} catch (Exception e) { } catch (Exception e) {
LOG.w("onSurfaceAvailable:", "Exception while binding camera to preview.", e); LOG.e("onSurfaceAvailable:", "Exception while binding camera to preview.", e);
throw new RuntimeException(e); throw new RuntimeException(e);
}
} }
} }
}); });
} }
/** // Preview surface did change its size. Compute a new preview size.
* Preview surface did change its size. Compute a new preview size. // This requires stopping and restarting the preview.
* This requires stopping and restarting the preview.
*/
@Override @Override
public void onSurfaceChanged() { public void onSurfaceChanged() {
LOG.i("onSurfaceChanged, size is", mPreview.getSurfaceSize()); LOG.i("onSurfaceChanged, size is", mPreview.getSurfaceSize());
if (mIsSetup && isCameraAvailable()) { schedule(null, true, new Runnable() {
// Compute a new camera preview size. @Override
Size newSize = computePreviewSize(); public void run() {
if (!newSize.equals(mPreviewSize)) { if (!mIsBound) return;
LOG.i("onSurfaceChanged:", "Computed a new preview size. Dispatching.");
mPreviewSize = newSize;
mCameraCallbacks.onCameraPreviewSizeChanged();
synchronized (mLock) {
LOG.i("onSurfaceChanged:", "Stopping preview.");
mCamera.stopPreview();
LOG.i("onSurfaceChanged:", "Stopped preview.");
Camera.Parameters params = mCamera.getParameters();
params.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
mCamera.setParameters(params);
}
boolean invertPreviewSizes = shouldFlipSizes();
mPreview.setDesiredSize(
invertPreviewSizes ? mPreviewSize.getHeight() : mPreviewSize.getWidth(),
invertPreviewSizes ? mPreviewSize.getWidth() : mPreviewSize.getHeight()
);
mCamera.setPreviewCallbackWithBuffer(null); // This clears the buffers // Compute a new camera preview size.
mCamera.setPreviewCallbackWithBuffer(this); // Reset Size newSize = computePreviewSize();
mFrameManager.allocate(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewSize); if (newSize.equals(mPreviewSize)) return;
LOG.i("onSurfaceChanged:", "Restarting preview."); // Apply.
mCamera.startPreview(); LOG.i("onSurfaceChanged:", "Computed a new preview size. Going on.");
LOG.i("onSurfaceChanged:", "Restarted preview."); mPreviewSize = newSize;
mCamera.stopPreview();
applySizesAndStartPreview("onSurfaceChanged:");
} }
} });
} }
private boolean shouldSetup() { private boolean shouldBindToSurface() {
return isCameraAvailable() && mPreview != null && mPreview.isReady() && !mIsSetup; return isCameraAvailable() && mPreview != null && mPreview.isReady() && !mIsBound;
} }
// The act of binding an "open" camera to a "ready" preview. // The act of binding an "open" camera to a "ready" preview.
// These can happen at different times but we want to end up here. // These can happen at different times but we want to end up here.
@WorkerThread @WorkerThread
private void setup() throws Exception { private void bindToSurface() throws Exception {
LOG.i("setup:", "Started"); LOG.i("bindToSurface:", "Started");
Object output = mPreview.getOutput(); Object output = mPreview.getOutput();
if (mPreview.getOutputClass() == SurfaceHolder.class) { if (mPreview.getOutputClass() == SurfaceHolder.class) {
mCamera.setPreviewDisplay((SurfaceHolder) output); mCamera.setPreviewDisplay((SurfaceHolder) output);
@ -137,34 +142,38 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
mCamera.setPreviewTexture((SurfaceTexture) output); mCamera.setPreviewTexture((SurfaceTexture) output);
} }
boolean invertPreviewSizes = shouldFlipSizes();
mCaptureSize = computeCaptureSize(); mCaptureSize = computeCaptureSize();
mPreviewSize = computePreviewSize(); mPreviewSize = computePreviewSize();
LOG.i("setup:", "Dispatching onCameraPreviewSizeChanged."); applySizesAndStartPreview("bindToSurface:");
mIsBound = true;
}
// To be called when the preview size is setup or changed.
private void applySizesAndStartPreview(String log) {
LOG.i(log, "Dispatching onCameraPreviewSizeChanged.");
mCameraCallbacks.onCameraPreviewSizeChanged(); mCameraCallbacks.onCameraPreviewSizeChanged();
boolean invertPreviewSizes = shouldFlipSizes();
mPreview.setDesiredSize( mPreview.setDesiredSize(
invertPreviewSizes ? mPreviewSize.getHeight() : mPreviewSize.getWidth(), invertPreviewSizes ? mPreviewSize.getHeight() : mPreviewSize.getWidth(),
invertPreviewSizes ? mPreviewSize.getWidth() : mPreviewSize.getHeight() invertPreviewSizes ? mPreviewSize.getWidth() : mPreviewSize.getHeight()
); );
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
mPreviewFormat = params.getPreviewFormat(); mPreviewFormat = params.getPreviewFormat();
params.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); // <- not allowed during preview params.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); // <- not allowed during preview
params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // <- allowed params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // <- allowed
mCamera.setParameters(params); mCamera.setParameters(params);
}
mCamera.setPreviewCallbackWithBuffer(null); // Release anything left mCamera.setPreviewCallbackWithBuffer(null); // Release anything left
mCamera.setPreviewCallbackWithBuffer(this); // Add ourselves mCamera.setPreviewCallbackWithBuffer(this); // Add ourselves
mFrameManager.allocate(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewSize); mFrameManager.allocate(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewSize);
LOG.i("setup:", "Starting preview with startPreview()."); LOG.i(log, "Starting preview with startPreview().");
mCamera.startPreview(); mCamera.startPreview();
LOG.i("setup:", "Started preview with startPreview()."); LOG.i(log, "Started preview.");
mIsSetup = true;
} }
@WorkerThread @WorkerThread
@Override @Override
void onStart() throws Exception { void onStart() throws Exception {
@ -176,22 +185,21 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
mCamera = Camera.open(mCameraId); mCamera = Camera.open(mCameraId);
// Set parameters that might have been set before the camera was opened. // Set parameters that might have been set before the camera was opened.
synchronized (mLock) { LOG.i("onStart:", "Applying default parameters.");
LOG.i("onStart:", "Applying default parameters."); Camera.Parameters params = mCamera.getParameters();
Camera.Parameters params = mCamera.getParameters(); mExtraProperties = new ExtraProperties(params);
mExtraProperties = new ExtraProperties(params); mOptions = new CameraOptions(params);
mOptions = new CameraOptions(params); applyDefaultFocus(params);
applyDefaultFocus(params); mergeFlash(params, Flash.DEFAULT);
mergeFlash(params, Flash.DEFAULT); mergeLocation(params, null);
mergeLocation(params, null); mergeWhiteBalance(params, WhiteBalance.DEFAULT);
mergeWhiteBalance(params, WhiteBalance.DEFAULT); mergeHdr(params, Hdr.DEFAULT);
params.setRecordingHint(mSessionType == SessionType.VIDEO); params.setRecordingHint(mSessionType == SessionType.VIDEO);
mCamera.setParameters(params); mCamera.setParameters(params);
}
// Try starting preview. // Try starting preview.
mCamera.setDisplayOrientation(computeSensorToDisplayOffset()); // <- not allowed during preview mCamera.setDisplayOrientation(computeSensorToDisplayOffset()); // <- not allowed during preview
if (shouldSetup()) setup(); if (shouldBindToSurface()) bindToSurface();
LOG.i("onStart:", "Ended"); LOG.i("onStart:", "Ended");
} }
} }
@ -205,7 +213,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
mFrameManager.release(); mFrameManager.release();
if (mCamera != null) { if (mCamera != null) {
LOG.i("onStop:", "Clean up.", "Ending video?", mIsCapturingVideo); LOG.i("onStop:", "Clean up.", "Ending video?", mIsCapturingVideo);
if (mIsCapturingVideo) endVideo(); if (mIsCapturingVideo) endVideo();
@ -233,7 +240,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
mCamera = null; mCamera = null;
mPreviewSize = null; mPreviewSize = null;
mCaptureSize = null; mCaptureSize = null;
mIsSetup = false; mIsBound = false;
if (error != null) throw error; if (error != null) throw error;
} }
@ -254,6 +261,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
@Override @Override
public void onBufferAvailable(byte[] buffer) { public void onBufferAvailable(byte[] buffer) {
// TODO: sync with handler?
if (isCameraAvailable()) { if (isCameraAvailable()) {
mCamera.addCallbackBuffer(buffer); mCamera.addCallbackBuffer(buffer);
} }
@ -263,22 +271,26 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
void setSessionType(SessionType sessionType) { void setSessionType(SessionType sessionType) {
if (sessionType != mSessionType) { if (sessionType != mSessionType) {
mSessionType = sessionType; mSessionType = sessionType;
if (isCameraAvailable()) { schedule(null, true, new Runnable() {
restart(); @Override
} public void run() {
restart();
}
});
} }
} }
@Override @Override
void setLocation(Location location) { void setLocation(Location location) {
Location oldLocation = mLocation; final Location oldLocation = mLocation;
mLocation = location; mLocation = location;
if (isCameraAvailable()) { schedule(mLocationTask, true, new Runnable() {
synchronized (mLock) { @Override
public void run() {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
if (mergeLocation(params, oldLocation)) mCamera.setParameters(params); if (mergeLocation(params, oldLocation)) mCamera.setParameters(params);
} }
} });
} }
private boolean mergeLocation(Camera.Parameters params, Location oldLocation) { private boolean mergeLocation(Camera.Parameters params, Location oldLocation) {
@ -301,22 +313,28 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
void setFacing(Facing facing) { void setFacing(Facing facing) {
if (facing != mFacing) { if (facing != mFacing) {
mFacing = facing; mFacing = facing;
if (collectCameraId() && isCameraAvailable()) { schedule(null, true, new Runnable() {
restart(); @Override
} public void run() {
if (collectCameraId()) {
restart();
}
}
});
} }
} }
@Override @Override
void setWhiteBalance(WhiteBalance whiteBalance) { void setWhiteBalance(WhiteBalance whiteBalance) {
WhiteBalance old = mWhiteBalance; final WhiteBalance old = mWhiteBalance;
mWhiteBalance = whiteBalance; mWhiteBalance = whiteBalance;
if (isCameraAvailable()) { schedule(mWhiteBalanceTask, true, new Runnable() {
synchronized (mLock) { @Override
public void run() {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
if (mergeWhiteBalance(params, old)) mCamera.setParameters(params); if (mergeWhiteBalance(params, old)) mCamera.setParameters(params);
} }
} });
} }
private boolean mergeWhiteBalance(Camera.Parameters params, WhiteBalance oldWhiteBalance) { private boolean mergeWhiteBalance(Camera.Parameters params, WhiteBalance oldWhiteBalance) {
@ -330,14 +348,15 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
@Override @Override
void setHdr(Hdr hdr) { void setHdr(Hdr hdr) {
Hdr old = mHdr; final Hdr old = mHdr;
mHdr = hdr; mHdr = hdr;
if (isCameraAvailable()) { schedule(mHdrTask, true, new Runnable() {
synchronized (mLock) { @Override
public void run() {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
if (mergeHdr(params, old)) mCamera.setParameters(params); if (mergeHdr(params, old)) mCamera.setParameters(params);
} }
} });
} }
private boolean mergeHdr(Camera.Parameters params, Hdr oldHdr) { private boolean mergeHdr(Camera.Parameters params, Hdr oldHdr) {
@ -354,7 +373,8 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
void setAudio(Audio audio) { void setAudio(Audio audio) {
if (mAudio != audio) { if (mAudio != audio) {
if (mIsCapturingVideo) { if (mIsCapturingVideo) {
LOG.w("Changing audio mode while recording. Changes will take place starting from next video"); LOG.w("Audio setting was changed while recording. " +
"Changes will take place starting from next video");
} }
mAudio = audio; mAudio = audio;
} }
@ -362,14 +382,15 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
@Override @Override
void setFlash(Flash flash) { void setFlash(Flash flash) {
Flash old = mFlash; final Flash old = mFlash;
mFlash = flash; mFlash = flash;
if (isCameraAvailable()) { schedule(mFlashTask, true, new Runnable() {
synchronized (mLock) { @Override
public void run() {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
if (mergeFlash(params, old)) mCamera.setParameters(params); if (mergeFlash(params, old)) mCamera.setParameters(params);
} }
} });
} }
@ -412,124 +433,131 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
@Override @Override
void setVideoQuality(VideoQuality videoQuality) { void setVideoQuality(VideoQuality videoQuality) {
if (mIsCapturingVideo) { final VideoQuality old = mVideoQuality;
// TODO: actually any call to getParameters() could fail while recording a video.
// See. https://stackoverflow.com/questions/14941625/correct-handling-of-exception-getparameters-failed-empty-parameters
throw new IllegalStateException("Can't change video quality while recording a video.");
}
mVideoQuality = videoQuality; mVideoQuality = videoQuality;
if (isCameraAvailable() && mSessionType == SessionType.VIDEO) { schedule(mVideoQualityTask, true, new Runnable() {
// Change capture size to a size that fits the video aspect ratio. @Override
Size oldSize = mCaptureSize; public void run() {
mCaptureSize = computeCaptureSize(); if (mIsCapturingVideo) {
if (!mCaptureSize.equals(oldSize)) { // TODO: actually any call to getParameters() could fail while recording a video.
// New video quality triggers a new aspect ratio. // See. https://stackoverflow.com/questions/14941625/
// Go on and see if preview size should change also. mVideoQuality = old;
synchronized (mLock) { throw new IllegalStateException("Can't change video quality while recording a video.");
Camera.Parameters params = mCamera.getParameters(); }
params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight());
mCamera.setParameters(params); if (mSessionType == SessionType.VIDEO) {
// Change capture size to a size that fits the video aspect ratio.
Size oldSize = mCaptureSize;
mCaptureSize = computeCaptureSize();
if (!mCaptureSize.equals(oldSize)) {
// New video quality triggers a new aspect ratio.
// Go on and see if preview size should change also.
Camera.Parameters params = mCamera.getParameters();
params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight());
mCamera.setParameters(params);
onSurfaceChanged();
}
LOG.i("setVideoQuality:", "captureSize:", mCaptureSize);
LOG.i("setVideoQuality:", "previewSize:", mPreviewSize);
} }
onSurfaceChanged();
} }
LOG.i("setVideoQuality:", "captureSize:", mCaptureSize); });
LOG.i("setVideoQuality:", "previewSize:", mPreviewSize);
}
} }
@Override @Override
void capturePicture() { void capturePicture() {
if (mIsCapturingImage) return; schedule(null, true, new Runnable() {
if (!isCameraAvailable()) return; @Override
if (mSessionType == SessionType.VIDEO && mIsCapturingVideo) { public void run() {
if (!mOptions.isVideoSnapshotSupported()) return; if (mIsCapturingImage) return;
} if (mSessionType == SessionType.VIDEO && mIsCapturingVideo) {
if (!mOptions.isVideoSnapshotSupported()) return;
}
// Set boolean to wait for image callback // Set boolean to wait for image callback
mIsCapturingImage = true; mIsCapturingImage = true;
final int exifRotation = computeExifRotation(); final int exifRotation = computeExifRotation();
final boolean exifFlip = computeExifFlip(); final boolean exifFlip = computeExifFlip();
final int sensorToDisplay = computeSensorToDisplayOffset(); final int sensorToDisplay = computeSensorToDisplayOffset();
synchronized (mLock) { Camera.Parameters params = mCamera.getParameters();
Camera.Parameters params = mCamera.getParameters(); params.setRotation(exifRotation);
params.setRotation(exifRotation); mCamera.setParameters(params);
mCamera.setParameters(params);
} // Is the final picture (decoded respecting EXIF) consistent with CameraView orientation?
// Is the final picture (decoded respecting EXIF) consistent with CameraView orientation? // We must consider exifOrientation to bring back the picture in the sensor world.
// We must consider exifOrientation to bring back the picture in the sensor world. // Then use sensorToDisplay to move to the display world, where CameraView lives.
// Then use sensorToDisplay to move to the display world, where CameraView lives. final boolean consistentWithView = (exifRotation + sensorToDisplay + 180) % 180 == 0;
final boolean consistentWithView = (exifRotation + sensorToDisplay + 180) % 180 == 0; mCamera.takePicture(
mCamera.takePicture( new Camera.ShutterCallback() {
new Camera.ShutterCallback() {
@Override
public void onShutter() {
mCameraCallbacks.onShutter(false);
}
},
null,
null,
new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, final Camera camera) {
mIsCapturingImage = false;
mHandler.post(new Runnable() {
@Override @Override
public void run() { public void onShutter() {
// This is needed, read somewhere in the docs. mCameraCallbacks.onShutter(false);
camera.startPreview();
} }
}); },
mCameraCallbacks.processImage(data, consistentWithView, exifFlip); null,
} null,
}); new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, final Camera camera) {
mIsCapturingImage = false;
mCameraCallbacks.processImage(data, consistentWithView, exifFlip);
camera.startPreview(); // This is needed, read somewhere in the docs.
}
}
);
}
});
} }
@Override @Override
void captureSnapshot() { void captureSnapshot() {
if (!isCameraAvailable()) return; schedule(null, true, new Runnable() {
if (mIsCapturingImage) return;
// This won't work while capturing a video.
// Switch to capturePicture.
if (mIsCapturingVideo) {
capturePicture();
return;
}
mIsCapturingImage = true;
mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {
@Override @Override
public void onPreviewFrame(final byte[] data, Camera camera) { public void run() {
mCameraCallbacks.onShutter(true); if (mIsCapturingImage) return;
// This won't work while capturing a video.
// Got to rotate the preview frame, since byte[] data here does not include // Switch to capturePicture.
// EXIF tags automatically set by camera. So either we add EXIF, or we rotate. if (mIsCapturingVideo) {
// Adding EXIF to a byte array, unfortunately, is hard. capturePicture();
final int sensorToDevice = computeExifRotation(); return;
final int sensorToDisplay = computeSensorToDisplayOffset(); }
final boolean exifFlip = computeExifFlip(); mIsCapturingImage = true;
final boolean flip = sensorToDevice % 180 != 0; mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {
final int preWidth = mPreviewSize.getWidth();
final int preHeight = mPreviewSize.getHeight();
final int postWidth = flip ? preHeight : preWidth;
final int postHeight = flip ? preWidth : preHeight;
final int format = mPreviewFormat;
WorkerHandler.run(new Runnable() {
@Override @Override
public void run() { public void onPreviewFrame(final byte[] data, Camera camera) {
mCameraCallbacks.onShutter(true);
// Got to rotate the preview frame, since byte[] data here does not include
// EXIF tags automatically set by camera. So either we add EXIF, or we rotate.
// Adding EXIF to a byte array, unfortunately, is hard.
final int sensorToDevice = computeExifRotation();
final int sensorToDisplay = computeSensorToDisplayOffset();
final boolean exifFlip = computeExifFlip();
final boolean flip = sensorToDevice % 180 != 0;
final int preWidth = mPreviewSize.getWidth();
final int preHeight = mPreviewSize.getHeight();
final int postWidth = flip ? preHeight : preWidth;
final int postHeight = flip ? preWidth : preHeight;
final int format = mPreviewFormat;
WorkerHandler.run(new Runnable() {
@Override
public void run() {
final boolean consistentWithView = (sensorToDevice + sensorToDisplay + 180) % 180 == 0;
byte[] rotatedData = RotationHelper.rotate(data, preWidth, preHeight, sensorToDevice);
YuvImage yuv = new YuvImage(rotatedData, format, postWidth, postHeight, null);
mCameraCallbacks.processSnapshot(yuv, consistentWithView, exifFlip);
mIsCapturingImage = false;
}
});
final boolean consistentWithView = (sensorToDevice + sensorToDisplay + 180) % 180 == 0; // It seems that the buffers are already cleared here, so we need to allocate again.
byte[] rotatedData = RotationHelper.rotate(data, preWidth, preHeight, sensorToDevice); mCamera.setPreviewCallbackWithBuffer(null); // Release anything left
YuvImage yuv = new YuvImage(rotatedData, format, postWidth, postHeight, null); mCamera.setPreviewCallbackWithBuffer(Camera1.this); // Add ourselves
mCameraCallbacks.processSnapshot(yuv, consistentWithView, exifFlip); mFrameManager.allocate(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewSize);
mIsCapturingImage = false;
} }
}); });
// It seems that the buffers are already cleared here, so we need to allocate again.
mCamera.setPreviewCallbackWithBuffer(null); // Release anything left
mCamera.setPreviewCallbackWithBuffer(Camera1.this); // Add ourselves
mFrameManager.allocate(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewSize);
} }
}); });
} }
@ -559,12 +587,11 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
// If we are going to be closed, don't act on camera. // If we are going to be closed, don't act on camera.
// Even if mCamera != null, it might have been released. // Even if mCamera != null, it might have been released.
case STATE_STOPPING: return false; case STATE_STOPPING: return false;
// If we are started, act as long as there is no stop/restart scheduled. // If we are started, mCamera should never be null.
// At this point mCamera should never be null. case STATE_STARTED: return true;
case STATE_STARTED: return !mScheduledForStop && !mScheduledForRestart;
// If we are starting, theoretically we could act. // If we are starting, theoretically we could act.
// Just check that camera is available. // Just check that camera is available.
case STATE_STARTING: return mCamera != null && !mScheduledForStop && !mScheduledForRestart; case STATE_STARTING: return mCamera != null;
} }
return false; return false;
} }
@ -628,7 +655,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
// The Camcorder internally checks for cameraParameters.getSupportedVideoSizes() etc. // The Camcorder internally checks for cameraParameters.getSupportedVideoSizes() etc.
// We want the picture size to be the max picture consistent with the video aspect ratio. // We want the picture size to be the max picture consistent with the video aspect ratio.
List<Size> captureSizes = sizesFromList(params.getSupportedPictureSizes()); List<Size> captureSizes = sizesFromList(params.getSupportedPictureSizes());
CamcorderProfile profile = getCamcorderProfile(mVideoQuality); CamcorderProfile profile = getCamcorderProfile(mCameraId, mVideoQuality);
AspectRatio targetRatio = AspectRatio.of(profile.videoFrameWidth, profile.videoFrameHeight); AspectRatio targetRatio = AspectRatio.of(profile.videoFrameWidth, profile.videoFrameHeight);
LOG.i("size:", "computeCaptureSize:", "videoQuality:", mVideoQuality, "targetRatio:", targetRatio); LOG.i("size:", "computeCaptureSize:", "videoQuality:", mVideoQuality, "targetRatio:", targetRatio);
return matchSize(captureSizes, targetRatio, new Size(0, 0), true); return matchSize(captureSizes, targetRatio, new Size(0, 0), true);
@ -650,29 +677,43 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
@Override @Override
void startVideo(@NonNull File videoFile) { void startVideo(@NonNull final File videoFile) {
if (mIsCapturingVideo) return; schedule(null, true, new Runnable() {
if (!isCameraAvailable()) return; @Override
if (mSessionType == SessionType.VIDEO) { public void run() {
mVideoFile = videoFile; if (mIsCapturingVideo) return;
mIsCapturingVideo = true; if (mSessionType == SessionType.VIDEO) {
initMediaRecorder(); mVideoFile = videoFile;
try { mIsCapturingVideo = true;
mMediaRecorder.prepare(); initMediaRecorder();
mMediaRecorder.start(); try {
} catch (Exception e) { mMediaRecorder.prepare();
LOG.e("Error while starting MediaRecorder. Swallowing.", e); mMediaRecorder.start();
mVideoFile = null; } catch (Exception e) {
mCamera.lock(); LOG.e("Error while starting MediaRecorder. Swallowing.", e);
endVideo(); mVideoFile = null;
mCamera.lock();
endVideoNow();
}
} else {
throw new IllegalStateException("Can't record video while session type is picture");
}
} }
} else { });
throw new IllegalStateException("Can't record video while session type is picture");
}
} }
@Override @Override
void endVideo() { void endVideo() {
schedule(null, false, new Runnable() {
@Override
public void run() {
endVideoNow();
}
});
}
@WorkerThread
private void endVideoNow() {
if (mIsCapturingVideo) { if (mIsCapturingVideo) {
mIsCapturingVideo = false; mIsCapturingVideo = false;
if (mMediaRecorder != null) { if (mMediaRecorder != null) {
@ -693,7 +734,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
} }
} }
@WorkerThread
private void initMediaRecorder() { private void initMediaRecorder() {
mMediaRecorder = new MediaRecorder(); mMediaRecorder = new MediaRecorder();
mCamera.unlock(); mCamera.unlock();
@ -701,7 +742,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
CamcorderProfile profile = getCamcorderProfile(mVideoQuality); CamcorderProfile profile = getCamcorderProfile(mCameraId, mVideoQuality);
if (mAudio == Audio.ON) { if (mAudio == Audio.ON) {
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mMediaRecorder.setProfile(profile); mMediaRecorder.setProfile(profile);
@ -724,48 +765,47 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
// Not needed. mMediaRecorder.setPreviewDisplay(mPreview.getSurface()); // Not needed. mMediaRecorder.setPreviewDisplay(mPreview.getSurface());
} }
@NonNull @NonNull
private CamcorderProfile getCamcorderProfile(VideoQuality videoQuality) { private static CamcorderProfile getCamcorderProfile(int cameraId, VideoQuality videoQuality) {
switch (videoQuality) { switch (videoQuality) {
case HIGHEST: case HIGHEST:
return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_HIGH); return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH);
case MAX_2160P: case MAX_2160P:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_2160P)) { CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_2160P)) {
return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_2160P); return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P);
} }
// Don't break. // Don't break.
case MAX_1080P: case MAX_1080P:
if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_1080P)) { if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) {
return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_1080P); return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P);
} }
// Don't break. // Don't break.
case MAX_720P: case MAX_720P:
if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_720P)) { if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) {
return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_720P); return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P);
} }
// Don't break. // Don't break.
case MAX_480P: case MAX_480P:
if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_480P)) { if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) {
return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_480P); return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P);
} }
// Don't break. // Don't break.
case MAX_QVGA: case MAX_QVGA:
if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_QVGA)) { if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) {
return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_QVGA); return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA);
} }
// Don't break. // Don't break.
case LOWEST: case LOWEST:
default: default:
// Fallback to lowest. // Fallback to lowest.
return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_LOW); return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW);
} }
} }
@ -774,42 +814,48 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
@Override @Override
void setZoom(float zoom, PointF[] points, boolean notify) { void setZoom(final float zoom, final PointF[] points, final boolean notify) {
if (!isCameraAvailable()) return; schedule(mZoomTask, true, new Runnable() {
if (!mOptions.isZoomSupported()) return; @Override
public void run() {
if (!mOptions.isZoomSupported()) return;
mZoomValue = zoom; mZoomValue = zoom;
synchronized (mLock) { Camera.Parameters params = mCamera.getParameters();
Camera.Parameters params = mCamera.getParameters(); float max = params.getMaxZoom();
float max = params.getMaxZoom(); params.setZoom((int) (zoom * max));
params.setZoom((int) (zoom * max)); mCamera.setParameters(params);
mCamera.setParameters(params);
}
if (notify) { if (notify) {
mCameraCallbacks.dispatchOnZoomChanged(zoom, points); mCameraCallbacks.dispatchOnZoomChanged(zoom, points);
} }
}
});
} }
@Override @Override
void setExposureCorrection(float EVvalue, float[] bounds, PointF[] points, boolean notify) { void setExposureCorrection(final float EVvalue, final float[] bounds,
if (!isCameraAvailable()) return; final PointF[] points, final boolean notify) {
if (!mOptions.isExposureCorrectionSupported()) return; schedule(mExposureCorrectionTask, true, new Runnable() {
@Override
float max = mOptions.getExposureCorrectionMaxValue(); public void run() {
float min = mOptions.getExposureCorrectionMinValue(); if (!mOptions.isExposureCorrectionSupported()) return;
EVvalue = EVvalue < min ? min : EVvalue > max ? max : EVvalue; // cap
mExposureCorrectionValue = EVvalue;
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters();
int indexValue = (int) (EVvalue / params.getExposureCompensationStep());
params.setExposureCompensation(indexValue);
mCamera.setParameters(params);
}
if (notify) { float value = EVvalue;
mCameraCallbacks.dispatchOnExposureCorrectionChanged(EVvalue, bounds, points); float max = mOptions.getExposureCorrectionMaxValue();
} float min = mOptions.getExposureCorrectionMinValue();
value = value < min ? min : value > max ? max : value; // cap
mExposureCorrectionValue = value;
Camera.Parameters params = mCamera.getParameters();
int indexValue = (int) (value / params.getExposureCompensationStep());
params.setExposureCompensation(indexValue);
mCamera.setParameters(params);
if (notify) {
mCameraCallbacks.dispatchOnExposureCorrectionChanged(value, bounds, points);
}
}
});
} }
// ----------------- // -----------------
@ -817,44 +863,58 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
@Override @Override
void startAutoFocus(@Nullable final Gesture gesture, PointF point) { void startAutoFocus(@Nullable final Gesture gesture, final PointF point) {
if (!isCameraAvailable()) return; // Must get width and height from the UI thread.
if (!mOptions.isAutoFocusSupported()) return; int viewWidth = 0, viewHeight = 0;
final PointF p = new PointF(point.x, point.y); // copy. if (mPreview != null && mPreview.isReady()) {
List<Camera.Area> meteringAreas2 = computeMeteringAreas(p.x, p.y); viewWidth = mPreview.getView().getWidth();
List<Camera.Area> meteringAreas1 = meteringAreas2.subList(0, 1); viewHeight = mPreview.getView().getHeight();
synchronized (mLock) { }
// At this point we are sure that camera supports auto focus... right? Look at CameraView.onTouchEvent(). final int viewWidthF = viewWidth;
Camera.Parameters params = mCamera.getParameters(); final int viewHeightF = viewHeight;
int maxAF = params.getMaxNumFocusAreas(); // Schedule.
int maxAE = params.getMaxNumMeteringAreas(); schedule(null, true, new Runnable() {
if (maxAF > 0) params.setFocusAreas(maxAF > 1 ? meteringAreas2 : meteringAreas1); @Override
if (maxAE > 0) params.setMeteringAreas(maxAE > 1 ? meteringAreas2 : meteringAreas1); public void run() {
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); if (!mOptions.isAutoFocusSupported()) return;
mCamera.setParameters(params); final PointF p = new PointF(point.x, point.y); // copy.
mCameraCallbacks.dispatchOnFocusStart(gesture, p); List<Camera.Area> meteringAreas2 = computeMeteringAreas(p.x, p.y,
// TODO this is not guaranteed to be called... Fix. viewWidthF, viewHeightF, computeSensorToDisplayOffset());
mCamera.autoFocus(new Camera.AutoFocusCallback() { List<Camera.Area> meteringAreas1 = meteringAreas2.subList(0, 1);
@Override
public void onAutoFocus(boolean success, Camera camera) { // At this point we are sure that camera supports auto focus... right? Look at CameraView.onTouchEvent().
// TODO lock auto exposure and white balance for a while Camera.Parameters params = mCamera.getParameters();
mCameraCallbacks.dispatchOnFocusEnd(gesture, success, p); int maxAF = params.getMaxNumFocusAreas();
mHandler.get().removeCallbacks(mPostFocusResetRunnable); int maxAE = params.getMaxNumMeteringAreas();
mHandler.get().postDelayed(mPostFocusResetRunnable, mPostFocusResetDelay); if (maxAF > 0) params.setFocusAreas(maxAF > 1 ? meteringAreas2 : meteringAreas1);
} if (maxAE > 0) params.setMeteringAreas(maxAE > 1 ? meteringAreas2 : meteringAreas1);
}); params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
} mCamera.setParameters(params);
mCameraCallbacks.dispatchOnFocusStart(gesture, p);
// TODO this is not guaranteed to be called... Fix.
mCamera.autoFocus(new Camera.AutoFocusCallback() {
@Override
public void onAutoFocus(boolean success, Camera camera) {
// TODO lock auto exposure and white balance for a while
mCameraCallbacks.dispatchOnFocusEnd(gesture, success, p);
mHandler.get().removeCallbacks(mPostFocusResetRunnable);
mHandler.get().postDelayed(mPostFocusResetRunnable, mPostFocusResetDelay);
}
});
}
});
} }
private List<Camera.Area> computeMeteringAreas(double viewClickX, double viewClickY) { @WorkerThread
private static List<Camera.Area> computeMeteringAreas(double viewClickX, double viewClickY,
int viewWidth, int viewHeight,
int sensorToDisplay) {
// Event came in view coordinates. We must rotate to sensor coordinates. // Event came in view coordinates. We must rotate to sensor coordinates.
// First, rescale to the -1000 ... 1000 range. // First, rescale to the -1000 ... 1000 range.
int displayToSensor = -computeSensorToDisplayOffset(); int displayToSensor = -sensorToDisplay;
double viewWidth = mPreview.getView().getWidth(); viewClickX = -1000d + (viewClickX / (double) viewWidth) * 2000d;
double viewHeight = mPreview.getView().getHeight(); viewClickY = -1000d + (viewClickY / (double) viewHeight) * 2000d;
viewClickX = -1000d + (viewClickX / viewWidth) * 2000d;
viewClickY = -1000d + (viewClickY / viewHeight) * 2000d;
// Apply rotation to this point. // Apply rotation to this point.
// https://academo.org/demos/rotation-about-point/ // https://academo.org/demos/rotation-about-point/
@ -877,7 +937,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
} }
private Rect computeMeteringArea(double centerX, double centerY, double size) { private static Rect computeMeteringArea(double centerX, double centerY, double size) {
double delta = size / 2d; double delta = size / 2d;
int top = (int) Math.max(centerY - delta, -1000); int top = (int) Math.max(centerY - delta, -1000);
int bottom = (int) Math.min(centerY + delta, 1000); int bottom = (int) Math.min(centerY + delta, 1000);

@ -43,15 +43,19 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
protected int mDisplayOffset; protected int mDisplayOffset;
protected int mDeviceOrientation; protected int mDeviceOrientation;
protected boolean mScheduledForStart = false;
protected boolean mScheduledForStop = false;
protected boolean mScheduledForRestart = false;
protected int mState = STATE_STOPPED; protected int mState = STATE_STOPPED;
protected final Object mLock = new Object();
protected WorkerHandler mHandler; protected WorkerHandler mHandler;
// Used for testing.
Task<Void> mZoomTask = new Task<>();
Task<Void> mExposureCorrectionTask = new Task<>();
Task<Void> mFlashTask = new Task<>();
Task<Void> mWhiteBalanceTask = new Task<>();
Task<Void> mHdrTask = new Task<>();
Task<Void> mLocationTask = new Task<>();
Task<Void> mVideoQualityTask = new Task<>();
CameraController(CameraView.CameraCallbacks callback) { CameraController(CameraView.CameraCallbacks callback) {
mCameraCallbacks = callback; mCameraCallbacks = callback;
mHandler = WorkerHandler.get("CameraViewController"); mHandler = WorkerHandler.get("CameraViewController");
@ -95,13 +99,11 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
// Starts the preview asynchronously. // Starts the preview asynchronously.
final void start() { final void start() {
LOG.i("Start:", "posting runnable. State:", ss()); LOG.i("Start:", "posting runnable. State:", ss());
mScheduledForStart = true;
mHandler.post(new Runnable() { mHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
LOG.i("Start:", "executing. State:", ss()); LOG.i("Start:", "executing. State:", ss());
mScheduledForStart = false;
if (mState >= STATE_STARTING) return; if (mState >= STATE_STARTING) return;
mState = STATE_STARTING; mState = STATE_STARTING;
LOG.i("Start:", "about to call onStart()", ss()); LOG.i("Start:", "about to call onStart()", ss());
@ -121,13 +123,11 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
// Stops the preview asynchronously. // Stops the preview asynchronously.
final void stop() { final void stop() {
LOG.i("Stop:", "posting runnable. State:", ss()); LOG.i("Stop:", "posting runnable. State:", ss());
mScheduledForStop = true;
mHandler.post(new Runnable() { mHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
LOG.i("Stop:", "executing. State:", ss()); LOG.i("Stop:", "executing. State:", ss());
mScheduledForStop = false;
if (mState <= STATE_STOPPED) return; if (mState <= STATE_STOPPED) return;
mState = STATE_STOPPING; mState = STATE_STOPPING;
LOG.i("Stop:", "about to call onStop()"); LOG.i("Stop:", "about to call onStop()");
@ -165,13 +165,11 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
// Forces a restart. // Forces a restart.
protected final void restart() { protected final void restart() {
LOG.i("Restart:", "posting runnable"); LOG.i("Restart:", "posting runnable");
mScheduledForRestart = true;
mHandler.post(new Runnable() { mHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
LOG.i("Restart:", "executing. Needs stopping:", mState > STATE_STOPPED, ss()); LOG.i("Restart:", "executing. Needs stopping:", mState > STATE_STOPPED, ss());
mScheduledForRestart = false;
// Don't stop if stopped. // Don't stop if stopped.
if (mState > STATE_STOPPED) { if (mState > STATE_STOPPED) {
mState = STATE_STOPPING; mState = STATE_STOPPING;

@ -78,8 +78,8 @@ public abstract class CameraListener {
/** /**
* Notifies that user interacted with the screen and started focus with a gesture, * Notifies that user interacted with the screen and started focus with a gesture,
* and the autofocus is trying to focus around that area. * and the autofocus is trying to focus around that area. This can be used to draw things on screen.
* This can be used to draw things on screen. * Can also be triggered by {@link CameraView#startAutoFocus(float, float)}.
* *
* @param point coordinates with respect to CameraView.getWidth() and CameraView.getHeight() * @param point coordinates with respect to CameraView.getWidth() and CameraView.getHeight()
*/ */
@ -93,6 +93,7 @@ public abstract class CameraListener {
* Notifies that a gesture focus event just ended, and the camera converged * Notifies that a gesture focus event just ended, and the camera converged
* to a new focus (and possibly exposure and white balance). * to a new focus (and possibly exposure and white balance).
* This might succeed or not. * This might succeed or not.
* Can also be triggered by {@link CameraView#startAutoFocus(float, float)}.
* *
* @param successful whether camera succeeded * @param successful whether camera succeeded
* @param point coordinates with respect to CameraView.getWidth() and CameraView.getHeight() * @param point coordinates with respect to CameraView.getWidth() and CameraView.getHeight()

@ -18,6 +18,10 @@ class Task<T> {
Task() { Task() {
} }
Task(boolean startListening) {
if (startListening) listen();
}
private boolean listening() { private boolean listening() {
return mLatch != null; return mLatch != null;
} }

Loading…
Cancel
Save