/*
* Copyright (C) 2011 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 android.filterpacks.videosrc;
import android.filterfw.core.Filter;
import android.filterfw.core.FilterContext;
import android.filterfw.core.Frame;
import android.filterfw.core.FrameFormat;
import android.filterfw.core.GenerateFieldPort;
import android.filterfw.core.GenerateFinalPort;
import android.filterfw.core.GLFrame;
import android.filterfw.core.MutableFrameFormat;
import android.filterfw.core.ShaderProgram;
import android.filterfw.format.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.opengl.Matrix;
import java.io.IOException;
import java.util.List;
import android.util.Log;
/**
* @hide
*/
public class CameraSource extends Filter {
/** User-visible parameters */
/** Camera ID to use for input. Defaults to 0. */
@GenerateFieldPort(name = "id", hasDefault = true)
private int mCameraId = 0;
/** Frame width to request from camera. Actual size may not match requested. */
@GenerateFieldPort(name = "width", hasDefault = true)
private int mWidth = 320;
/** Frame height to request from camera. Actual size may not match requested. */
@GenerateFieldPort(name = "height", hasDefault = true)
private int mHeight = 240;
/** Stream framerate to request from camera. Actual frame rate may not match requested. */
@GenerateFieldPort(name = "framerate", hasDefault = true)
private int mFps = 30;
/** Whether the filter should always wait for a new frame from the camera
* before providing output. If set to false, the filter will keep
* outputting the last frame it received from the camera if multiple process
* calls are received before the next update from the Camera. Defaults to true.
*/
@GenerateFinalPort(name = "waitForNewFrame", hasDefault = true)
private boolean mWaitForNewFrame = true;
private Camera mCamera;
private GLFrame mCameraFrame;
private SurfaceTexture mSurfaceTexture;
private ShaderProgram mFrameExtractor;
private MutableFrameFormat mOutputFormat;
private float[] mCameraTransform;
private float[] mMappedCoords;
// These default source coordinates perform the necessary flip
// for converting from OpenGL origin to MFF/Bitmap origin.
private static final float[] mSourceCoords = { 0, 1, 0, 1,
1, 1, 0, 1,
0, 0, 0, 1,
1, 0, 0, 1 };
private static final int NEWFRAME_TIMEOUT = 100; //ms
private static final int NEWFRAME_TIMEOUT_REPEAT = 10;
private boolean mNewFrameAvailable;
private Camera.Parameters mCameraParameters;
private static final String mFrameShader =
"#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";
private final boolean mLogVerbose;
private static final String TAG = "CameraSource";
public CameraSource(String name) {
super(name);
mCameraTransform = new float[16];
mMappedCoords = new float[16];
mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE);
}
@Override
public void setupPorts() {
// Add input port
addOutputPort("video", ImageFormat.create(ImageFormat.COLORSPACE_RGBA,
FrameFormat.TARGET_GPU));
}
private void createFormats() {
mOutputFormat = ImageFormat.create(mWidth, mHeight,
ImageFormat.COLORSPACE_RGBA,
FrameFormat.TARGET_GPU);
}
@Override
public void prepare(FilterContext context) {
if (mLogVerbose) Log.v(TAG, "Preparing");
// Compile shader TODO: Move to onGLEnvSomething?
mFrameExtractor = new ShaderProgram(context, mFrameShader);
}
@Override
public void open(FilterContext context) {
if (mLogVerbose) Log.v(TAG, "Opening");
// Open camera
mCamera = Camera.open(mCameraId);
// Set parameters
getCameraParameters();
mCamera.setParameters(mCameraParameters);
// Create frame formats
createFormats();
// Bind it to our camera frame
mCameraFrame = (GLFrame)context.getFrameManager().newBoundFrame(mOutputFormat,
GLFrame.EXTERNAL_TEXTURE,
0);
mSurfaceTexture = new SurfaceTexture(mCameraFrame.getTextureId());
try {
mCamera.setPreviewTexture(mSurfaceTexture);
} catch (IOException e) {
throw new RuntimeException("Could not bind camera surface texture: " +
e.getMessage() + "!");
}
// Connect SurfaceTexture to callback
mSurfaceTexture.setOnFrameAvailableListener(onCameraFrameAvailableListener);
// Start the preview
mNewFrameAvailable = false;
mCamera.startPreview();
}
@Override
public void process(FilterContext context) {
if (mLogVerbose) Log.v(TAG, "Processing new frame");
if (mWaitForNewFrame) {
int waitCount = 0;
while (!mNewFrameAvailable) {
if (waitCount == NEWFRAME_TIMEOUT_REPEAT) {
throw new RuntimeException("Timeout waiting for new frame");
}
try {
this.wait(NEWFRAME_TIMEOUT);
} catch (InterruptedException e) {
if (mLogVerbose) Log.v(TAG, "Interrupted while waiting for new frame");
}
}
mNewFrameAvailable = false;
if (mLogVerbose) Log.v(TAG, "Got new frame");
}
mSurfaceTexture.updateTexImage();
if (mLogVerbose) Log.v(TAG, "Using frame extractor in thread: " + Thread.currentThread());
mSurfaceTexture.getTransformMatrix(mCameraTransform);
Matrix.multiplyMM(mMappedCoords, 0,
mCameraTransform, 0,
mSourceCoords, 0);
mFrameExtractor.setSourceRegion(mMappedCoords[0], mMappedCoords[1],
mMappedCoords[4], mMappedCoords[5],
mMappedCoords[8], mMappedCoords[9],
mMappedCoords[12], mMappedCoords[13]);
Frame output = context.getFrameManager().newFrame(mOutputFormat);
mFrameExtractor.process(mCameraFrame, output);
long timestamp = mSurfaceTexture.getTimestamp();
if (mLogVerbose) Log.v(TAG, "Timestamp: " + (timestamp / 1000000000.0) + " s");
output.setTimestamp(timestamp);
pushOutput("video", output);
// Release pushed frame
output.release();
if (mLogVerbose) Log.v(TAG, "Done processing new frame");
}
@Override
public void close(FilterContext context) {
if (mLogVerbose) Log.v(TAG, "Closing");
mCamera.release();
mCamera = null;
mSurfaceTexture.release();
mSurfaceTexture = null;
}
@Override
public void tearDown(FilterContext context) {
if (mCameraFrame != null) {
mCameraFrame.release();
}
}
@Override
public void fieldPortValueUpdated(String name, FilterContext context) {
if (name.equals("framerate")) {
getCameraParameters();
int closestRange[] = findClosestFpsRange(mFps, mCameraParameters);
mCameraParameters.setPreviewFpsRange(closestRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
closestRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]);
mCamera.setParameters(mCameraParameters);
}
}
synchronized public Camera.Parameters getCameraParameters() {
boolean closeCamera = false;
if (mCameraParameters == null) {
if (mCamera == null) {
mCamera = Camera.open(mCameraId);
closeCamera = true;
}
mCameraParameters = mCamera.getParameters();
if (closeCamera) {
mCamera.release();
mCamera = null;
}
}
int closestSize[] = findClosestSize(mWidth, mHeight, mCameraParameters);
mWidth = closestSize[0];
mHeight = closestSize[1];
mCameraParameters.setPreviewSize(mWidth, mHeight);
int closestRange[] = findClosestFpsRange(mFps, mCameraParameters);
mCameraParameters.setPreviewFpsRange(closestRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
closestRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]);
return mCameraParameters;
}
/** Update camera parameters. Image resolution cannot be changed. */
synchronized public void setCameraParameters(Camera.Parameters params) {
params.setPreviewSize(mWidth, mHeight);
mCameraParameters = params;
if (isOpen()) {
mCamera.setParameters(mCameraParameters);
}
}
private int[] findClosestSize(int width, int height, Camera.Parameters parameters) {
List<Camera.Size> previewSizes = parameters.getSupportedPreviewSizes();
int closestWidth = -1;
int closestHeight = -1;
int smallestWidth = previewSizes.get(0).width;
int smallestHeight = previewSizes.get(0).height;
for (Camera.Size size : previewSizes) {
// 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;
}
if (mLogVerbose) {
Log.v(TAG,
"Requested resolution: (" + width + ", " + height
+ "). Closest match: (" + closestWidth + ", "
+ closestHeight + ").");
}
int[] closestSize = {closestWidth, closestHeight};
return closestSize;
}
private int[] findClosestFpsRange(int fps, Camera.Parameters params) {
List<int[]> supportedFpsRanges = params.getSupportedPreviewFpsRange();
int[] closestRange = supportedFpsRanges.get(0);
for (int[] range : supportedFpsRanges) {
if (range[Camera.Parameters.PREVIEW_FPS_MIN_INDEX] < fps*1000 &&
range[Camera.Parameters.PREVIEW_FPS_MAX_INDEX] > fps*1000 &&
range[Camera.Parameters.PREVIEW_FPS_MIN_INDEX] >
closestRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX] &&
range[Camera.Parameters.PREVIEW_FPS_MAX_INDEX] <
closestRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]) {
closestRange = range;
}
}
if (mLogVerbose) Log.v(TAG, "Requested fps: " + fps
+ ".Closest frame rate range: ["
+ closestRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX] / 1000.
+ ","
+ closestRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX] / 1000.
+ "]");
return closestRange;
}
private SurfaceTexture.OnFrameAvailableListener onCameraFrameAvailableListener =
new SurfaceTexture.OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
if (mLogVerbose) Log.v(TAG, "New frame from camera");
synchronized(CameraSource.this) {
mNewFrameAvailable = true;
CameraSource.this.notify();
}
}
};
}