1 /* 2 * Copyright (C) 2009 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 package android.media.cts; 17 18 19 import android.content.pm.PackageManager; 20 import android.cts.util.MediaUtils; 21 import android.graphics.Canvas; 22 import android.graphics.Color; 23 import android.graphics.Paint; 24 import android.hardware.Camera; 25 import android.media.CamcorderProfile; 26 import android.media.EncoderCapabilities; 27 import android.media.MediaCodec; 28 import android.media.MediaFormat; 29 import android.media.MediaMetadataRetriever; 30 import android.media.MediaRecorder; 31 import android.media.EncoderCapabilities.VideoEncoderCap; 32 import android.media.MediaRecorder.OnErrorListener; 33 import android.media.MediaRecorder.OnInfoListener; 34 import android.media.MediaMetadataRetriever; 35 import android.os.Environment; 36 import android.os.ConditionVariable; 37 import android.test.ActivityInstrumentationTestCase2; 38 import android.test.UiThreadTest; 39 import android.view.Surface; 40 41 import android.util.Log; 42 43 import java.io.File; 44 import java.io.FileDescriptor; 45 import java.io.FileOutputStream; 46 import java.lang.InterruptedException; 47 import java.lang.Runnable; 48 import java.util.List; 49 import java.util.concurrent.CountDownLatch; 50 import java.util.concurrent.TimeUnit; 51 52 public class MediaRecorderTest extends ActivityInstrumentationTestCase2<MediaStubActivity> { 53 private final String TAG = "MediaRecorderTest"; 54 private final String OUTPUT_PATH; 55 private final String OUTPUT_PATH2; 56 private static final float TOLERANCE = 0.0002f; 57 private static final int RECORD_TIME_MS = 3000; 58 private static final int RECORD_TIME_LAPSE_MS = 4000; 59 private static final int RECORD_TIME_LONG_MS = 20000; 60 private static final int RECORDED_DUR_TOLERANCE_MS = 1000; 61 private static final int VIDEO_WIDTH = 176; 62 private static final int VIDEO_HEIGHT = 144; 63 private static final int VIDEO_BIT_RATE_IN_BPS = 128000; 64 private static final double VIDEO_TIMELAPSE_CAPTURE_RATE_FPS = 1.0; 65 private static final int AUDIO_BIT_RATE_IN_BPS = 12200; 66 private static final int AUDIO_NUM_CHANNELS = 1; 67 private static final int AUDIO_SAMPLE_RATE_HZ = 8000; 68 private static final long MAX_FILE_SIZE = 5000; 69 private static final int MAX_FILE_SIZE_TIMEOUT_MS = 5 * 60 * 1000; 70 private static final int MAX_DURATION_MSEC = 2000; 71 private static final float LATITUDE = 0.0000f; 72 private static final float LONGITUDE = -180.0f; 73 private static final int NORMAL_FPS = 30; 74 private static final int TIME_LAPSE_FPS = 5; 75 private static final int SLOW_MOTION_FPS = 120; 76 private static final List<VideoEncoderCap> mVideoEncoders = 77 EncoderCapabilities.getVideoEncoders(); 78 79 private boolean mOnInfoCalled; 80 private boolean mOnErrorCalled; 81 private File mOutFile; 82 private File mOutFile2; 83 private Camera mCamera; 84 private MediaStubActivity mActivity = null; 85 86 private MediaRecorder mMediaRecorder; 87 private ConditionVariable mMaxDurationCond; 88 private ConditionVariable mMaxFileSizeCond; 89 MediaRecorderTest()90 public MediaRecorderTest() { 91 super("com.android.cts.media", MediaStubActivity.class); 92 OUTPUT_PATH = new File(Environment.getExternalStorageDirectory(), 93 "record.out").getAbsolutePath(); 94 OUTPUT_PATH2 = new File(Environment.getExternalStorageDirectory(), 95 "record2.out").getAbsolutePath(); 96 } 97 completeOnUiThread(final Runnable runnable)98 private void completeOnUiThread(final Runnable runnable) { 99 final CountDownLatch latch = new CountDownLatch(1); 100 getActivity().runOnUiThread(new Runnable() { 101 @Override 102 public void run() { 103 runnable.run(); 104 latch.countDown(); 105 } 106 }); 107 try { 108 // if UI thread does not run, things will fail anyway 109 assertTrue(latch.await(10, TimeUnit.SECONDS)); 110 } catch (java.lang.InterruptedException e) { 111 fail("should not be interrupted"); 112 } 113 } 114 115 @Override setUp()116 protected void setUp() throws Exception { 117 mActivity = getActivity(); 118 completeOnUiThread(new Runnable() { 119 @Override 120 public void run() { 121 mMediaRecorder = new MediaRecorder(); 122 mOutFile = new File(OUTPUT_PATH); 123 mOutFile2 = new File(OUTPUT_PATH2); 124 125 mMaxDurationCond = new ConditionVariable(); 126 mMaxFileSizeCond = new ConditionVariable(); 127 128 mMediaRecorder.setOutputFile(OUTPUT_PATH); 129 mMediaRecorder.setOnInfoListener(new OnInfoListener() { 130 public void onInfo(MediaRecorder mr, int what, int extra) { 131 mOnInfoCalled = true; 132 if (what == 133 MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) { 134 Log.v(TAG, "max duration reached"); 135 mMaxDurationCond.open(); 136 } else if (what == 137 MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { 138 Log.v(TAG, "max file size reached"); 139 mMaxFileSizeCond.open(); 140 } 141 } 142 }); 143 mMediaRecorder.setOnErrorListener(new OnErrorListener() { 144 public void onError(MediaRecorder mr, int what, int extra) { 145 mOnErrorCalled = true; 146 } 147 }); 148 } 149 }); 150 super.setUp(); 151 } 152 153 @Override tearDown()154 protected void tearDown() throws Exception { 155 if (mMediaRecorder != null) { 156 mMediaRecorder.release(); 157 mMediaRecorder = null; 158 } 159 if (mOutFile != null && mOutFile.exists()) { 160 mOutFile.delete(); 161 } 162 if (mOutFile2 != null && mOutFile2.exists()) { 163 mOutFile2.delete(); 164 } 165 if (mCamera != null) { 166 mCamera.release(); 167 mCamera = null; 168 } 169 mMaxDurationCond.close(); 170 mMaxDurationCond = null; 171 mMaxFileSizeCond.close(); 172 mMaxFileSizeCond = null; 173 mActivity = null; 174 super.tearDown(); 175 } 176 testRecorderCamera()177 public void testRecorderCamera() throws Exception { 178 int width; 179 int height; 180 Camera camera = null; 181 if (!hasCamera()) { 182 return; 183 } 184 // Try to get camera profile for QUALITY_LOW; if unavailable, 185 // set the video size to default value. 186 CamcorderProfile profile = CamcorderProfile.get( 187 0 /* cameraId */, CamcorderProfile.QUALITY_LOW); 188 if (profile != null) { 189 width = profile.videoFrameWidth; 190 height = profile.videoFrameHeight; 191 } else { 192 width = VIDEO_WIDTH; 193 height = VIDEO_HEIGHT; 194 } 195 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); 196 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT); 197 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT); 198 mMediaRecorder.setVideoSize(width, height); 199 mMediaRecorder.setVideoEncodingBitRate(VIDEO_BIT_RATE_IN_BPS); 200 mMediaRecorder.setPreviewDisplay(mActivity.getSurfaceHolder().getSurface()); 201 mMediaRecorder.prepare(); 202 mMediaRecorder.start(); 203 Thread.sleep(RECORD_TIME_MS); 204 mMediaRecorder.stop(); 205 checkOutputExist(); 206 } 207 208 @UiThreadTest testSetCamera()209 public void testSetCamera() throws Exception { 210 recordVideoUsingCamera(false); 211 } 212 testRecorderTimelapsedVideo()213 public void testRecorderTimelapsedVideo() throws Exception { 214 recordVideoUsingCamera(true); 215 } 216 recordVideoUsingCamera(boolean timelapse)217 private void recordVideoUsingCamera(boolean timelapse) throws Exception { 218 int nCamera = Camera.getNumberOfCameras(); 219 int durMs = timelapse? RECORD_TIME_LAPSE_MS: RECORD_TIME_MS; 220 for (int cameraId = 0; cameraId < nCamera; cameraId++) { 221 mCamera = Camera.open(cameraId); 222 recordVideoUsingCamera(mCamera, OUTPUT_PATH, durMs, timelapse); 223 mCamera.release(); 224 mCamera = null; 225 assertTrue(checkLocationInFile(OUTPUT_PATH)); 226 } 227 } 228 recordVideoUsingCamera( Camera camera, String fileName, int durMs, boolean timelapse)229 private void recordVideoUsingCamera( 230 Camera camera, String fileName, int durMs, boolean timelapse) throws Exception { 231 // FIXME: 232 // We should add some test case to use Camera.Parameters.getPreviewFpsRange() 233 // to get the supported video frame rate range. 234 Camera.Parameters params = camera.getParameters(); 235 int frameRate = params.getPreviewFrameRate(); 236 237 camera.unlock(); 238 mMediaRecorder.setCamera(camera); 239 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); 240 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT); 241 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT); 242 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT); 243 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT); 244 mMediaRecorder.setVideoFrameRate(frameRate); 245 mMediaRecorder.setVideoSize(VIDEO_WIDTH, VIDEO_HEIGHT); 246 mMediaRecorder.setPreviewDisplay(mActivity.getSurfaceHolder().getSurface()); 247 mMediaRecorder.setOutputFile(fileName); 248 mMediaRecorder.setLocation(LATITUDE, LONGITUDE); 249 final double captureRate = VIDEO_TIMELAPSE_CAPTURE_RATE_FPS; 250 if (timelapse) { 251 mMediaRecorder.setCaptureRate(captureRate); 252 } 253 254 mMediaRecorder.prepare(); 255 mMediaRecorder.start(); 256 Thread.sleep(durMs); 257 mMediaRecorder.stop(); 258 assertTrue(mOutFile.exists()); 259 260 int targetDurMs = timelapse? ((int) (durMs * (captureRate / frameRate))): durMs; 261 boolean hasVideo = true; 262 boolean hasAudio = timelapse? false: true; 263 checkTracksAndDuration(targetDurMs, hasVideo, hasAudio, fileName); 264 } 265 checkTracksAndDuration( int targetMs, boolean hasVideo, boolean hasAudio, String fileName)266 private void checkTracksAndDuration( 267 int targetMs, boolean hasVideo, boolean hasAudio, String fileName) throws Exception { 268 MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 269 retriever.setDataSource(fileName); 270 String hasVideoStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO); 271 String hasAudioStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO); 272 assertTrue(hasVideo? hasVideoStr != null : hasVideoStr == null); 273 assertTrue(hasAudio? hasAudioStr != null : hasAudioStr == null); 274 // FIXME: 275 // If we could use fixed frame rate for video recording, we could also do more accurate 276 // check on the duration. 277 String durStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); 278 assertTrue(durStr != null); 279 assertTrue(Integer.parseInt(durStr) > 0); 280 retriever.release(); 281 retriever = null; 282 } 283 checkLocationInFile(String fileName)284 private boolean checkLocationInFile(String fileName) { 285 MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 286 retriever.setDataSource(fileName); 287 String location = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION); 288 if (location == null) { 289 retriever.release(); 290 Log.v(TAG, "No location information found in file " + fileName); 291 return false; 292 } 293 294 // parsing String location and recover the location inforamtion in floats 295 // Make sure the tolerance is very small - due to rounding errors?. 296 Log.v(TAG, "location: " + location); 297 298 // Get the position of the -/+ sign in location String, which indicates 299 // the beginning of the longtitude. 300 int index = location.lastIndexOf('-'); 301 if (index == -1) { 302 index = location.lastIndexOf('+'); 303 } 304 assertTrue("+ or - is not found", index != -1); 305 assertTrue("+ or - is only found at the beginning", index != 0); 306 float latitude = Float.parseFloat(location.substring(0, index - 1)); 307 float longitude = Float.parseFloat(location.substring(index)); 308 assertTrue("Incorrect latitude: " + latitude, Math.abs(latitude - LATITUDE) <= TOLERANCE); 309 assertTrue("Incorrect longitude: " + longitude, Math.abs(longitude - LONGITUDE) <= TOLERANCE); 310 retriever.release(); 311 return true; 312 } 313 checkOutputExist()314 private void checkOutputExist() { 315 assertTrue(mOutFile.exists()); 316 assertTrue(mOutFile.length() > 0); 317 assertTrue(mOutFile.delete()); 318 } 319 testRecorderVideo()320 public void testRecorderVideo() throws Exception { 321 if (!hasCamera()) { 322 return; 323 } 324 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); 325 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT); 326 mMediaRecorder.setOutputFile(OUTPUT_PATH2); 327 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT); 328 mMediaRecorder.setPreviewDisplay(mActivity.getSurfaceHolder().getSurface()); 329 mMediaRecorder.setVideoSize(VIDEO_WIDTH, VIDEO_HEIGHT); 330 331 FileOutputStream fos = new FileOutputStream(OUTPUT_PATH2); 332 FileDescriptor fd = fos.getFD(); 333 mMediaRecorder.setOutputFile(fd); 334 long maxFileSize = MAX_FILE_SIZE * 10; 335 recordMedia(maxFileSize, mOutFile2); 336 assertFalse(checkLocationInFile(OUTPUT_PATH2)); 337 fos.close(); 338 } 339 testRecordingAudioInRawFormats()340 public void testRecordingAudioInRawFormats() throws Exception { 341 int testsRun = 0; 342 if (hasAmrNb()) { 343 testsRun += testRecordAudioInRawFormat( 344 MediaRecorder.OutputFormat.AMR_NB, 345 MediaRecorder.AudioEncoder.AMR_NB); 346 } 347 348 if (hasAmrWb()) { 349 testsRun += testRecordAudioInRawFormat( 350 MediaRecorder.OutputFormat.AMR_WB, 351 MediaRecorder.AudioEncoder.AMR_WB); 352 } 353 354 if (hasAac()) { 355 testsRun += testRecordAudioInRawFormat( 356 MediaRecorder.OutputFormat.AAC_ADTS, 357 MediaRecorder.AudioEncoder.AAC); 358 } 359 if (testsRun == 0) { 360 MediaUtils.skipTest("no audio codecs or microphone"); 361 } 362 } 363 testRecordAudioInRawFormat( int fileFormat, int codec)364 private int testRecordAudioInRawFormat( 365 int fileFormat, int codec) throws Exception { 366 if (!hasMicrophone()) { 367 return 0; // skip 368 } 369 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); 370 mMediaRecorder.setOutputFormat(fileFormat); 371 mMediaRecorder.setOutputFile(OUTPUT_PATH); 372 mMediaRecorder.setAudioEncoder(codec); 373 recordMedia(MAX_FILE_SIZE, mOutFile); 374 return 1; 375 } 376 testGetAudioSourceMax()377 public void testGetAudioSourceMax() throws Exception { 378 final int max = MediaRecorder.getAudioSourceMax(); 379 assertTrue(MediaRecorder.AudioSource.DEFAULT <= max); 380 assertTrue(MediaRecorder.AudioSource.MIC <= max); 381 assertTrue(MediaRecorder.AudioSource.CAMCORDER <= max); 382 assertTrue(MediaRecorder.AudioSource.VOICE_CALL <= max); 383 assertTrue(MediaRecorder.AudioSource.VOICE_COMMUNICATION <= max); 384 assertTrue(MediaRecorder.AudioSource.VOICE_DOWNLINK <= max); 385 assertTrue(MediaRecorder.AudioSource.VOICE_RECOGNITION <= max); 386 assertTrue(MediaRecorder.AudioSource.VOICE_UPLINK <= max); 387 } 388 testRecorderAudio()389 public void testRecorderAudio() throws Exception { 390 if (!hasMicrophone() || !hasAmrNb()) { 391 MediaUtils.skipTest("no audio codecs or microphone"); 392 return; 393 } 394 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); 395 assertEquals(0, mMediaRecorder.getMaxAmplitude()); 396 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); 397 mMediaRecorder.setOutputFile(OUTPUT_PATH); 398 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 399 mMediaRecorder.setAudioChannels(AUDIO_NUM_CHANNELS); 400 mMediaRecorder.setAudioSamplingRate(AUDIO_SAMPLE_RATE_HZ); 401 mMediaRecorder.setAudioEncodingBitRate(AUDIO_BIT_RATE_IN_BPS); 402 recordMedia(MAX_FILE_SIZE, mOutFile); 403 } 404 testOnInfoListener()405 public void testOnInfoListener() throws Exception { 406 if (!hasMicrophone() || !hasAmrNb()) { 407 MediaUtils.skipTest("no audio codecs or microphone"); 408 return; 409 } 410 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); 411 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); 412 mMediaRecorder.setMaxDuration(MAX_DURATION_MSEC); 413 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 414 mMediaRecorder.prepare(); 415 mMediaRecorder.start(); 416 Thread.sleep(RECORD_TIME_MS); 417 assertTrue(mOnInfoCalled); 418 } 419 testSetMaxDuration()420 public void testSetMaxDuration() throws Exception { 421 if (!hasMicrophone() || !hasAmrNb()) { 422 MediaUtils.skipTest("no audio codecs or microphone"); 423 return; 424 } 425 testSetMaxDuration(RECORD_TIME_LONG_MS, RECORDED_DUR_TOLERANCE_MS); 426 } 427 testSetMaxDuration(long durationMs, long toleranceMs)428 private void testSetMaxDuration(long durationMs, long toleranceMs) throws Exception { 429 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); 430 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); 431 mMediaRecorder.setMaxDuration((int)durationMs); 432 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 433 mMediaRecorder.prepare(); 434 mMediaRecorder.start(); 435 long startTimeMs = System.currentTimeMillis(); 436 if (!mMaxDurationCond.block(durationMs + toleranceMs)) { 437 fail("timed out waiting for MEDIA_RECORDER_INFO_MAX_DURATION_REACHED"); 438 } 439 long endTimeMs = System.currentTimeMillis(); 440 long actualDurationMs = endTimeMs - startTimeMs; 441 mMediaRecorder.stop(); 442 checkRecordedTime(durationMs, actualDurationMs, toleranceMs); 443 } 444 checkRecordedTime(long expectedMs, long actualMs, long tolerance)445 private void checkRecordedTime(long expectedMs, long actualMs, long tolerance) { 446 assertEquals(expectedMs, actualMs, tolerance); 447 long actualFileDurationMs = getRecordedFileDurationMs(OUTPUT_PATH); 448 assertEquals(actualFileDurationMs, actualMs, tolerance); 449 } 450 getRecordedFileDurationMs(final String fileName)451 private int getRecordedFileDurationMs(final String fileName) { 452 MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 453 retriever.setDataSource(fileName); 454 String durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); 455 assertNotNull(durationStr); 456 return Integer.parseInt(durationStr); 457 } 458 testSetMaxFileSize()459 public void testSetMaxFileSize() throws Exception { 460 testSetMaxFileSize(512 * 1024, 50 * 1024); 461 } 462 testSetMaxFileSize( long fileSize, long tolerance)463 private void testSetMaxFileSize( 464 long fileSize, long tolerance) throws Exception { 465 if (!hasMicrophone() || !hasCamera() || !hasAmrNb() || !hasH264()) { 466 MediaUtils.skipTest("no microphone, camera, or codecs"); 467 return; 468 } 469 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); 470 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); 471 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); 472 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 473 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); 474 mMediaRecorder.setVideoSize(VIDEO_WIDTH, VIDEO_HEIGHT); 475 mMediaRecorder.setVideoEncodingBitRate(256000); 476 mMediaRecorder.setPreviewDisplay(mActivity.getSurfaceHolder().getSurface()); 477 mMediaRecorder.setMaxFileSize(fileSize); 478 mMediaRecorder.prepare(); 479 mMediaRecorder.start(); 480 481 // Recording a scene with moving objects would greatly help reduce 482 // the time for waiting. 483 if (!mMaxFileSizeCond.block(MAX_FILE_SIZE_TIMEOUT_MS)) { 484 fail("timed out waiting for MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED"); 485 } 486 mMediaRecorder.stop(); 487 checkOutputFileSize(OUTPUT_PATH, fileSize, tolerance); 488 } 489 checkOutputFileSize(final String fileName, long fileSize, long tolerance)490 private void checkOutputFileSize(final String fileName, long fileSize, long tolerance) { 491 assertTrue(mOutFile.exists()); 492 assertEquals(fileSize, mOutFile.length(), tolerance); 493 assertTrue(mOutFile.delete()); 494 } 495 testOnErrorListener()496 public void testOnErrorListener() throws Exception { 497 if (!hasMicrophone() || !hasAmrNb()) { 498 MediaUtils.skipTest("no audio codecs or microphone"); 499 return; 500 } 501 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT); 502 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); 503 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 504 505 recordMedia(MAX_FILE_SIZE, mOutFile); 506 // TODO: how can we trigger a recording error? 507 assertFalse(mOnErrorCalled); 508 } 509 setupRecorder(String filename, boolean useSurface, boolean hasAudio)510 private void setupRecorder(String filename, boolean useSurface, boolean hasAudio) 511 throws Exception { 512 int codec = MediaRecorder.VideoEncoder.H264; 513 int frameRate = getMaxFrameRateForCodec(codec); 514 if (mMediaRecorder == null) { 515 mMediaRecorder = new MediaRecorder(); 516 } 517 518 if (!useSurface) { 519 mCamera = Camera.open(0); 520 Camera.Parameters params = mCamera.getParameters(); 521 frameRate = params.getPreviewFrameRate(); 522 mCamera.unlock(); 523 mMediaRecorder.setCamera(mCamera); 524 mMediaRecorder.setPreviewDisplay(mActivity.getSurfaceHolder().getSurface()); 525 } 526 527 mMediaRecorder.setVideoSource(useSurface ? 528 MediaRecorder.VideoSource.SURFACE : MediaRecorder.VideoSource.CAMERA); 529 530 if (hasAudio) { 531 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); 532 } 533 534 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); 535 mMediaRecorder.setOutputFile(filename); 536 537 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); 538 mMediaRecorder.setVideoFrameRate(frameRate); 539 mMediaRecorder.setVideoSize(VIDEO_WIDTH, VIDEO_HEIGHT); 540 541 if (hasAudio) { 542 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 543 } 544 } 545 tryGetSurface(boolean shouldThrow)546 private Surface tryGetSurface(boolean shouldThrow) throws Exception { 547 Surface surface = null; 548 try { 549 surface = mMediaRecorder.getSurface(); 550 assertFalse("failed to throw IllegalStateException", shouldThrow); 551 } catch (IllegalStateException e) { 552 assertTrue("threw unexpected exception: " + e, shouldThrow); 553 } 554 return surface; 555 } 556 validateGetSurface(boolean useSurface)557 private boolean validateGetSurface(boolean useSurface) { 558 Log.v(TAG,"validateGetSurface, useSurface=" + useSurface); 559 if (!useSurface && !hasCamera()) { 560 // pass if testing camera source but no hardware 561 return true; 562 } 563 Surface surface = null; 564 boolean success = true; 565 try { 566 setupRecorder(OUTPUT_PATH, useSurface, false /* hasAudio */); 567 568 /* Test: getSurface() before prepare() 569 * should throw IllegalStateException 570 */ 571 surface = tryGetSurface(true /* shouldThow */); 572 573 mMediaRecorder.prepare(); 574 575 /* Test: getSurface() after prepare() 576 * should succeed for surface source 577 * should fail for camera source 578 */ 579 surface = tryGetSurface(!useSurface); 580 581 mMediaRecorder.start(); 582 583 /* Test: getSurface() after start() 584 * should succeed for surface source 585 * should fail for camera source 586 */ 587 surface = tryGetSurface(!useSurface); 588 589 try { 590 mMediaRecorder.stop(); 591 } catch (Exception e) { 592 // stop() could fail if the recording is empty, as we didn't render anything. 593 // ignore any failure in stop, we just want it stopped. 594 } 595 596 /* Test: getSurface() after stop() 597 * should throw IllegalStateException 598 */ 599 surface = tryGetSurface(true /* shouldThow */); 600 } catch (Exception e) { 601 Log.d(TAG, e.toString()); 602 success = false; 603 } finally { 604 // reset to clear states, as stop() might have failed 605 mMediaRecorder.reset(); 606 607 if (mCamera != null) { 608 mCamera.release(); 609 mCamera = null; 610 } 611 if (surface != null) { 612 surface.release(); 613 surface = null; 614 } 615 } 616 617 return success; 618 } 619 trySetInputSurface(Surface surface)620 private void trySetInputSurface(Surface surface) throws Exception { 621 boolean testBadArgument = (surface == null); 622 try { 623 mMediaRecorder.setInputSurface(testBadArgument ? new Surface() : surface); 624 fail("failed to throw exception"); 625 } catch (IllegalArgumentException e) { 626 // OK only if testing bad arg 627 assertTrue("threw unexpected exception: " + e, testBadArgument); 628 } catch (IllegalStateException e) { 629 // OK only if testing error case other than bad arg 630 assertFalse("threw unexpected exception: " + e, testBadArgument); 631 } 632 } 633 validatePersistentSurface(boolean errorCase)634 private boolean validatePersistentSurface(boolean errorCase) { 635 Log.v(TAG, "validatePersistentSurface, errorCase=" + errorCase); 636 637 Surface surface = MediaCodec.createPersistentInputSurface(); 638 if (surface == null) { 639 return false; 640 } 641 Surface dummy = null; 642 643 boolean success = true; 644 try { 645 setupRecorder(OUTPUT_PATH, true /* useSurface */, false /* hasAudio */); 646 647 if (errorCase) { 648 /* 649 * Test: should throw if called with non-persistent surface 650 */ 651 trySetInputSurface(null); 652 } else { 653 /* 654 * Test: should succeed if called with a persistent surface before prepare() 655 */ 656 mMediaRecorder.setInputSurface(surface); 657 } 658 659 /* 660 * Test: getSurface() should fail before prepare 661 */ 662 dummy = tryGetSurface(true /* shouldThow */); 663 664 mMediaRecorder.prepare(); 665 666 /* 667 * Test: setInputSurface() should fail after prepare 668 */ 669 trySetInputSurface(surface); 670 671 /* 672 * Test: getSurface() should fail if setInputSurface() succeeded 673 */ 674 dummy = tryGetSurface(!errorCase /* shouldThow */); 675 676 mMediaRecorder.start(); 677 678 /* 679 * Test: setInputSurface() should fail after start 680 */ 681 trySetInputSurface(surface); 682 683 /* 684 * Test: getSurface() should fail if setInputSurface() succeeded 685 */ 686 dummy = tryGetSurface(!errorCase /* shouldThow */); 687 688 try { 689 mMediaRecorder.stop(); 690 } catch (Exception e) { 691 // stop() could fail if the recording is empty, as we didn't render anything. 692 // ignore any failure in stop, we just want it stopped. 693 } 694 695 /* 696 * Test: getSurface() should fail after stop 697 */ 698 dummy = tryGetSurface(true /* shouldThow */); 699 } catch (Exception e) { 700 Log.d(TAG, e.toString()); 701 success = false; 702 } finally { 703 // reset to clear states, as stop() might have failed 704 mMediaRecorder.reset(); 705 706 if (mCamera != null) { 707 mCamera.release(); 708 mCamera = null; 709 } 710 if (surface != null) { 711 surface.release(); 712 surface = null; 713 } 714 if (dummy != null) { 715 dummy.release(); 716 dummy = null; 717 } 718 } 719 720 return success; 721 } 722 testGetSurfaceApi()723 public void testGetSurfaceApi() { 724 if (!hasH264()) { 725 MediaUtils.skipTest("no codecs"); 726 return; 727 } 728 729 if (hasCamera()) { 730 // validate getSurface() with CAMERA source 731 assertTrue(validateGetSurface(false /* useSurface */)); 732 } 733 734 // validate getSurface() with SURFACE source 735 assertTrue(validateGetSurface(true /* useSurface */)); 736 } 737 testPersistentSurfaceApi()738 public void testPersistentSurfaceApi() { 739 if (!hasH264()) { 740 MediaUtils.skipTest("no codecs"); 741 return; 742 } 743 744 // test valid use case 745 assertTrue(validatePersistentSurface(false /* errorCase */)); 746 747 // test invalid use case 748 assertTrue(validatePersistentSurface(true /* errorCase */)); 749 } 750 getMaxFrameRateForCodec(int codec)751 private static int getMaxFrameRateForCodec(int codec) { 752 for (VideoEncoderCap cap : mVideoEncoders) { 753 if (cap.mCodec == codec) { 754 return cap.mMaxFrameRate < NORMAL_FPS ? cap.mMaxFrameRate : NORMAL_FPS; 755 } 756 } 757 fail("didn't find max FPS for codec"); 758 return -1; 759 } 760 recordFromSurface( String filename, int captureRate, boolean hasAudio, Surface persistentSurface)761 private boolean recordFromSurface( 762 String filename, 763 int captureRate, 764 boolean hasAudio, 765 Surface persistentSurface) { 766 Log.v(TAG, "recordFromSurface"); 767 Surface surface = null; 768 try { 769 setupRecorder(filename, true /* useSurface */, hasAudio); 770 771 int sleepTimeMs; 772 if (captureRate > 0) { 773 mMediaRecorder.setCaptureRate(captureRate); 774 sleepTimeMs = 1000 / captureRate; 775 } else { 776 sleepTimeMs = 1000 / getMaxFrameRateForCodec(MediaRecorder.VideoEncoder.H264); 777 } 778 779 if (persistentSurface != null) { 780 Log.v(TAG, "using persistent surface"); 781 surface = persistentSurface; 782 mMediaRecorder.setInputSurface(surface); 783 } 784 785 mMediaRecorder.prepare(); 786 787 if (persistentSurface == null) { 788 surface = mMediaRecorder.getSurface(); 789 } 790 791 Paint paint = new Paint(); 792 paint.setTextSize(16); 793 paint.setColor(Color.RED); 794 int i; 795 796 /* Test: draw 10 frames at 30fps before start 797 * these should be dropped and not causing malformed stream. 798 */ 799 for(i = 0; i < 10; i++) { 800 Canvas canvas = surface.lockCanvas(null); 801 int background = (i * 255 / 99); 802 canvas.drawARGB(255, background, background, background); 803 String text = "Frame #" + i; 804 canvas.drawText(text, 50, 50, paint); 805 surface.unlockCanvasAndPost(canvas); 806 Thread.sleep(sleepTimeMs); 807 } 808 809 Log.v(TAG, "start"); 810 mMediaRecorder.start(); 811 812 /* Test: draw another 90 frames at 30fps after start */ 813 for(i = 10; i < 100; i++) { 814 Canvas canvas = surface.lockCanvas(null); 815 int background = (i * 255 / 99); 816 canvas.drawARGB(255, background, background, background); 817 String text = "Frame #" + i; 818 canvas.drawText(text, 50, 50, paint); 819 surface.unlockCanvasAndPost(canvas); 820 Thread.sleep(sleepTimeMs); 821 } 822 823 Log.v(TAG, "stop"); 824 mMediaRecorder.stop(); 825 } catch (Exception e) { 826 Log.v(TAG, "record video failed: " + e.toString()); 827 return false; 828 } finally { 829 // We need to test persistent surface across multiple MediaRecorder 830 // instances, so must destroy mMediaRecorder here. 831 if (mMediaRecorder != null) { 832 mMediaRecorder.release(); 833 mMediaRecorder = null; 834 } 835 836 // release surface if not using persistent surface 837 if (persistentSurface == null && surface != null) { 838 surface.release(); 839 surface = null; 840 } 841 } 842 return true; 843 } 844 checkCaptureFps(String filename, int captureRate)845 private boolean checkCaptureFps(String filename, int captureRate) { 846 MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 847 848 retriever.setDataSource(filename); 849 850 // verify capture rate meta key is present and correct 851 String captureFps = retriever.extractMetadata( 852 MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE); 853 854 if (captureFps == null) { 855 Log.d(TAG, "METADATA_KEY_CAPTURE_FRAMERATE is missing"); 856 return false; 857 } 858 859 if (Math.abs(Float.parseFloat(captureFps) - captureRate) > 0.001) { 860 Log.d(TAG, "METADATA_KEY_CAPTURE_FRAMERATE is incorrect: " 861 + captureFps + "vs. " + captureRate); 862 return false; 863 } 864 865 // verify other meta keys here if necessary 866 return true; 867 } 868 testRecordFromSurface(boolean persistent, boolean timelapse)869 private boolean testRecordFromSurface(boolean persistent, boolean timelapse) { 870 Log.v(TAG, "testRecordFromSurface: " + 871 "persistent=" + persistent + ", timelapse=" + timelapse); 872 boolean success = false; 873 Surface surface = null; 874 int noOfFailure = 0; 875 try { 876 if (persistent) { 877 surface = MediaCodec.createPersistentInputSurface(); 878 } 879 880 for (int k = 0; k < 2; k++) { 881 String filename = (k == 0) ? OUTPUT_PATH : OUTPUT_PATH2; 882 boolean hasAudio = false; 883 int captureRate = 0; 884 885 if (timelapse) { 886 // if timelapse/slow-mo, k chooses between low/high capture fps 887 captureRate = (k == 0) ? TIME_LAPSE_FPS : SLOW_MOTION_FPS; 888 } else { 889 // otherwise k chooses between no-audio and audio 890 hasAudio = (k == 0) ? false : true; 891 } 892 893 if (hasAudio && (!hasMicrophone() || !hasAmrNb())) { 894 // audio test waived if no audio support 895 continue; 896 } 897 898 Log.v(TAG, "testRecordFromSurface - round " + k); 899 success = recordFromSurface(filename, captureRate, hasAudio, surface); 900 if (success) { 901 checkTracksAndDuration(0, true /* hasVideo */, hasAudio, filename); 902 903 // verify capture fps meta key 904 if (timelapse && !checkCaptureFps(filename, captureRate)) { 905 noOfFailure++; 906 } 907 } 908 if (!success) { 909 noOfFailure++; 910 } 911 } 912 } catch (Exception e) { 913 Log.v(TAG, e.toString()); 914 noOfFailure++; 915 } finally { 916 if (surface != null) { 917 Log.v(TAG, "releasing persistent surface"); 918 surface.release(); 919 surface = null; 920 } 921 } 922 return (noOfFailure == 0); 923 } 924 925 // Test recording from surface source with/without audio) testSurfaceRecording()926 public void testSurfaceRecording() { 927 assertTrue(testRecordFromSurface(false /* persistent */, false /* timelapse */)); 928 } 929 930 // Test recording from persistent surface source with/without audio testPersistentSurfaceRecording()931 public void testPersistentSurfaceRecording() { 932 assertTrue(testRecordFromSurface(true /* persistent */, false /* timelapse */)); 933 } 934 935 // Test timelapse recording from surface without audio testSurfaceRecordingTimeLapse()936 public void testSurfaceRecordingTimeLapse() { 937 assertTrue(testRecordFromSurface(false /* persistent */, true /* timelapse */)); 938 } 939 940 // Test timelapse recording from persisent surface without audio testPersistentSurfaceRecordingTimeLapse()941 public void testPersistentSurfaceRecordingTimeLapse() { 942 assertTrue(testRecordFromSurface(true /* persistent */, true /* timelapse */)); 943 } 944 recordMedia(long maxFileSize, File outFile)945 private void recordMedia(long maxFileSize, File outFile) throws Exception { 946 mMediaRecorder.setMaxFileSize(maxFileSize); 947 mMediaRecorder.prepare(); 948 mMediaRecorder.start(); 949 Thread.sleep(RECORD_TIME_MS); 950 mMediaRecorder.stop(); 951 952 assertTrue(outFile.exists()); 953 954 // The max file size is always guaranteed. 955 // We just make sure that the margin is not too big 956 assertTrue(outFile.length() < 1.1 * maxFileSize); 957 assertTrue(outFile.length() > 0); 958 } 959 hasCamera()960 private boolean hasCamera() { 961 return mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA); 962 } 963 hasMicrophone()964 private boolean hasMicrophone() { 965 return mActivity.getPackageManager().hasSystemFeature( 966 PackageManager.FEATURE_MICROPHONE); 967 } 968 hasAmrNb()969 private static boolean hasAmrNb() { 970 return MediaUtils.hasEncoder(MediaFormat.MIMETYPE_AUDIO_AMR_NB); 971 } 972 hasAmrWb()973 private static boolean hasAmrWb() { 974 return MediaUtils.hasEncoder(MediaFormat.MIMETYPE_AUDIO_AMR_WB); 975 } 976 hasAac()977 private static boolean hasAac() { 978 return MediaUtils.hasEncoder(MediaFormat.MIMETYPE_AUDIO_AAC); 979 } 980 hasH264()981 private static boolean hasH264() { 982 return MediaUtils.hasEncoder(MediaFormat.MIMETYPE_VIDEO_AVC); 983 } 984 } 985