diff options
Diffstat (limited to 'src/com/android/camera/EffectsRecorder.java')
-rw-r--r-- | src/com/android/camera/EffectsRecorder.java | 1239 |
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); + } + } +} |