| /* |
| * 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<EGLContext, ImageShader> mTargetShaders |
| = new HashMap<EGLContext, ImageShader>(); |
| |
| /** Map of target textures (one for each target context) */ |
| protected HashMap<EGLContext, TextureSource> mTargetTextures |
| = new HashMap<EGLContext, TextureSource>(); |
| |
| /** Map of set of clients (one for each target context) */ |
| protected HashMap<EGLContext, Set<FrameClient>> mContextClients |
| = new HashMap<EGLContext, Set<FrameClient>>(); |
| |
| /** List of clients that are consuming camera frames. */ |
| protected Vector<FrameClient> mClients = new Vector<FrameClient>(); |
| |
| 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<FrameClient> clientTargets = clientsForContext(context); |
| clientTargets.add(client); |
| mClients.add(client); |
| onRegisterClient(client, context); |
| } |
| |
| public void unregisterClient(FrameClient client) { |
| EGLContext context = RenderTarget.currentContext(); |
| Set<FrameClient> 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<FrameClient> clientsForContext(EGLContext context) { |
| Set<FrameClient> clients = mContextClients.get(context); |
| if (clients == null) { |
| clients = new HashSet<FrameClient>(); |
| 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<EGLContext, SurfaceTexture> mTargetSurfaceTextures |
| = new HashMap<EGLContext, SurfaceTexture>(); |
| |
| /** Map of RenderTargets for client SurfaceTextures */ |
| protected HashMap<SurfaceTexture, RenderTarget> mClientRenderTargets |
| = new HashMap<SurfaceTexture, RenderTarget>(); |
| |
| /** 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<FrameClient> 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<Event> mEventQueue = new LinkedBlockingQueue<Event>(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<CameraListener> mCamListeners = new HashSet<CameraListener>(); |
| |
| 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<Camera.Size> previewSizes = parameters.getSupportedPreviewSizes(); |
| return findClosestSizeFromList(width, height, previewSizes); |
| } |
| |
| private int[] findClosestPictureSize(int width, int height, Camera.Parameters parameters) { |
| List<Camera.Size> pictureSizes = parameters.getSupportedPictureSizes(); |
| return findClosestSizeFromList(width, height, pictureSizes); |
| } |
| |
| private int[] findClosestSizeFromList(int width, int height, List<Camera.Size> 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<int[]> 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:<p> |
| * {@link #createRecorder}<p> |
| * {@link #startRecording}<p> |
| * {@link #stopRecording}<p> |
| * {@link #releaseRecorder}<p> |
| * @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:<p> |
| * {@link #createRecorder}<p> |
| * {@link #startRecording}<p> |
| * {@link #stopRecording}<p> |
| * {@link #releaseRecorder}<p> |
| * @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); |
| } |
| } |
| |