blob: 4601ab9ec2de7d04eab338241a6f50c72f0d9ea6 [file] [log] [blame]
/*
* 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 com.android.camera;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.android.gallery3d.common.ApiHelper;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* Encapsulates the mobile filter framework components needed to record video
* with effects applied. Modeled after MediaRecorder.
*/
@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) // uses SurfaceTexture
public class EffectsRecorder {
private static final String TAG = "EffectsRecorder";
private static Class<?> sClassFilter;
private static Method sFilterIsAvailable;
private static EffectsRecorder sEffectsRecorder;
// The index of the current effects recorder.
private static int sEffectsRecorderIndex;
private static boolean sReflectionInited = false;
private static Class<?> sClsLearningDoneListener;
private static Class<?> sClsOnRunnerDoneListener;
private static Class<?> sClsOnRecordingDoneListener;
private static Class<?> sClsSurfaceTextureSourceListener;
private static Method sFilterSetInputValue;
private static Constructor<?> sCtPoint;
private static Constructor<?> sCtQuad;
private static Method sLearningDoneListenerOnLearningDone;
private static Method sObjectEquals;
private static Method sObjectToString;
private static Class<?> sClsGraphRunner;
private static Method sGraphRunnerGetGraph;
private static Method sGraphRunnerSetDoneCallback;
private static Method sGraphRunnerRun;
private static Method sGraphRunnerGetError;
private static Method sGraphRunnerStop;
private static Method sFilterGraphGetFilter;
private static Method sFilterGraphTearDown;
private static Method sOnRunnerDoneListenerOnRunnerDone;
private static Class<?> sClsGraphEnvironment;
private static Constructor<?> sCtGraphEnvironment;
private static Method sGraphEnvironmentCreateGLEnvironment;
private static Method sGraphEnvironmentGetRunner;
private static Method sGraphEnvironmentAddReferences;
private static Method sGraphEnvironmentLoadGraph;
private static Method sGraphEnvironmentGetContext;
private static Method sFilterContextGetGLEnvironment;
private static Method sGLEnvironmentIsActive;
private static Method sGLEnvironmentActivate;
private static Method sGLEnvironmentDeactivate;
private static Method sSurfaceTextureTargetDisconnect;
private static Method sOnRecordingDoneListenerOnRecordingDone;
private static Method sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady;
private Object mLearningDoneListener;
private Object mRunnerDoneCallback;
private Object mSourceReadyCallback;
// A callback to finalize the media after the recording is done.
private Object mRecordingDoneListener;
static {
try {
sClassFilter = Class.forName("android.filterfw.core.Filter");
sFilterIsAvailable = sClassFilter.getMethod("isAvailable",
String.class);
} catch (ClassNotFoundException ex) {
Log.v(TAG, "Can't find the class android.filterfw.core.Filter");
} catch (NoSuchMethodException e) {
Log.v(TAG, "Can't find the method Filter.isAvailable");
}
}
public static final int EFFECT_NONE = 0;
public static final int EFFECT_GOOFY_FACE = 1;
public static final int EFFECT_BACKDROPPER = 2;
public static final int EFFECT_GF_SQUEEZE = 0;
public static final int EFFECT_GF_BIG_EYES = 1;
public static final int EFFECT_GF_BIG_MOUTH = 2;
public static final int EFFECT_GF_SMALL_MOUTH = 3;
public static final int EFFECT_GF_BIG_NOSE = 4;
public static final int EFFECT_GF_SMALL_EYES = 5;
public static final int NUM_OF_GF_EFFECTS = EFFECT_GF_SMALL_EYES + 1;
public static final int EFFECT_MSG_STARTED_LEARNING = 0;
public static final int EFFECT_MSG_DONE_LEARNING = 1;
public static final int EFFECT_MSG_SWITCHING_EFFECT = 2;
public static final int EFFECT_MSG_EFFECTS_STOPPED = 3;
public static final int EFFECT_MSG_RECORDING_DONE = 4;
public static final int EFFECT_MSG_PREVIEW_RUNNING = 5;
private Context mContext;
private Handler mHandler;
private CameraManager.CameraProxy mCameraDevice;
private CamcorderProfile mProfile;
private double mCaptureRate = 0;
private SurfaceTexture mPreviewSurfaceTexture;
private int mPreviewWidth;
private int mPreviewHeight;
private MediaRecorder.OnInfoListener mInfoListener;
private MediaRecorder.OnErrorListener mErrorListener;
private String mOutputFile;
private FileDescriptor mFd;
private int mOrientationHint = 0;
private long mMaxFileSize = 0;
private int mMaxDurationMs = 0;
private int mCameraFacing = Camera.CameraInfo.CAMERA_FACING_BACK;
private int mCameraDisplayOrientation;
private int mEffect = EFFECT_NONE;
private int mCurrentEffect = EFFECT_NONE;
private EffectsListener mEffectsListener;
private Object mEffectParameter;
private Object mGraphEnv;
private int mGraphId;
private Object mRunner = null;
private Object mOldRunner = null;
private SurfaceTexture mTextureSource;
private static final int STATE_CONFIGURE = 0;
private static final int STATE_WAITING_FOR_SURFACE = 1;
private static final int STATE_STARTING_PREVIEW = 2;
private static final int STATE_PREVIEW = 3;
private static final int STATE_RECORD = 4;
private static final int STATE_RELEASED = 5;
private int mState = STATE_CONFIGURE;
private boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE);
private SoundClips.Player mSoundPlayer;
/** Determine if a given effect is supported at runtime
* Some effects require libraries not available on all devices
*/
public static boolean isEffectSupported(int effectId) {
if (sFilterIsAvailable == null) return false;
try {
switch (effectId) {
case EFFECT_GOOFY_FACE:
return (Boolean) sFilterIsAvailable.invoke(null,
"com.google.android.filterpacks.facedetect.GoofyRenderFilter");
case EFFECT_BACKDROPPER:
return (Boolean) sFilterIsAvailable.invoke(null,
"android.filterpacks.videoproc.BackDropperFilter");
default:
return false;
}
} catch (Exception ex) {
Log.e(TAG, "Fail to check filter", ex);
}
return false;
}
public EffectsRecorder(Context context) {
if (mLogVerbose) Log.v(TAG, "EffectsRecorder created (" + this + ")");
if (!sReflectionInited) {
try {
sFilterSetInputValue = sClassFilter.getMethod("setInputValue",
new Class[] {String.class, Object.class});
Class<?> clsPoint = Class.forName("android.filterfw.geometry.Point");
sCtPoint = clsPoint.getConstructor(new Class[] {float.class,
float.class});
Class<?> clsQuad = Class.forName("android.filterfw.geometry.Quad");
sCtQuad = clsQuad.getConstructor(new Class[] {clsPoint, clsPoint,
clsPoint, clsPoint});
Class<?> clsBackDropperFilter = Class.forName(
"android.filterpacks.videoproc.BackDropperFilter");
sClsLearningDoneListener = Class.forName(
"android.filterpacks.videoproc.BackDropperFilter$LearningDoneListener");
sLearningDoneListenerOnLearningDone = sClsLearningDoneListener
.getMethod("onLearningDone", new Class[] {clsBackDropperFilter});
sObjectEquals = Object.class.getMethod("equals", new Class[] {Object.class});
sObjectToString = Object.class.getMethod("toString");
sClsOnRunnerDoneListener = Class.forName(
"android.filterfw.core.GraphRunner$OnRunnerDoneListener");
sOnRunnerDoneListenerOnRunnerDone = sClsOnRunnerDoneListener.getMethod(
"onRunnerDone", new Class[] {int.class});
sClsGraphRunner = Class.forName("android.filterfw.core.GraphRunner");
sGraphRunnerGetGraph = sClsGraphRunner.getMethod("getGraph");
sGraphRunnerSetDoneCallback = sClsGraphRunner.getMethod(
"setDoneCallback", new Class[] {sClsOnRunnerDoneListener});
sGraphRunnerRun = sClsGraphRunner.getMethod("run");
sGraphRunnerGetError = sClsGraphRunner.getMethod("getError");
sGraphRunnerStop = sClsGraphRunner.getMethod("stop");
Class<?> clsFilterContext = Class.forName("android.filterfw.core.FilterContext");
sFilterContextGetGLEnvironment = clsFilterContext.getMethod(
"getGLEnvironment");
Class<?> clsFilterGraph = Class.forName("android.filterfw.core.FilterGraph");
sFilterGraphGetFilter = clsFilterGraph.getMethod("getFilter",
new Class[] {String.class});
sFilterGraphTearDown = clsFilterGraph.getMethod("tearDown",
new Class[] {clsFilterContext});
sClsGraphEnvironment = Class.forName("android.filterfw.GraphEnvironment");
sCtGraphEnvironment = sClsGraphEnvironment.getConstructor();
sGraphEnvironmentCreateGLEnvironment = sClsGraphEnvironment.getMethod(
"createGLEnvironment");
sGraphEnvironmentGetRunner = sClsGraphEnvironment.getMethod(
"getRunner", new Class[] {int.class, int.class});
sGraphEnvironmentAddReferences = sClsGraphEnvironment.getMethod(
"addReferences", new Class[] {Object[].class});
sGraphEnvironmentLoadGraph = sClsGraphEnvironment.getMethod(
"loadGraph", new Class[] {Context.class, int.class});
sGraphEnvironmentGetContext = sClsGraphEnvironment.getMethod(
"getContext");
Class<?> clsGLEnvironment = Class.forName("android.filterfw.core.GLEnvironment");
sGLEnvironmentIsActive = clsGLEnvironment.getMethod("isActive");
sGLEnvironmentActivate = clsGLEnvironment.getMethod("activate");
sGLEnvironmentDeactivate = clsGLEnvironment.getMethod("deactivate");
Class<?> clsSurfaceTextureTarget = Class.forName(
"android.filterpacks.videosrc.SurfaceTextureTarget");
sSurfaceTextureTargetDisconnect = clsSurfaceTextureTarget.getMethod(
"disconnect", new Class[] {clsFilterContext});
sClsOnRecordingDoneListener = Class.forName(
"android.filterpacks.videosink.MediaEncoderFilter$OnRecordingDoneListener");
sOnRecordingDoneListenerOnRecordingDone =
sClsOnRecordingDoneListener.getMethod("onRecordingDone");
sClsSurfaceTextureSourceListener = Class.forName(
"android.filterpacks.videosrc.SurfaceTextureSource$SurfaceTextureSourceListener");
sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady =
sClsSurfaceTextureSourceListener.getMethod(
"onSurfaceTextureSourceReady",
new Class[] {SurfaceTexture.class});
} catch (Exception ex) {
throw new RuntimeException(ex);
}
sReflectionInited = true;
}
sEffectsRecorderIndex++;
Log.v(TAG, "Current effects recorder index is " + sEffectsRecorderIndex);
sEffectsRecorder = this;
SerializableInvocationHandler sih = new SerializableInvocationHandler(
sEffectsRecorderIndex);
mLearningDoneListener = Proxy.newProxyInstance(
sClsLearningDoneListener.getClassLoader(),
new Class[] {sClsLearningDoneListener}, sih);
mRunnerDoneCallback = Proxy.newProxyInstance(
sClsOnRunnerDoneListener.getClassLoader(),
new Class[] {sClsOnRunnerDoneListener}, sih);
mSourceReadyCallback = Proxy.newProxyInstance(
sClsSurfaceTextureSourceListener.getClassLoader(),
new Class[] {sClsSurfaceTextureSourceListener}, sih);
mRecordingDoneListener = Proxy.newProxyInstance(
sClsOnRecordingDoneListener.getClassLoader(),
new Class[] {sClsOnRecordingDoneListener}, sih);
mContext = context;
mHandler = new Handler(Looper.getMainLooper());
mSoundPlayer = SoundClips.getPlayer(context);
}
public synchronized void setCamera(CameraManager.CameraProxy cameraDevice) {
switch (mState) {
case STATE_PREVIEW:
throw new RuntimeException("setCamera cannot be called while previewing!");
case STATE_RECORD:
throw new RuntimeException("setCamera cannot be called while recording!");
case STATE_RELEASED:
throw new RuntimeException("setCamera called on an already released recorder!");
default:
break;
}
mCameraDevice = cameraDevice;
}
public void setProfile(CamcorderProfile profile) {
switch (mState) {
case STATE_RECORD:
throw new RuntimeException("setProfile cannot be called while recording!");
case STATE_RELEASED:
throw new RuntimeException("setProfile called on an already released recorder!");
default:
break;
}
mProfile = profile;
}
public void setOutputFile(String outputFile) {
switch (mState) {
case STATE_RECORD:
throw new RuntimeException("setOutputFile cannot be called while recording!");
case STATE_RELEASED:
throw new RuntimeException("setOutputFile called on an already released recorder!");
default:
break;
}
mOutputFile = outputFile;
mFd = null;
}
public void setOutputFile(FileDescriptor fd) {
switch (mState) {
case STATE_RECORD:
throw new RuntimeException("setOutputFile cannot be called while recording!");
case STATE_RELEASED:
throw new RuntimeException("setOutputFile called on an already released recorder!");
default:
break;
}
mOutputFile = null;
mFd = fd;
}
/**
* Sets the maximum filesize (in bytes) of the recording session.
* This will be passed on to the MediaEncoderFilter and then to the
* MediaRecorder ultimately. If zero or negative, the MediaRecorder will
* disable the limit
*/
public synchronized void setMaxFileSize(long maxFileSize) {
switch (mState) {
case STATE_RECORD:
throw new RuntimeException("setMaxFileSize cannot be called while recording!");
case STATE_RELEASED:
throw new RuntimeException(
"setMaxFileSize called on an already released recorder!");
default:
break;
}
mMaxFileSize = maxFileSize;
}
/**
* Sets the maximum recording duration (in ms) for the next recording session
* Setting it to zero (the default) disables the limit.
*/
public synchronized void setMaxDuration(int maxDurationMs) {
switch (mState) {
case STATE_RECORD:
throw new RuntimeException("setMaxDuration cannot be called while recording!");
case STATE_RELEASED:
throw new RuntimeException(
"setMaxDuration called on an already released recorder!");
default:
break;
}
mMaxDurationMs = maxDurationMs;
}
public void setCaptureRate(double fps) {
switch (mState) {
case STATE_RECORD:
throw new RuntimeException("setCaptureRate cannot be called while recording!");
case STATE_RELEASED:
throw new RuntimeException(
"setCaptureRate called on an already released recorder!");
default:
break;
}
if (mLogVerbose) Log.v(TAG, "Setting time lapse capture rate to " + fps + " fps");
mCaptureRate = fps;
}
public void setPreviewSurfaceTexture(SurfaceTexture previewSurfaceTexture,
int previewWidth,
int previewHeight) {
if (mLogVerbose) Log.v(TAG, "setPreviewSurfaceTexture(" + this + ")");
switch (mState) {
case STATE_RECORD:
throw new RuntimeException(
"setPreviewSurfaceTexture cannot be called while recording!");
case STATE_RELEASED:
throw new RuntimeException(
"setPreviewSurfaceTexture called on an already released recorder!");
default:
break;
}
mPreviewSurfaceTexture = previewSurfaceTexture;
mPreviewWidth = previewWidth;
mPreviewHeight = previewHeight;
switch (mState) {
case STATE_WAITING_FOR_SURFACE:
startPreview();
break;
case STATE_STARTING_PREVIEW:
case STATE_PREVIEW:
initializeEffect(true);
break;
}
}
public void setEffect(int effect, Object effectParameter) {
if (mLogVerbose) Log.v(TAG,
"setEffect: effect ID " + effect +
", parameter " + effectParameter.toString());
switch (mState) {
case STATE_RECORD:
throw new RuntimeException("setEffect cannot be called while recording!");
case STATE_RELEASED:
throw new RuntimeException("setEffect called on an already released recorder!");
default:
break;
}
mEffect = effect;
mEffectParameter = effectParameter;
if (mState == STATE_PREVIEW ||
mState == STATE_STARTING_PREVIEW) {
initializeEffect(false);
}
}
public interface EffectsListener {
public void onEffectsUpdate(int effectId, int effectMsg);
public void onEffectsError(Exception exception, String filePath);
}
public void setEffectsListener(EffectsListener listener) {
mEffectsListener = listener;
}
private void setFaceDetectOrientation() {
if (mCurrentEffect == EFFECT_GOOFY_FACE) {
Object rotateFilter = getGraphFilter(mRunner, "rotate");
Object metaRotateFilter = getGraphFilter(mRunner, "metarotate");
setInputValue(rotateFilter, "rotation", mOrientationHint);
int reverseDegrees = (360 - mOrientationHint) % 360;
setInputValue(metaRotateFilter, "rotation", reverseDegrees);
}
}
private void setRecordingOrientation() {
if (mState != STATE_RECORD && mRunner != null) {
Object bl = newInstance(sCtPoint, new Object[] {0, 0});
Object br = newInstance(sCtPoint, new Object[] {1, 0});
Object tl = newInstance(sCtPoint, new Object[] {0, 1});
Object tr = newInstance(sCtPoint, new Object[] {1, 1});
Object recordingRegion;
if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_BACK) {
// The back camera is not mirrored, so use a identity transform
recordingRegion = newInstance(sCtQuad, new Object[] {bl, br, tl, tr});
} else {
// Recording region needs to be tweaked for front cameras, since they
// mirror their preview
if (mOrientationHint == 0 || mOrientationHint == 180) {
// Horizontal flip in landscape
recordingRegion = newInstance(sCtQuad, new Object[] {br, bl, tr, tl});
} else {
// Horizontal flip in portrait
recordingRegion = newInstance(sCtQuad, new Object[] {tl, tr, bl, br});
}
}
Object recorder = getGraphFilter(mRunner, "recorder");
setInputValue(recorder, "inputRegion", recordingRegion);
}
}
public void setOrientationHint(int degrees) {
switch (mState) {
case STATE_RELEASED:
throw new RuntimeException(
"setOrientationHint called on an already released recorder!");
default:
break;
}
if (mLogVerbose) Log.v(TAG, "Setting orientation hint to: " + degrees);
mOrientationHint = degrees;
setFaceDetectOrientation();
setRecordingOrientation();
}
public void setCameraDisplayOrientation(int orientation) {
if (mState != STATE_CONFIGURE) {
throw new RuntimeException(
"setCameraDisplayOrientation called after configuration!");
}
mCameraDisplayOrientation = orientation;
}
public void setCameraFacing(int facing) {
switch (mState) {
case STATE_RELEASED:
throw new RuntimeException(
"setCameraFacing called on alrady released recorder!");
default:
break;
}
mCameraFacing = facing;
setRecordingOrientation();
}
public void setOnInfoListener(MediaRecorder.OnInfoListener infoListener) {
switch (mState) {
case STATE_RECORD:
throw new RuntimeException("setInfoListener cannot be called while recording!");
case STATE_RELEASED:
throw new RuntimeException(
"setInfoListener called on an already released recorder!");
default:
break;
}
mInfoListener = infoListener;
}
public void setOnErrorListener(MediaRecorder.OnErrorListener errorListener) {
switch (mState) {
case STATE_RECORD:
throw new RuntimeException("setErrorListener cannot be called while recording!");
case STATE_RELEASED:
throw new RuntimeException(
"setErrorListener called on an already released recorder!");
default:
break;
}
mErrorListener = errorListener;
}
private void initializeFilterFramework() {
mGraphEnv = newInstance(sCtGraphEnvironment);
invoke(mGraphEnv, sGraphEnvironmentCreateGLEnvironment);
int videoFrameWidth = mProfile.videoFrameWidth;
int videoFrameHeight = mProfile.videoFrameHeight;
if (mCameraDisplayOrientation == 90 || mCameraDisplayOrientation == 270) {
int tmp = videoFrameWidth;
videoFrameWidth = videoFrameHeight;
videoFrameHeight = tmp;
}
invoke(mGraphEnv, sGraphEnvironmentAddReferences,
new Object[] {new Object[] {
"textureSourceCallback", mSourceReadyCallback,
"recordingWidth", videoFrameWidth,
"recordingHeight", videoFrameHeight,
"recordingProfile", mProfile,
"learningDoneListener", mLearningDoneListener,
"recordingDoneListener", mRecordingDoneListener}});
mRunner = null;
mGraphId = -1;
mCurrentEffect = EFFECT_NONE;
}
private synchronized void initializeEffect(boolean forceReset) {
if (forceReset ||
mCurrentEffect != mEffect ||
mCurrentEffect == EFFECT_BACKDROPPER) {
invoke(mGraphEnv, sGraphEnvironmentAddReferences,
new Object[] {new Object[] {
"previewSurfaceTexture", mPreviewSurfaceTexture,
"previewWidth", mPreviewWidth,
"previewHeight", mPreviewHeight,
"orientation", mOrientationHint}});
if (mState == STATE_PREVIEW ||
mState == STATE_STARTING_PREVIEW) {
// Switching effects while running. Inform video camera.
sendMessage(mCurrentEffect, EFFECT_MSG_SWITCHING_EFFECT);
}
switch (mEffect) {
case EFFECT_GOOFY_FACE:
mGraphId = (Integer) invoke(mGraphEnv,
sGraphEnvironmentLoadGraph,
new Object[] {mContext, R.raw.goofy_face});
break;
case EFFECT_BACKDROPPER:
sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_STARTED_LEARNING);
mGraphId = (Integer) invoke(mGraphEnv,
sGraphEnvironmentLoadGraph,
new Object[] {mContext, R.raw.backdropper});
break;
default:
throw new RuntimeException("Unknown effect ID" + mEffect + "!");
}
mCurrentEffect = mEffect;
mOldRunner = mRunner;
mRunner = invoke(mGraphEnv, sGraphEnvironmentGetRunner,
new Object[] {mGraphId,
getConstant(sClsGraphEnvironment, "MODE_ASYNCHRONOUS")});
invoke(mRunner, sGraphRunnerSetDoneCallback, new Object[] {mRunnerDoneCallback});
if (mLogVerbose) {
Log.v(TAG, "New runner: " + mRunner
+ ". Old runner: " + mOldRunner);
}
if (mState == STATE_PREVIEW ||
mState == STATE_STARTING_PREVIEW) {
// Switching effects while running. Stop existing runner.
// The stop callback will take care of starting new runner.
mCameraDevice.stopPreview();
mCameraDevice.setPreviewTextureAsync(null);
invoke(mOldRunner, sGraphRunnerStop);
}
}
switch (mCurrentEffect) {
case EFFECT_GOOFY_FACE:
tryEnableVideoStabilization(true);
Object goofyFilter = getGraphFilter(mRunner, "goofyrenderer");
setInputValue(goofyFilter, "currentEffect",
((Integer) mEffectParameter).intValue());
break;
case EFFECT_BACKDROPPER:
tryEnableVideoStabilization(false);
Object backgroundSrc = getGraphFilter(mRunner, "background");
if (ApiHelper.HAS_EFFECTS_RECORDING_CONTEXT_INPUT) {
// Set the context first before setting sourceUrl to
// guarantee the content URI get resolved properly.
setInputValue(backgroundSrc, "context", mContext);
}
setInputValue(backgroundSrc, "sourceUrl", mEffectParameter);
// For front camera, the background video needs to be mirrored in the
// backdropper filter
if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
Object replacer = getGraphFilter(mRunner, "replacer");
setInputValue(replacer, "mirrorBg", true);
if (mLogVerbose) Log.v(TAG, "Setting the background to be mirrored");
}
break;
default:
break;
}
setFaceDetectOrientation();
setRecordingOrientation();
}
public synchronized void startPreview() {
if (mLogVerbose) Log.v(TAG, "Starting preview (" + this + ")");
switch (mState) {
case STATE_STARTING_PREVIEW:
case STATE_PREVIEW:
// Already running preview
Log.w(TAG, "startPreview called when already running preview");
return;
case STATE_RECORD:
throw new RuntimeException("Cannot start preview when already recording!");
case STATE_RELEASED:
throw new RuntimeException("setEffect called on an already released recorder!");
default:
break;
}
if (mEffect == EFFECT_NONE) {
throw new RuntimeException("No effect selected!");
}
if (mEffectParameter == null) {
throw new RuntimeException("No effect parameter provided!");
}
if (mProfile == null) {
throw new RuntimeException("No recording profile provided!");
}
if (mPreviewSurfaceTexture == null) {
if (mLogVerbose) Log.v(TAG, "Passed a null surface; waiting for valid one");
mState = STATE_WAITING_FOR_SURFACE;
return;
}
if (mCameraDevice == null) {
throw new RuntimeException("No camera to record from!");
}
if (mLogVerbose) Log.v(TAG, "Initializing filter framework and running the graph.");
initializeFilterFramework();
initializeEffect(true);
mState = STATE_STARTING_PREVIEW;
invoke(mRunner, sGraphRunnerRun);
// Rest of preview startup handled in mSourceReadyCallback
}
private Object invokeObjectEquals(Object proxy, Object[] args) {
return Boolean.valueOf(proxy == args[0]);
}
private Object invokeObjectToString() {
return "Proxy-" + toString();
}
private void invokeOnLearningDone() {
if (mLogVerbose) Log.v(TAG, "Learning done callback triggered");
// Called in a processing thread, so have to post message back to UI
// thread
sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_DONE_LEARNING);
enable3ALocks(true);
}
private void invokeOnRunnerDone(Object[] args) {
int runnerDoneResult = (Integer) args[0];
synchronized (EffectsRecorder.this) {
if (mLogVerbose) {
Log.v(TAG,
"Graph runner done (" + EffectsRecorder.this
+ ", mRunner " + mRunner
+ ", mOldRunner " + mOldRunner + ")");
}
if (runnerDoneResult ==
(Integer) getConstant(sClsGraphRunner, "RESULT_ERROR")) {
// Handle error case
Log.e(TAG, "Error running filter graph!");
Exception e = null;
if (mRunner != null) {
e = (Exception) invoke(mRunner, sGraphRunnerGetError);
} else if (mOldRunner != null) {
e = (Exception) invoke(mOldRunner, sGraphRunnerGetError);
}
raiseError(e);
}
if (mOldRunner != null) {
// Tear down old graph if available
if (mLogVerbose) Log.v(TAG, "Tearing down old graph.");
Object glEnv = getContextGLEnvironment(mGraphEnv);
if (glEnv != null && !(Boolean) invoke(glEnv, sGLEnvironmentIsActive)) {
invoke(glEnv, sGLEnvironmentActivate);
}
getGraphTearDown(mOldRunner,
invoke(mGraphEnv, sGraphEnvironmentGetContext));
if (glEnv != null && (Boolean) invoke(glEnv, sGLEnvironmentIsActive)) {
invoke(glEnv, sGLEnvironmentDeactivate);
}
mOldRunner = null;
}
if (mState == STATE_PREVIEW ||
mState == STATE_STARTING_PREVIEW) {
// Switching effects, start up the new runner
if (mLogVerbose) {
Log.v(TAG, "Previous effect halted. Running graph again. state: "
+ mState);
}
tryEnable3ALocks(false);
// In case of an error, the graph restarts from beginning and in case
// of the BACKDROPPER effect, the learner re-learns the background.
// Hence, we need to show the learning dialogue to the user
// to avoid recording before the learning is done. Else, the user
// could start recording before the learning is done and the new
// background comes up later leading to an end result video
// with a heterogeneous background.
// For BACKDROPPER effect, this path is also executed sometimes at
// the end of a normal recording session. In such a case, the graph
// does not restart and hence the learner does not re-learn. So we
// do not want to show the learning dialogue then.
if (runnerDoneResult == (Integer) getConstant(
sClsGraphRunner, "RESULT_ERROR")
&& mCurrentEffect == EFFECT_BACKDROPPER) {
sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_STARTED_LEARNING);
}
invoke(mRunner, sGraphRunnerRun);
} else if (mState != STATE_RELEASED) {
// Shutting down effects
if (mLogVerbose) Log.v(TAG, "Runner halted, restoring direct preview");
tryEnable3ALocks(false);
sendMessage(EFFECT_NONE, EFFECT_MSG_EFFECTS_STOPPED);
} else {
// STATE_RELEASED - camera will be/has been released as well, do nothing.
}
}
}
private void invokeOnSurfaceTextureSourceReady(Object[] args) {
SurfaceTexture source = (SurfaceTexture) args[0];
if (mLogVerbose) Log.v(TAG, "SurfaceTexture ready callback received");
synchronized (EffectsRecorder.this) {
mTextureSource = source;
if (mState == STATE_CONFIGURE) {
// Stop preview happened while the runner was doing startup tasks
// Since we haven't started anything up, don't do anything
// Rest of cleanup will happen in onRunnerDone
if (mLogVerbose) Log.v(TAG, "Ready callback: Already stopped, skipping.");
return;
}
if (mState == STATE_RELEASED) {
// EffectsRecorder has been released, so don't touch the camera device
// or anything else
if (mLogVerbose) Log.v(TAG, "Ready callback: Already released, skipping.");
return;
}
if (source == null) {
if (mLogVerbose) {
Log.v(TAG, "Ready callback: source null! Looks like graph was closed!");
}
if (mState == STATE_PREVIEW ||
mState == STATE_STARTING_PREVIEW ||
mState == STATE_RECORD) {
// A null source here means the graph is shutting down
// unexpectedly, so we need to turn off preview before
// the surface texture goes away.
if (mLogVerbose) {
Log.v(TAG, "Ready callback: State: " + mState
+ ". stopCameraPreview");
}
stopCameraPreview();
}
return;
}
// Lock AE/AWB to reduce transition flicker
tryEnable3ALocks(true);
mCameraDevice.stopPreview();
if (mLogVerbose) Log.v(TAG, "Runner active, connecting effects preview");
mCameraDevice.setPreviewTextureAsync(mTextureSource);
mCameraDevice.startPreviewAsync();
// Unlock AE/AWB after preview started
tryEnable3ALocks(false);
mState = STATE_PREVIEW;
if (mLogVerbose) Log.v(TAG, "Start preview/effect switch complete");
// Sending a message to listener that preview is complete
sendMessage(mCurrentEffect, EFFECT_MSG_PREVIEW_RUNNING);
}
}
private void invokeOnRecordingDone() {
// Forward the callback to the VideoModule object (as an asynchronous event).
if (mLogVerbose) Log.v(TAG, "Recording done callback triggered");
sendMessage(EFFECT_NONE, EFFECT_MSG_RECORDING_DONE);
}
public synchronized void startRecording() {
if (mLogVerbose) Log.v(TAG, "Starting recording (" + this + ")");
switch (mState) {
case STATE_RECORD:
throw new RuntimeException("Already recording, cannot begin anew!");
case STATE_RELEASED:
throw new RuntimeException(
"startRecording called on an already released recorder!");
default:
break;
}
if ((mOutputFile == null) && (mFd == null)) {
throw new RuntimeException("No output file name or descriptor provided!");
}
if (mState == STATE_CONFIGURE) {
startPreview();
}
Object recorder = getGraphFilter(mRunner, "recorder");
if (mFd != null) {
setInputValue(recorder, "outputFileDescriptor", mFd);
} else {
setInputValue(recorder, "outputFile", mOutputFile);
}
// It is ok to set the audiosource without checking for timelapse here
// since that check will be done in the MediaEncoderFilter itself
setInputValue(recorder, "audioSource", MediaRecorder.AudioSource.CAMCORDER);
setInputValue(recorder, "recordingProfile", mProfile);
setInputValue(recorder, "orientationHint", mOrientationHint);
// Important to set the timelapseinterval to 0 if the capture rate is not >0
// since the recorder does not get created every time the recording starts.
// The recorder infers whether the capture is timelapsed based on the value of
// this interval
boolean captureTimeLapse = mCaptureRate > 0;
if (captureTimeLapse) {
double timeBetweenFrameCapture = 1 / mCaptureRate;
setInputValue(recorder, "timelapseRecordingIntervalUs",
(long) (1000000 * timeBetweenFrameCapture));
} else {
setInputValue(recorder, "timelapseRecordingIntervalUs", 0L);
}
if (mInfoListener != null) {
setInputValue(recorder, "infoListener", mInfoListener);
}
if (mErrorListener != null) {
setInputValue(recorder, "errorListener", mErrorListener);
}
setInputValue(recorder, "maxFileSize", mMaxFileSize);
setInputValue(recorder, "maxDurationMs", mMaxDurationMs);
setInputValue(recorder, "recording", true);
mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING);
mState = STATE_RECORD;
}
public synchronized void stopRecording() {
if (mLogVerbose) Log.v(TAG, "Stop recording (" + this + ")");
switch (mState) {
case STATE_CONFIGURE:
case STATE_STARTING_PREVIEW:
case STATE_PREVIEW:
Log.w(TAG, "StopRecording called when recording not active!");
return;
case STATE_RELEASED:
throw new RuntimeException("stopRecording called on released EffectsRecorder!");
default:
break;
}
Object recorder = getGraphFilter(mRunner, "recorder");
setInputValue(recorder, "recording", false);
mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING);
mState = STATE_PREVIEW;
}
// Called to tell the filter graph that the display surfacetexture is not valid anymore.
// So the filter graph should not hold any reference to the surface created with that.
public synchronized void disconnectDisplay() {
if (mLogVerbose) Log.v(TAG, "Disconnecting the graph from the " +
"SurfaceTexture");
Object display = getGraphFilter(mRunner, "display");
invoke(display, sSurfaceTextureTargetDisconnect, new Object[] {
invoke(mGraphEnv, sGraphEnvironmentGetContext)});
}
// The VideoModule will call this to notify that the camera is being
// released to the outside world. This call should happen after the
// stopRecording call. Else, the effects may throw an exception.
// With the recording stopped, the stopPreview call will not try to
// release the camera again.
// This must be called in onPause() if the effects are ON.
public synchronized void disconnectCamera() {
if (mLogVerbose) Log.v(TAG, "Disconnecting the effects from Camera");
stopCameraPreview();
mCameraDevice = null;
}
// In a normal case, when the disconnect is not called, we should not
// set the camera device to null, since on return callback, we try to
// enable 3A locks, which need the cameradevice.
public synchronized void stopCameraPreview() {
if (mLogVerbose) Log.v(TAG, "Stopping camera preview.");
if (mCameraDevice == null) {
Log.d(TAG, "Camera already null. Nothing to disconnect");
return;
}
mCameraDevice.stopPreview();
mCameraDevice.setPreviewTextureAsync(null);
}
// Stop and release effect resources
public synchronized void stopPreview() {
if (mLogVerbose) Log.v(TAG, "Stopping preview (" + this + ")");
switch (mState) {
case STATE_CONFIGURE:
Log.w(TAG, "StopPreview called when preview not active!");
return;
case STATE_RELEASED:
throw new RuntimeException("stopPreview called on released EffectsRecorder!");
default:
break;
}
if (mState == STATE_RECORD) {
stopRecording();
}
mCurrentEffect = EFFECT_NONE;
// This will not do anything if the camera has already been disconnected.
stopCameraPreview();
mState = STATE_CONFIGURE;
mOldRunner = mRunner;
invoke(mRunner, sGraphRunnerStop);
mRunner = null;
// Rest of stop and release handled in mRunnerDoneCallback
}
// Try to enable/disable video stabilization if supported; otherwise return false
// It is called from a synchronized block.
boolean tryEnableVideoStabilization(boolean toggle) {
if (mLogVerbose) Log.v(TAG, "tryEnableVideoStabilization.");
if (mCameraDevice == null) {
Log.d(TAG, "Camera already null. Not enabling video stabilization.");
return false;
}
Camera.Parameters params = mCameraDevice.getParameters();
String vstabSupported = params.get("video-stabilization-supported");
if ("true".equals(vstabSupported)) {
if (mLogVerbose) Log.v(TAG, "Setting video stabilization to " + toggle);
params.set("video-stabilization", toggle ? "true" : "false");
mCameraDevice.setParameters(params);
return true;
}
if (mLogVerbose) Log.v(TAG, "Video stabilization not supported");
return false;
}
// Try to enable/disable 3A locks if supported; otherwise return false
@TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
synchronized boolean tryEnable3ALocks(boolean toggle) {
if (mLogVerbose) Log.v(TAG, "tryEnable3ALocks");
if (mCameraDevice == null) {
Log.d(TAG, "Camera already null. Not tryenabling 3A locks.");
return false;
}
Camera.Parameters params = mCameraDevice.getParameters();
if (Util.isAutoExposureLockSupported(params) &&
Util.isAutoWhiteBalanceLockSupported(params)) {
params.setAutoExposureLock(toggle);
params.setAutoWhiteBalanceLock(toggle);
mCameraDevice.setParameters(params);
return true;
}
return false;
}
// Try to enable/disable 3A locks if supported; otherwise, throw error
// Use this when locks are essential to success
synchronized void enable3ALocks(boolean toggle) {
if (mLogVerbose) Log.v(TAG, "Enable3ALocks");
if (mCameraDevice == null) {
Log.d(TAG, "Camera already null. Not enabling 3A locks.");
return;
}
Camera.Parameters params = mCameraDevice.getParameters();
if (!tryEnable3ALocks(toggle)) {
throw new RuntimeException("Attempt to lock 3A on camera with no locking support!");
}
}
static class SerializableInvocationHandler
implements InvocationHandler, Serializable {
private final int mEffectsRecorderIndex;
public SerializableInvocationHandler(int index) {
mEffectsRecorderIndex = index;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if (sEffectsRecorder == null) return null;
if (mEffectsRecorderIndex != sEffectsRecorderIndex) {
Log.v(TAG, "Ignore old callback " + mEffectsRecorderIndex);
return null;
}
if (method.equals(sObjectEquals)) {
return sEffectsRecorder.invokeObjectEquals(proxy, args);
} else if (method.equals(sObjectToString)) {
return sEffectsRecorder.invokeObjectToString();
} else if (method.equals(sLearningDoneListenerOnLearningDone)) {
sEffectsRecorder.invokeOnLearningDone();
} else if (method.equals(sOnRunnerDoneListenerOnRunnerDone)) {
sEffectsRecorder.invokeOnRunnerDone(args);
} else if (method.equals(
sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady)) {
sEffectsRecorder.invokeOnSurfaceTextureSourceReady(args);
} else if (method.equals(sOnRecordingDoneListenerOnRecordingDone)) {
sEffectsRecorder.invokeOnRecordingDone();
}
return null;
}
}
// Indicates that all camera/recording activity needs to halt
public synchronized void release() {
if (mLogVerbose) Log.v(TAG, "Releasing (" + this + ")");
switch (mState) {
case STATE_RECORD:
case STATE_STARTING_PREVIEW:
case STATE_PREVIEW:
stopPreview();
// Fall-through
default:
if (mSoundPlayer != null) {
mSoundPlayer.release();
mSoundPlayer = null;
}
mState = STATE_RELEASED;
break;
}
sEffectsRecorder = null;
}
private void sendMessage(final int effect, final int msg) {
if (mEffectsListener != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
mEffectsListener.onEffectsUpdate(effect, msg);
}
});
}
}
private void raiseError(final Exception exception) {
if (mEffectsListener != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
if (mFd != null) {
mEffectsListener.onEffectsError(exception, null);
} else {
mEffectsListener.onEffectsError(exception, mOutputFile);
}
}
});
}
}
// invoke method on receiver with no arguments
private Object invoke(Object receiver, Method method) {
try {
return method.invoke(receiver);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
// invoke method on receiver with arguments
private Object invoke(Object receiver, Method method, Object[] args) {
try {
return method.invoke(receiver, args);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private void setInputValue(Object receiver, String key, Object value) {
try {
sFilterSetInputValue.invoke(receiver, new Object[] {key, value});
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private Object newInstance(Constructor<?> ct, Object[] initArgs) {
try {
return ct.newInstance(initArgs);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private Object newInstance(Constructor<?> ct) {
try {
return ct.newInstance();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private Object getGraphFilter(Object receiver, String name) {
try {
return sFilterGraphGetFilter.invoke(sGraphRunnerGetGraph
.invoke(receiver), new Object[] {name});
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private Object getContextGLEnvironment(Object receiver) {
try {
return sFilterContextGetGLEnvironment
.invoke(sGraphEnvironmentGetContext.invoke(receiver));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private void getGraphTearDown(Object receiver, Object filterContext) {
try {
sFilterGraphTearDown.invoke(sGraphRunnerGetGraph.invoke(receiver),
new Object[]{filterContext});
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private Object getConstant(Class<?> cls, String name) {
try {
return cls.getDeclaredField(name).get(null);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}