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 
17 package com.android.camera;
18 
19 import android.content.Context;
20 import android.content.res.AssetFileDescriptor;
21 import android.filterfw.GraphEnvironment;
22 import android.filterfw.core.Filter;
23 import android.filterfw.core.GLEnvironment;
24 import android.filterfw.core.GraphRunner;
25 import android.filterfw.core.GraphRunner.OnRunnerDoneListener;
26 import android.filterfw.geometry.Point;
27 import android.filterfw.geometry.Quad;
28 import android.filterpacks.videoproc.BackDropperFilter;
29 import android.filterpacks.videoproc.BackDropperFilter.LearningDoneListener;
30 import android.filterpacks.videosink.MediaEncoderFilter.OnRecordingDoneListener;
31 import android.filterpacks.videosrc.SurfaceTextureSource.SurfaceTextureSourceListener;
32 
33 import android.graphics.SurfaceTexture;
34 import android.hardware.Camera;
35 import android.media.MediaActionSound;
36 import android.media.MediaRecorder;
37 import android.media.CamcorderProfile;
38 import android.os.Handler;
39 import android.os.Looper;
40 import android.os.ParcelFileDescriptor;
41 import android.util.Log;
42 import android.view.Surface;
43 import android.view.SurfaceHolder;
44 
45 import java.io.IOException;
46 import java.io.FileNotFoundException;
47 import java.io.File;
48 import java.lang.Runnable;
49 import java.io.FileDescriptor;
50 
51 
52 /**
53  * Encapsulates the mobile filter framework components needed to record video with
54  * effects applied. Modeled after MediaRecorder.
55  */
56 public class EffectsRecorder {
57 
58     public static final int  EFFECT_NONE        = 0;
59     public static final int  EFFECT_GOOFY_FACE  = 1;
60     public static final int  EFFECT_BACKDROPPER = 2;
61 
62     public static final int  EFFECT_GF_SQUEEZE     = 0;
63     public static final int  EFFECT_GF_BIG_EYES    = 1;
64     public static final int  EFFECT_GF_BIG_MOUTH   = 2;
65     public static final int  EFFECT_GF_SMALL_MOUTH = 3;
66     public static final int  EFFECT_GF_BIG_NOSE    = 4;
67     public static final int  EFFECT_GF_SMALL_EYES  = 5;
68     public static final int  NUM_OF_GF_EFFECTS = EFFECT_GF_SMALL_EYES + 1;
69 
70     public static final int  EFFECT_MSG_STARTED_LEARNING = 0;
71     public static final int  EFFECT_MSG_DONE_LEARNING    = 1;
72     public static final int  EFFECT_MSG_SWITCHING_EFFECT = 2;
73     public static final int  EFFECT_MSG_EFFECTS_STOPPED  = 3;
74     public static final int  EFFECT_MSG_RECORDING_DONE   = 4;
75     public static final int  EFFECT_MSG_PREVIEW_RUNNING  = 5;
76 
77     private Context mContext;
78     private Handler mHandler;
79     private boolean mReleased;
80 
81     private Camera mCameraDevice;
82     private CamcorderProfile mProfile;
83     private double mCaptureRate = 0;
84     private SurfaceHolder mPreviewSurfaceHolder;
85     private int mPreviewWidth;
86     private int mPreviewHeight;
87     private MediaRecorder.OnInfoListener mInfoListener;
88     private MediaRecorder.OnErrorListener mErrorListener;
89 
90     private String mOutputFile;
91     private FileDescriptor mFd;
92     private int mOrientationHint = 0;
93     private long mMaxFileSize = 0;
94     private int mMaxDurationMs = 0;
95     private int mCameraFacing = Camera.CameraInfo.CAMERA_FACING_BACK;
96     private boolean mAppIsLandscape;
97 
98     private int mEffect = EFFECT_NONE;
99     private int mCurrentEffect = EFFECT_NONE;
100     private EffectsListener mEffectsListener;
101 
102     private Object mEffectParameter;
103 
104     private GraphEnvironment mGraphEnv;
105     private int mGraphId;
106     private GraphRunner mRunner = null;
107     private GraphRunner mOldRunner = null;
108 
109     private SurfaceTexture mTextureSource;
110 
111     private static final int STATE_CONFIGURE              = 0;
112     private static final int STATE_WAITING_FOR_SURFACE    = 1;
113     private static final int STATE_STARTING_PREVIEW       = 2;
114     private static final int STATE_PREVIEW                = 3;
115     private static final int STATE_RECORD                 = 4;
116     private static final int STATE_RELEASED               = 5;
117     private int mState = STATE_CONFIGURE;
118 
119     private boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE);
120     private static final String TAG = "effectsrecorder";
121     private MediaActionSound mCameraSound;
122 
123     /** Determine if a given effect is supported at runtime
124      * Some effects require libraries not available on all devices
125      */
isEffectSupported(int effectId)126     public static boolean isEffectSupported(int effectId) {
127         switch (effectId) {
128             case EFFECT_GOOFY_FACE:
129                 return Filter.isAvailable("com.google.android.filterpacks.facedetect.GoofyRenderFilter");
130             case EFFECT_BACKDROPPER:
131                 return Filter.isAvailable("android.filterpacks.videoproc.BackDropperFilter");
132             default:
133                 return false;
134         }
135     }
136 
EffectsRecorder(Context context)137     public EffectsRecorder(Context context) {
138         if (mLogVerbose) Log.v(TAG, "EffectsRecorder created (" + this + ")");
139         mContext = context;
140         mHandler = new Handler(Looper.getMainLooper());
141         mCameraSound = new MediaActionSound();
142     }
143 
setCamera(Camera cameraDevice)144     public void setCamera(Camera cameraDevice) {
145         switch (mState) {
146             case STATE_PREVIEW:
147                 throw new RuntimeException("setCamera cannot be called while previewing!");
148             case STATE_RECORD:
149                 throw new RuntimeException("setCamera cannot be called while recording!");
150             case STATE_RELEASED:
151                 throw new RuntimeException("setCamera called on an already released recorder!");
152             default:
153                 break;
154         }
155 
156         mCameraDevice = cameraDevice;
157     }
158 
setProfile(CamcorderProfile profile)159     public void setProfile(CamcorderProfile profile) {
160         switch (mState) {
161             case STATE_RECORD:
162                 throw new RuntimeException("setProfile cannot be called while recording!");
163             case STATE_RELEASED:
164                 throw new RuntimeException("setProfile called on an already released recorder!");
165             default:
166                 break;
167         }
168         mProfile = profile;
169     }
170 
setOutputFile(String outputFile)171     public void setOutputFile(String outputFile) {
172         switch (mState) {
173             case STATE_RECORD:
174                 throw new RuntimeException("setOutputFile cannot be called while recording!");
175             case STATE_RELEASED:
176                 throw new RuntimeException("setOutputFile called on an already released recorder!");
177             default:
178                 break;
179         }
180 
181         mOutputFile = outputFile;
182         mFd = null;
183     }
184 
setOutputFile(FileDescriptor fd)185     public void setOutputFile(FileDescriptor fd) {
186         switch (mState) {
187             case STATE_RECORD:
188                 throw new RuntimeException("setOutputFile cannot be called while recording!");
189             case STATE_RELEASED:
190                 throw new RuntimeException("setOutputFile called on an already released recorder!");
191             default:
192                 break;
193         }
194 
195         mOutputFile = null;
196         mFd = fd;
197     }
198 
199     /**
200      * Sets the maximum filesize (in bytes) of the recording session.
201      * This will be passed on to the MediaEncoderFilter and then to the
202      * MediaRecorder ultimately. If zero or negative, the MediaRecorder will
203      * disable the limit
204     */
setMaxFileSize(long maxFileSize)205     public synchronized void setMaxFileSize(long maxFileSize) {
206         switch (mState) {
207             case STATE_RECORD:
208                 throw new RuntimeException("setMaxFileSize cannot be called while recording!");
209             case STATE_RELEASED:
210                 throw new RuntimeException("setMaxFileSize called on an already released recorder!");
211             default:
212                 break;
213         }
214         mMaxFileSize = maxFileSize;
215     }
216 
217     /**
218     * Sets the maximum recording duration (in ms) for the next recording session
219     * Setting it to zero (the default) disables the limit.
220     */
setMaxDuration(int maxDurationMs)221     public synchronized void setMaxDuration(int maxDurationMs) {
222         switch (mState) {
223             case STATE_RECORD:
224                 throw new RuntimeException("setMaxDuration cannot be called while recording!");
225             case STATE_RELEASED:
226                 throw new RuntimeException("setMaxDuration called on an already released recorder!");
227             default:
228                 break;
229         }
230         mMaxDurationMs = maxDurationMs;
231     }
232 
233 
setCaptureRate(double fps)234     public void setCaptureRate(double fps) {
235         switch (mState) {
236             case STATE_RECORD:
237                 throw new RuntimeException("setCaptureRate cannot be called while recording!");
238             case STATE_RELEASED:
239                 throw new RuntimeException("setCaptureRate called on an already released recorder!");
240             default:
241                 break;
242         }
243 
244         if (mLogVerbose) Log.v(TAG, "Setting time lapse capture rate to " + fps + " fps");
245         mCaptureRate = fps;
246     }
247 
setPreviewDisplay(SurfaceHolder previewSurfaceHolder, int previewWidth, int previewHeight)248     public void setPreviewDisplay(SurfaceHolder previewSurfaceHolder,
249                                   int previewWidth,
250                                   int previewHeight) {
251         if (mLogVerbose) Log.v(TAG, "setPreviewDisplay (" + this + ")");
252         switch (mState) {
253             case STATE_RECORD:
254                 throw new RuntimeException("setPreviewDisplay cannot be called while recording!");
255             case STATE_RELEASED:
256                 throw new RuntimeException("setPreviewDisplay called on an already released recorder!");
257             default:
258                 break;
259         }
260 
261         mPreviewSurfaceHolder = previewSurfaceHolder;
262         mPreviewWidth = previewWidth;
263         mPreviewHeight = previewHeight;
264 
265         switch (mState) {
266             case STATE_WAITING_FOR_SURFACE:
267                 startPreview();
268                 break;
269             case STATE_STARTING_PREVIEW:
270             case STATE_PREVIEW:
271                 initializeEffect(true);
272                 break;
273         }
274     }
275 
setEffect(int effect, Object effectParameter)276     public void setEffect(int effect, Object effectParameter) {
277         if (mLogVerbose) Log.v(TAG,
278                                "setEffect: effect ID " + effect +
279                                ", parameter " + effectParameter.toString() );
280         switch (mState) {
281             case STATE_RECORD:
282                 throw new RuntimeException("setEffect cannot be called while recording!");
283             case STATE_RELEASED:
284                 throw new RuntimeException("setEffect called on an already released recorder!");
285             default:
286                 break;
287         }
288 
289         mEffect = effect;
290         mEffectParameter = effectParameter;
291 
292         if (mState == STATE_PREVIEW ||
293                 mState == STATE_STARTING_PREVIEW) {
294             initializeEffect(false);
295         }
296     }
297 
298     public interface EffectsListener {
onEffectsUpdate(int effectId, int effectMsg)299         public void onEffectsUpdate(int effectId, int effectMsg);
onEffectsError(Exception exception, String filePath)300         public void onEffectsError(Exception exception, String filePath);
301     }
302 
setEffectsListener(EffectsListener listener)303     public void setEffectsListener(EffectsListener listener) {
304         mEffectsListener = listener;
305     }
306 
setFaceDetectOrientation()307     private void setFaceDetectOrientation() {
308         if (mCurrentEffect == EFFECT_GOOFY_FACE) {
309             Filter rotateFilter = mRunner.getGraph().getFilter("rotate");
310             Filter metaRotateFilter = mRunner.getGraph().getFilter("metarotate");
311             rotateFilter.setInputValue("rotation", mOrientationHint);
312             int reverseDegrees = (360 - mOrientationHint) % 360;
313             metaRotateFilter.setInputValue("rotation", reverseDegrees);
314         }
315     }
316 
setRecordingOrientation()317     private void setRecordingOrientation() {
318         if ( mState != STATE_RECORD && mRunner != null) {
319             Point bl = new Point(0, 0);
320             Point br = new Point(1, 0);
321             Point tl = new Point(0, 1);
322             Point tr = new Point(1, 1);
323             Quad recordingRegion;
324             if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_BACK) {
325                 // The back camera is not mirrored, so use a identity transform
326                 recordingRegion = new Quad(bl, br, tl, tr);
327             } else {
328                 // Recording region needs to be tweaked for front cameras, since they
329                 // mirror their preview
330                 if (mOrientationHint == 0 || mOrientationHint == 180) {
331                     // Horizontal flip in landscape
332                     recordingRegion = new Quad(br, bl, tr, tl);
333                 } else {
334                     // Horizontal flip in portrait
335                     recordingRegion = new Quad(tl, tr, bl, br);
336                 }
337             }
338             Filter recorder = mRunner.getGraph().getFilter("recorder");
339             recorder.setInputValue("inputRegion", recordingRegion);
340         }
341     }
setOrientationHint(int degrees)342     public void setOrientationHint(int degrees) {
343         switch (mState) {
344             case STATE_RELEASED:
345                 throw new RuntimeException(
346                         "setOrientationHint called on an already released recorder!");
347             default:
348                 break;
349         }
350         if (mLogVerbose) Log.v(TAG, "Setting orientation hint to: " + degrees);
351         mOrientationHint = degrees;
352         setFaceDetectOrientation();
353         setRecordingOrientation();
354     }
355 
356     /** Passes the native orientation of the Camera app (device dependent)
357      * to allow for correct output aspect ratio. Defaults to portrait */
setAppToLandscape(boolean landscape)358     public void setAppToLandscape(boolean landscape) {
359         if (mState != STATE_CONFIGURE) {
360             throw new RuntimeException(
361                 "setAppToLandscape called after configuration!");
362         }
363         mAppIsLandscape = landscape;
364     }
365 
setCameraFacing(int facing)366     public void setCameraFacing(int facing) {
367         switch (mState) {
368             case STATE_RELEASED:
369                 throw new RuntimeException(
370                     "setCameraFacing called on alrady released recorder!");
371             default:
372                 break;
373         }
374         mCameraFacing = facing;
375         setRecordingOrientation();
376     }
377 
setOnInfoListener(MediaRecorder.OnInfoListener infoListener)378     public void setOnInfoListener(MediaRecorder.OnInfoListener infoListener) {
379         switch (mState) {
380             case STATE_RECORD:
381                 throw new RuntimeException("setInfoListener cannot be called while recording!");
382             case STATE_RELEASED:
383                 throw new RuntimeException("setInfoListener called on an already released recorder!");
384             default:
385                 break;
386         }
387         mInfoListener = infoListener;
388     }
389 
setOnErrorListener(MediaRecorder.OnErrorListener errorListener)390     public void setOnErrorListener(MediaRecorder.OnErrorListener errorListener) {
391         switch (mState) {
392             case STATE_RECORD:
393                 throw new RuntimeException("setErrorListener cannot be called while recording!");
394             case STATE_RELEASED:
395                 throw new RuntimeException("setErrorListener called on an already released recorder!");
396             default:
397                 break;
398         }
399         mErrorListener = errorListener;
400     }
401 
initializeFilterFramework()402     private void initializeFilterFramework() {
403         mGraphEnv = new GraphEnvironment();
404         mGraphEnv.createGLEnvironment();
405 
406         if (mLogVerbose) {
407             Log.v(TAG, "Effects framework initializing. Recording size "
408                   + mProfile.videoFrameWidth + ", " + mProfile.videoFrameHeight);
409         }
410         if (!mAppIsLandscape) {
411             int tmp;
412             tmp = mProfile.videoFrameWidth;
413             mProfile.videoFrameWidth = mProfile.videoFrameHeight;
414             mProfile.videoFrameHeight = tmp;
415         }
416         mGraphEnv.addReferences(
417                 "textureSourceCallback", mSourceReadyCallback,
418                 "recordingWidth", mProfile.videoFrameWidth,
419                 "recordingHeight", mProfile.videoFrameHeight,
420                 "recordingProfile", mProfile,
421                 "learningDoneListener", mLearningDoneListener,
422                 "recordingDoneListener", mRecordingDoneListener);
423         mRunner = null;
424         mGraphId = -1;
425         mCurrentEffect = EFFECT_NONE;
426     }
427 
initializeEffect(boolean forceReset)428     private synchronized void initializeEffect(boolean forceReset) {
429         if (forceReset ||
430             mCurrentEffect != mEffect ||
431             mCurrentEffect == EFFECT_BACKDROPPER) {
432             if (mLogVerbose) {
433                 Log.v(TAG, "Effect initializing. Preview size "
434                        + mPreviewWidth + ", " + mPreviewHeight);
435             }
436 
437             mGraphEnv.addReferences(
438                     "previewSurface", mPreviewSurfaceHolder.getSurface(),
439                     "previewWidth", mPreviewWidth,
440                     "previewHeight", mPreviewHeight,
441                     "orientation", mOrientationHint);
442             if (mState == STATE_PREVIEW ||
443                     mState == STATE_STARTING_PREVIEW) {
444                 // Switching effects while running. Inform video camera.
445                 sendMessage(mCurrentEffect, EFFECT_MSG_SWITCHING_EFFECT);
446             }
447 
448             switch (mEffect) {
449                 case EFFECT_GOOFY_FACE:
450                     mGraphId = mGraphEnv.loadGraph(mContext, R.raw.goofy_face);
451                     break;
452                 case EFFECT_BACKDROPPER:
453                     sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_STARTED_LEARNING);
454                     mGraphId = mGraphEnv.loadGraph(mContext, R.raw.backdropper);
455                     break;
456                 default:
457                     throw new RuntimeException("Unknown effect ID" + mEffect + "!");
458             }
459             mCurrentEffect = mEffect;
460 
461             mOldRunner = mRunner;
462             mRunner = mGraphEnv.getRunner(mGraphId, GraphEnvironment.MODE_ASYNCHRONOUS);
463             mRunner.setDoneCallback(mRunnerDoneCallback);
464             if (mLogVerbose) {
465                 Log.v(TAG, "New runner: " + mRunner
466                       + ". Old runner: " + mOldRunner);
467             }
468             if (mState == STATE_PREVIEW ||
469                     mState == STATE_STARTING_PREVIEW) {
470                 // Switching effects while running. Stop existing runner.
471                 // The stop callback will take care of starting new runner.
472                 mCameraDevice.stopPreview();
473                 try {
474                     mCameraDevice.setPreviewTexture(null);
475                 } catch(IOException e) {
476                     throw new RuntimeException("Unable to connect camera to effect input", e);
477                 }
478                 mOldRunner.stop();
479             }
480         }
481 
482         switch (mCurrentEffect) {
483             case EFFECT_GOOFY_FACE:
484                 tryEnableVideoStabilization(true);
485                 Filter goofyFilter = mRunner.getGraph().getFilter("goofyrenderer");
486                 goofyFilter.setInputValue("currentEffect",
487                                           ((Integer)mEffectParameter).intValue());
488                 break;
489             case EFFECT_BACKDROPPER:
490                 tryEnableVideoStabilization(false);
491                 Filter backgroundSrc = mRunner.getGraph().getFilter("background");
492                 backgroundSrc.setInputValue("sourceUrl",
493                                             (String)mEffectParameter);
494                 // For front camera, the background video needs to be mirrored in the
495                 // backdropper filter
496                 if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
497                     Filter replacer = mRunner.getGraph().getFilter("replacer");
498                     replacer.setInputValue("mirrorBg", true);
499                     if (mLogVerbose) Log.v(TAG, "Setting the background to be mirrored");
500                 }
501                 break;
502             default:
503                 break;
504         }
505         setFaceDetectOrientation();
506         setRecordingOrientation();
507     }
508 
startPreview()509     public synchronized void startPreview() {
510         if (mLogVerbose) Log.v(TAG, "Starting preview (" + this + ")");
511 
512         switch (mState) {
513             case STATE_STARTING_PREVIEW:
514             case STATE_PREVIEW:
515                 // Already running preview
516                 Log.w(TAG, "startPreview called when already running preview");
517                 return;
518             case STATE_RECORD:
519                 throw new RuntimeException("Cannot start preview when already recording!");
520             case STATE_RELEASED:
521                 throw new RuntimeException("setEffect called on an already released recorder!");
522             default:
523                 break;
524         }
525 
526         if (mEffect == EFFECT_NONE) {
527             throw new RuntimeException("No effect selected!");
528         }
529         if (mEffectParameter == null) {
530             throw new RuntimeException("No effect parameter provided!");
531         }
532         if (mProfile == null) {
533             throw new RuntimeException("No recording profile provided!");
534         }
535         if (mPreviewSurfaceHolder == null) {
536             if (mLogVerbose) Log.v(TAG, "Passed a null surface holder; waiting for valid one");
537             mState = STATE_WAITING_FOR_SURFACE;
538             return;
539         }
540         if (mCameraDevice == null) {
541             throw new RuntimeException("No camera to record from!");
542         }
543 
544         if (mLogVerbose) Log.v(TAG, "Initializing filter graph");
545 
546         initializeFilterFramework();
547 
548         initializeEffect(true);
549 
550         if (mLogVerbose) Log.v(TAG, "Starting filter graph");
551 
552         mState = STATE_STARTING_PREVIEW;
553         mRunner.run();
554         // Rest of preview startup handled in mSourceReadyCallback
555     }
556 
557     private SurfaceTextureSourceListener mSourceReadyCallback =
558             new SurfaceTextureSourceListener() {
559         public void onSurfaceTextureSourceReady(SurfaceTexture source) {
560             if (mLogVerbose) Log.v(TAG, "SurfaceTexture ready callback received");
561             synchronized(EffectsRecorder.this) {
562                 mTextureSource = source;
563 
564                 if (mState == STATE_CONFIGURE) {
565                     // Stop preview happened while the runner was doing startup tasks
566                     // Since we haven't started anything up, don't do anything
567                     // Rest of cleanup will happen in onRunnerDone
568                     if (mLogVerbose) Log.v(TAG, "Ready callback: Already stopped, skipping.");
569                     return;
570                 }
571                 if (mState == STATE_RELEASED) {
572                     // EffectsRecorder has been released, so don't touch the camera device
573                     // or anything else
574                     if (mLogVerbose) Log.v(TAG, "Ready callback: Already released, skipping.");
575                     return;
576                 }
577                 if (source == null) {
578                     if (mState == STATE_PREVIEW ||
579                             mState == STATE_STARTING_PREVIEW ||
580                             mState == STATE_RECORD) {
581                         // A null source here means the graph is shutting down
582                         // unexpectedly, so we need to turn off preview before
583                         // the surface texture goes away.
584                         mCameraDevice.stopPreview();
585                         try {
586                             mCameraDevice.setPreviewTexture(null);
587                         } catch(IOException e) {
588                             throw new RuntimeException("Unable to disconnect " +
589                                     "camera from effect input", e);
590                         }
591                     }
592                     return;
593                 }
594 
595                 // Lock AE/AWB to reduce transition flicker
596                 tryEnable3ALocks(true);
597 
598                 mCameraDevice.stopPreview();
599                 if (mLogVerbose) Log.v(TAG, "Runner active, connecting effects preview");
600                 try {
601                     mCameraDevice.setPreviewTexture(mTextureSource);
602                 } catch(IOException e) {
603                     throw new RuntimeException("Unable to connect camera to effect input", e);
604                 }
605 
606                 mCameraDevice.startPreview();
607 
608                 // Unlock AE/AWB after preview started
609                 tryEnable3ALocks(false);
610 
611                 mState = STATE_PREVIEW;
612 
613                 if (mLogVerbose) Log.v(TAG, "Start preview/effect switch complete");
614 
615                 // Sending a message to listener that preview is complete
616                 sendMessage(mCurrentEffect, EFFECT_MSG_PREVIEW_RUNNING);
617             }
618         }
619     };
620 
621     private LearningDoneListener mLearningDoneListener =
622             new LearningDoneListener() {
623         public void onLearningDone(BackDropperFilter filter) {
624             if (mLogVerbose) Log.v(TAG, "Learning done callback triggered");
625             // Called in a processing thread, so have to post message back to UI
626             // thread
627             sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_DONE_LEARNING);
628             enable3ALocks(true);
629         }
630     };
631 
632     // A callback to finalize the media after the recording is done.
633     private OnRecordingDoneListener mRecordingDoneListener =
634             new OnRecordingDoneListener() {
635         // Forward the callback to the VideoCamera object (as an asynchronous event).
636         public void onRecordingDone() {
637             if (mLogVerbose) Log.v(TAG, "Recording done callback triggered");
638             sendMessage(EFFECT_NONE, EFFECT_MSG_RECORDING_DONE);
639         }
640     };
641 
startRecording()642     public synchronized void startRecording() {
643         if (mLogVerbose) Log.v(TAG, "Starting recording (" + this + ")");
644 
645         switch (mState) {
646             case STATE_RECORD:
647                 throw new RuntimeException("Already recording, cannot begin anew!");
648             case STATE_RELEASED:
649                 throw new RuntimeException("startRecording called on an already released recorder!");
650             default:
651                 break;
652         }
653 
654         if ((mOutputFile == null) && (mFd == null)) {
655             throw new RuntimeException("No output file name or descriptor provided!");
656         }
657 
658         if (mState == STATE_CONFIGURE) {
659             startPreview();
660         }
661 
662         Filter recorder = mRunner.getGraph().getFilter("recorder");
663         if (mFd != null) {
664             recorder.setInputValue("outputFileDescriptor", mFd);
665         } else {
666             recorder.setInputValue("outputFile", mOutputFile);
667         }
668         // It is ok to set the audiosource without checking for timelapse here
669         // since that check will be done in the MediaEncoderFilter itself
670         recorder.setInputValue("audioSource", MediaRecorder.AudioSource.CAMCORDER);
671 
672         recorder.setInputValue("recordingProfile", mProfile);
673         recorder.setInputValue("orientationHint", mOrientationHint);
674         // Important to set the timelapseinterval to 0 if the capture rate is not >0
675         // since the recorder does not get created every time the recording starts.
676         // The recorder infers whether the capture is timelapsed based on the value of
677         // this interval
678         boolean captureTimeLapse = mCaptureRate > 0;
679         if (captureTimeLapse) {
680             double timeBetweenFrameCapture = 1 / mCaptureRate;
681             recorder.setInputValue("timelapseRecordingIntervalUs",
682                     (long) (1000000 * timeBetweenFrameCapture));
683         } else {
684             recorder.setInputValue("timelapseRecordingIntervalUs", 0L);
685         }
686 
687         if (mInfoListener != null) {
688             recorder.setInputValue("infoListener", mInfoListener);
689         }
690         if (mErrorListener != null) {
691             recorder.setInputValue("errorListener", mErrorListener);
692         }
693         recorder.setInputValue("maxFileSize", mMaxFileSize);
694         recorder.setInputValue("maxDurationMs", mMaxDurationMs);
695         recorder.setInputValue("recording", true);
696         mCameraSound.play(MediaActionSound.START_VIDEO_RECORDING);
697         mState = STATE_RECORD;
698     }
699 
stopRecording()700     public synchronized void stopRecording() {
701         if (mLogVerbose) Log.v(TAG, "Stop recording (" + this + ")");
702 
703         switch (mState) {
704             case STATE_CONFIGURE:
705             case STATE_STARTING_PREVIEW:
706             case STATE_PREVIEW:
707                 Log.w(TAG, "StopRecording called when recording not active!");
708                 return;
709             case STATE_RELEASED:
710                 throw new RuntimeException("stopRecording called on released EffectsRecorder!");
711             default:
712                 break;
713         }
714         Filter recorder = mRunner.getGraph().getFilter("recorder");
715         recorder.setInputValue("recording", false);
716         mCameraSound.play(MediaActionSound.STOP_VIDEO_RECORDING);
717         mState = STATE_PREVIEW;
718     }
719 
720     // Stop and release effect resources
stopPreview()721     public synchronized void stopPreview() {
722         if (mLogVerbose) Log.v(TAG, "Stopping preview (" + this + ")");
723 
724         switch (mState) {
725             case STATE_CONFIGURE:
726                 Log.w(TAG, "StopPreview called when preview not active!");
727                 return;
728             case STATE_RELEASED:
729                 throw new RuntimeException("stopPreview called on released EffectsRecorder!");
730             default:
731                 break;
732         }
733 
734         if (mState == STATE_RECORD) {
735             stopRecording();
736         }
737 
738         mCurrentEffect = EFFECT_NONE;
739 
740         mCameraDevice.stopPreview();
741         try {
742             mCameraDevice.setPreviewTexture(null);
743         } catch(IOException e) {
744             throw new RuntimeException("Unable to connect camera to effect input", e);
745         }
746         mCameraSound.release();
747 
748         mState = STATE_CONFIGURE;
749         mOldRunner = mRunner;
750         mRunner.stop();
751         mRunner = null;
752         // Rest of stop and release handled in mRunnerDoneCallback
753     }
754 
755     // Try to enable/disable video stabilization if supported; otherwise return false
tryEnableVideoStabilization(boolean toggle)756     boolean tryEnableVideoStabilization(boolean toggle) {
757         Camera.Parameters params = mCameraDevice.getParameters();
758 
759         String vstabSupported = params.get("video-stabilization-supported");
760         if ("true".equals(vstabSupported)) {
761             if (mLogVerbose) Log.v(TAG, "Setting video stabilization to " + toggle);
762             params.set("video-stabilization", toggle ? "true" : "false");
763             mCameraDevice.setParameters(params);
764             return true;
765         }
766         if (mLogVerbose) Log.v(TAG, "Video stabilization not supported");
767         return false;
768     }
769 
770     // Try to enable/disable 3A locks if supported; otherwise return false
tryEnable3ALocks(boolean toggle)771     boolean tryEnable3ALocks(boolean toggle) {
772         Camera.Parameters params = mCameraDevice.getParameters();
773         if (params.isAutoExposureLockSupported() &&
774             params.isAutoWhiteBalanceLockSupported() ) {
775             params.setAutoExposureLock(toggle);
776             params.setAutoWhiteBalanceLock(toggle);
777             mCameraDevice.setParameters(params);
778             return true;
779         }
780         return false;
781     }
782 
783     // Try to enable/disable 3A locks if supported; otherwise, throw error
784     // Use this when locks are essential to success
enable3ALocks(boolean toggle)785     void enable3ALocks(boolean toggle) {
786         Camera.Parameters params = mCameraDevice.getParameters();
787         if (!tryEnable3ALocks(toggle)) {
788             throw new RuntimeException("Attempt to lock 3A on camera with no locking support!");
789         }
790     }
791 
792     private OnRunnerDoneListener mRunnerDoneCallback =
793             new OnRunnerDoneListener() {
794         public void onRunnerDone(int result) {
795             synchronized(EffectsRecorder.this) {
796                 if (mLogVerbose) {
797                     Log.v(TAG,
798                           "Graph runner done (" + EffectsRecorder.this
799                           + ", mRunner " + mRunner
800                           + ", mOldRunner " + mOldRunner + ")");
801                 }
802                 if (result == GraphRunner.RESULT_ERROR) {
803                     // Handle error case
804                     Log.e(TAG, "Error running filter graph!");
805                     Exception e = null;
806                     if (mRunner != null) {
807                         e = mRunner.getError();
808                     } else if (mOldRunner != null) {
809                         e = mOldRunner.getError();
810                     }
811                     raiseError(e);
812                 }
813                 if (mOldRunner != null) {
814                     // Tear down old graph if available
815                     if (mLogVerbose) Log.v(TAG, "Tearing down old graph.");
816                     GLEnvironment glEnv = mGraphEnv.getContext().getGLEnvironment();
817                     if (glEnv != null && !glEnv.isActive()) {
818                         glEnv.activate();
819                     }
820                     mOldRunner.getGraph().tearDown(mGraphEnv.getContext());
821                     if (glEnv != null && glEnv.isActive()) {
822                         glEnv.deactivate();
823                     }
824                     mOldRunner = null;
825                 }
826                 if (mState == STATE_PREVIEW ||
827                         mState == STATE_STARTING_PREVIEW) {
828                     // Switching effects, start up the new runner
829                     if (mLogVerbose) Log.v(TAG, "Previous effect halted, starting new effect.");
830                     tryEnable3ALocks(false);
831                     mRunner.run();
832                 } else if (mState != STATE_RELEASED) {
833                     // Shutting down effects
834                     if (mLogVerbose) Log.v(TAG, "Runner halted, restoring direct preview");
835                     tryEnable3ALocks(false);
836                     sendMessage(EFFECT_NONE, EFFECT_MSG_EFFECTS_STOPPED);
837                 } else {
838                     // STATE_RELEASED - camera will be/has been released as well, do nothing.
839                 }
840             }
841         }
842     };
843 
844     // Indicates that all camera/recording activity needs to halt
release()845     public synchronized void release() {
846         if (mLogVerbose) Log.v(TAG, "Releasing (" + this + ")");
847 
848         switch (mState) {
849             case STATE_RECORD:
850             case STATE_STARTING_PREVIEW:
851             case STATE_PREVIEW:
852                 stopPreview();
853                 // Fall-through
854             default:
855                 mState = STATE_RELEASED;
856                 break;
857         }
858     }
859 
sendMessage(final int effect, final int msg)860     private void sendMessage(final int effect, final int msg) {
861         if (mEffectsListener != null) {
862             mHandler.post(new Runnable() {
863                 public void run() {
864                     mEffectsListener.onEffectsUpdate(effect, msg);
865                 }
866             });
867         }
868     }
869 
raiseError(final Exception exception)870     private void raiseError(final Exception exception) {
871         if (mEffectsListener != null) {
872             mHandler.post(new Runnable() {
873                 public void run() {
874                     if (mFd != null) {
875                         mEffectsListener.onEffectsError(exception, null);
876                     } else {
877                         mEffectsListener.onEffectsError(exception, mOutputFile);
878                     }
879                 }
880             });
881         }
882     }
883 
884 }
885