1 /*
2  * Copyright 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.mediaframeworktest.stress;
18 
19 import static com.android.ex.camera2.blocking.BlockingSessionCallback.SESSION_CLOSED;
20 import static com.android.mediaframeworktest.helpers.CameraTestUtils.CAPTURE_IMAGE_TIMEOUT_MS;
21 import static com.android.mediaframeworktest.helpers.CameraTestUtils.SESSION_CLOSE_TIMEOUT_MS;
22 import static com.android.mediaframeworktest.helpers.CameraTestUtils.SIZE_BOUND_1080P;
23 import static com.android.mediaframeworktest.helpers.CameraTestUtils.SIZE_BOUND_2160P;
24 import static com.android.mediaframeworktest.helpers.CameraTestUtils.SimpleCaptureCallback;
25 import static com.android.mediaframeworktest.helpers.CameraTestUtils.SimpleImageReaderListener;
26 import static com.android.mediaframeworktest.helpers.CameraTestUtils.configureCameraSession;
27 import static com.android.mediaframeworktest.helpers.CameraTestUtils.configureCameraSessionWithParameters;
28 import static com.android.mediaframeworktest.helpers.CameraTestUtils.getSupportedVideoSizes;
29 import static com.android.mediaframeworktest.helpers.CameraTestUtils.getValueNotNull;
30 
31 import android.graphics.ImageFormat;
32 import android.hardware.camera2.CameraCaptureSession;
33 import android.hardware.camera2.CameraCharacteristics;
34 import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession;
35 import android.hardware.camera2.CameraDevice;
36 import android.hardware.camera2.CaptureRequest;
37 import android.hardware.camera2.CaptureResult;
38 import android.hardware.camera2.params.StreamConfigurationMap;
39 import android.media.CamcorderProfile;
40 import android.media.Image;
41 import android.media.ImageReader;
42 import android.media.MediaExtractor;
43 import android.media.MediaFormat;
44 import android.media.MediaRecorder;
45 import android.os.SystemClock;
46 import android.test.suitebuilder.annotation.LargeTest;
47 import android.util.Log;
48 import android.util.Range;
49 import android.util.Size;
50 import android.view.Surface;
51 
52 import androidx.test.InstrumentationRegistry;
53 
54 import com.android.ex.camera2.blocking.BlockingSessionCallback;
55 import com.android.mediaframeworktest.Camera2SurfaceViewTestCase;
56 import com.android.mediaframeworktest.helpers.CameraTestUtils;
57 
58 import junit.framework.AssertionFailedError;
59 
60 import java.io.File;
61 import java.util.ArrayList;
62 import java.util.Arrays;
63 import java.util.HashMap;
64 import java.util.List;
65 
66 /**
67  * CameraDevice video recording use case tests by using MediaRecorder and
68  * MediaCodec.
69  *
70  * adb shell am instrument \
71  *    -e class com.android.mediaframeworktest.stress.Camera2RecordingTest#testBasicRecording \
72  *    -e iterations 10 \
73  *    -e waitIntervalMs 1000 \
74  *    -e resultToFile false \
75  *    -r -w com.android.mediaframeworktest/androidx.test.runner.AndroidJUnitRunner
76  */
77 @LargeTest
78 public class Camera2RecordingTest extends Camera2SurfaceViewTestCase {
79     private static final String TAG = "RecordingTest";
80     private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
81     private static final boolean DEBUG_DUMP = Log.isLoggable(TAG, Log.DEBUG);
82     private static final int RECORDING_DURATION_MS = 3000;
83     private static final float DURATION_MARGIN = 0.2f;
84     private static final double FRAME_DURATION_ERROR_TOLERANCE_MS = 3.0;
85     private static final int BIT_RATE_1080P = 16000000;
86     private static final int BIT_RATE_MIN = 64000;
87     private static final int BIT_RATE_MAX = 40000000;
88     private static final int VIDEO_FRAME_RATE = 30;
89     private static final String VIDEO_FILE_PATH = InstrumentationRegistry
90             .getInstrumentation().getTargetContext()
91             .getExternalFilesDir(null).getPath();
92     private static final int[] mCamcorderProfileList = {
93             CamcorderProfile.QUALITY_HIGH,
94             CamcorderProfile.QUALITY_2160P,
95             CamcorderProfile.QUALITY_1080P,
96             CamcorderProfile.QUALITY_720P,
97             CamcorderProfile.QUALITY_480P,
98             CamcorderProfile.QUALITY_CIF,
99             CamcorderProfile.QUALITY_QCIF,
100             CamcorderProfile.QUALITY_QVGA,
101             CamcorderProfile.QUALITY_LOW,
102     };
103     private static final int MAX_VIDEO_SNAPSHOT_IMAGES = 5;
104     private static final int BURST_VIDEO_SNAPSHOT_NUM = 3;
105     private static final int SLOWMO_SLOW_FACTOR = 4;
106     private static final int MAX_NUM_FRAME_DROP_INTERVAL_ALLOWED = 4;
107     private List<Size> mSupportedVideoSizes;
108     private Surface mRecordingSurface;
109     private Surface mPersistentSurface;
110     private MediaRecorder mMediaRecorder;
111     private String mOutMediaFileName;
112     private int mVideoFrameRate;
113     private Size mVideoSize;
114     private long mRecordingStartTime;
115 
116     @Override
setUp()117     protected void setUp() throws Exception {
118         super.setUp();
119     }
120 
121     @Override
tearDown()122     protected void tearDown() throws Exception {
123         super.tearDown();
124     }
125 
doBasicRecording(boolean useVideoStab)126     private void doBasicRecording(boolean useVideoStab) throws Exception {
127         for (int i = 0; i < mCameraIds.length; i++) {
128             try {
129                 Log.i(TAG, "Testing basic recording for camera " + mCameraIds[i]);
130                 // Re-use the MediaRecorder object for the same camera device.
131                 mMediaRecorder = new MediaRecorder();
132                 openDevice(mCameraIds[i]);
133                 if (!mStaticInfo.isColorOutputSupported()) {
134                     Log.i(TAG, "Camera " + mCameraIds[i] +
135                             " does not support color outputs, skipping");
136                     continue;
137                 }
138 
139                 if (!mStaticInfo.isVideoStabilizationSupported() && useVideoStab) {
140                     Log.i(TAG, "Camera " + mCameraIds[i] +
141                             " does not support video stabilization, skipping the stabilization"
142                             + " test");
143                     continue;
144                 }
145 
146                 initSupportedVideoSize(mCameraIds[i]);
147 
148                 // Test iteration starts...
149                 for (int iteration = 0; iteration < getIterationCount(); ++iteration) {
150                     Log.v(TAG, String.format("Recording video: %d/%d", iteration + 1,
151                             getIterationCount()));
152                     basicRecordingTestByCamera(mCamcorderProfileList, useVideoStab);
153                     getResultPrinter().printStatus(getIterationCount(), iteration + 1,
154                             mCameraIds[i]);
155                     Thread.sleep(getTestWaitIntervalMs());
156                 }
157             } finally {
158                 closeDevice();
159                 releaseRecorder();
160             }
161         }
162     }
163 
164     /**
165      * <p>
166      * Test basic camera recording.
167      * </p>
168      * <p>
169      * This test covers the typical basic use case of camera recording.
170      * MediaRecorder is used to record the audio and video, CamcorderProfile is
171      * used to configure the MediaRecorder. It goes through the pre-defined
172      * CamcorderProfile list, test each profile configuration and validate the
173      * recorded video. Preview is set to the video size.
174      * </p>
175      */
testBasicRecording()176     public void testBasicRecording() throws Exception {
177         doBasicRecording(/*useVideoStab*/false);
178     }
179 
180     /**
181      * <p>
182      * Test video snapshot for each camera.
183      * </p>
184      * <p>
185      * This test covers video snapshot typical use case. The MediaRecorder is used to record the
186      * video for each available video size. The largest still capture size is selected to
187      * capture the JPEG image. The still capture images are validated according to the capture
188      * configuration. The timestamp of capture result before and after video snapshot is also
189      * checked to make sure no frame drop caused by video snapshot.
190      * </p>
191      */
testVideoSnapshot()192     public void testVideoSnapshot() throws Exception {
193         videoSnapshotHelper(/*burstTest*/false);
194     }
195 
testConstrainedHighSpeedRecording()196     public void testConstrainedHighSpeedRecording() throws Exception {
197         constrainedHighSpeedRecording();
198     }
199 
constrainedHighSpeedRecording()200     private void constrainedHighSpeedRecording() throws Exception {
201         for (String id : mCameraIds) {
202             try {
203                 Log.i(TAG, "Testing constrained high speed recording for camera " + id);
204                 // Re-use the MediaRecorder object for the same camera device.
205                 mMediaRecorder = new MediaRecorder();
206                 openDevice(id);
207 
208                 if (!mStaticInfo.isConstrainedHighSpeedVideoSupported()) {
209                     Log.i(TAG, "Camera " + id + " doesn't support high speed recording, skipping.");
210                     continue;
211                 }
212 
213                 // Test iteration starts...
214                 for (int iteration = 0; iteration < getIterationCount(); ++iteration) {
215                     Log.v(TAG, String.format("Constrained high speed recording: %d/%d",
216                             iteration + 1, getIterationCount()));
217 
218                     StreamConfigurationMap config =
219                             mStaticInfo.getValueFromKeyNonNull(
220                                     CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
221                     Size[] highSpeedVideoSizes = config.getHighSpeedVideoSizes();
222                     for (Size size : highSpeedVideoSizes) {
223                         List<Range<Integer>> fixedFpsRanges =
224                                 getHighSpeedFixedFpsRangeForSize(config, size);
225                         mCollector.expectTrue("Unable to find the fixed frame rate fps range for " +
226                                 "size " + size, fixedFpsRanges.size() > 0);
227                         // Test recording for each FPS range
228                         for (Range<Integer> fpsRange : fixedFpsRanges) {
229                             int captureRate = fpsRange.getLower();
230                             final int VIDEO_FRAME_RATE = 30;
231                             // Skip the test if the highest recording FPS supported by CamcorderProfile
232                             if (fpsRange.getUpper() > getFpsFromHighSpeedProfileForSize(size)) {
233                                 Log.w(TAG, "high speed recording " + size + "@" + captureRate + "fps"
234                                         + " is not supported by CamcorderProfile");
235                                 continue;
236                             }
237 
238                             mOutMediaFileName = VIDEO_FILE_PATH + "/test_cslowMo_video_" + captureRate +
239                                     "fps_" + id + "_" + size.toString() + ".mp4";
240 
241                             prepareRecording(size, VIDEO_FRAME_RATE, captureRate);
242 
243                             // prepare preview surface by using video size.
244                             updatePreviewSurfaceWithVideo(size, captureRate);
245 
246                             // Start recording
247                             SimpleCaptureCallback resultListener = new SimpleCaptureCallback();
248                             startSlowMotionRecording(/*useMediaRecorder*/true, VIDEO_FRAME_RATE,
249                                     captureRate, fpsRange, resultListener,
250                                     /*useHighSpeedSession*/true);
251 
252                             // Record certain duration.
253                             SystemClock.sleep(RECORDING_DURATION_MS);
254 
255                             // Stop recording and preview
256                             stopRecording(/*useMediaRecorder*/true);
257                             // Convert number of frames camera produced into the duration in unit of ms.
258                             int durationMs = (int) (resultListener.getTotalNumFrames() * 1000.0f /
259                                             VIDEO_FRAME_RATE);
260 
261                             // Validation.
262                             validateRecording(size, durationMs);
263                         }
264 
265                     getResultPrinter().printStatus(getIterationCount(), iteration + 1, id);
266                     Thread.sleep(getTestWaitIntervalMs());
267                     }
268                 }
269 
270             } finally {
271                 closeDevice();
272                 releaseRecorder();
273             }
274         }
275     }
276 
277     /**
278      * Get high speed FPS from CamcorderProfiles for a given size.
279      *
280      * @param size The size used to search the CamcorderProfiles for the FPS.
281      * @return high speed video FPS, 0 if the given size is not supported by the CamcorderProfiles.
282      */
getFpsFromHighSpeedProfileForSize(Size size)283     private int getFpsFromHighSpeedProfileForSize(Size size) {
284         for (int quality = CamcorderProfile.QUALITY_HIGH_SPEED_480P;
285                 quality <= CamcorderProfile.QUALITY_HIGH_SPEED_2160P; quality++) {
286             if (CamcorderProfile.hasProfile(quality)) {
287                 CamcorderProfile profile = CamcorderProfile.get(quality);
288                 if (size.equals(new Size(profile.videoFrameWidth, profile.videoFrameHeight))){
289                     return profile.videoFrameRate;
290                 }
291             }
292         }
293 
294         return 0;
295     }
296 
getHighSpeedFixedFpsRangeForSize(StreamConfigurationMap config, Size size)297     private List<Range<Integer>> getHighSpeedFixedFpsRangeForSize(StreamConfigurationMap config,
298             Size size) {
299         Range<Integer>[] availableFpsRanges = config.getHighSpeedVideoFpsRangesFor(size);
300         List<Range<Integer>> fixedRanges = new ArrayList<Range<Integer>>();
301         for (Range<Integer> range : availableFpsRanges) {
302             if (range.getLower().equals(range.getUpper())) {
303                 fixedRanges.add(range);
304             }
305         }
306         return fixedRanges;
307     }
308 
startSlowMotionRecording(boolean useMediaRecorder, int videoFrameRate, int captureRate, Range<Integer> fpsRange, CameraCaptureSession.CaptureCallback listener, boolean useHighSpeedSession)309     private void startSlowMotionRecording(boolean useMediaRecorder, int videoFrameRate,
310             int captureRate, Range<Integer> fpsRange,
311             CameraCaptureSession.CaptureCallback listener, boolean useHighSpeedSession) throws Exception {
312         List<Surface> outputSurfaces = new ArrayList<Surface>(2);
313         assertTrue("Both preview and recording surfaces should be valid",
314                 mPreviewSurface.isValid() && mRecordingSurface.isValid());
315         outputSurfaces.add(mPreviewSurface);
316         outputSurfaces.add(mRecordingSurface);
317         // Video snapshot surface
318         if (mReaderSurface != null) {
319             outputSurfaces.add(mReaderSurface);
320         }
321         mSessionListener = new BlockingSessionCallback();
322         mSession = configureCameraSession(mCamera, outputSurfaces, useHighSpeedSession,
323                 mSessionListener, mHandler);
324 
325         // Create slow motion request list
326         List<CaptureRequest> slowMoRequests = null;
327         if (useHighSpeedSession) {
328             CaptureRequest.Builder requestBuilder =
329                     mCamera.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
330             requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);
331             requestBuilder.addTarget(mPreviewSurface);
332             requestBuilder.addTarget(mRecordingSurface);
333             slowMoRequests = ((CameraConstrainedHighSpeedCaptureSession) mSession).
334                     createHighSpeedRequestList(requestBuilder.build());
335         } else {
336             CaptureRequest.Builder recordingRequestBuilder =
337                     mCamera.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
338             recordingRequestBuilder.set(CaptureRequest.CONTROL_MODE,
339                     CaptureRequest.CONTROL_MODE_USE_SCENE_MODE);
340             recordingRequestBuilder.set(CaptureRequest.CONTROL_SCENE_MODE,
341                     CaptureRequest.CONTROL_SCENE_MODE_HIGH_SPEED_VIDEO);
342 
343             CaptureRequest.Builder recordingOnlyBuilder =
344                     mCamera.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
345             recordingOnlyBuilder.set(CaptureRequest.CONTROL_MODE,
346                     CaptureRequest.CONTROL_MODE_USE_SCENE_MODE);
347             recordingOnlyBuilder.set(CaptureRequest.CONTROL_SCENE_MODE,
348                     CaptureRequest.CONTROL_SCENE_MODE_HIGH_SPEED_VIDEO);
349             int slowMotionFactor = captureRate / videoFrameRate;
350 
351             // Make sure camera output frame rate is set to correct value.
352             recordingRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);
353             recordingRequestBuilder.addTarget(mRecordingSurface);
354             recordingRequestBuilder.addTarget(mPreviewSurface);
355             recordingOnlyBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);
356             recordingOnlyBuilder.addTarget(mRecordingSurface);
357 
358             slowMoRequests = new ArrayList<CaptureRequest>();
359             slowMoRequests.add(recordingRequestBuilder.build());// Preview + recording.
360 
361             for (int i = 0; i < slowMotionFactor - 1; i++) {
362                 slowMoRequests.add(recordingOnlyBuilder.build()); // Recording only.
363             }
364         }
365 
366         mSession.setRepeatingBurst(slowMoRequests, listener, mHandler);
367 
368         if (useMediaRecorder) {
369             mMediaRecorder.start();
370         } else {
371             // TODO: need implement MediaCodec path.
372         }
373 
374     }
375 
376     /**
377      * Test camera recording by using each available CamcorderProfile for a
378      * given camera. preview size is set to the video size.
379      */
basicRecordingTestByCamera(int[] camcorderProfileList, boolean useVideoStab)380     private void basicRecordingTestByCamera(int[] camcorderProfileList, boolean useVideoStab)
381             throws Exception {
382         Size maxPreviewSize = mOrderedPreviewSizes.get(0);
383         List<Range<Integer> > fpsRanges = Arrays.asList(
384                 mStaticInfo.getAeAvailableTargetFpsRangesChecked());
385         int cameraId = Integer.parseInt(mCamera.getId());
386         int maxVideoFrameRate = -1;
387         for (int profileId : camcorderProfileList) {
388             if (!CamcorderProfile.hasProfile(cameraId, profileId) ||
389                     allowedUnsupported(cameraId, profileId)) {
390                 continue;
391             }
392 
393             CamcorderProfile profile = CamcorderProfile.get(cameraId, profileId);
394             Size videoSz = new Size(profile.videoFrameWidth, profile.videoFrameHeight);
395             Range<Integer> fpsRange = new Range(profile.videoFrameRate, profile.videoFrameRate);
396             if (maxVideoFrameRate < profile.videoFrameRate) {
397                 maxVideoFrameRate = profile.videoFrameRate;
398             }
399             if (mStaticInfo.isHardwareLevelLegacy() &&
400                     (videoSz.getWidth() > maxPreviewSize.getWidth() ||
401                      videoSz.getHeight() > maxPreviewSize.getHeight())) {
402                 // Skip. Legacy mode can only do recording up to max preview size
403                 continue;
404             }
405             assertTrue("Video size " + videoSz.toString() + " for profile ID " + profileId +
406                             " must be one of the camera device supported video size!",
407                             mSupportedVideoSizes.contains(videoSz));
408             assertTrue("Frame rate range " + fpsRange + " (for profile ID " + profileId +
409                     ") must be one of the camera device available FPS range!",
410                     fpsRanges.contains(fpsRange));
411 
412             if (VERBOSE) {
413                 Log.v(TAG, "Testing camera recording with video size " + videoSz.toString());
414             }
415 
416             // Configure preview and recording surfaces.
417             mOutMediaFileName = VIDEO_FILE_PATH + "/test_video.mp4";
418             if (DEBUG_DUMP) {
419                 mOutMediaFileName = VIDEO_FILE_PATH + "/test_video_" + cameraId + "_"
420                         + videoSz.toString() + ".mp4";
421             }
422 
423             prepareRecordingWithProfile(profile);
424 
425             // prepare preview surface by using video size.
426             updatePreviewSurfaceWithVideo(videoSz, profile.videoFrameRate);
427 
428             // Start recording
429             SimpleCaptureCallback resultListener = new SimpleCaptureCallback();
430             startRecording(/* useMediaRecorder */true, resultListener, useVideoStab);
431 
432             // Record certain duration.
433             SystemClock.sleep(RECORDING_DURATION_MS);
434 
435             // Stop recording and preview
436             stopRecording(/* useMediaRecorder */true);
437             // Convert number of frames camera produced into the duration in unit of ms.
438             int durationMs = (int) (resultListener.getTotalNumFrames() * 1000.0f /
439                             profile.videoFrameRate);
440 
441             if (VERBOSE) {
442                 Log.v(TAG, "video frame rate: " + profile.videoFrameRate +
443                                 ", num of frames produced: " + resultListener.getTotalNumFrames());
444             }
445 
446             // Validation.
447             validateRecording(videoSz, durationMs);
448         }
449         if (maxVideoFrameRate != -1) {
450             // At least one CamcorderProfile is present, check FPS
451             assertTrue("At least one CamcorderProfile must support >= 24 FPS",
452                     maxVideoFrameRate >= 24);
453         }
454     }
455 
456     /**
457      * Initialize the supported video sizes.
458      */
initSupportedVideoSize(String cameraId)459     private void initSupportedVideoSize(String cameraId)  throws Exception {
460         Size maxVideoSize = SIZE_BOUND_1080P;
461         if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_2160P)) {
462             maxVideoSize = SIZE_BOUND_2160P;
463         }
464         mSupportedVideoSizes =
465                 getSupportedVideoSizes(cameraId, mCameraManager, maxVideoSize);
466     }
467 
468     /**
469      * Simple wrapper to wrap normal/burst video snapshot tests
470      */
videoSnapshotHelper(boolean burstTest)471     private void videoSnapshotHelper(boolean burstTest) throws Exception {
472             for (String id : mCameraIds) {
473                 try {
474                     Log.i(TAG, "Testing video snapshot for camera " + id);
475                     // Re-use the MediaRecorder object for the same camera device.
476                     mMediaRecorder = new MediaRecorder();
477 
478                     openDevice(id);
479 
480                     if (!mStaticInfo.isColorOutputSupported()) {
481                         Log.i(TAG, "Camera " + id +
482                                 " does not support color outputs, skipping");
483                         continue;
484                     }
485 
486                     initSupportedVideoSize(id);
487 
488                     // Test iteration starts...
489                     for (int iteration = 0; iteration < getIterationCount(); ++iteration) {
490                         Log.v(TAG, String.format("Video snapshot: %d/%d", iteration + 1,
491                                 getIterationCount()));
492                         videoSnapshotTestByCamera(burstTest);
493                         getResultPrinter().printStatus(getIterationCount(), iteration + 1, id);
494                         Thread.sleep(getTestWaitIntervalMs());
495                     }
496                 } finally {
497                     closeDevice();
498                     releaseRecorder();
499                 }
500             }
501     }
502 
503     /**
504      * Returns {@code true} if the {@link CamcorderProfile} ID is allowed to be unsupported.
505      *
506      * <p>This only allows unsupported profiles when using the LEGACY mode of the Camera API.</p>
507      *
508      * @param profileId a {@link CamcorderProfile} ID to check.
509      * @return {@code true} if supported.
510      */
allowedUnsupported(int cameraId, int profileId)511     private boolean allowedUnsupported(int cameraId, int profileId) {
512         if (!mStaticInfo.isHardwareLevelLegacy()) {
513             return false;
514         }
515 
516         switch(profileId) {
517             case CamcorderProfile.QUALITY_2160P:
518             case CamcorderProfile.QUALITY_1080P:
519             case CamcorderProfile.QUALITY_HIGH:
520                 return !CamcorderProfile.hasProfile(cameraId, profileId) ||
521                         CamcorderProfile.get(cameraId, profileId).videoFrameWidth >= 1080;
522         }
523         return false;
524     }
525 
526     /**
527      * Test video snapshot for each  available CamcorderProfile for a given camera.
528      *
529      * <p>
530      * Preview size is set to the video size. For the burst test, frame drop and jittering
531      * is not checked.
532      * </p>
533      *
534      * @param burstTest Perform burst capture or single capture. For burst capture
535      *                  {@value #BURST_VIDEO_SNAPSHOT_NUM} capture requests will be sent.
536      */
videoSnapshotTestByCamera(boolean burstTest)537     private void videoSnapshotTestByCamera(boolean burstTest)
538             throws Exception {
539         final int NUM_SINGLE_SHOT_TEST = 5;
540         final int FRAMEDROP_TOLERANCE = 8;
541         final int FRAME_SIZE_15M = 15000000;
542         final float FRAME_DROP_TOLERENCE_FACTOR = 1.5f;
543         int kFrameDrop_Tolerence = FRAMEDROP_TOLERANCE;
544 
545         for (int profileId : mCamcorderProfileList) {
546             int cameraId = Integer.parseInt(mCamera.getId());
547             if (!CamcorderProfile.hasProfile(cameraId, profileId) ||
548                     allowedUnsupported(cameraId, profileId)) {
549                 continue;
550             }
551 
552             CamcorderProfile profile = CamcorderProfile.get(cameraId, profileId);
553             Size videoSz = new Size(profile.videoFrameWidth, profile.videoFrameHeight);
554             Size maxPreviewSize = mOrderedPreviewSizes.get(0);
555 
556             if (mStaticInfo.isHardwareLevelLegacy() &&
557                     (videoSz.getWidth() > maxPreviewSize.getWidth() ||
558                      videoSz.getHeight() > maxPreviewSize.getHeight())) {
559                 // Skip. Legacy mode can only do recording up to max preview size
560                 continue;
561             }
562 
563             if (!mSupportedVideoSizes.contains(videoSz)) {
564                 mCollector.addMessage("Video size " + videoSz.toString() + " for profile ID " +
565                         profileId + " must be one of the camera device supported video size!");
566                 continue;
567             }
568 
569             // For LEGACY, find closest supported smaller or equal JPEG size to the current video
570             // size; if no size is smaller than the video, pick the smallest JPEG size.  The assert
571             // for video size above guarantees that for LIMITED or FULL, we select videoSz here.
572             // Also check for minFrameDuration here to make sure jpeg stream won't slow down
573             // video capture
574             Size videoSnapshotSz = mOrderedStillSizes.get(mOrderedStillSizes.size() - 1);
575             // Allow a bit tolerance so we don't fail for a few nano seconds of difference
576             final float FRAME_DURATION_TOLERANCE = 0.01f;
577             long videoFrameDuration = (long) (1e9 / profile.videoFrameRate *
578                     (1.0 + FRAME_DURATION_TOLERANCE));
579             HashMap<Size, Long> minFrameDurationMap = mStaticInfo.
580                     getAvailableMinFrameDurationsForFormatChecked(ImageFormat.JPEG);
581             for (int i = mOrderedStillSizes.size() - 2; i >= 0; i--) {
582                 Size candidateSize = mOrderedStillSizes.get(i);
583                 if (mStaticInfo.isHardwareLevelLegacy()) {
584                     // Legacy level doesn't report min frame duration
585                     if (candidateSize.getWidth() <= videoSz.getWidth() &&
586                             candidateSize.getHeight() <= videoSz.getHeight()) {
587                         videoSnapshotSz = candidateSize;
588                     }
589                 } else {
590                     Long jpegFrameDuration = minFrameDurationMap.get(candidateSize);
591                     assertTrue("Cannot find minimum frame duration for jpeg size " + candidateSize,
592                             jpegFrameDuration != null);
593                     if (candidateSize.getWidth() <= videoSz.getWidth() &&
594                             candidateSize.getHeight() <= videoSz.getHeight() &&
595                             jpegFrameDuration <= videoFrameDuration) {
596                         videoSnapshotSz = candidateSize;
597                     }
598                 }
599             }
600 
601             /**
602              * Only test full res snapshot when below conditions are all true.
603              * 1. Camera is a FULL device
604              * 2. video size is up to max preview size, which will be bounded by 1080p.
605              * 3. Full resolution jpeg stream can keep up to video stream speed.
606              *    When full res jpeg stream cannot keep up to video stream speed, search
607              *    the largest jpeg size that can susptain video speed instead.
608              */
609             if (mStaticInfo.isHardwareLevelFull() &&
610                     videoSz.getWidth() <= maxPreviewSize.getWidth() &&
611                     videoSz.getHeight() <= maxPreviewSize.getHeight()) {
612                 for (Size jpegSize : mOrderedStillSizes) {
613                     Long jpegFrameDuration = minFrameDurationMap.get(jpegSize);
614                     assertTrue("Cannot find minimum frame duration for jpeg size " + jpegSize,
615                             jpegFrameDuration != null);
616                     if (jpegFrameDuration <= videoFrameDuration) {
617                         videoSnapshotSz = jpegSize;
618                         break;
619                     }
620                     if (jpegSize.equals(videoSz)) {
621                         throw new AssertionFailedError(
622                                 "Cannot find adequate video snapshot size for video size" +
623                                         videoSz);
624                     }
625                 }
626             }
627 
628             Log.i(TAG, "Testing video snapshot size " + videoSnapshotSz +
629                     " for video size " + videoSz);
630             if (videoSnapshotSz.getWidth() * videoSnapshotSz.getHeight() > FRAME_SIZE_15M)
631                 kFrameDrop_Tolerence = (int)(FRAMEDROP_TOLERANCE * FRAME_DROP_TOLERENCE_FACTOR);
632 
633             createImageReader(
634                     videoSnapshotSz, ImageFormat.JPEG,
635                     MAX_VIDEO_SNAPSHOT_IMAGES, /*listener*/null);
636 
637             if (VERBOSE) {
638                 Log.v(TAG, "Testing camera recording with video size " + videoSz.toString());
639             }
640 
641             // Configure preview and recording surfaces.
642             mOutMediaFileName = VIDEO_FILE_PATH + "/test_video.mp4";
643             if (DEBUG_DUMP) {
644                 mOutMediaFileName = VIDEO_FILE_PATH + "/test_video_" + cameraId + "_"
645                         + videoSz.toString() + ".mp4";
646             }
647 
648             int numTestIterations = burstTest ? 1 : NUM_SINGLE_SHOT_TEST;
649             int totalDroppedFrames = 0;
650 
651             for (int numTested = 0; numTested < numTestIterations; numTested++) {
652                 prepareRecordingWithProfile(profile);
653 
654                 // prepare video snapshot
655                 SimpleCaptureCallback resultListener = new SimpleCaptureCallback();
656                 SimpleImageReaderListener imageListener = new SimpleImageReaderListener();
657                 CaptureRequest.Builder videoSnapshotRequestBuilder =
658                         mCamera.createCaptureRequest((mStaticInfo.isHardwareLevelLegacy()) ?
659                                 CameraDevice.TEMPLATE_RECORD :
660                                 CameraDevice.TEMPLATE_VIDEO_SNAPSHOT);
661 
662                 // prepare preview surface by using video size.
663                 updatePreviewSurfaceWithVideo(videoSz, profile.videoFrameRate);
664 
665                 prepareVideoSnapshot(videoSnapshotRequestBuilder, imageListener);
666                 Range<Integer> fpsRange = Range.create(profile.videoFrameRate,
667                         profile.videoFrameRate);
668                 videoSnapshotRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
669                         fpsRange);
670                 CaptureRequest request = videoSnapshotRequestBuilder.build();
671 
672                 // Start recording
673                 startRecording(/* useMediaRecorder */true, resultListener, /*useVideoStab*/false);
674                 long startTime = SystemClock.elapsedRealtime();
675 
676                 // Record certain duration.
677                 SystemClock.sleep(RECORDING_DURATION_MS / 2);
678 
679                 // take video snapshot
680                 if (burstTest) {
681                     List<CaptureRequest> requests =
682                             new ArrayList<CaptureRequest>(BURST_VIDEO_SNAPSHOT_NUM);
683                     for (int i = 0; i < BURST_VIDEO_SNAPSHOT_NUM; i++) {
684                         requests.add(request);
685                     }
686                     mSession.captureBurst(requests, resultListener, mHandler);
687                 } else {
688                     mSession.capture(request, resultListener, mHandler);
689                 }
690 
691                 // make sure recording is still going after video snapshot
692                 SystemClock.sleep(RECORDING_DURATION_MS / 2);
693 
694                 // Stop recording and preview
695                 int durationMs = stopRecording(/* useMediaRecorder */true);
696                 // For non-burst test, use number of frames to also double check video frame rate.
697                 // Burst video snapshot is allowed to cause frame rate drop, so do not use number
698                 // of frames to estimate duration
699                 if (!burstTest) {
700                     durationMs = (int) (resultListener.getTotalNumFrames() * 1000.0f /
701                         profile.videoFrameRate);
702                 }
703 
704                 // Validation recorded video
705                 validateRecording(videoSz, durationMs);
706 
707                 if (burstTest) {
708                     for (int i = 0; i < BURST_VIDEO_SNAPSHOT_NUM; i++) {
709                         Image image = imageListener.getImage(CAPTURE_IMAGE_TIMEOUT_MS);
710                         validateVideoSnapshotCapture(image, videoSnapshotSz);
711                         image.close();
712                     }
713                 } else {
714                     // validate video snapshot image
715                     Image image = imageListener.getImage(CAPTURE_IMAGE_TIMEOUT_MS);
716                     validateVideoSnapshotCapture(image, videoSnapshotSz);
717 
718                     // validate if there is framedrop around video snapshot
719                     totalDroppedFrames +=  validateFrameDropAroundVideoSnapshot(
720                             resultListener, image.getTimestamp());
721 
722                     //TODO: validate jittering. Should move to PTS
723                     //validateJittering(resultListener);
724 
725                     image.close();
726                 }
727             }
728 
729             if (!burstTest) {
730                 Log.w(TAG, String.format("Camera %d Video size %s: Number of dropped frames " +
731                         "detected in %d trials is %d frames.", cameraId, videoSz.toString(),
732                         numTestIterations, totalDroppedFrames));
733                 mCollector.expectLessOrEqual(
734                         String.format(
735                                 "Camera %d Video size %s: Number of dropped frames %d must not"
736                                 + " be larger than %d",
737                                 cameraId, videoSz.toString(), totalDroppedFrames,
738                                 kFrameDrop_Tolerence),
739                         kFrameDrop_Tolerence, totalDroppedFrames);
740             }
741             closeImageReader();
742         }
743     }
744 
745     /**
746      * Configure video snapshot request according to the still capture size
747      */
prepareVideoSnapshot( CaptureRequest.Builder requestBuilder, ImageReader.OnImageAvailableListener imageListener)748     private void prepareVideoSnapshot(
749             CaptureRequest.Builder requestBuilder,
750             ImageReader.OnImageAvailableListener imageListener)
751             throws Exception {
752         mReader.setOnImageAvailableListener(imageListener, mHandler);
753         assertNotNull("Recording surface must be non-null!", mRecordingSurface);
754         requestBuilder.addTarget(mRecordingSurface);
755         assertNotNull("Preview surface must be non-null!", mPreviewSurface);
756         requestBuilder.addTarget(mPreviewSurface);
757         assertNotNull("Reader surface must be non-null!", mReaderSurface);
758         requestBuilder.addTarget(mReaderSurface);
759     }
760 
761     /**
762      * Update preview size with video size.
763      *
764      * <p>Preview size will be capped with max preview size.</p>
765      *
766      * @param videoSize The video size used for preview.
767      * @param videoFrameRate The video frame rate
768      *
769      */
updatePreviewSurfaceWithVideo(Size videoSize, int videoFrameRate)770     private void updatePreviewSurfaceWithVideo(Size videoSize, int videoFrameRate) {
771         if (mOrderedPreviewSizes == null) {
772             throw new IllegalStateException("supported preview size list is not initialized yet");
773         }
774         final float FRAME_DURATION_TOLERANCE = 0.01f;
775         long videoFrameDuration = (long) (1e9 / videoFrameRate *
776                 (1.0 + FRAME_DURATION_TOLERANCE));
777         HashMap<Size, Long> minFrameDurationMap = mStaticInfo.
778                 getAvailableMinFrameDurationsForFormatChecked(ImageFormat.PRIVATE);
779         Size maxPreviewSize = mOrderedPreviewSizes.get(0);
780         Size previewSize = null;
781         if (videoSize.getWidth() > maxPreviewSize.getWidth() ||
782                 videoSize.getHeight() > maxPreviewSize.getHeight()) {
783             for (Size s : mOrderedPreviewSizes) {
784                 Long frameDuration = minFrameDurationMap.get(s);
785                 if (mStaticInfo.isHardwareLevelLegacy()) {
786                     // Legacy doesn't report min frame duration
787                     frameDuration = new Long(0);
788                 }
789                 assertTrue("Cannot find minimum frame duration for private size" + s,
790                         frameDuration != null);
791                 if (frameDuration <= videoFrameDuration &&
792                         s.getWidth() <= videoSize.getWidth() &&
793                         s.getHeight() <= videoSize.getHeight()) {
794                     Log.w(TAG, "Overwrite preview size from " + videoSize.toString() +
795                             " to " + s.toString());
796                     previewSize = s;
797                     break;
798                     // If all preview size doesn't work then we fallback to video size
799                 }
800             }
801         }
802         if (previewSize == null) {
803             previewSize = videoSize;
804         }
805         updatePreviewSurface(previewSize);
806     }
807 
808     /**
809      * Configure MediaRecorder recording session with CamcorderProfile, prepare
810      * the recording surface.
811      */
prepareRecordingWithProfile(CamcorderProfile profile)812     private void prepareRecordingWithProfile(CamcorderProfile profile)
813             throws Exception {
814         // Prepare MediaRecorder.
815         mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
816         mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
817         mMediaRecorder.setProfile(profile);
818         mMediaRecorder.setOutputFile(mOutMediaFileName);
819         if (mPersistentSurface != null) {
820             mMediaRecorder.setInputSurface(mPersistentSurface);
821             mRecordingSurface = mPersistentSurface;
822         }
823         mMediaRecorder.prepare();
824         if (mPersistentSurface == null) {
825             mRecordingSurface = mMediaRecorder.getSurface();
826         }
827         assertNotNull("Recording surface must be non-null!", mRecordingSurface);
828         mVideoFrameRate = profile.videoFrameRate;
829         mVideoSize = new Size(profile.videoFrameWidth, profile.videoFrameHeight);
830     }
831 
832     /**
833      * Configure MediaRecorder recording session with CamcorderProfile, prepare
834      * the recording surface. Use AVC for video compression, AAC for audio compression.
835      * Both are required for android devices by android CDD.
836      */
prepareRecording(Size sz, int videoFrameRate, int captureRate)837     private void prepareRecording(Size sz, int videoFrameRate, int captureRate)
838             throws Exception {
839         // Prepare MediaRecorder.
840         mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
841         mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
842         mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
843         mMediaRecorder.setOutputFile(mOutMediaFileName);
844         mMediaRecorder.setVideoEncodingBitRate(getVideoBitRate(sz));
845         mMediaRecorder.setVideoFrameRate(videoFrameRate);
846         mMediaRecorder.setCaptureRate(captureRate);
847         mMediaRecorder.setVideoSize(sz.getWidth(), sz.getHeight());
848         mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
849         mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
850         if (mPersistentSurface != null) {
851             mMediaRecorder.setInputSurface(mPersistentSurface);
852             mRecordingSurface = mPersistentSurface;
853         }
854         mMediaRecorder.prepare();
855         if (mPersistentSurface == null) {
856             mRecordingSurface = mMediaRecorder.getSurface();
857         }
858         assertNotNull("Recording surface must be non-null!", mRecordingSurface);
859         mVideoFrameRate = videoFrameRate;
860         mVideoSize = sz;
861     }
862 
startRecording(boolean useMediaRecorder, CameraCaptureSession.CaptureCallback listener, boolean useVideoStab)863     private void startRecording(boolean useMediaRecorder,
864             CameraCaptureSession.CaptureCallback listener, boolean useVideoStab) throws Exception {
865         if (!mStaticInfo.isVideoStabilizationSupported() && useVideoStab) {
866             throw new IllegalArgumentException("Video stabilization is not supported");
867         }
868 
869         List<Surface> outputSurfaces = new ArrayList<Surface>(2);
870         assertTrue("Both preview and recording surfaces should be valid",
871                 mPreviewSurface.isValid() && mRecordingSurface.isValid());
872         outputSurfaces.add(mPreviewSurface);
873         outputSurfaces.add(mRecordingSurface);
874         // Video snapshot surface
875         if (mReaderSurface != null) {
876             outputSurfaces.add(mReaderSurface);
877         }
878         mSessionListener = new BlockingSessionCallback();
879 
880         CaptureRequest.Builder recordingRequestBuilder =
881                 mCamera.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
882         // Make sure camera output frame rate is set to correct value.
883         Range<Integer> fpsRange = Range.create(mVideoFrameRate, mVideoFrameRate);
884         recordingRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);
885         if (useVideoStab) {
886             recordingRequestBuilder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
887                     CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON);
888         }
889         recordingRequestBuilder.addTarget(mRecordingSurface);
890         recordingRequestBuilder.addTarget(mPreviewSurface);
891         CaptureRequest recordingRequest = recordingRequestBuilder.build();
892         mSession = configureCameraSessionWithParameters(mCamera, outputSurfaces, mSessionListener,
893                 mHandler, recordingRequest);
894         mSession.setRepeatingRequest(recordingRequest, listener, mHandler);
895 
896         if (useMediaRecorder) {
897             mMediaRecorder.start();
898         } else {
899             // TODO: need implement MediaCodec path.
900         }
901         mRecordingStartTime = SystemClock.elapsedRealtime();
902     }
903 
stopCameraStreaming()904     private void stopCameraStreaming() throws Exception {
905         if (VERBOSE) {
906             Log.v(TAG, "Stopping camera streaming and waiting for idle");
907         }
908         // Stop repeating, wait for captures to complete, and disconnect from
909         // surfaces
910         mSession.close();
911         mSessionListener.getStateWaiter().waitForState(SESSION_CLOSED, SESSION_CLOSE_TIMEOUT_MS);
912     }
913 
914     // Stop recording and return the estimated video duration in milliseconds.
stopRecording(boolean useMediaRecorder)915     private int stopRecording(boolean useMediaRecorder) throws Exception {
916         long stopRecordingTime = SystemClock.elapsedRealtime();
917         if (useMediaRecorder) {
918             stopCameraStreaming();
919 
920             mMediaRecorder.stop();
921             // Can reuse the MediaRecorder object after reset.
922             mMediaRecorder.reset();
923         } else {
924             // TODO: need implement MediaCodec path.
925         }
926         if (mPersistentSurface == null && mRecordingSurface != null) {
927             mRecordingSurface.release();
928             mRecordingSurface = null;
929         }
930         return (int) (stopRecordingTime - mRecordingStartTime);
931     }
932 
releaseRecorder()933     private void releaseRecorder() {
934         if (mMediaRecorder != null) {
935             mMediaRecorder.release();
936             mMediaRecorder = null;
937         }
938     }
939 
validateRecording(Size sz, int expectedDurationMs)940     private void validateRecording(Size sz, int expectedDurationMs) throws Exception {
941         File outFile = new File(mOutMediaFileName);
942         assertTrue("No video is recorded", outFile.exists());
943 
944         MediaExtractor extractor = new MediaExtractor();
945         try {
946             extractor.setDataSource(mOutMediaFileName);
947             long durationUs = 0;
948             int width = -1, height = -1;
949             int numTracks = extractor.getTrackCount();
950             final String VIDEO_MIME_TYPE = "video";
951             for (int i = 0; i < numTracks; i++) {
952                 MediaFormat format = extractor.getTrackFormat(i);
953                 String mime = format.getString(MediaFormat.KEY_MIME);
954                 if (mime.contains(VIDEO_MIME_TYPE)) {
955                     Log.i(TAG, "video format is: " + format.toString());
956                     durationUs = format.getLong(MediaFormat.KEY_DURATION);
957                     width = format.getInteger(MediaFormat.KEY_WIDTH);
958                     height = format.getInteger(MediaFormat.KEY_HEIGHT);
959                     break;
960                 }
961             }
962             Size videoSz = new Size(width, height);
963             assertTrue("Video size doesn't match, expected " + sz.toString() +
964                     " got " + videoSz.toString(), videoSz.equals(sz));
965             int duration = (int) (durationUs / 1000);
966             if (VERBOSE) {
967                 Log.v(TAG, String.format("Video duration: recorded %dms, expected %dms",
968                                          duration, expectedDurationMs));
969             }
970 
971             // TODO: Don't skip this for video snapshot
972             if (!mStaticInfo.isHardwareLevelLegacy()) {
973                 assertTrue(String.format(
974                         "Camera %s: Video duration doesn't match: recorded %dms, expected %dms.",
975                         mCamera.getId(), duration, expectedDurationMs),
976                         Math.abs(duration - expectedDurationMs) <
977                         DURATION_MARGIN * expectedDurationMs);
978             }
979         } finally {
980             extractor.release();
981             if (!DEBUG_DUMP) {
982                 outFile.delete();
983             }
984         }
985     }
986 
987     /**
988      * Validate video snapshot capture image object soundness and test.
989      *
990      * <p> Check for size, format and jpeg decoding</p>
991      *
992      * @param image The JPEG image to be verified.
993      * @param size The JPEG capture size to be verified against.
994      */
995     private void validateVideoSnapshotCapture(Image image, Size size) {
996         CameraTestUtils.validateImage(image, size.getWidth(), size.getHeight(),
997                 ImageFormat.JPEG, /*filePath*/null);
998     }
999 
1000     /**
1001      * Validate if video snapshot causes frame drop.
1002      * Here frame drop is defined as frame duration >= 2 * expected frame duration.
1003      * Return the estimated number of frames dropped during video snapshot
1004      */
1005     private int validateFrameDropAroundVideoSnapshot(
1006             SimpleCaptureCallback resultListener, long imageTimeStamp) {
1007         double expectedDurationMs = 1000.0 / mVideoFrameRate;
1008         CaptureResult prevResult = resultListener.getCaptureResult(WAIT_FOR_RESULT_TIMEOUT_MS);
1009         long prevTS = getValueNotNull(prevResult, CaptureResult.SENSOR_TIMESTAMP);
1010         while (!resultListener.hasMoreResults()) {
1011             CaptureResult currentResult =
1012                     resultListener.getCaptureResult(WAIT_FOR_RESULT_TIMEOUT_MS);
1013             long currentTS = getValueNotNull(currentResult, CaptureResult.SENSOR_TIMESTAMP);
1014             if (currentTS == imageTimeStamp) {
1015                 // validate the timestamp before and after, then return
1016                 CaptureResult nextResult =
1017                         resultListener.getCaptureResult(WAIT_FOR_RESULT_TIMEOUT_MS);
1018                 long nextTS = getValueNotNull(nextResult, CaptureResult.SENSOR_TIMESTAMP);
1019                 double durationMs = (currentTS - prevTS) / 1000000.0;
1020                 int totalFramesDropped = 0;
1021 
1022                 // Snapshots in legacy mode pause the preview briefly.  Skip the duration
1023                 // requirements for legacy mode unless this is fixed.
1024                 if (!mStaticInfo.isHardwareLevelLegacy()) {
1025                     mCollector.expectTrue(
1026                             String.format(
1027                                     "Video %dx%d Frame drop detected before video snapshot: " +
1028                                             "duration %.2fms (expected %.2fms)",
1029                                     mVideoSize.getWidth(), mVideoSize.getHeight(),
1030                                     durationMs, expectedDurationMs
1031                             ),
1032                             durationMs <= (expectedDurationMs * MAX_NUM_FRAME_DROP_INTERVAL_ALLOWED)
1033                     );
1034                     // Log a warning is there is any frame drop detected.
1035                     if (durationMs >= expectedDurationMs * 2) {
1036                         Log.w(TAG, String.format(
1037                                 "Video %dx%d Frame drop detected before video snapshot: " +
1038                                         "duration %.2fms (expected %.2fms)",
1039                                 mVideoSize.getWidth(), mVideoSize.getHeight(),
1040                                 durationMs, expectedDurationMs
1041                         ));
1042                     }
1043 
1044                     durationMs = (nextTS - currentTS) / 1000000.0;
1045                     mCollector.expectTrue(
1046                             String.format(
1047                                     "Video %dx%d Frame drop detected after video snapshot: " +
1048                                             "duration %.2fms (expected %.2fms)",
1049                                     mVideoSize.getWidth(), mVideoSize.getHeight(),
1050                                     durationMs, expectedDurationMs
1051                             ),
1052                             durationMs <= (expectedDurationMs * MAX_NUM_FRAME_DROP_INTERVAL_ALLOWED)
1053                     );
1054                     // Log a warning is there is any frame drop detected.
1055                     if (durationMs >= expectedDurationMs * 2) {
1056                         Log.w(TAG, String.format(
1057                                 "Video %dx%d Frame drop detected after video snapshot: " +
1058                                         "duration %fms (expected %fms)",
1059                                 mVideoSize.getWidth(), mVideoSize.getHeight(),
1060                                 durationMs, expectedDurationMs
1061                         ));
1062                     }
1063 
1064                     double totalDurationMs = (nextTS - prevTS) / 1000000.0;
1065                     // Minus 2 for the expected 2 frames interval
1066                     totalFramesDropped = (int) (totalDurationMs / expectedDurationMs) - 2;
1067                     if (totalFramesDropped < 0) {
1068                         Log.w(TAG, "totalFrameDropped is " + totalFramesDropped +
1069                                 ". Video frame rate might be too fast.");
1070                     }
1071                     totalFramesDropped = Math.max(0, totalFramesDropped);
1072                 }
1073                 return totalFramesDropped;
1074             }
1075             prevTS = currentTS;
1076         }
1077         throw new AssertionFailedError(
1078                 "Video snapshot timestamp does not match any of capture results!");
1079     }
1080 
1081     /**
1082      * Calculate a video bit rate based on the size. The bit rate is scaled
1083      * based on ratio of video size to 1080p size.
1084      */
1085     private int getVideoBitRate(Size sz) {
1086         int rate = BIT_RATE_1080P;
1087         float scaleFactor = sz.getHeight() * sz.getWidth() / (float)(1920 * 1080);
1088         rate = (int)(rate * scaleFactor);
1089 
1090         // Clamp to the MIN, MAX range.
1091         return Math.max(BIT_RATE_MIN, Math.min(BIT_RATE_MAX, rate));
1092     }
1093 }
1094