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