diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraPreviewTest.java index 33136d80..8b56d56a 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraPreviewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraPreviewTest.java @@ -79,7 +79,7 @@ public abstract class CameraPreviewTest extends BaseTest { @Test public void testDefaults() { ensureAvailable(); - assertTrue(preview.isReady()); + assertTrue(preview.hasSurface()); assertNotNull(preview.getView()); assertNotNull(preview.getOutputClass()); } @@ -128,7 +128,7 @@ public abstract class CameraPreviewTest extends BaseTest { // Since desired is 'desired', let's fake a new view size that is consistent with it. // Ensure crop is not happening anymore. preview.mCropTask.listen(); - preview.onSurfaceSizeChanged((int) (50f * desired), 50); // Wait... + preview.dispatchOnOutputSurfaceSizeChanged((int) (50f * desired), 50); // Wait... preview.mCropTask.await(); assertEquals(desired, getViewAspectRatioWithScale(), 0.01f); assertFalse(preview.isCropping()); diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java index 994aacce..a050101c 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java @@ -22,7 +22,7 @@ public class TextureCameraPreviewTest extends CameraPreviewTest { if (isHardwareAccelerated()) { super.ensureAvailable(); } else { - preview.onSurfaceAvailable( + preview.dispatchOnOutputSurfaceAvailable( surfaceSize.getWidth(), surfaceSize.getHeight()); } @@ -33,7 +33,7 @@ public class TextureCameraPreviewTest extends CameraPreviewTest { super.ensureDestroyed(); if (!isHardwareAccelerated()) { // Ensure it is called. - preview.onSurfaceDestroyed(); + preview.dispatchOnOutputSurfaceDestroyed(); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java index 72182f61..b36f5504 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java @@ -12,7 +12,6 @@ import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; -import android.util.Log; import android.support.media.ExifInterface; import android.view.SurfaceHolder; @@ -77,6 +76,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera public void run() { LOG.i("onSurfaceAvailable:", "Inside handler. About to bind."); if (shouldBindToSurface()) bindToSurface(); + if (shouldStartPreview()) startPreview("onSurfaceAvailable"); } }); } @@ -99,13 +99,25 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera LOG.i("onSurfaceChanged:", "Computed a new preview size. Going on."); mPreviewSize = newSize; mCamera.stopPreview(); - applySizesAndStartPreview("onSurfaceChanged:"); + startPreview("onSurfaceChanged:"); + } + }); + } + + @Override + public void onSurfaceDestroyed() { + LOG.i("onSurfaceDestroyed"); + schedule(null, true, new Runnable() { + @Override + public void run() { + stopPreview(); + if (mIsBound) unbindFromSurface(); } }); } private boolean shouldBindToSurface() { - return isCameraAvailable() && mPreview != null && mPreview.isReady() && !mIsBound; + return isCameraAvailable() && mPreview != null && mPreview.hasSurface() && !mIsBound; } // The act of binding an "open" camera to a "ready" preview. @@ -121,18 +133,37 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera mCamera.setPreviewTexture((SurfaceTexture) output); } } catch (IOException e) { - Log.e("bindToSurface:", "Failed to bind.", e); + LOG.e("bindToSurface:", "Failed to bind.", e); throw new CameraException(e, CameraException.REASON_FAILED_TO_START_PREVIEW); } mCaptureSize = computeCaptureSize(); mPreviewSize = computePreviewSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes())); - applySizesAndStartPreview("bindToSurface:"); mIsBound = true; } + @WorkerThread + private void unbindFromSurface() { + mIsBound = false; + mPreviewSize = null; + mCaptureSize = null; + try { + if (mPreview.getOutputClass() == SurfaceHolder.class) { + mCamera.setPreviewDisplay(null); + } else { + mCamera.setPreviewTexture(null); + } + } catch (IOException e) { + LOG.e("unbindFromSurface", "Could not release surface", e); + } + } + + private boolean shouldStartPreview() { + return isCameraAvailable() && mIsBound; + } + // To be called when the preview size is setup or changed. - private void applySizesAndStartPreview(String log) { + private void startPreview(String log) { LOG.i(log, "Dispatching onCameraPreviewSizeChanged."); mCameraCallbacks.onCameraPreviewSizeChanged(); @@ -159,6 +190,53 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera LOG.i(log, "Started preview."); } + private void stopPreview() { + mPreviewFormat = 0; + mFrameManager.release(); + mCamera.setPreviewCallbackWithBuffer(null); // Release anything left + try { + mCamera.stopPreview(); + } catch (Exception e) { + LOG.e("stopPreview", "Could not stop preview", e); + } + } + + private void createCamera() { + try { + mCamera = Camera.open(mCameraId); + } catch (Exception e) { + LOG.e("createCamera:", "Failed to connect. Maybe in use by another app?"); + throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT); + } + mCamera.setErrorCallback(this); + + // Set parameters that might have been set before the camera was opened. + LOG.i("createCamera:", "Applying default parameters."); + Camera.Parameters params = mCamera.getParameters(); + mCameraOptions = new CameraOptions(params, flip(REF_SENSOR, REF_VIEW)); + applyDefaultFocus(params); + mergeFlash(params, Flash.DEFAULT); + mergeLocation(params, null); + mergeWhiteBalance(params, WhiteBalance.DEFAULT); + mergeHdr(params, Hdr.DEFAULT); + mergePlaySound(mPlaySounds); + params.setRecordingHint(mMode == Mode.VIDEO); + mCamera.setParameters(params); + mCamera.setDisplayOrientation(offset(REF_SENSOR, REF_VIEW)); // <- not allowed during preview + } + + private void destroyCamera() { + try { + LOG.i("destroyCamera:", "Clean up.", "Releasing camera."); + mCamera.release(); + LOG.i("destroyCamera:", "Clean up.", "Released camera."); + } catch (Exception e) { + LOG.w("destroyCamera:", "Clean up.", "Exception while releasing camera.", e); + } + mCamera = null; + mCameraOptions = null; + } + @WorkerThread @Override void onStart() { @@ -167,30 +245,9 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera onStop(); // Should not happen. } if (collectCameraId()) { - try { - mCamera = Camera.open(mCameraId); - } catch (Exception e) { - LOG.e("onStart:", "Failed to connect. Maybe in use by another app?"); - throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT); - } - mCamera.setErrorCallback(this); - - // Set parameters that might have been set before the camera was opened. - LOG.i("onStart:", "Applying default parameters."); - Camera.Parameters params = mCamera.getParameters(); - mCameraOptions = new CameraOptions(params, flip(REF_SENSOR, REF_VIEW)); - applyDefaultFocus(params); - mergeFlash(params, Flash.DEFAULT); - mergeLocation(params, null); - mergeWhiteBalance(params, WhiteBalance.DEFAULT); - mergeHdr(params, Hdr.DEFAULT); - mergePlaySound(mPlaySounds); - params.setRecordingHint(mMode == Mode.VIDEO); - mCamera.setParameters(params); - - // Try starting preview. - mCamera.setDisplayOrientation(offset(REF_SENSOR, REF_VIEW)); // <- not allowed during preview + createCamera(); if (shouldBindToSurface()) bindToSurface(); + if (shouldStartPreview()) startPreview("onStart"); LOG.i("onStart:", "Ended"); } } @@ -200,31 +257,14 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera void onStop() { LOG.i("onStop:", "About to clean up."); mHandler.get().removeCallbacks(mPostFocusResetRunnable); - mFrameManager.release(); - + if (mVideoRecorder != null) { + mVideoRecorder.stop(); + mVideoRecorder = null; + } if (mCamera != null) { - LOG.i("onStop:", "Clean up.", "Ending video. mVideoRecorder is null?", mVideoRecorder == null); - if (mVideoRecorder != null) { - mVideoRecorder.stop(); - mVideoRecorder = null; - } - - try { - LOG.i("onStop:", "Clean up.", "Stopping preview."); - mCamera.setPreviewCallbackWithBuffer(null); - mCamera.stopPreview(); - LOG.i("onStop:", "Clean up.", "Stopped preview."); - } catch (Exception e) { - LOG.w("onStop:", "Clean up.", "Exception while stopping preview.", e); - } - - try { - LOG.i("onStop:", "Clean up.", "Releasing camera."); - mCamera.release(); - LOG.i("onStop:", "Clean up.", "Released camera."); - } catch (Exception e) { - LOG.w("onStop:", "Clean up.", "Exception while releasing camera.", e); - } + stopPreview(); + unbindFromSurface(); + destroyCamera(); } mCameraOptions = null; mCamera = null; @@ -767,7 +807,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera void startAutoFocus(@Nullable final Gesture gesture, final PointF point) { // Must get width and height from the UI thread. int viewWidth = 0, viewHeight = 0; - if (mPreview != null && mPreview.isReady()) { + if (mPreview != null && mPreview.hasSurface()) { viewWidth = mPreview.getView().getWidth(); viewHeight = mPreview.getView().getHeight(); } diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/CameraPreview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/CameraPreview.java index 7827c03e..626e4e43 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/CameraPreview.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/CameraPreview.java @@ -17,6 +17,7 @@ abstract class CameraPreview { interface SurfaceCallback { void onSurfaceAvailable(); void onSurfaceChanged(); + void onSurfaceDestroyed(); } private SurfaceCallback mSurfaceCallback; @@ -55,7 +56,9 @@ abstract class CameraPreview { LOG.i("setInputStreamSize:", "desiredW=", width, "desiredH=", height); mInputStreamWidth = width; mInputStreamHeight = height; - crop(); + if (mInputStreamWidth > 0 && mInputStreamHeight > 0) { + crop(); + } } final Size getInputStreamSize() { @@ -75,30 +78,35 @@ abstract class CameraPreview { } - protected final void onSurfaceAvailable(int width, int height) { - LOG.i("onSurfaceAvailable:", "w=", width, "h=", height); + protected final void dispatchOnOutputSurfaceAvailable(int width, int height) { + LOG.i("dispatchOnOutputSurfaceAvailable:", "w=", width, "h=", height); mOutputSurfaceWidth = width; mOutputSurfaceHeight = height; - crop(); + if (mOutputSurfaceWidth > 0 && mOutputSurfaceHeight > 0) { + crop(); + } mSurfaceCallback.onSurfaceAvailable(); } // As far as I can see, these are the view/surface dimensions. // This is called by subclasses. - protected final void onSurfaceSizeChanged(int width, int height) { - LOG.i("onSurfaceSizeChanged:", "w=", width, "h=", height); + protected final void dispatchOnOutputSurfaceSizeChanged(int width, int height) { + LOG.i("dispatchOnOutputSurfaceSizeChanged:", "w=", width, "h=", height); if (width != mOutputSurfaceWidth || height != mOutputSurfaceHeight) { mOutputSurfaceWidth = width; mOutputSurfaceHeight = height; - crop(); + if (width > 0 && height > 0) { + crop(); + } mSurfaceCallback.onSurfaceChanged(); } } - protected final void onSurfaceDestroyed() { + protected final void dispatchOnOutputSurfaceDestroyed() { mOutputSurfaceWidth = 0; mOutputSurfaceHeight = 0; + mSurfaceCallback.onSurfaceDestroyed(); } void onResume() {} @@ -107,7 +115,7 @@ abstract class CameraPreview { void onDestroy() {} - final boolean isReady() { + final boolean hasSurface() { return mOutputSurfaceWidth > 0 && mOutputSurfaceHeight > 0; } diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/GLCameraPreview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/GLCameraPreview.java index 460bdd4d..ddd75dfa 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/GLCameraPreview.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/GLCameraPreview.java @@ -69,7 +69,6 @@ class GLCameraPreview extends CameraPreview imple glView.setEGLContextClientVersion(2); glView.setRenderer(this); glView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); - // glView.getHolder().setFixedSize(600, 300); glView.getHolder().addCallback(new SurfaceHolder.Callback() { public void surfaceCreated(SurfaceHolder holder) {} public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { @@ -78,7 +77,7 @@ class GLCameraPreview extends CameraPreview imple @Override public void surfaceDestroyed(SurfaceHolder holder) { - onSurfaceDestroyed(); + dispatchOnOutputSurfaceDestroyed(); } }); return glView; @@ -99,7 +98,12 @@ class GLCameraPreview extends CameraPreview imple @Override void onDestroy() { super.onDestroy(); + releaseInputSurfaceTexture(); + } + + private void releaseInputSurfaceTexture() { if (mInputSurfaceTexture != null) { + mInputSurfaceTexture.setOnFrameAvailableListener(null); mInputSurfaceTexture.release(); mInputSurfaceTexture = null; } @@ -109,9 +113,7 @@ class GLCameraPreview extends CameraPreview imple } } - // Renderer thread - @Override - public void onSurfaceCreated(GL10 gl, EGLConfig config) { + private void createInputSurfaceTexture() { mOutputViewport = new GLViewport(); mOutputTextureId = mOutputViewport.createTexture(); mInputSurfaceTexture = new SurfaceTexture(mOutputTextureId); @@ -129,18 +131,52 @@ class GLCameraPreview extends CameraPreview imple // Renderer thread @Override - public void onSurfaceChanged(GL10 gl, int width, int height) { + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + createInputSurfaceTexture(); + } + + // Renderer thread + @Override + public void onSurfaceChanged(GL10 gl, final int width, final int height) { if (!mDispatched) { - onSurfaceAvailable(width, height); + dispatchOnOutputSurfaceAvailable(width, height); mDispatched = true; - } else { - onSurfaceSizeChanged(width, height); + } else if (mOutputSurfaceWidth != width || mOutputSurfaceHeight != height) { + // It did actually change. (Got some cases where onSurfaceChanged is called with same values). Go on. + // Since surface size changed does not work, we try to destroy and recreate. + // dispatchOnOutputSurfaceSizeChanged(width, height); + + // Recreate the texture... + releaseInputSurfaceTexture(); + + // Tell the controller that the output surface was destroyed.. + // And recreated. + dispatchOnOutputSurfaceDestroyed(); + getView().post(new Runnable() { + @Override + public void run() { + // getView().setPreserveEGLContextOnPause(true); + getView().onPause(); + getView().onResume(); + // getView().setPreserveEGLContextOnPause(false); + getView().queueEvent(new Runnable() { + @Override + public void run() { + createInputSurfaceTexture(); + dispatchOnOutputSurfaceAvailable(width, height); + } + }); + } + }); + } } @Override public void onDrawFrame(GL10 gl) { + if (mInputSurfaceTexture == null) return; + // Latch the latest frame. If there isn't anything new, // we'll just re-use whatever was there before. mInputSurfaceTexture.updateTexImage(); @@ -151,10 +187,19 @@ class GLCameraPreview extends CameraPreview imple // Draw the video frame. mInputSurfaceTexture.getTransformMatrix(mTransformMatrix); - Matrix.scaleM(mTransformMatrix, 0, mScaleX, mScaleY, 1); + if (isCropping()) { + Matrix.scaleM(mTransformMatrix, 0, mScaleX, mScaleY, 1); + } + if (mOutputViewport == null) return; mOutputViewport.drawFrame(mOutputTextureId, mTransformMatrix); } + @Override + void setInputStreamSize(int width, int height) { + // mInputSurfaceTexture.setDefaultBufferSize(width, height); + super.setInputStreamSize(width, height); + } + @Override Class getOutputClass() { return SurfaceTexture.class; @@ -165,7 +210,6 @@ class GLCameraPreview extends CameraPreview imple return mInputSurfaceTexture; } - @Override boolean supportsCropping() { return true; @@ -200,6 +244,7 @@ class GLCameraPreview extends CameraPreview imple // We must increase width. scaleX = target.toFloat() / current.toFloat(); } + mCropping = scaleX > 1.02f || scaleY > 1.02f; mScaleX = 1F / scaleX; mScaleY = 1F / scaleY; getView().requestRender(); diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceCameraPreview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceCameraPreview.java index 697c595f..170be7bd 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceCameraPreview.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceCameraPreview.java @@ -3,7 +3,6 @@ package com.otaliastudios.cameraview; import android.content.Context; import android.support.annotation.NonNull; import android.view.LayoutInflater; -import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; @@ -46,17 +45,17 @@ class SurfaceCameraPreview extends CameraPreview { public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { LOG.i("callback:", "surfaceChanged", "w:", width, "h:", height, "firstTime:", mFirstTime); if (mFirstTime) { - onSurfaceAvailable(width, height); + dispatchOnOutputSurfaceAvailable(width, height); mFirstTime = false; } else { - onSurfaceSizeChanged(width, height); + dispatchOnOutputSurfaceSizeChanged(width, height); } } @Override public void surfaceDestroyed(SurfaceHolder holder) { LOG.i("callback:", "surfaceDestroyed"); - onSurfaceDestroyed(); + dispatchOnOutputSurfaceDestroyed(); mFirstTime = true; } }); diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/TextureCameraPreview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/TextureCameraPreview.java index 20ac3906..422af7fa 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/TextureCameraPreview.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/TextureCameraPreview.java @@ -25,17 +25,17 @@ class TextureCameraPreview extends CameraPreview { @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { - onSurfaceAvailable(width, height); + dispatchOnOutputSurfaceAvailable(width, height); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { - onSurfaceSizeChanged(width, height); + dispatchOnOutputSurfaceSizeChanged(width, height); } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { - onSurfaceDestroyed(); + dispatchOnOutputSurfaceDestroyed(); return true; }