- Handle close camera device when the user leaves the screen before camera has been opened.

- Create Release function for Orchestrator to cancel all pending tasks.
- Handle NullPointerExceptions.
- Create function to force close camera.
pull/946/head
MayconCardoso 5 years ago
parent daf7a0bf44
commit 92a0644c93
  1. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  2. 63
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  3. 94
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  4. 29
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraOrchestrator.java
  5. 1
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java

@ -347,9 +347,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@NonNull
protected CameraEngine instantiateCameraEngine(@NonNull Engine engine,
@NonNull CameraEngine.Callback callback) {
if (mExperimental
&& engine == Engine.CAMERA2
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (mExperimental && engine == Engine.CAMERA2) {
return new Camera2Engine(callback);
} else {
mEngine = Engine.CAMERA1;
@ -2160,7 +2158,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@NonNull
@Override
public Context getContext() {
return CameraView.this.getContext();
return CameraView.this.getContext().getApplicationContext();
}
@Override

@ -222,7 +222,11 @@ public class Camera2Engine extends CameraBaseEngine implements
* Removes default surfaces from the repeating request builder.
*/
private void removeRepeatingRequestBuilderSurfaces() {
mRepeatingRequestBuilder.removeTarget(mPreviewStreamSurface);
if(mRepeatingRequestBuilder == null) return;
if(mPreviewStreamSurface != null){
mRepeatingRequestBuilder.removeTarget(mPreviewStreamSurface);
}
if (mFrameProcessingSurface != null) {
mRepeatingRequestBuilder.removeTarget(mFrameProcessingSurface);
}
@ -486,7 +490,7 @@ public class Camera2Engine extends CameraBaseEngine implements
// Create a preview surface with the correct size.
final Class outputClass = mPreview.getOutputClass();
final Object output = mPreview.getOutput();
if (outputClass == SurfaceHolder.class) {
if (output != null && outputClass == SurfaceHolder.class) {
try {
// This must be called from the UI thread...
Tasks.await(Tasks.call(new Callable<Void>() {
@ -502,12 +506,12 @@ public class Camera2Engine extends CameraBaseEngine implements
throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT);
}
mPreviewStreamSurface = ((SurfaceHolder) output).getSurface();
} else if (outputClass == SurfaceTexture.class) {
} else if (output != null && outputClass == SurfaceTexture.class) {
((SurfaceTexture) output).setDefaultBufferSize(
mPreviewStreamSize.getWidth(),
mPreviewStreamSize.getHeight());
mPreviewStreamSurface = new Surface((SurfaceTexture) output);
} else {
} else if(output != null) {
throw new RuntimeException("Unknown CameraPreview output class.");
}
outputSurfaces.add(mPreviewStreamSurface);
@ -581,7 +585,6 @@ public class Camera2Engine extends CameraBaseEngine implements
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
// This SHOULD be a library error so we throw a RuntimeException.
String message = LOG.e("onConfigureFailed! Session", session);
throw new RuntimeException(message);
}
@Override
@ -704,22 +707,48 @@ public class Camera2Engine extends CameraBaseEngine implements
mPreviewStreamSize = null;
mCaptureSize = null;
mFrameProcessingSize = null;
if (mFrameProcessingReader != null) {
// WARNING: This call synchronously releases all Images and their underlying
// properties. This can cause issues if the Image is being used.
mFrameProcessingReader.close();
mFrameProcessingReader = null;
}
if (mPictureReader != null) {
mPictureReader.close();
mPictureReader = null;
}
mSession.close();
mSession = null;
closeFrameProcessor();
closePictureRender();
closeSession();
LOG.i("onStopBind:", "Returning.");
return Tasks.forResult(null);
}
private void closeFrameProcessor() {
try{
if (mFrameProcessingReader != null) {
// WARNING: This call synchronously releases all Images and their underlying
// properties. This can cause issues if the Image is being used.
mFrameProcessingReader.close();
mFrameProcessingReader = null;
}
}catch (Exception ex){
LOG.e("cameraEngine", "Error while closing FrameProcessor", ex);
}
}
private void closePictureRender() {
try{
if (mPictureReader != null) {
mPictureReader.close();
mPictureReader = null;
}
}catch (Exception ex){
LOG.e("cameraEngine", "Error while closing PictureRender", ex);
}
}
private void closeSession() {
try{
if(mSession != null){
mSession.close();
mSession = null;
}
}catch (Exception ex){
LOG.e("cameraEngine", "Error while closing session", ex);
}
}
@EngineThread
@NonNull
@Override

@ -51,8 +51,6 @@ import androidx.annotation.VisibleForTesting;
import java.io.File;
import java.io.FileDescriptor;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
@ -132,14 +130,11 @@ public abstract class CameraEngine implements
protected static final String TAG = CameraEngine.class.getSimpleName();
protected static final CameraLogger LOG = CameraLogger.create(TAG);
// If this is 2, this means we'll try to run destroy() twice.
private static final int DESTROY_RETRIES = 2;
private WorkerHandler mHandler;
@VisibleForTesting Handler mCrashHandler;
private final Callback mCallback;
private final CameraStateOrchestrator mOrchestrator
= new CameraStateOrchestrator(new CameraOrchestrator.Callback() {
private final CameraStateOrchestrator mOrchestrator = new CameraStateOrchestrator(new CameraOrchestrator.Callback() {
@Override
@NonNull
public WorkerHandler getJobWorker(@NonNull String job) {
@ -286,48 +281,22 @@ public abstract class CameraEngine implements
* awaiting for {@link #stop(boolean)} to return.
*/
public void destroy(boolean unrecoverably) {
destroy(unrecoverably, 0);
}
private void destroy(boolean unrecoverably, int depth) {
LOG.i("DESTROY:", "state:", getState(),
"thread:", Thread.currentThread(),
"depth:", depth,
"unrecoverably:", unrecoverably);
"thread:", Thread.currentThread(),
"unrecoverably:", unrecoverably);
if (unrecoverably) {
// Prevent CameraEngine leaks. Don't set to null, or exceptions
// inside the standard stop() method might crash the main thread.
mHandler.getThread().setUncaughtExceptionHandler(new NoOpExceptionHandler());
}
// Cannot use Tasks.await() because we might be on the UI thread.
final CountDownLatch latch = new CountDownLatch(1);
stop(true).addOnCompleteListener(
mHandler.getExecutor(),
new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
latch.countDown();
}
});
try {
boolean success = latch.await(6, TimeUnit.SECONDS);
if (!success) {
// This thread is likely stuck. The reason might be deadlock issues in the internal
// camera implementation, at least in emulators: see Camera1Engine and Camera2Engine
// onStopEngine() implementation and comments.
LOG.e("DESTROY: Could not destroy synchronously after 6 seconds.",
"Current thread:", Thread.currentThread(),
"Handler thread:", mHandler.getThread());
depth++;
if (depth < DESTROY_RETRIES) {
recreateHandler(true);
LOG.e("DESTROY: Trying again on thread:", mHandler.getThread());
destroy(unrecoverably, depth);
} else {
LOG.w("DESTROY: Giving up because DESTROY_RETRIES was reached.");
}
}
} catch (InterruptedException ignore) {}
// Release orchestrator, to cancel all current pending process.
// It will make future tasks that will open the camera not be executed.
mOrchestrator.release();
// Close camera device.
forceStop();
}
@SuppressWarnings("WeakerAccess")
@ -340,12 +309,47 @@ public abstract class CameraEngine implements
@NonNull
public Task<Void> start() {
LOG.i("START:", "scheduled. State:", getState());
Task<Void> engine = startEngine();
startBind();
startPreview();
// The user may have left screen when the camera is properly started.
// So, if that happen, the orchestrator will be released and we close the camera in order to avoid memory leak.
Task<Void> engine = startEngine().addOnCompleteListener(new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
if(mOrchestrator.isReleased()){
onStopEngine();
}
}
});
startBind().addOnCompleteListener(new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
if(mOrchestrator.isReleased()){
onStopBind();
}
}
});
startPreview().addOnCompleteListener(new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
if(mOrchestrator.isReleased()){
onStopPreview();
}
}
});
return engine;
}
public void forceStop(){
mHandler.post(new Runnable() {
@Override
public void run() {
LOG.i("STOP:", "scheduled. State:", getState());
onStopPreview();
onStopBind();
onStopEngine();
}
});
}
@NonNull
public Task<Void> stop(final boolean swallowExceptions) {
LOG.i("STOP:", "scheduled. State:", getState());

@ -59,6 +59,7 @@ public class CameraOrchestrator {
protected final ArrayDeque<Token> mJobs = new ArrayDeque<>();
protected final Object mLock = new Object();
private final Map<String, Runnable> mDelayedJobs = new HashMap<>();
private boolean released = false;
public CameraOrchestrator(@NonNull Callback callback) {
mCallback = callback;
@ -78,6 +79,17 @@ public class CameraOrchestrator {
});
}
public void release(){
mJobs.clear();
mDelayedJobs.clear();
reset();
released = true;
}
public boolean isReleased(){
return released;
}
@SuppressWarnings("unchecked")
@NonNull
public <T> Task<T> schedule(@NonNull final String name,
@ -87,11 +99,17 @@ public class CameraOrchestrator {
final TaskCompletionSource<T> source = new TaskCompletionSource<>();
final WorkerHandler handler = mCallback.getJobWorker(name);
synchronized (mLock) {
applyCompletionListener(mJobs.getLast().task, handler,
new OnCompleteListener() {
if(released){
return source.getTask();
}
applyCompletionListener(mJobs.getLast().task, handler, new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
synchronized (mLock) {
if (mJobs.isEmpty()) {
return;
}
mJobs.removeFirst();
ensureToken();
}
@ -119,7 +137,9 @@ public class CameraOrchestrator {
});
} catch (Exception e) {
LOG.i(name.toUpperCase(), "- Finished.", e);
if (dispatchExceptions) mCallback.handleJobException(name, e);
if (dispatchExceptions) {
mCallback.handleJobException(name, e);
}
source.trySetException(e);
}
}
@ -132,6 +152,8 @@ public class CameraOrchestrator {
public void scheduleDelayed(@NonNull final String name,
long minDelay,
@NonNull final Runnable runnable) {
if(released) return;
Runnable wrapper = new Runnable() {
@Override
public void run() {
@ -165,6 +187,7 @@ public class CameraOrchestrator {
public void reset() {
synchronized (mLock) {
released = false;
List<String> all = new ArrayList<>();
//noinspection CollectionAddAllCanBeReplacedWithConstructor
all.addAll(mDelayedJobs.keySet());

@ -139,7 +139,6 @@ public abstract class CameraPreview<T extends View, Output> {
* or a SurfaceTexture).
* @return the surface object
*/
@NonNull
public abstract Output getOutput();
/**

Loading…
Cancel
Save