EffectsRecorder.java revision 07d5f8c27d144f7e72820b48fe5cb08f691435ad
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.camera;
18
19import android.content.Context;
20import android.filterfw.GraphEnvironment;
21import android.filterfw.core.Filter;
22import android.filterfw.core.GLEnvironment;
23import android.filterfw.core.GraphRunner;
24import android.filterfw.core.GraphRunner.OnRunnerDoneListener;
25import android.filterfw.geometry.Point;
26import android.filterfw.geometry.Quad;
27import android.filterpacks.videosrc.SurfaceTextureSource.SurfaceTextureSourceListener;
28import android.filterpacks.videoproc.BackDropperFilter;
29import android.filterpacks.videoproc.BackDropperFilter.LearningDoneListener;
30
31import android.graphics.SurfaceTexture;
32import android.hardware.Camera;
33import android.media.MediaRecorder;
34import android.media.CamcorderProfile;
35import android.os.ConditionVariable;
36import android.os.Handler;
37import android.os.Looper;
38import android.os.ParcelFileDescriptor;
39import android.util.Log;
40import android.view.Surface;
41import android.view.SurfaceHolder;
42
43import java.io.IOException;
44import java.lang.Runnable;
45
46/**
47 * Encapsulates the mobile filter framework components needed to record video with
48 * effects applied. Modeled after MediaRecorder.
49 */
50public class EffectsRecorder {
51
52    public static final int  EFFECT_NONE        = 0;
53    public static final int  EFFECT_GOOFY_FACE  = 1;
54    public static final int  EFFECT_BACKDROPPER = 2;
55
56    public static final int  EFFECT_GF_SQUEEZE     = 0;
57    public static final int  EFFECT_GF_BIG_EYES    = 1;
58    public static final int  EFFECT_GF_BIG_MOUTH   = 2;
59    public static final int  EFFECT_GF_SMALL_MOUTH = 3;
60    public static final int  EFFECT_GF_BIG_NOSE    = 4;
61    public static final int  EFFECT_GF_SMALL_EYES  = 5;
62    public static final int  NUM_OF_GF_EFFECTS = EFFECT_GF_SMALL_EYES + 1;
63
64    public static final int  EFFECT_MSG_STARTED_LEARNING = 0;
65    public static final int  EFFECT_MSG_DONE_LEARNING    = 1;
66    public static final int  EFFECT_MSG_SWITCHING_EFFECT = 2;
67    public static final int  EFFECT_MSG_EFFECTS_STOPPED  = 3;
68
69    private Context mContext;
70    private Handler mHandler;
71    private boolean mReleased;
72
73    private Camera mCameraDevice;
74    private CamcorderProfile mProfile;
75    private SurfaceHolder mPreviewSurfaceHolder;
76    private int mPreviewWidth;
77    private int mPreviewHeight;
78    private MediaRecorder.OnInfoListener mInfoListener;
79    private MediaRecorder.OnErrorListener mErrorListener;
80
81    private String mOutputFile;
82    private int mOrientationHint = 0;
83    private int mCameraFacing = Camera.CameraInfo.CAMERA_FACING_BACK;
84
85    private int mEffect = EFFECT_NONE;
86    private int mCurrentEffect = EFFECT_NONE;
87    private EffectsListener mEffectsListener;
88
89    private Object mEffectParameter;
90
91    private GraphEnvironment mGraphEnv;
92    private int mGraphId;
93    private GraphRunner mRunner = null;
94    private GraphRunner mOldRunner = null;
95
96    private SurfaceTexture mTextureSource;
97
98    private static final int STATE_CONFIGURE              = 0;
99    private static final int STATE_WAITING_FOR_SURFACE    = 1;
100    private static final int STATE_PREVIEW                = 2;
101    private static final int STATE_RECORD                 = 3;
102    private static final int STATE_RELEASED               = 4;
103    private int mState = STATE_CONFIGURE;
104
105    private boolean mLogVerbose = true; //Log.isLoggable(TAG, Log.VERBOSE);
106    private static final String TAG = "effectsrecorder";
107
108    /** Determine if a given effect is supported at runtime
109     * Some effects require libraries not available on all devices
110     */
111    public static boolean isEffectSupported(int effectId) {
112        switch (effectId) {
113            case EFFECT_GOOFY_FACE:
114                return Filter.isAvailable("com.google.android.filterpacks.facedetect.GoofyRenderFilter");
115            case EFFECT_BACKDROPPER:
116                return Filter.isAvailable("android.filterpacks.videoproc.BackDropperFilter");
117            default:
118                return false;
119        }
120    }
121
122    public EffectsRecorder(Context context) {
123        if (mLogVerbose) Log.v(TAG, "EffectsRecorder created (" + this + ")");
124        mContext = context;
125        mHandler = new Handler(Looper.getMainLooper());
126    }
127
128    public void setCamera(Camera cameraDevice) {
129        switch (mState) {
130            case STATE_PREVIEW:
131                throw new RuntimeException("setCamera cannot be called while previewing!");
132            case STATE_RECORD:
133                throw new RuntimeException("setCamera cannot be called while recording!");
134            case STATE_RELEASED:
135                throw new RuntimeException("setCamera called on an already released recorder!");
136            default:
137                break;
138        }
139
140        mCameraDevice = cameraDevice;
141    }
142
143    public void setProfile(CamcorderProfile profile) {
144        switch (mState) {
145            case STATE_RECORD:
146                throw new RuntimeException("setProfile cannot be called while recording!");
147            case STATE_RELEASED:
148                throw new RuntimeException("setProfile called on an already released recorder!");
149            default:
150                break;
151        }
152        mProfile = profile;
153    }
154
155    public void setOutputFile(String outputFile) {
156        switch (mState) {
157            case STATE_RECORD:
158                throw new RuntimeException("setOutputFile cannot be called while recording!");
159            case STATE_RELEASED:
160                throw new RuntimeException("setOutputFile called on an already released recorder!");
161            default:
162                break;
163        }
164
165        mOutputFile = outputFile;
166    }
167
168    public void setPreviewDisplay(SurfaceHolder previewSurfaceHolder,
169                                  int previewWidth,
170                                  int previewHeight) {
171        if (mLogVerbose) Log.v(TAG, "setPreviewDisplay (" + this + ")");
172        switch (mState) {
173            case STATE_RECORD:
174                throw new RuntimeException("setPreviewDisplay cannot be called while recording!");
175            case STATE_RELEASED:
176                throw new RuntimeException("setPreviewDisplay called on an already released recorder!");
177            default:
178                break;
179        }
180
181        mPreviewSurfaceHolder = previewSurfaceHolder;
182        mPreviewWidth = previewWidth;
183        mPreviewHeight = previewHeight;
184
185        switch (mState) {
186            case STATE_WAITING_FOR_SURFACE:
187                startPreview();
188                break;
189            case STATE_PREVIEW:
190                initializeEffect(true);
191                break;
192        }
193    }
194
195    public void setEffect(int effect, Object effectParameter) {
196        if (mLogVerbose) Log.v(TAG,
197                               "setEffect: effect ID " + effect +
198                               ", parameter " + effectParameter.toString() );
199        switch (mState) {
200            case STATE_RECORD:
201                throw new RuntimeException("setEffect cannot be called while recording!");
202            case STATE_RELEASED:
203                throw new RuntimeException("setEffect called on an already released recorder!");
204            default:
205                break;
206        }
207
208        mEffect = effect;
209        mEffectParameter = effectParameter;
210
211        if (mState == STATE_PREVIEW) {
212            initializeEffect(false);
213        }
214    }
215
216    public interface EffectsListener {
217        public void onEffectsUpdate(int effectId, int effectMsg);
218        public void onEffectsError(Exception exception, String filePath);
219    }
220
221    public void setEffectsListener(EffectsListener listener) {
222        mEffectsListener = listener;
223    }
224
225    private void setFaceDetectOrientation(int degrees) {
226        if (mCurrentEffect == EFFECT_GOOFY_FACE) {
227            Filter rotateFilter = mRunner.getGraph().getFilter("rotate");
228            Filter metaRotateFilter = mRunner.getGraph().getFilter("metarotate");
229            rotateFilter.setInputValue("rotation", degrees);
230            int reverseDegrees = (360 - degrees) % 360;
231            metaRotateFilter.setInputValue("rotation", reverseDegrees);
232        }
233    }
234
235    private void setRecordingOrientation() {
236        if ( mState <= STATE_PREVIEW && mRunner != null ) {
237            Point bl = new Point(0, 0);
238            Point br = new Point(1, 0);
239            Point tl = new Point(0, 1);
240            Point tr = new Point(1, 1);
241            Quad recordingRegion;
242            if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_BACK) {
243                // The back camera is not mirrored, so use a identity transform
244                recordingRegion = new Quad(bl, br, tl, tr);
245            } else {
246                // Recording region needs to be tweaked for front cameras, since they
247                // mirror their preview
248                if (mOrientationHint == 0 || mOrientationHint == 180) {
249                    // Horizontal flip in landscape
250                    recordingRegion = new Quad(br, bl, tr, tl);
251                } else {
252                    // Horizontal flip in portrait
253                    recordingRegion = new Quad(tl, tr, bl, br);
254                }
255            }
256            Filter recorder = mRunner.getGraph().getFilter("recorder");
257            recorder.setInputValue("inputRegion", recordingRegion);
258        }
259    }
260    public void setOrientationHint(int degrees) {
261        switch (mState) {
262            case STATE_RELEASED:
263                throw new RuntimeException(
264                        "setOrientationHint called on an already released recorder!");
265            default:
266                break;
267        }
268        if (mLogVerbose) Log.v(TAG, "Setting orientation hint to: " + degrees);
269
270        mOrientationHint = degrees;
271        setFaceDetectOrientation(degrees);
272        setRecordingOrientation();
273    }
274
275    public void setCameraFacing(int facing) {
276        switch (mState) {
277            case STATE_RELEASED:
278                throw new RuntimeException(
279                    "setCameraFacing called on alrady released recorder!");
280            default:
281                break;
282        }
283        mCameraFacing = facing;
284        setRecordingOrientation();
285    }
286
287    public void setOnInfoListener(MediaRecorder.OnInfoListener infoListener) {
288        switch (mState) {
289            case STATE_RECORD:
290                throw new RuntimeException("setInfoListener cannot be called while recording!");
291            case STATE_RELEASED:
292                throw new RuntimeException("setInfoListener called on an already released recorder!");
293            default:
294                break;
295        }
296        mInfoListener = infoListener;
297    }
298
299    public void setOnErrorListener(MediaRecorder.OnErrorListener errorListener) {
300        switch (mState) {
301            case STATE_RECORD:
302                throw new RuntimeException("setErrorListener cannot be called while recording!");
303            case STATE_RELEASED:
304                throw new RuntimeException("setErrorListener called on an already released recorder!");
305            default:
306                break;
307        }
308        mErrorListener = errorListener;
309    }
310
311    private void initializeFilterFramework() {
312        mGraphEnv = new GraphEnvironment();
313        mGraphEnv.createGLEnvironment();
314
315        if (mLogVerbose) {
316            Log.v(TAG, "Effects framework initializing. Recording size "
317                  + mProfile.videoFrameWidth + ", " + mProfile.videoFrameHeight);
318        }
319
320        mGraphEnv.addReferences(
321                "textureSourceCallback", mSourceReadyCallback,
322                "recordingWidth", mProfile.videoFrameWidth,
323                "recordingHeight", mProfile.videoFrameHeight,
324                "recordingProfile", mProfile,
325                "audioSource", MediaRecorder.AudioSource.CAMCORDER,
326                "learningDoneListener", mLearningDoneListener);
327
328        mRunner = null;
329        mGraphId = -1;
330        mCurrentEffect = EFFECT_NONE;
331    }
332
333    private synchronized void initializeEffect(boolean forceReset) {
334        if (forceReset ||
335            mCurrentEffect != mEffect ||
336            mCurrentEffect == EFFECT_BACKDROPPER) {
337            if (mLogVerbose) {
338                Log.v(TAG, "Effect initializing. Preview size "
339                       + mPreviewWidth + ", " + mPreviewHeight);
340            }
341
342            mGraphEnv.addReferences(
343                    "previewSurface", mPreviewSurfaceHolder.getSurface(),
344                    "previewWidth", mPreviewWidth,
345                    "previewHeight", mPreviewHeight,
346                    "orientation", mOrientationHint);
347            if (mState == STATE_PREVIEW) {
348                // Switching effects while running. Inform video camera.
349                sendMessage(mCurrentEffect, EFFECT_MSG_SWITCHING_EFFECT);
350            }
351
352            switch (mEffect) {
353                case EFFECT_GOOFY_FACE:
354                    mGraphId = mGraphEnv.loadGraph(mContext, R.raw.goofy_face);
355                    break;
356                case EFFECT_BACKDROPPER:
357                    sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_STARTED_LEARNING);
358                    mGraphId = mGraphEnv.loadGraph(mContext, R.raw.backdropper);
359                    break;
360                default:
361                    throw new RuntimeException("Unknown effect ID" + mEffect + "!");
362            }
363            mCurrentEffect = mEffect;
364
365            mOldRunner = mRunner;
366            mRunner = mGraphEnv.getRunner(mGraphId, GraphEnvironment.MODE_ASYNCHRONOUS);
367            mRunner.setDoneCallback(mRunnerDoneCallback);
368            if (mLogVerbose) {
369                Log.v(TAG, "New runner: " + mRunner
370                      + ". Old runner: " + mOldRunner);
371            }
372            if (mState == STATE_PREVIEW) {
373                // Switching effects while running. Stop existing runner.
374                // The stop callback will take care of starting new runner.
375                mCameraDevice.stopPreview();
376                try {
377                    mCameraDevice.setPreviewTexture(null);
378                } catch(IOException e) {
379                    throw new RuntimeException("Unable to connect camera to effect input", e);
380                }
381                mOldRunner.stop();
382            }
383        }
384
385        switch (mCurrentEffect) {
386            case EFFECT_GOOFY_FACE:
387                tryEnableVideoStabilization(true);
388                Filter goofyFilter = mRunner.getGraph().getFilter("goofyrenderer");
389                goofyFilter.setInputValue("currentEffect",
390                                          ((Integer)mEffectParameter).intValue());
391                break;
392            case EFFECT_BACKDROPPER:
393                tryEnableVideoStabilization(false);
394                Filter backgroundSrc = mRunner.getGraph().getFilter("background");
395                backgroundSrc.setInputValue("sourceUrl",
396                                            (String)mEffectParameter);
397                break;
398            default:
399                break;
400        }
401        setFaceDetectOrientation(mOrientationHint);
402        setRecordingOrientation();
403    }
404
405    public synchronized void startPreview() {
406        if (mLogVerbose) Log.v(TAG, "Starting preview (" + this + ")");
407
408        switch (mState) {
409            case STATE_PREVIEW:
410                // Already running preview
411                Log.w(TAG, "startPreview called when already running preview");
412                return;
413            case STATE_RECORD:
414                throw new RuntimeException("Cannot start preview when already recording!");
415            case STATE_RELEASED:
416                throw new RuntimeException("setEffect called on an already released recorder!");
417            default:
418                break;
419        }
420
421        if (mEffect == EFFECT_NONE) {
422            throw new RuntimeException("No effect selected!");
423        }
424        if (mEffectParameter == null) {
425            throw new RuntimeException("No effect parameter provided!");
426        }
427        if (mProfile == null) {
428            throw new RuntimeException("No recording profile provided!");
429        }
430        if (mPreviewSurfaceHolder == null) {
431            if (mLogVerbose) Log.v(TAG, "Passed a null surface holder; waiting for valid one");
432            mState = STATE_WAITING_FOR_SURFACE;
433            return;
434        }
435        if (mCameraDevice == null) {
436            throw new RuntimeException("No camera to record from!");
437        }
438
439        if (mLogVerbose) Log.v(TAG, "Initializing filter graph");
440
441        initializeFilterFramework();
442
443        initializeEffect(true);
444
445        if (mLogVerbose) Log.v(TAG, "Starting filter graph");
446
447        mRunner.run();
448        // Rest of preview startup handled in mSourceReadyCallback
449    }
450
451    private SurfaceTextureSourceListener mSourceReadyCallback =
452            new SurfaceTextureSourceListener() {
453        public void onSurfaceTextureSourceReady(SurfaceTexture source) {
454            if (mLogVerbose) Log.v(TAG, "SurfaceTexture ready callback received");
455            synchronized(EffectsRecorder.this) {
456                mTextureSource = source;
457
458                // When shutting down a graph, we receive a null SurfaceTexture to
459                // indicate that. Don't want to connect up the camera in that case.
460                if (source == null) return;
461
462                if (mState == STATE_RELEASED) return;
463
464                // Lock AE/AWB to reduce transition flicker
465                tryEnable3ALocks(true);
466
467                mCameraDevice.stopPreview();
468                if (mLogVerbose) Log.v(TAG, "Runner active, connecting effects preview");
469                try {
470                    mCameraDevice.setPreviewTexture(mTextureSource);
471                } catch(IOException e) {
472                    throw new RuntimeException("Unable to connect camera to effect input", e);
473                }
474
475                mCameraDevice.startPreview();
476
477                // Unlock AE/AWB after preview started
478                tryEnable3ALocks(false);
479
480                mState = STATE_PREVIEW;
481
482                if (mLogVerbose) Log.v(TAG, "Start preview/effect switch complete");
483            }
484        }
485    };
486
487    private LearningDoneListener mLearningDoneListener =
488            new LearningDoneListener() {
489        public void onLearningDone(BackDropperFilter filter) {
490            if (mLogVerbose) Log.v(TAG, "Learning done callback triggered");
491            // Called in a processing thread, so have to post message back to UI
492            // thread
493            sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_DONE_LEARNING);
494            enable3ALocks(true);
495        }
496    };
497
498    public synchronized void startRecording() {
499        if (mLogVerbose) Log.v(TAG, "Starting recording (" + this + ")");
500
501        switch (mState) {
502            case STATE_RECORD:
503                throw new RuntimeException("Already recording, cannot begin anew!");
504            case STATE_RELEASED:
505                throw new RuntimeException("startRecording called on an already released recorder!");
506            default:
507                break;
508        }
509
510        if (mOutputFile == null) {
511            throw new RuntimeException("No output file name provided!");
512        }
513
514        if (mState == STATE_CONFIGURE) {
515            startPreview();
516        }
517        Filter recorder = mRunner.getGraph().getFilter("recorder");
518        recorder.setInputValue("outputFile", mOutputFile);
519        recorder.setInputValue("orientationHint", mOrientationHint);
520        if (mInfoListener != null) {
521            recorder.setInputValue("infoListener", mInfoListener);
522        }
523        if (mErrorListener != null) {
524            recorder.setInputValue("errorListener", mErrorListener);
525        }
526        recorder.setInputValue("recording", true);
527        mState = STATE_RECORD;
528    }
529
530    public synchronized void stopRecording() {
531        if (mLogVerbose) Log.v(TAG, "Stop recording (" + this + ")");
532
533        switch (mState) {
534            case STATE_CONFIGURE:
535            case STATE_PREVIEW:
536                Log.w(TAG, "StopRecording called when recording not active!");
537                return;
538            case STATE_RELEASED:
539                throw new RuntimeException("stopRecording called on released EffectsRecorder!");
540            default:
541                break;
542        }
543        Filter recorder = mRunner.getGraph().getFilter("recorder");
544        recorder.setInputValue("recording", false);
545        mState = STATE_PREVIEW;
546    }
547
548    // Stop and release effect resources
549    public synchronized void stopPreview() {
550        if (mLogVerbose) Log.v(TAG, "Stopping preview (" + this + ")");
551
552        switch (mState) {
553            case STATE_CONFIGURE:
554                Log.w(TAG, "StopPreview called when preview not active!");
555                return;
556            case STATE_RELEASED:
557                throw new RuntimeException("stopPreview called on released EffectsRecorder!");
558            default:
559                break;
560        }
561
562        if (mState == STATE_RECORD) {
563            stopRecording();
564        }
565
566        mCurrentEffect = EFFECT_NONE;
567
568        mCameraDevice.stopPreview();
569        try {
570            mCameraDevice.setPreviewTexture(null);
571        } catch(IOException e) {
572            throw new RuntimeException("Unable to connect camera to effect input", e);
573        }
574
575        mState = STATE_CONFIGURE;
576        mOldRunner = mRunner;
577        mRunner.stop();
578        mRunner = null;
579        // Rest of stop and release handled in mRunnerDoneCallback
580    }
581
582    // Try to enable/disable video stabilization if supported; otherwise return false
583    boolean tryEnableVideoStabilization(boolean toggle) {
584        Camera.Parameters params = mCameraDevice.getParameters();
585
586        String vstabSupported = params.get("video-stabilization-supported");
587        if ("true".equals(vstabSupported)) {
588            if (mLogVerbose) Log.v(TAG, "Setting video stabilization to " + toggle);
589            params.set("video-stabilization", toggle ? "true" : "false");
590            mCameraDevice.setParameters(params);
591            return true;
592        }
593        if (mLogVerbose) Log.v(TAG, "Video stabilization not supported");
594        return false;
595    }
596
597    // Try to enable/disable 3A locks if supported; otherwise return false
598    boolean tryEnable3ALocks(boolean toggle) {
599        Camera.Parameters params = mCameraDevice.getParameters();
600        if (params.isAutoExposureLockSupported() &&
601            params.isAutoWhiteBalanceLockSupported() ) {
602            params.setAutoExposureLock(toggle);
603            params.setAutoWhiteBalanceLock(toggle);
604            mCameraDevice.setParameters(params);
605            return true;
606        }
607        return false;
608    }
609
610    // Try to enable/disable 3A locks if supported; otherwise, throw error
611    // Use this when locks are essential to success
612    void enable3ALocks(boolean toggle) {
613        Camera.Parameters params = mCameraDevice.getParameters();
614        if (!tryEnable3ALocks(toggle)) {
615            throw new RuntimeException("Attempt to lock 3A on camera with no locking support!");
616        }
617    }
618
619    private OnRunnerDoneListener mRunnerDoneCallback =
620            new OnRunnerDoneListener() {
621        public void onRunnerDone(int result) {
622            synchronized(EffectsRecorder.this) {
623                if (mLogVerbose) {
624                    Log.v(TAG,
625                          "Graph runner done (" + EffectsRecorder.this
626                          + ", mRunner " + mRunner
627                          + ", mOldRunner " + mOldRunner + ")");
628                }
629                if (result == GraphRunner.RESULT_ERROR) {
630                    // Handle error case
631                    Log.e(TAG, "Error running filter graph!");
632                    raiseError(mRunner == null ? null : mRunner.getError());
633                }
634                if (mOldRunner != null) {
635                    // Tear down old graph if available
636                    if (mLogVerbose) Log.v(TAG, "Tearing down old graph.");
637                    GLEnvironment glEnv = mGraphEnv.getContext().getGLEnvironment();
638                    if (glEnv != null && !glEnv.isActive()) {
639                        glEnv.activate();
640                    }
641                    mOldRunner.getGraph().tearDown(mGraphEnv.getContext());
642                    if (glEnv != null && glEnv.isActive()) {
643                        glEnv.deactivate();
644                    }
645                    mOldRunner = null;
646                }
647                if (mState == STATE_PREVIEW) {
648                    // Switching effects, start up the new runner
649                    if (mLogVerbose) Log.v(TAG, "Previous effect halted, starting new effect.");
650                    tryEnable3ALocks(false);
651                    mRunner.run();
652                } else if (mState != STATE_RELEASED) {
653                    // Shutting down effects
654                    if (mLogVerbose) Log.v(TAG, "Runner halted, restoring direct preview");
655                    sendMessage(EFFECT_NONE, EFFECT_MSG_EFFECTS_STOPPED);
656                } else {
657                    // STATE_RELEASED - camera will be/has been released as well, do nothing.
658                }
659            }
660        }
661    };
662
663    // Indicates that all camera/recording activity needs to halt
664    public synchronized void release() {
665        if (mLogVerbose) Log.v(TAG, "Releasing (" + this + ")");
666
667        switch (mState) {
668            case STATE_RECORD:
669            case STATE_PREVIEW:
670                stopPreview();
671                // Fall-through
672            default:
673                mState = STATE_RELEASED;
674                break;
675        }
676    }
677
678    private void sendMessage(final int effect, final int msg) {
679        if (mEffectsListener != null) {
680            mHandler.post(new Runnable() {
681                public void run() {
682                    mEffectsListener.onEffectsUpdate(effect, msg);
683                }
684            });
685        }
686    }
687
688    private void raiseError(final Exception exception) {
689        if (mEffectsListener != null) {
690            mHandler.post(new Runnable() {
691                public void run() {
692                    mEffectsListener.onEffectsError(exception, mOutputFile);
693                }
694            });
695        }
696    }
697}
698