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