| /* |
| * 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); |
| } |
| } |
| } |