summaryrefslogtreecommitdiffstats
path: root/src/com/android/camera/EffectsRecorder.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/camera/EffectsRecorder.java')
-rw-r--r--src/com/android/camera/EffectsRecorder.java1239
1 files changed, 1239 insertions, 0 deletions
diff --git a/src/com/android/camera/EffectsRecorder.java b/src/com/android/camera/EffectsRecorder.java
new file mode 100644
index 000000000..4bf8d411e
--- /dev/null
+++ b/src/com/android/camera/EffectsRecorder.java
@@ -0,0 +1,1239 @@
+/*
+ * 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.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.io.FileDescriptor;
+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.setPreviewTexture(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.setPreviewTexture(mTextureSource);
+
+ mCameraDevice.startPreview();
+
+ // 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.setPreviewTexture(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);
+ }
+ }
+}