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