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