Home | History | Annotate | Download | only in v1
      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 
     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      */
     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      */
    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      */
    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      */
    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      */
    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      */
    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.
    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.
    294     private int getStreamState() {
    295         synchronized(mStateLock) {
    296             return mStreamState;
    297         }
    298     }
    299 
    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 
    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 
    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 
    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 
    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 
    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      */
    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 
    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      */
    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