• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of 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,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.testingcamera2.v1;
18 
19 import android.content.Context;
20 import android.hardware.camera2.CaptureRequest;
21 import android.util.Size;
22 import android.media.MediaCodec;
23 import android.media.MediaCodecInfo;
24 import android.media.MediaFormat;
25 import android.media.MediaMuxer;
26 import android.media.MediaRecorder;
27 import android.media.MediaScannerConnection;
28 import android.os.Environment;
29 import android.util.Log;
30 import android.util.Size;
31 import android.view.Surface;
32 
33 import java.io.File;
34 import java.io.IOException;
35 import java.nio.ByteBuffer;
36 import java.text.SimpleDateFormat;
37 import java.util.Date;
38 import java.util.List;
39 
40 /**
41  * Camera video recording class. It takes frames produced by camera and encoded
42  * with either MediaCodec or MediaRecorder. MediaRecorder path is not
43  * implemented yet.
44  */
45 public class CameraRecordingStream {
46     private static final String TAG = "CameraRecordingStream";
47     private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
48     private static final int STREAM_STATE_IDLE = 0;
49     private static final int STREAM_STATE_CONFIGURED = 1;
50     private static final int STREAM_STATE_RECORDING = 2;
51     private static final int FRAME_RATE = 30; // 30fps
52     private static final int IFRAME_INTERVAL = 1; // 1 seconds between I-frames
53     private static final int TIMEOUT_USEC = 10000; // Timeout value 10ms.
54     // Sync object to protect stream state access from multiple threads.
55     private final Object mStateLock = new Object();
56 
57     private int mStreamState = STREAM_STATE_IDLE;
58     private MediaCodec mEncoder;
59     private Surface mRecordingSurface;
60     private int mEncBitRate;
61     private int mOrientation;
62     private MediaCodec.BufferInfo mBufferInfo;
63     private MediaMuxer mMuxer;
64     private int mTrackIndex = -1;
65     private boolean mMuxerStarted;
66     private boolean mUseMediaCodec = false;
67     private Size mStreamSize = new Size(-1, -1);
68     private int mOutputFormat = MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4;
69     private Thread mRecordingThread;
70     private MediaRecorder mMediaRecorder;
71     private String mOutputFile;
72 
CameraRecordingStream()73     public CameraRecordingStream() {
74     }
75 
76     /**
77      * Configure stream with a size and encoder mode.
78      *
79      * @param ctx Application context.
80      * @param size Size of recording stream.
81      * @param useMediaCodec The encoder for this stream to use, either MediaCodec
82      * or MediaRecorder.
83      * @param bitRate Bit rate the encoder takes.
84      * @param orientation Recording orientation in degree (0,90,180,270)
85      * @param outputFormat Output file format as listed in {@link MediaMuxer.OutputFormat}
86      */
configure( Context ctx, Size size, boolean useMediaCodec, int bitRate, int orientation, int outputFormat)87     public synchronized void configure(
88             Context ctx, Size size, boolean useMediaCodec, int bitRate, int orientation,
89             int outputFormat) {
90         if (getStreamState() == STREAM_STATE_RECORDING) {
91             throw new IllegalStateException(
92                     "Stream can only be configured when stream is in IDLE state");
93         }
94 
95         boolean isConfigChanged =
96                 (!mStreamSize.equals(size)) ||
97                 (mUseMediaCodec != useMediaCodec) ||
98                 (mEncBitRate != bitRate) ||
99                 (mOrientation != orientation);
100 
101         mStreamSize = size;
102         mUseMediaCodec = useMediaCodec;
103         mEncBitRate = bitRate;
104         mOrientation = orientation;
105         mOutputFormat = outputFormat;
106 
107         if (mUseMediaCodec) {
108             if (getStreamState() == STREAM_STATE_CONFIGURED) {
109                 /**
110                  * Stream is already configured, need release encoder and muxer
111                  * first, then reconfigure only if configuration is changed.
112                  */
113                 if (!isConfigChanged) {
114                     /**
115                      * TODO: this is only the skeleton, it is tricky to
116                      * implement because muxer need reconfigure always. But
117                      * muxer is closely coupled with MediaCodec for now because
118                      * muxer can only be started once format change callback is
119                      * sent from mediacodec. We need decouple MediaCodec and
120                      * Muxer for future.
121                      */
122                 }
123                 releaseEncoder();
124                 releaseMuxer(ctx);
125                 configureMediaCodecEncoder();
126             } else {
127                 configureMediaCodecEncoder();
128             }
129         } else {
130             configureMediaRecorder();
131         }
132 
133         setStreamState(STREAM_STATE_CONFIGURED);
134     }
135 
136     /**
137      * Add the stream output surface to the target output surface list.
138      *
139      * @param outputSurfaces The output surface list where the stream can
140      * add/remove its output surface.
141      * @param detach Detach the recording surface from the outputSurfaces.
142      */
onConfiguringOutputs(List<Surface> outputSurfaces, boolean detach)143     public synchronized void onConfiguringOutputs(List<Surface> outputSurfaces,
144             boolean detach) {
145         if (detach) {
146             // Can detach the surface in CONFIGURED and RECORDING state
147             if (getStreamState() != STREAM_STATE_IDLE) {
148                 outputSurfaces.remove(mRecordingSurface);
149             } else {
150                 Log.w(TAG, "Can not detach surface when recording stream is in IDLE state");
151             }
152         } else {
153             // Can add surface only in CONFIGURED state.
154             if (getStreamState() == STREAM_STATE_CONFIGURED) {
155                 outputSurfaces.add(mRecordingSurface);
156             } else {
157                 Log.w(TAG, "Can only add surface when recording stream is in CONFIGURED state");
158             }
159         }
160     }
161 
162     /**
163      * Update capture request with configuration required for recording stream.
164      *
165      * @param requestBuilder Capture request builder that needs to be updated
166      * for recording specific camera settings.
167      * @param detach Detach the recording surface from the capture request.
168      */
onConfiguringRequest(CaptureRequest.Builder requestBuilder, boolean detach)169     public synchronized void onConfiguringRequest(CaptureRequest.Builder requestBuilder,
170             boolean detach) {
171         if (detach) {
172             // Can detach the surface in CONFIGURED and RECORDING state
173             if (getStreamState() != STREAM_STATE_IDLE) {
174                 requestBuilder.removeTarget(mRecordingSurface);
175             } else {
176                 Log.w(TAG, "Can not detach surface when recording stream is in IDLE state");
177             }
178         } else {
179             // Can add surface only in CONFIGURED state.
180             if (getStreamState() == STREAM_STATE_CONFIGURED) {
181                 requestBuilder.addTarget(mRecordingSurface);
182             } else {
183                 Log.w(TAG, "Can only add surface when recording stream is in CONFIGURED state");
184             }
185         }
186     }
187 
188     /**
189      * Start recording stream. Calling start on an already started stream has no
190      * effect.
191      */
start()192     public synchronized void start() {
193         if (getStreamState() == STREAM_STATE_RECORDING) {
194             Log.w(TAG, "Recording stream is already started");
195             return;
196         }
197 
198         if (getStreamState() != STREAM_STATE_CONFIGURED) {
199             throw new IllegalStateException("Recording stream is not configured yet");
200         }
201 
202         setStreamState(STREAM_STATE_RECORDING);
203         if (mUseMediaCodec) {
204             startMediaCodecRecording();
205         } else {
206             mMediaRecorder.start();
207         }
208     }
209 
210     /**
211      * <p>
212      * Stop recording stream. Calling stop on an already stopped stream has no
213      * effect. Producer(in this case, CameraDevice) should stop before this call
214      * to avoid sending buffers to a stopped encoder.
215      * </p>
216      * <p>
217      * TODO: We have to release encoder and muxer for MediaCodec mode because
218      * encoder is closely coupled with muxer, and muxser can not be reused
219      * across different recording session(by design, you can not reset/restart
220      * it). To save the subsequent start recording time, we need avoid releasing
221      * encoder for future.
222      * </p>
223      * @param ctx Application context.
224      */
stop(Context ctx)225     public synchronized void stop(Context ctx) {
226         if (getStreamState() != STREAM_STATE_RECORDING) {
227             Log.w(TAG, "Recording stream is not started yet");
228             return;
229         }
230 
231         setStreamState(STREAM_STATE_IDLE);
232         Log.e(TAG, "setting camera to idle");
233         if (mUseMediaCodec) {
234             // Wait until recording thread stop
235             try {
236                 mRecordingThread.join();
237             } catch (InterruptedException e) {
238                 throw new RuntimeException("Stop recording failed", e);
239             }
240             // Drain encoder
241             doMediaCodecEncoding(/* notifyEndOfStream */true);
242             releaseEncoder();
243             releaseMuxer(ctx);
244         } else {
245             try {
246                 mMediaRecorder.stop();
247             } catch (RuntimeException e) {
248                 // this can happen if there were no frames received by recorder
249                 Log.e(TAG, "Could not create output file");
250             }
251             releaseMediaRecorder();
252         }
253     }
254 
255     /**
256      * Starts MediaCodec mode recording.
257      */
startMediaCodecRecording()258     private void startMediaCodecRecording() {
259         /**
260          * Start video recording asynchronously. we need a loop to handle output
261          * data for each frame.
262          */
263         mRecordingThread = new Thread() {
264             @Override
265             public void run() {
266                 if (VERBOSE) {
267                     Log.v(TAG, "Recording thread starts");
268                 }
269 
270                 while (getStreamState() == STREAM_STATE_RECORDING) {
271                     // Feed encoder output into the muxer until recording stops.
272                     doMediaCodecEncoding(/* notifyEndOfStream */false);
273                 }
274                 if (VERBOSE) {
275                     Log.v(TAG, "Recording thread completes");
276                 }
277                 return;
278             }
279         };
280         mRecordingThread.start();
281     }
282 
283     // Thread-safe access to the stream state.
setStreamState(int state)284     private synchronized void setStreamState(int state) {
285         synchronized (mStateLock) {
286             if (state < STREAM_STATE_IDLE) {
287                 throw new IllegalStateException("try to set an invalid state");
288             }
289             mStreamState = state;
290         }
291     }
292 
293     // Thread-safe access to the stream state.
getStreamState()294     private int getStreamState() {
295         synchronized(mStateLock) {
296             return mStreamState;
297         }
298     }
299 
releaseEncoder()300     private void releaseEncoder() {
301         // Release encoder
302         if (VERBOSE) {
303             Log.v(TAG, "releasing encoder");
304         }
305         if (mEncoder != null) {
306             mEncoder.stop();
307             mEncoder.release();
308             if (mRecordingSurface != null) {
309                 mRecordingSurface.release();
310             }
311             mEncoder = null;
312         }
313     }
314 
releaseMuxer(Context ctx)315     private void releaseMuxer(Context ctx) {
316         if (VERBOSE) {
317             Log.v(TAG, "releasing muxer");
318         }
319 
320         if (mMuxer != null) {
321             mMuxer.stop();
322             mMuxer.release();
323             mMuxer = null;
324             MediaScannerConnection.scanFile(ctx, new String [] { mOutputFile }, null, null);
325         }
326     }
327 
releaseMediaRecorder()328     private void releaseMediaRecorder() {
329         if (VERBOSE) {
330             Log.v(TAG, "releasing media recorder");
331         }
332 
333         if (mMediaRecorder != null) {
334             mMediaRecorder.release();
335             mMediaRecorder = null;
336         }
337 
338         if (mRecordingSurface != null) {
339             mRecordingSurface.release();
340             mRecordingSurface = null;
341         }
342     }
343 
getOutputMime()344     private String getOutputMime() {
345         switch (mOutputFormat) {
346             case MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4:
347                 return "video/avc";
348 
349             case MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM:
350                 return "video/x-vnd.on2.vp8";
351 
352             default:
353                 throw new IllegalStateException("Configure with unrecognized format.");
354         }
355     }
356 
getOutputExtension()357     private String getOutputExtension() {
358         switch (mOutputFormat) {
359             case MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4:
360                 return ".mp4";
361 
362             case MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM:
363                 return ".webm";
364 
365             default:
366                 throw new IllegalStateException("Configure with unrecognized format.");
367         }
368     }
369 
getOutputMediaFileName()370     private String getOutputMediaFileName() {
371         String state = Environment.getExternalStorageState();
372         // Check if external storage is mounted
373         if (!Environment.MEDIA_MOUNTED.equals(state)) {
374             Log.e(TAG, "External storage is not mounted!");
375             return null;
376         }
377 
378         File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
379                 Environment.DIRECTORY_DCIM), "TestingCamera2");
380         // Create the storage directory if it does not exist
381         if (!mediaStorageDir.exists()) {
382             if (!mediaStorageDir.mkdirs()) {
383                 Log.e(TAG, "Failed to create directory " + mediaStorageDir.getPath()
384                         + " for pictures/video!");
385                 return null;
386             }
387         }
388 
389         // Create a media file name
390         String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
391         String mediaFileName = mediaStorageDir.getPath() + File.separator +
392                 "VID_" + timeStamp + getOutputExtension();
393 
394         Log.v(TAG, "Recording file name: " + mediaFileName);
395         return mediaFileName;
396     }
397 
398     /**
399      * Configures encoder and muxer state, and prepares the input Surface.
400      * Initializes mEncoder, mMuxer, mRecordingSurface, mBufferInfo,
401      * mTrackIndex, and mMuxerStarted.
402      */
configureMediaCodecEncoder()403     private void configureMediaCodecEncoder() {
404         mBufferInfo = new MediaCodec.BufferInfo();
405         MediaFormat format =
406                 MediaFormat.createVideoFormat(getOutputMime(),
407                         mStreamSize.getWidth(), mStreamSize.getHeight());
408         /**
409          * Set encoding properties. Failing to specify some of these can cause
410          * the MediaCodec configure() call to throw an exception.
411          */
412         format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
413                 MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
414         format.setInteger(MediaFormat.KEY_BIT_RATE, mEncBitRate);
415         format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
416         format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
417         Log.i(TAG, "configure video encoding format: " + format);
418 
419         // Create/configure a MediaCodec encoder.
420         try {
421             mEncoder = MediaCodec.createEncoderByType(getOutputMime());
422         } catch (IOException ioe) {
423             throw new IllegalStateException(
424                     "failed to create " + getOutputMime() + " encoder", ioe);
425         }
426         mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
427         mRecordingSurface = mEncoder.createInputSurface();
428         mEncoder.start();
429 
430         String outputFileName = getOutputMediaFileName();
431         if (outputFileName == null) {
432             throw new IllegalStateException("Failed to get video output file");
433         }
434 
435         /**
436          * Create a MediaMuxer. We can't add the video track and start() the
437          * muxer until the encoder starts and notifies the new media format.
438          */
439         try {
440             mOutputFile = outputFileName;
441             mMuxer = new MediaMuxer(mOutputFile, mOutputFormat);
442             mMuxer.setOrientationHint(mOrientation);
443         } catch (IOException ioe) {
444             throw new IllegalStateException("MediaMuxer creation failed", ioe);
445         }
446         mMuxerStarted = false;
447     }
448 
configureMediaRecorder()449     private void configureMediaRecorder() {
450         String outputFileName = getOutputMediaFileName();
451         if (outputFileName == null) {
452             throw new IllegalStateException("Failed to get video output file");
453         }
454         releaseMediaRecorder();
455         mMediaRecorder = new MediaRecorder();
456         try {
457             if (mOutputFormat == MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) {
458                 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
459                 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
460                 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
461                 mMediaRecorder.setOutputFile(outputFileName);
462                 mMediaRecorder.setVideoEncodingBitRate(mEncBitRate);
463                 mMediaRecorder.setVideoFrameRate(FRAME_RATE);
464                 mMediaRecorder.setVideoSize(mStreamSize.getWidth(), mStreamSize.getHeight());
465                 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
466                 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
467                 mMediaRecorder.setOrientationHint(mOrientation);
468             } else {
469                 // TODO audio support
470                 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
471                 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.WEBM);
472                 mMediaRecorder.setOutputFile(outputFileName);
473                 mMediaRecorder.setVideoEncodingBitRate(mEncBitRate);
474                 mMediaRecorder.setVideoFrameRate(FRAME_RATE);
475                 mMediaRecorder.setVideoSize(mStreamSize.getWidth(), mStreamSize.getHeight());
476                 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.VP8);
477             }
478             mMediaRecorder.prepare();
479             mRecordingSurface = mMediaRecorder.getSurface();
480         } catch (IllegalStateException e) {
481             Log.v(TAG, "MediaRecorder throws IllegalStateException " + e.toString());
482         } catch (IOException e) {
483             Log.v(TAG, "MediaRecorder throws IOException " + e.toString());
484         }
485     }
486 
487     /**
488      * Do encoding by using MediaCodec encoder, then extracts all pending data
489      * from the encoder and forwards it to the muxer.
490      * <p>
491      * If notifyEndOfStream is not set, this returns when there is no more data
492      * to output. If it is set, we send EOS to the encoder, and then iterate
493      * until we see EOS on the output. Calling this with notifyEndOfStream set
494      * should be done once, before stopping the muxer.
495      * </p>
496      * <p>
497      * We're just using the muxer to get a .mp4 file and audio is not included
498      * here.
499      * </p>
500      */
doMediaCodecEncoding(boolean notifyEndOfStream)501     private void doMediaCodecEncoding(boolean notifyEndOfStream) {
502         if (VERBOSE) {
503             Log.v(TAG, "doMediaCodecEncoding(" + notifyEndOfStream + ")");
504         }
505 
506         if (notifyEndOfStream) {
507             mEncoder.signalEndOfInputStream();
508         }
509 
510         ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers();
511         boolean notDone = true;
512         while (notDone) {
513             int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
514             if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
515                 if (!notifyEndOfStream) {
516                     /**
517                      * Break out of the while loop because the encoder is not
518                      * ready to output anything yet.
519                      */
520                     notDone = false;
521                 } else {
522                     if (VERBOSE) {
523                         Log.v(TAG, "no output available, spinning to await EOS");
524                     }
525                 }
526             } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
527                 // generic case for mediacodec, not likely occurs for encoder.
528                 encoderOutputBuffers = mEncoder.getOutputBuffers();
529             } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
530                 /**
531                  * should happen before receiving buffers, and should only
532                  * happen once
533                  */
534                 if (mMuxerStarted) {
535                     throw new IllegalStateException("format changed twice");
536                 }
537                 MediaFormat newFormat = mEncoder.getOutputFormat();
538                 if (VERBOSE) {
539                     Log.v(TAG, "encoder output format changed: " + newFormat);
540                 }
541                 mTrackIndex = mMuxer.addTrack(newFormat);
542                 mMuxer.start();
543                 mMuxerStarted = true;
544             } else if (encoderStatus < 0) {
545                 Log.w(TAG, "unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus);
546             } else {
547                 // Normal flow: get output encoded buffer, send to muxer.
548                 ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
549                 if (encodedData == null) {
550                     throw new RuntimeException("encoderOutputBuffer " + encoderStatus +
551                             " was null");
552                 }
553 
554                 if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
555                     /**
556                      * The codec config data was pulled out and fed to the muxer
557                      * when we got the INFO_OUTPUT_FORMAT_CHANGED status. Ignore
558                      * it.
559                      */
560                     if (VERBOSE) {
561                         Log.v(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
562                     }
563                     mBufferInfo.size = 0;
564                 }
565 
566                 if (mBufferInfo.size != 0) {
567                     if (!mMuxerStarted) {
568                         throw new RuntimeException("muxer hasn't started");
569                     }
570 
571                     /**
572                      * It's usually necessary to adjust the ByteBuffer values to
573                      * match BufferInfo.
574                      */
575                     encodedData.position(mBufferInfo.offset);
576                     encodedData.limit(mBufferInfo.offset + mBufferInfo.size);
577 
578                     mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
579                     if (VERBOSE) {
580                         Log.v(TAG, "sent " + mBufferInfo.size + " bytes to muxer");
581                     }
582                 }
583 
584                 mEncoder.releaseOutputBuffer(encoderStatus, false);
585 
586                 if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
587                     if (!notifyEndOfStream) {
588                         Log.w(TAG, "reached end of stream unexpectedly");
589                     } else {
590                         if (VERBOSE) {
591                             Log.v(TAG, "end of stream reached");
592                         }
593                     }
594                     // Finish encoding.
595                     notDone = false;
596                 }
597             }
598         } // End of while(notDone)
599     }
600 }
601