/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.media.filterfw; import android.annotation.TargetApi; import android.graphics.SurfaceTexture; import android.hardware.Camera; import android.hardware.Camera.CameraInfo; import android.hardware.Camera.PreviewCallback; import android.media.CamcorderProfile; import android.media.MediaRecorder; import android.opengl.GLES20; import android.os.Build.VERSION; import android.util.Log; import android.view.Display; import android.view.Surface; import android.view.SurfaceView; import java.io.IOException; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Vector; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import javax.microedition.khronos.egl.EGLContext; /** * The CameraStreamer streams Frames from a camera to connected clients. * * There is one centralized CameraStreamer object per MffContext, and only one stream can be * active at any time. The CameraStreamer acts as a Camera "server" that streams frames to any * number of connected clients. Typically, these are CameraSource filters that are part of a * graph, but other clients can be written as well. */ public class CameraStreamer { /** Camera Facing: Don't Care: Picks any available camera. */ public static final int FACING_DONTCARE = 0; /** Camera Facing: Front: Use the front facing camera. */ public static final int FACING_FRONT = 1; /** Camera Facing: Back: Use the rear facing camera. */ public static final int FACING_BACK = 2; /** How long the streamer should wait to acquire the camera before giving up. */ public static long MAX_CAMERA_WAIT_TIME = 5; /** * The global camera lock, that is closed when the camera is acquired by any CameraStreamer, * and opened when a streamer is done using the camera. */ static ReentrantLock mCameraLock = new ReentrantLock(); /** The Camera thread that grabs frames from the camera */ private CameraRunnable mCameraRunner = null; private abstract class CamFrameHandler { protected int mCameraWidth; protected int mCameraHeight; protected int mOutWidth; protected int mOutHeight; protected CameraRunnable mRunner; /** Map of GLSL shaders (one for each target context) */ protected HashMap mTargetShaders = new HashMap(); /** Map of target textures (one for each target context) */ protected HashMap mTargetTextures = new HashMap(); /** Map of set of clients (one for each target context) */ protected HashMap> mContextClients = new HashMap>(); /** List of clients that are consuming camera frames. */ protected Vector mClients = new Vector(); public void initWithRunner(CameraRunnable camRunner) { mRunner = camRunner; } public void setCameraSize(int width, int height) { mCameraWidth = width; mCameraHeight = height; } public void registerClient(FrameClient client) { EGLContext context = RenderTarget.currentContext(); Set clientTargets = clientsForContext(context); clientTargets.add(client); mClients.add(client); onRegisterClient(client, context); } public void unregisterClient(FrameClient client) { EGLContext context = RenderTarget.currentContext(); Set clientTargets = clientsForContext(context); clientTargets.remove(client); if (clientTargets.isEmpty()) { onCleanupContext(context); } mClients.remove(client); } public abstract void setupServerFrame(); public abstract void updateServerFrame(); public abstract void grabFrame(FrameImage2D targetFrame); public abstract void release(); public void onUpdateCameraOrientation(int orientation) { if (orientation % 180 != 0) { mOutWidth = mCameraHeight; mOutHeight = mCameraWidth; } else { mOutWidth = mCameraWidth; mOutHeight = mCameraHeight; } } protected Set clientsForContext(EGLContext context) { Set clients = mContextClients.get(context); if (clients == null) { clients = new HashSet(); mContextClients.put(context, clients); } return clients; } protected void onRegisterClient(FrameClient client, EGLContext context) { } protected void onCleanupContext(EGLContext context) { TextureSource texture = mTargetTextures.get(context); ImageShader shader = mTargetShaders.get(context); if (texture != null) { texture.release(); mTargetTextures.remove(context); } if (shader != null) { mTargetShaders.remove(context); } } protected TextureSource textureForContext(EGLContext context) { TextureSource texture = mTargetTextures.get(context); if (texture == null) { texture = createClientTexture(); mTargetTextures.put(context, texture); } return texture; } protected ImageShader shaderForContext(EGLContext context) { ImageShader shader = mTargetShaders.get(context); if (shader == null) { shader = createClientShader(); mTargetShaders.put(context, shader); } return shader; } protected ImageShader createClientShader() { return null; } protected TextureSource createClientTexture() { return null; } public boolean isFrontMirrored() { return true; } } // Jellybean (and later) back-end @TargetApi(16) private class CamFrameHandlerJB extends CamFrameHandlerICS { @Override public void setupServerFrame() { setupPreviewTexture(mRunner.mCamera); } @Override public synchronized void updateServerFrame() { updateSurfaceTexture(); informClients(); } @Override public synchronized void grabFrame(FrameImage2D targetFrame) { TextureSource targetTex = TextureSource.newExternalTexture(); ImageShader copyShader = shaderForContext(RenderTarget.currentContext()); if (targetTex == null || copyShader == null) { throw new RuntimeException("Attempting to grab camera frame from unknown " + "thread: " + Thread.currentThread() + "!"); } mPreviewSurfaceTexture.attachToGLContext(targetTex.getTextureId()); updateTransform(copyShader); updateShaderTargetRect(copyShader); targetFrame.resize(new int[] { mOutWidth, mOutHeight }); copyShader.process(targetTex, targetFrame.lockRenderTarget(), mOutWidth, mOutHeight); targetFrame.setTimestamp(mPreviewSurfaceTexture.getTimestamp()); targetFrame.unlock(); mPreviewSurfaceTexture.detachFromGLContext(); targetTex.release(); } @Override protected void updateShaderTargetRect(ImageShader shader) { if ((mRunner.mActualFacing == FACING_FRONT) && mRunner.mFlipFront) { shader.setTargetRect(1f, 1f, -1f, -1f); } else { shader.setTargetRect(0f, 1f, 1f, -1f); } } @Override protected void setupPreviewTexture(Camera camera) { super.setupPreviewTexture(camera); mPreviewSurfaceTexture.detachFromGLContext(); } protected void updateSurfaceTexture() { mPreviewSurfaceTexture.attachToGLContext(mPreviewTexture.getTextureId()); mPreviewSurfaceTexture.updateTexImage(); mPreviewSurfaceTexture.detachFromGLContext(); } protected void informClients() { synchronized (mClients) { for (FrameClient client : mClients) { client.onCameraFrameAvailable(); } } } } // ICS (and later) back-end @TargetApi(15) private class CamFrameHandlerICS extends CamFrameHandler { protected static final String mCopyShaderSource = "#extension GL_OES_EGL_image_external : require\n" + "precision mediump float;\n" + "uniform samplerExternalOES tex_sampler_0;\n" + "varying vec2 v_texcoord;\n" + "void main() {\n" + " gl_FragColor = texture2D(tex_sampler_0, v_texcoord);\n" + "}\n"; /** The camera transform matrix */ private float[] mCameraTransform = new float[16]; /** The texture the camera streams to */ protected TextureSource mPreviewTexture = null; protected SurfaceTexture mPreviewSurfaceTexture = null; /** Map of target surface textures (one for each target context) */ protected HashMap mTargetSurfaceTextures = new HashMap(); /** Map of RenderTargets for client SurfaceTextures */ protected HashMap mClientRenderTargets = new HashMap(); /** Server side copy shader */ protected ImageShader mCopyShader = null; @Override public synchronized void setupServerFrame() { setupPreviewTexture(mRunner.mCamera); } @Override public synchronized void updateServerFrame() { mPreviewSurfaceTexture.updateTexImage(); distributeFrames(); } @Override public void onUpdateCameraOrientation(int orientation) { super.onUpdateCameraOrientation(orientation); mRunner.mCamera.setDisplayOrientation(orientation); updateSurfaceTextureSizes(); } @Override public synchronized void onRegisterClient(FrameClient client, EGLContext context) { final Set clientTargets = clientsForContext(context); // Make sure we have texture, shader, and surfacetexture setup for this context. TextureSource clientTex = textureForContext(context); ImageShader copyShader = shaderForContext(context); SurfaceTexture surfTex = surfaceTextureForContext(context); // Listen to client-side surface texture updates surfTex.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() { @Override public void onFrameAvailable(SurfaceTexture surfaceTexture) { for (FrameClient clientTarget : clientTargets) { clientTarget.onCameraFrameAvailable(); } } }); } @Override public synchronized void grabFrame(FrameImage2D targetFrame) { // Get the GL objects for the receiver's context EGLContext clientContext = RenderTarget.currentContext(); TextureSource clientTex = textureForContext(clientContext); ImageShader copyShader = shaderForContext(clientContext); SurfaceTexture surfTex = surfaceTextureForContext(clientContext); if (clientTex == null || copyShader == null || surfTex == null) { throw new RuntimeException("Attempting to grab camera frame from unknown " + "thread: " + Thread.currentThread() + "!"); } // Copy from client ST to client tex surfTex.updateTexImage(); targetFrame.resize(new int[] { mOutWidth, mOutHeight }); copyShader.process(clientTex, targetFrame.lockRenderTarget(), mOutWidth, mOutHeight); targetFrame.setTimestamp(mPreviewSurfaceTexture.getTimestamp()); targetFrame.unlock(); } @Override public synchronized void release() { if (mPreviewTexture != null) { mPreviewTexture.release(); mPreviewTexture = null; } if (mPreviewSurfaceTexture != null) { mPreviewSurfaceTexture.release(); mPreviewSurfaceTexture = null; } } @Override protected ImageShader createClientShader() { return new ImageShader(mCopyShaderSource); } @Override protected TextureSource createClientTexture() { return TextureSource.newExternalTexture(); } protected void distributeFrames() { updateTransform(getCopyShader()); updateShaderTargetRect(getCopyShader()); for (SurfaceTexture clientTexture : mTargetSurfaceTextures.values()) { RenderTarget clientTarget = renderTargetFor(clientTexture); clientTarget.focus(); getCopyShader().process(mPreviewTexture, clientTarget, mOutWidth, mOutHeight); GLToolbox.checkGlError("distribute frames"); clientTarget.swapBuffers(); } } protected RenderTarget renderTargetFor(SurfaceTexture surfaceTex) { RenderTarget target = mClientRenderTargets.get(surfaceTex); if (target == null) { target = RenderTarget.currentTarget().forSurfaceTexture(surfaceTex); mClientRenderTargets.put(surfaceTex, target); } return target; } protected void setupPreviewTexture(Camera camera) { if (mPreviewTexture == null) { mPreviewTexture = TextureSource.newExternalTexture(); } if (mPreviewSurfaceTexture == null) { mPreviewSurfaceTexture = new SurfaceTexture(mPreviewTexture.getTextureId()); try { camera.setPreviewTexture(mPreviewSurfaceTexture); } catch (IOException e) { throw new RuntimeException("Could not bind camera surface texture: " + e.getMessage() + "!"); } mPreviewSurfaceTexture.setOnFrameAvailableListener(mOnCameraFrameListener); } } protected ImageShader getCopyShader() { if (mCopyShader == null) { mCopyShader = new ImageShader(mCopyShaderSource); } return mCopyShader; } protected SurfaceTexture surfaceTextureForContext(EGLContext context) { SurfaceTexture surfTex = mTargetSurfaceTextures.get(context); if (surfTex == null) { TextureSource texture = textureForContext(context); if (texture != null) { surfTex = new SurfaceTexture(texture.getTextureId()); surfTex.setDefaultBufferSize(mOutWidth, mOutHeight); mTargetSurfaceTextures.put(context, surfTex); } } return surfTex; } protected void updateShaderTargetRect(ImageShader shader) { if ((mRunner.mActualFacing == FACING_FRONT) && mRunner.mFlipFront) { shader.setTargetRect(1f, 0f, -1f, 1f); } else { shader.setTargetRect(0f, 0f, 1f, 1f); } } protected synchronized void updateSurfaceTextureSizes() { for (SurfaceTexture clientTexture : mTargetSurfaceTextures.values()) { clientTexture.setDefaultBufferSize(mOutWidth, mOutHeight); } } protected void updateTransform(ImageShader shader) { mPreviewSurfaceTexture.getTransformMatrix(mCameraTransform); shader.setSourceTransform(mCameraTransform); } @Override protected void onCleanupContext(EGLContext context) { super.onCleanupContext(context); SurfaceTexture surfaceTex = mTargetSurfaceTextures.get(context); if (surfaceTex != null) { surfaceTex.release(); mTargetSurfaceTextures.remove(context); } } protected SurfaceTexture.OnFrameAvailableListener mOnCameraFrameListener = new SurfaceTexture.OnFrameAvailableListener() { @Override public void onFrameAvailable(SurfaceTexture surfaceTexture) { mRunner.signalNewFrame(); } }; } // Gingerbread (and later) back-end @TargetApi(9) private final class CamFrameHandlerGB extends CamFrameHandler { private SurfaceView mSurfaceView; private byte[] mFrameBufferFront; private byte[] mFrameBufferBack; private boolean mWriteToBack = true; private float[] mTargetCoords = new float[] { 0f, 0f, 1f, 0f, 0f, 1f, 1f, 1f }; final Object mBufferLock = new Object(); private String mNV21ToRGBAFragment = "precision mediump float;\n" + "\n" + "uniform sampler2D tex_sampler_0;\n" + "varying vec2 v_y_texcoord;\n" + "varying vec2 v_vu_texcoord;\n" + "varying vec2 v_pixcoord;\n" + "\n" + "vec3 select(vec4 yyyy, vec4 vuvu, int s) {\n" + " if (s == 0) {\n" + " return vec3(yyyy.r, vuvu.g, vuvu.r);\n" + " } else if (s == 1) {\n" + " return vec3(yyyy.g, vuvu.g, vuvu.r);\n" + " } else if (s == 2) {\n" + " return vec3(yyyy.b, vuvu.a, vuvu.b);\n" + " } else {\n" + " return vec3(yyyy.a, vuvu.a, vuvu.b);\n" + " }\n" + "}\n" + "\n" + "vec3 yuv2rgb(vec3 yuv) {\n" + " mat4 conversion = mat4(1.0, 0.0, 1.402, -0.701,\n" + " 1.0, -0.344, -0.714, 0.529,\n" + " 1.0, 1.772, 0.0, -0.886,\n" + " 0, 0, 0, 0);" + " return (vec4(yuv, 1.0) * conversion).rgb;\n" + "}\n" + "\n" + "void main() {\n" + " vec4 yyyy = texture2D(tex_sampler_0, v_y_texcoord);\n" + " vec4 vuvu = texture2D(tex_sampler_0, v_vu_texcoord);\n" + " int s = int(mod(floor(v_pixcoord.x), 4.0));\n" + " vec3 yuv = select(yyyy, vuvu, s);\n" + " vec3 rgb = yuv2rgb(yuv);\n" + " gl_FragColor = vec4(rgb, 1.0);\n" + "}"; private String mNV21ToRGBAVertex = "attribute vec4 a_position;\n" + "attribute vec2 a_y_texcoord;\n" + "attribute vec2 a_vu_texcoord;\n" + "attribute vec2 a_pixcoord;\n" + "varying vec2 v_y_texcoord;\n" + "varying vec2 v_vu_texcoord;\n" + "varying vec2 v_pixcoord;\n" + "void main() {\n" + " gl_Position = a_position;\n" + " v_y_texcoord = a_y_texcoord;\n" + " v_vu_texcoord = a_vu_texcoord;\n" + " v_pixcoord = a_pixcoord;\n" + "}\n"; private byte[] readBuffer() { synchronized (mBufferLock) { return mWriteToBack ? mFrameBufferFront : mFrameBufferBack; } } private byte[] writeBuffer() { synchronized (mBufferLock) { return mWriteToBack ? mFrameBufferBack : mFrameBufferFront; } } private synchronized void swapBuffers() { synchronized (mBufferLock) { mWriteToBack = !mWriteToBack; } } private PreviewCallback mPreviewCallback = new PreviewCallback() { @Override public void onPreviewFrame(byte[] data, Camera camera) { swapBuffers(); camera.addCallbackBuffer(writeBuffer()); mRunner.signalNewFrame(); } }; @Override public void setupServerFrame() { checkCameraDimensions(); Camera camera = mRunner.mCamera; int bufferSize = mCameraWidth * (mCameraHeight + mCameraHeight/2); mFrameBufferFront = new byte[bufferSize]; mFrameBufferBack = new byte[bufferSize]; camera.addCallbackBuffer(writeBuffer()); camera.setPreviewCallbackWithBuffer(mPreviewCallback); SurfaceView previewDisplay = getPreviewDisplay(); if (previewDisplay != null) { try { camera.setPreviewDisplay(previewDisplay.getHolder()); } catch (IOException e) { throw new RuntimeException("Could not start camera with given preview " + "display!"); } } } private void checkCameraDimensions() { if (mCameraWidth % 4 != 0) { throw new RuntimeException("Camera width must be a multiple of 4!"); } else if (mCameraHeight % 2 != 0) { throw new RuntimeException("Camera height must be a multiple of 2!"); } } @Override public void updateServerFrame() { // Server frame has been updated already, simply inform clients here. informClients(); } @Override public void grabFrame(FrameImage2D targetFrame) { EGLContext clientContext = RenderTarget.currentContext(); // Copy camera data to the client YUV texture TextureSource clientTex = textureForContext(clientContext); int texWidth = mCameraWidth / 4; int texHeight = mCameraHeight + mCameraHeight / 2; synchronized(mBufferLock) { // Don't swap buffers while we are reading ByteBuffer pixels = ByteBuffer.wrap(readBuffer()); clientTex.allocateWithPixels(pixels, texWidth, texHeight); } clientTex.setParameter(GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST); clientTex.setParameter(GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); // Setup the YUV-2-RGBA shader ImageShader transferShader = shaderForContext(clientContext); transferShader.setTargetCoords(mTargetCoords); updateShaderPixelSize(transferShader); // Convert pixels into target frame targetFrame.resize(new int[] { mOutWidth, mOutHeight }); transferShader.process(clientTex, targetFrame.lockRenderTarget(), mOutWidth, mOutHeight); targetFrame.unlock(); } @Override public void onUpdateCameraOrientation(int orientation) { super.onUpdateCameraOrientation(orientation); if ((mRunner.mActualFacing == FACING_FRONT) && mRunner.mFlipFront) { switch (orientation) { case 0: mTargetCoords = new float[] { 1f, 0f, 0f, 0f, 1f, 1f, 0f, 1f }; break; case 90: mTargetCoords = new float[] { 0f, 0f, 0f, 1f, 1f, 0f, 1f, 1f }; break; case 180: mTargetCoords = new float[] { 0f, 1f, 1f, 1f, 0f, 0f, 1f, 0f }; break; case 270: mTargetCoords = new float[] { 1f, 1f, 1f, 0f, 0f, 1f, 0f, 0f }; break; } } else { switch (orientation) { case 0: mTargetCoords = new float[] { 0f, 0f, 1f, 0f, 0f, 1f, 1f, 1f }; break; case 90: mTargetCoords = new float[] { 1f, 0f, 1f, 1f, 0f, 0f, 0f, 1f }; break; case 180: mTargetCoords = new float[] { 1f, 1f, 0f, 1f, 1f, 0f, 0f, 0f }; break; case 270: mTargetCoords = new float[] { 0f, 1f, 0f, 0f, 1f, 1f, 1f, 0f }; break; } } } @Override public void release() { mFrameBufferBack = null; mFrameBufferFront = null; } @Override public boolean isFrontMirrored() { return false; } @Override protected ImageShader createClientShader() { ImageShader shader = new ImageShader(mNV21ToRGBAVertex, mNV21ToRGBAFragment); // TODO: Make this a VBO float[] yCoords = new float[] { 0f, 0f, 1f, 0f, 0f, 2f / 3f, 1f, 2f / 3f }; float[] uvCoords = new float[] { 0f, 2f / 3f, 1f, 2f / 3f, 0f, 1f, 1f, 1f }; shader.setAttributeValues("a_y_texcoord", yCoords, 2); shader.setAttributeValues("a_vu_texcoord", uvCoords, 2); return shader; } @Override protected TextureSource createClientTexture() { TextureSource texture = TextureSource.newTexture(); texture.setParameter(GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST); texture.setParameter(GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); return texture; } private void updateShaderPixelSize(ImageShader shader) { float[] pixCoords = new float[] { 0f, 0f, mCameraWidth, 0f, 0f, mCameraHeight, mCameraWidth, mCameraHeight }; shader.setAttributeValues("a_pixcoord", pixCoords, 2); } private SurfaceView getPreviewDisplay() { if (mSurfaceView == null) { mSurfaceView = mRunner.getContext().getDummySurfaceView(); } return mSurfaceView; } private void informClients() { synchronized (mClients) { for (FrameClient client : mClients) { client.onCameraFrameAvailable(); } } } } private static class State { public static final int STATE_RUNNING = 1; public static final int STATE_STOPPED = 2; public static final int STATE_HALTED = 3; private AtomicInteger mCurrent = new AtomicInteger(STATE_STOPPED); public int current() { return mCurrent.get(); } public void set(int newState) { mCurrent.set(newState); } } private static class Event { public static final int START = 1; public static final int FRAME = 2; public static final int STOP = 3; public static final int HALT = 4; public static final int RESTART = 5; public static final int UPDATE = 6; public static final int TEARDOWN = 7; public int code; public Event(int code) { this.code = code; } } private final class CameraRunnable implements Runnable { /** On slower devices the event queue can easily fill up. We bound the queue to this. */ private final static int MAX_EVENTS = 32; /** The runner's state */ private State mState = new State(); /** The CameraRunner's event queue */ private LinkedBlockingQueue mEventQueue = new LinkedBlockingQueue(MAX_EVENTS); /** The requested FPS */ private int mRequestedFramesPerSec = 30; /** The actual FPS */ private int mActualFramesPerSec = 0; /** The requested preview width and height */ private int mRequestedPreviewWidth = 640; private int mRequestedPreviewHeight = 480; /** The requested picture width and height */ private int mRequestedPictureWidth = 640; private int mRequestedPictureHeight = 480; /** The actual camera width and height */ private int[] mActualDims = null; /** The requested facing */ private int mRequestedFacing = FACING_DONTCARE; /** The actual facing */ private int mActualFacing = FACING_DONTCARE; /** Whether to horizontally flip the front facing camera */ private boolean mFlipFront = true; /** The display the camera streamer is bound to. */ private Display mDisplay = null; /** The camera and screen orientation. */ private int mCamOrientation = 0; private int mOrientation = -1; /** The camera rotation (used for capture). */ private int mCamRotation = 0; /** The camera flash mode */ private String mFlashMode = Camera.Parameters.FLASH_MODE_OFF; /** The camera object */ private Camera mCamera = null; private MediaRecorder mRecorder = null; /** The ID of the currently used camera */ int mCamId = 0; /** The platform-dependent camera frame handler. */ private CamFrameHandler mCamFrameHandler = null; /** The set of camera listeners. */ private Set mCamListeners = new HashSet(); private ReentrantLock mCameraReadyLock = new ReentrantLock(true); // mCameraReady condition is used when waiting for the camera getting ready. private Condition mCameraReady = mCameraReadyLock.newCondition(); // external camera lock used to provide the capability of external camera access. private ExternalCameraLock mExternalCameraLock = new ExternalCameraLock(); private RenderTarget mRenderTarget; private MffContext mContext; /** * This provides the capability of locking and unlocking from different threads. * The thread will wait until the lock state is idle. Any thread can wake up * a waiting thread by calling unlock (i.e. signal), provided that unlock * are called using the same context when lock was called. Using context prevents * from rogue usage of unlock. */ private class ExternalCameraLock { public static final int IDLE = 0; public static final int IN_USE = 1; private int mLockState = IDLE; private Object mLockContext; private final ReentrantLock mLock = new ReentrantLock(true); private final Condition mInUseLockCondition= mLock.newCondition(); public boolean lock(Object context) { if (context == null) { throw new RuntimeException("Null context when locking"); } mLock.lock(); if (mLockState == IN_USE) { try { mInUseLockCondition.await(); } catch (InterruptedException e) { return false; } } mLockState = IN_USE; mLockContext = context; mLock.unlock(); return true; } public void unlock(Object context) { mLock.lock(); if (mLockState != IN_USE) { throw new RuntimeException("Not in IN_USE state"); } if (context != mLockContext) { throw new RuntimeException("Lock is not owned by this context"); } mLockState = IDLE; mLockContext = null; mInUseLockCondition.signal(); mLock.unlock(); } } public CameraRunnable(MffContext context) { mContext = context; createCamFrameHandler(); mCamFrameHandler.initWithRunner(this); launchThread(); } public MffContext getContext() { return mContext; } public void loop() { while (true) { try { Event event = nextEvent(); if (event == null) continue; switch (event.code) { case Event.START: onStart(); break; case Event.STOP: onStop(); break; case Event.FRAME: onFrame(); break; case Event.HALT: onHalt(); break; case Event.RESTART: onRestart(); break; case Event.UPDATE: onUpdate(); break; case Event.TEARDOWN: onTearDown(); break; } } catch (Exception e) { e.printStackTrace(); } } } @Override public void run() { loop(); } public void signalNewFrame() { pushEvent(Event.FRAME, false); } public void pushEvent(int eventId, boolean required) { try { if (required) { mEventQueue.put(new Event(eventId)); } else { mEventQueue.offer(new Event(eventId)); } } catch (InterruptedException e) { // We should never get here (as we do not limit capacity in the queue), but if // we do, we log an error. Log.e("CameraStreamer", "Dropping event " + eventId + "!"); } } public void launchThread() { Thread cameraThread = new Thread(this); cameraThread.start(); } @Deprecated public Camera getCamera() { synchronized (mState) { return mCamera; } } public Camera lockCamera(Object context) { mExternalCameraLock.lock(context); /** * since lockCamera can happen right after closeCamera, * the camera handle can be null, wait until valid handle * is acquired. */ while (mCamera == null) { mExternalCameraLock.unlock(context); mCameraReadyLock.lock(); try { mCameraReady.await(); } catch (InterruptedException e) { throw new RuntimeException("Condition interrupted", e); } mCameraReadyLock.unlock(); mExternalCameraLock.lock(context); } return mCamera; } public void unlockCamera(Object context) { mExternalCameraLock.unlock(context); } public int getCurrentCameraId() { synchronized (mState) { return mCamId; } } public boolean isRunning() { return mState.current() != State.STATE_STOPPED; } public void addListener(CameraListener listener) { synchronized (mCamListeners) { mCamListeners.add(listener); } } public void removeListener(CameraListener listener) { synchronized (mCamListeners) { mCamListeners.remove(listener); } } public synchronized void bindToDisplay(Display display) { mDisplay = display; } public synchronized void setDesiredPreviewSize(int width, int height) { if (width != mRequestedPreviewWidth || height != mRequestedPreviewHeight) { mRequestedPreviewWidth = width; mRequestedPreviewHeight = height; onParamsUpdated(); } } public synchronized void setDesiredPictureSize(int width, int height) { if (width != mRequestedPictureWidth || height != mRequestedPictureHeight) { mRequestedPictureWidth = width; mRequestedPictureHeight = height; onParamsUpdated(); } } public synchronized void setDesiredFrameRate(int fps) { if (fps != mRequestedFramesPerSec) { mRequestedFramesPerSec = fps; onParamsUpdated(); } } public synchronized void setFacing(int facing) { if (facing != mRequestedFacing) { switch (facing) { case FACING_DONTCARE: case FACING_FRONT: case FACING_BACK: mRequestedFacing = facing; break; default: throw new IllegalArgumentException("Unknown facing value '" + facing + "' passed to setFacing!"); } onParamsUpdated(); } } public synchronized void setFlipFrontCamera(boolean flipFront) { if (mFlipFront != flipFront) { mFlipFront = flipFront; } } public synchronized void setFlashMode(String flashMode) { if (!flashMode.equals(mFlashMode)) { mFlashMode = flashMode; onParamsUpdated(); } } public synchronized int getCameraFacing() { return mActualFacing; } public synchronized int getCameraRotation() { return mCamRotation; } public synchronized boolean supportsHardwareFaceDetection() { //return mCamFrameHandler.supportsHardwareFaceDetection(); // TODO return true; } public synchronized int getCameraWidth() { return (mActualDims != null) ? mActualDims[0] : 0; } public synchronized int getCameraHeight() { return (mActualDims != null) ? mActualDims[1] : 0; } public synchronized int getCameraFrameRate() { return mActualFramesPerSec; } public synchronized String getFlashMode() { return mCamera.getParameters().getFlashMode(); } public synchronized boolean canStart() { // If we can get a camera id without error we should be able to start. try { getCameraId(); } catch (RuntimeException e) { return false; } return true; } public boolean grabFrame(FrameImage2D targetFrame) { // Make sure we stay in state running while we are grabbing the frame. synchronized (mState) { if (mState.current() != State.STATE_RUNNING) { return false; } // we may not have the camera ready, this might happen when in the middle // of switching camera. if (mCamera == null) { return false; } mCamFrameHandler.grabFrame(targetFrame); return true; } } public CamFrameHandler getCamFrameHandler() { return mCamFrameHandler; } private void onParamsUpdated() { pushEvent(Event.UPDATE, true); } private Event nextEvent() { try { return mEventQueue.take(); } catch (InterruptedException e) { // Ignore and keep going. Log.w("GraphRunner", "Event queue processing was interrupted."); return null; } } private void onStart() { if (mState.current() == State.STATE_STOPPED) { mState.set(State.STATE_RUNNING); getRenderTarget().focus(); openCamera(); } } private void onStop() { if (mState.current() == State.STATE_RUNNING) { closeCamera(); RenderTarget.focusNone(); } // Set state to stop (halted becomes stopped). mState.set(State.STATE_STOPPED); } private void onHalt() { // Only halt if running. Stopped overrides halt. if (mState.current() == State.STATE_RUNNING) { closeCamera(); RenderTarget.focusNone(); mState.set(State.STATE_HALTED); } } private void onRestart() { // Only restart if halted if (mState.current() == State.STATE_HALTED) { mState.set(State.STATE_RUNNING); getRenderTarget().focus(); openCamera(); } } private void onUpdate() { if (mState.current() == State.STATE_RUNNING) { pushEvent(Event.STOP, true); pushEvent(Event.START, true); } } private void onFrame() { if (mState.current() == State.STATE_RUNNING) { updateRotation(); mCamFrameHandler.updateServerFrame(); } } private void onTearDown() { if (mState.current() == State.STATE_STOPPED) { // Remove all listeners. This will release their resources for (CameraListener listener : mCamListeners) { removeListener(listener); } mCamListeners.clear(); } else { Log.e("CameraStreamer", "Could not tear-down CameraStreamer as camera still " + "seems to be running!"); } } private void createCamFrameHandler() { // TODO: For now we simply assert that OpenGL is supported. Later on, we should add // a CamFrameHandler that does not depend on OpenGL. getContext().assertOpenGLSupported(); if (VERSION.SDK_INT >= 16) { mCamFrameHandler = new CamFrameHandlerJB(); } else if (VERSION.SDK_INT >= 15) { mCamFrameHandler = new CamFrameHandlerICS(); } else { mCamFrameHandler = new CamFrameHandlerGB(); } } private void updateRotation() { if (mDisplay != null) { updateDisplayRotation(mDisplay.getRotation()); } } private synchronized void updateDisplayRotation(int rotation) { switch (rotation) { case Surface.ROTATION_0: onUpdateOrientation(0); break; case Surface.ROTATION_90: onUpdateOrientation(90); break; case Surface.ROTATION_180: onUpdateOrientation(180); break; case Surface.ROTATION_270: onUpdateOrientation(270); break; default: throw new IllegalArgumentException("Unsupported display rotation constant! Use " + "one of the Surface.ROTATION_ constants!"); } } private RenderTarget getRenderTarget() { if (mRenderTarget == null) { mRenderTarget = RenderTarget.newTarget(1, 1); } return mRenderTarget; } private void updateCamera() { synchronized (mState) { mCamId = getCameraId(); updateCameraOrientation(mCamId); mCamera = Camera.open(mCamId); initCameraParameters(); } } private void updateCameraOrientation(int camId) { CameraInfo cameraInfo = new CameraInfo(); Camera.getCameraInfo(camId, cameraInfo); mCamOrientation = cameraInfo.orientation; mOrientation = -1; // Forces recalculation to match display mActualFacing = (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) ? FACING_FRONT : FACING_BACK; } private int getCameraId() { int camCount = Camera.getNumberOfCameras(); if (camCount == 0) { throw new RuntimeException("Device does not have any cameras!"); } else if (mRequestedFacing == FACING_DONTCARE) { // Simply return first camera if mRequestedFacing is don't care return 0; } // Attempt to find requested camera boolean useFrontCam = (mRequestedFacing == FACING_FRONT); CameraInfo cameraInfo = new CameraInfo(); for (int i = 0; i < camCount; ++i) { Camera.getCameraInfo(i, cameraInfo); if ((cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) == useFrontCam) { return i; } } throw new RuntimeException("Could not find a camera facing (" + mRequestedFacing + ")!"); } private void initCameraParameters() { Camera.Parameters params = mCamera.getParameters(); // Find closest preview size mActualDims = findClosestPreviewSize(mRequestedPreviewWidth, mRequestedPreviewHeight, params); mCamFrameHandler.setCameraSize(mActualDims[0], mActualDims[1]); params.setPreviewSize(mActualDims[0], mActualDims[1]); // Find closest picture size int[] dims = findClosestPictureSize(mRequestedPictureWidth, mRequestedPictureHeight, params); params.setPictureSize(dims[0], dims[1]); // Find closest FPS int closestRange[] = findClosestFpsRange(mRequestedFramesPerSec, params); params.setPreviewFpsRange(closestRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX], closestRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]); // Set flash mode (if supported) if (params.getFlashMode() != null) { params.setFlashMode(mFlashMode); } mCamera.setParameters(params); } private int[] findClosestPreviewSize(int width, int height, Camera.Parameters parameters) { List previewSizes = parameters.getSupportedPreviewSizes(); return findClosestSizeFromList(width, height, previewSizes); } private int[] findClosestPictureSize(int width, int height, Camera.Parameters parameters) { List pictureSizes = parameters.getSupportedPictureSizes(); return findClosestSizeFromList(width, height, pictureSizes); } private int[] findClosestSizeFromList(int width, int height, List sizes) { int closestWidth = -1; int closestHeight = -1; int smallestWidth = sizes.get(0).width; int smallestHeight = sizes.get(0).height; for (Camera.Size size : sizes) { // Best match defined as not being larger in either dimension than // the requested size, but as close as possible. The below isn't a // stable selection (reording the size list can give different // results), but since this is a fallback nicety, that's acceptable. if ( size.width <= width && size.height <= height && size.width >= closestWidth && size.height >= closestHeight) { closestWidth = size.width; closestHeight = size.height; } if ( size.width < smallestWidth && size.height < smallestHeight) { smallestWidth = size.width; smallestHeight = size.height; } } if (closestWidth == -1) { // Requested size is smaller than any listed size; match with smallest possible closestWidth = smallestWidth; closestHeight = smallestHeight; } int[] closestSize = {closestWidth, closestHeight}; return closestSize; } private int[] findClosestFpsRange(int fps, Camera.Parameters params) { List supportedFpsRanges = params.getSupportedPreviewFpsRange(); int[] closestRange = supportedFpsRanges.get(0); int fpsk = fps * 1000; int minDiff = 1000000; for (int[] range : supportedFpsRanges) { int low = range[Camera.Parameters.PREVIEW_FPS_MIN_INDEX]; int high = range[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]; if (low <= fpsk && high >= fpsk) { int diff = (fpsk - low) + (high - fpsk); if (diff < minDiff) { closestRange = range; minDiff = diff; } } } mActualFramesPerSec = closestRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX] / 1000; return closestRange; } private void onUpdateOrientation(int orientation) { // First we calculate the camera rotation. int rotation = (mActualFacing == FACING_FRONT) ? (mCamOrientation + orientation) % 360 : (mCamOrientation - orientation + 360) % 360; if (rotation != mCamRotation) { synchronized (this) { mCamRotation = rotation; } } // We compensate for mirroring in the orientation. This differs from the rotation, // where we are invariant to mirroring. int fixedOrientation = rotation; if (mActualFacing == FACING_FRONT && mCamFrameHandler.isFrontMirrored()) { fixedOrientation = (360 - rotation) % 360; // compensate the mirror } if (mOrientation != fixedOrientation) { mOrientation = fixedOrientation; mCamFrameHandler.onUpdateCameraOrientation(mOrientation); } } private void openCamera() { // Acquire lock for camera try { if (!mCameraLock.tryLock(MAX_CAMERA_WAIT_TIME, TimeUnit.SECONDS)) { throw new RuntimeException("Timed out while waiting to acquire camera!"); } } catch (InterruptedException e) { throw new RuntimeException("Interrupted while waiting to acquire camera!"); } // Make sure external entities are not holding camera. We need to hold the lock until // the preview is started again. Object lockContext = new Object(); mExternalCameraLock.lock(lockContext); // Need to synchronize this as many of the member values are modified during setup. synchronized (this) { updateCamera(); updateRotation(); mCamFrameHandler.setupServerFrame(); } mCamera.startPreview(); // Inform listeners synchronized (mCamListeners) { for (CameraListener listener : mCamListeners) { listener.onCameraOpened(CameraStreamer.this); } } mExternalCameraLock.unlock(lockContext); // New camera started mCameraReadyLock.lock(); mCameraReady.signal(); mCameraReadyLock.unlock(); } /** * Creates an instance of MediaRecorder to be used for the streamer. * User should call the functions in the following sequence:

* {@link #createRecorder}

* {@link #startRecording}

* {@link #stopRecording}

* {@link #releaseRecorder}

* @param outputPath the output video path for the recorder * @param profile the recording {@link CamcorderProfile} which has parameters indicating * the resolution, quality etc. */ public void createRecorder(String outputPath, CamcorderProfile profile) { lockCamera(this); mCamera.unlock(); if (mRecorder != null) { mRecorder.release(); } mRecorder = new MediaRecorder(); mRecorder.setCamera(mCamera); mRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); mRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); mRecorder.setProfile(profile); mRecorder.setOutputFile(outputPath); try { mRecorder.prepare(); } catch (Exception e) { throw new RuntimeException(e); } } /** * Starts recording video using the created MediaRecorder object */ public void startRecording() { if (mRecorder == null) { throw new RuntimeException("No recorder created"); } mRecorder.start(); } /** * Stops recording video */ public void stopRecording() { if (mRecorder == null) { throw new RuntimeException("No recorder created"); } mRecorder.stop(); } /** * Release the resources held by the MediaRecorder, call this after done recording. */ public void releaseRecorder() { if (mRecorder == null) { throw new RuntimeException("No recorder created"); } mRecorder.release(); mRecorder = null; mCamera.lock(); unlockCamera(this); } private void closeCamera() { Object lockContext = new Object(); mExternalCameraLock.lock(lockContext); if (mCamera != null) { mCamera.stopPreview(); mCamera.release(); mCamera = null; } mCameraLock.unlock(); mCamFrameHandler.release(); mExternalCameraLock.unlock(lockContext); // Inform listeners synchronized (mCamListeners) { for (CameraListener listener : mCamListeners) { listener.onCameraClosed(CameraStreamer.this); } } } } /** * The frame-client callback interface. * FrameClients, that wish to receive Frames from the camera must implement this callback * method. * Note, that this method is called on the Camera server thread. However, the * {@code getLatestFrame()} method must be called from the client thread. */ public static interface FrameClient { public void onCameraFrameAvailable(); } /** * The CameraListener callback interface. * This interface allows observers to monitor the CameraStreamer and respond to stream open * and close events. */ public static interface CameraListener { /** * Called when the camera is opened and begins producing frames. * This is also called when settings have changed that caused the camera to be reopened. */ public void onCameraOpened(CameraStreamer camera); /** * Called when the camera is closed and stops producing frames. */ public void onCameraClosed(CameraStreamer camera); } /** * Manually update the display rotation. * You do not need to call this, if the camera is bound to a display, or your app does not * support multiple orientations. */ public void updateDisplayRotation(int rotation) { mCameraRunner.updateDisplayRotation(rotation); } /** * Bind the camera to your Activity's display. * Use this, if your Activity supports multiple display orientation, and you would like the * camera to update accordingly when the orientation is changed. */ public void bindToDisplay(Display display) { mCameraRunner.bindToDisplay(display); } /** * Sets the desired preview size. * Note that the actual width and height may vary. * * @param width The desired width of the preview camera stream. * @param height The desired height of the preview camera stream. */ public void setDesiredPreviewSize(int width, int height) { mCameraRunner.setDesiredPreviewSize(width, height); } /** * Sets the desired picture size. * Note that the actual width and height may vary. * * @param width The desired picture width. * @param height The desired picture height. */ public void setDesiredPictureSize(int width, int height) { mCameraRunner.setDesiredPictureSize(width, height); } /** * Sets the desired camera frame-rate. * Note, that the actual frame-rate may vary. * * @param fps The desired FPS. */ public void setDesiredFrameRate(int fps) { mCameraRunner.setDesiredFrameRate(fps); } /** * Sets the camera facing direction. * * Specify {@code FACING_DONTCARE} (default) if you would like the CameraStreamer to choose * the direction. When specifying any other direction be sure to first check whether the * device supports the desired facing. * * @param facing The desired camera facing direction. */ public void setFacing(int facing) { mCameraRunner.setFacing(facing); } /** * Set whether to flip the camera image horizontally when using the front facing camera. */ public void setFlipFrontCamera(boolean flipFront) { mCameraRunner.setFlipFrontCamera(flipFront); } /** * Sets the camera flash mode. * * This must be one of the String constants defined in the Camera.Parameters class. * * @param flashMode A String constant specifying the flash mode. */ public void setFlashMode(String flashMode) { mCameraRunner.setFlashMode(flashMode); } /** * Returns the current flash mode. * * This returns the currently running camera's flash-mode, or NULL if flash modes are not * supported on that camera. * * @return The flash mode String, or NULL if flash modes are not supported. */ public String getFlashMode() { return mCameraRunner.getFlashMode(); } /** * Get the actual camera facing. * Returns 0 if actual facing is not yet known. */ public int getCameraFacing() { return mCameraRunner.getCameraFacing(); } /** * Get the current camera rotation. * * Use this rotation if you want to snap pictures from the camera and need to rotate the * picture to be up-right. * * @return the current camera rotation. */ public int getCameraRotation() { return mCameraRunner.getCameraRotation(); } /** * Specifies whether or not the camera supports hardware face detection. * @return true, if the camera supports hardware face detection. */ public boolean supportsHardwareFaceDetection() { return mCameraRunner.supportsHardwareFaceDetection(); } /** * Returns the camera facing that is chosen when DONT_CARE is specified. * Returns 0 if neither a front nor back camera could be found. */ public static int getDefaultFacing() { int camCount = Camera.getNumberOfCameras(); if (camCount == 0) { return 0; } else { CameraInfo cameraInfo = new CameraInfo(); Camera.getCameraInfo(0, cameraInfo); return (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) ? FACING_FRONT : FACING_BACK; } } /** * Get the actual camera width. * Returns 0 if actual width is not yet known. */ public int getCameraWidth() { return mCameraRunner.getCameraWidth(); } /** * Get the actual camera height. * Returns 0 if actual height is not yet known. */ public int getCameraHeight() { return mCameraRunner.getCameraHeight(); } /** * Get the actual camera frame-rate. * Returns 0 if actual frame-rate is not yet known. */ public int getCameraFrameRate() { return mCameraRunner.getCameraFrameRate(); } /** * Returns true if the camera can be started at this point. */ public boolean canStart() { return mCameraRunner.canStart(); } /** * Returns true if the camera is currently running. */ public boolean isRunning() { return mCameraRunner.isRunning(); } /** * Starts the camera. */ public void start() { mCameraRunner.pushEvent(Event.START, true); } /** * Stops the camera. */ public void stop() { mCameraRunner.pushEvent(Event.STOP, true); } /** * Stops the camera and waits until it is completely closed. Generally, this should not be * called in the UI thread, but may be necessary if you need the camera to be closed before * performing subsequent steps. */ public void stopAndWait() { mCameraRunner.pushEvent(Event.STOP, true); try { if (!mCameraLock.tryLock(MAX_CAMERA_WAIT_TIME, TimeUnit.SECONDS)) { Log.w("CameraStreamer", "Time-out waiting for camera to close!"); } } catch (InterruptedException e) { Log.w("CameraStreamer", "Interrupted while waiting for camera to close!"); } mCameraLock.unlock(); } /** * Registers a listener to handle camera state changes. */ public void addListener(CameraListener listener) { mCameraRunner.addListener(listener); } /** * Unregisters a listener to handle camera state changes. */ public void removeListener(CameraListener listener) { mCameraRunner.removeListener(listener); } /** * Registers the frame-client with the camera. * This MUST be called from the client thread! */ public void registerClient(FrameClient client) { mCameraRunner.getCamFrameHandler().registerClient(client); } /** * Unregisters the frame-client with the camera. * This MUST be called from the client thread! */ public void unregisterClient(FrameClient client) { mCameraRunner.getCamFrameHandler().unregisterClient(client); } /** * Gets the latest camera frame for the client. * * This must be called from the same thread as the {@link #registerClient(FrameClient)} call! * The frame passed in will be resized by the camera streamer to fit the camera frame. * Returns false if the frame could not be grabbed. This may happen if the camera has been * closed in the meantime, and its resources let go. * * @return true, if the frame was grabbed successfully. */ public boolean getLatestFrame(FrameImage2D targetFrame) { return mCameraRunner.grabFrame(targetFrame); } /** * Expose the underlying android.hardware.Camera object. * Use the returned object with care: some camera functions may break the functionality * of CameraStreamer. * @return the Camera object. */ @Deprecated public Camera getCamera() { return mCameraRunner.getCamera(); } /** * Obtain access to the underlying android.hardware.Camera object. * This grants temporary access to the internal Camera handle. Once you are done using the * handle you must call {@link #unlockCamera(Object)}. While you are holding the Camera, * it will not be modified or released by the CameraStreamer. The Camera object return is * guaranteed to have the preview running. * * The CameraStreamer does not account for changes you make to the Camera. That is, if you * change the Camera unexpectedly this may cause unintended behavior by the streamer. * * Note that the returned object may be null. This can happen when the CameraStreamer is not * running, or is just transitioning to another Camera, such as during a switch from front to * back Camera. * @param context an object used as a context for locking and unlocking. lockCamera and * unlockCamera should use the same context object. * @return The Camera object. */ public Camera lockCamera(Object context) { return mCameraRunner.lockCamera(context); } /** * Release the acquire Camera object. * @param context the context object that used when lockCamera is called. */ public void unlockCamera(Object context) { mCameraRunner.unlockCamera(context); } /** * Creates an instance of MediaRecorder to be used for the streamer. * User should call the functions in the following sequence:

* {@link #createRecorder}

* {@link #startRecording}

* {@link #stopRecording}

* {@link #releaseRecorder}

* @param path the output video path for the recorder * @param profile the recording {@link CamcorderProfile} which has parameters indicating * the resolution, quality etc. */ public void createRecorder(String path, CamcorderProfile profile) { mCameraRunner.createRecorder(path, profile); } public void releaseRecorder() { mCameraRunner.releaseRecorder(); } public void startRecording() { mCameraRunner.startRecording(); } public void stopRecording() { mCameraRunner.stopRecording(); } /** * Retrieve the ID of the currently used camera. * @return the ID of the currently used camera. */ public int getCameraId() { return mCameraRunner.getCurrentCameraId(); } /** * @return The number of cameras available for streaming on this device. */ public static int getNumberOfCameras() { // Currently, this is just the number of cameras that are available on the device. return Camera.getNumberOfCameras(); } CameraStreamer(MffContext context) { mCameraRunner = new CameraRunnable(context); } /** Halt is like stop, but may be resumed using restart(). */ void halt() { mCameraRunner.pushEvent(Event.HALT, true); } /** Restart starts the camera only if previously halted. */ void restart() { mCameraRunner.pushEvent(Event.RESTART, true); } static boolean requireDummySurfaceView() { return VERSION.SDK_INT < 15; } void tearDown() { mCameraRunner.pushEvent(Event.TEARDOWN, true); } }