Merge "ITS: doBasicRecording command impl." into tm-dev
diff --git a/apps/CameraITS/tests/scene4/test_video_aspect_ratio_and_crop.py b/apps/CameraITS/tests/scene4/test_video_aspect_ratio_and_crop.py
index 00ebf2a..af7e5d5 100644
--- a/apps/CameraITS/tests/scene4/test_video_aspect_ratio_and_crop.py
+++ b/apps/CameraITS/tests/scene4/test_video_aspect_ratio_and_crop.py
@@ -24,6 +24,7 @@
_ANDROID13_API_LEVEL = 32
_NAME = os.path.splitext(os.path.basename(__file__))[0]
+_VIDEO_RECORDING_DURATION_SECONDS = 2
class VideoAspectRatioAndCropTest(its_base_test.ItsBaseTest):
@@ -103,10 +104,17 @@
self.tablet, chart_distance=0)
supported_video_qualities = cam.get_supported_video_qualities(
self.camera_id)
- test_quality_list = video_processing_utils.create_test_format_list(
- supported_video_qualities)
- logging.debug('Video qualities to be tested: %s', test_quality_list)
- # TODO(ruchamk):Add video recordin, aspect ratio and crop checks.
+ logging.debug('Supported video qualities: %s', supported_video_qualities)
+
+ for quality_profile_id_pair in supported_video_qualities:
+ quality = quality_profile_id_pair.split(':')[0]
+ profile_id = quality_profile_id_pair.split(':')[-1]
+ # Check if we support testing this quality.
+ if quality in video_processing_utils._ITS_SUPPORTED_QUALITIES:
+ logging.debug("Testing video recording for quality: %s" % quality)
+ cam.do_basic_recording(profile_id, quality, _VIDEO_RECORDING_DURATION_SECONDS)
+ # TODO(ruchamk): Add processing of video recordings, aspect ratio
+ # and crop checks.
if __name__ == '__main__':
test_runner.main()
diff --git a/apps/CameraITS/utils/its_session_utils.py b/apps/CameraITS/utils/its_session_utils.py
index 45a499c..85ac686 100644
--- a/apps/CameraITS/utils/its_session_utils.py
+++ b/apps/CameraITS/utils/its_session_utils.py
@@ -452,12 +452,53 @@
self.sock.settimeout(self.SOCK_TIMEOUT)
return data['objValue']
+ def do_basic_recording(self, profile_id, quality, duration):
+ """Issue a recording request and read back the video recording object.
+
+ The recording will be done with the format specified in quality. These
+ quality levels correspond to the profiles listed in CamcorderProfile.
+ The duration is the time in seconds for which the video will be recorded.
+ The recorded object consists of a path on the device at which the
+ recorded video is saved.
+
+ Args:
+ profile_id: int; profile id corresponding to the quality level.
+ quality: Video recording quality such as High, Low, VGA.
+ duration: The time in seconds for which the video will be recorded.
+ Returns:
+ video_recorded_object: The recorded object returned from ItsService which
+ contains path at which the recording is saved on the device, quality of the
+ recorded video, video size of the recorded video, video frame rate.
+ Ex:
+ VideoRecordingObject: {
+ 'tag': 'recordingResponse',
+ 'objValue': {
+ 'recordedOutputPath': '/storage/emulated/0/Android/data/com.android.cts.verifier'
+ '/files/VideoITS/VID_20220324_080414_0_CIF_352x288.mp4',
+ 'quality': 'CIF',
+ 'videoFrameRate': 30,
+ 'videoSize': '352x288'
+ }
+ }
+ """
+ cmd = {'cmdName': 'doBasicRecording', 'cameraId': self._camera_id,
+ 'profileId': profile_id, 'quality': quality, 'recordingDuration': duration}
+ self.sock.send(json.dumps(cmd).encode() + '\n'.encode())
+ timeout = self.SOCK_TIMEOUT + self.EXTRA_SOCK_TIMEOUT
+ self.sock.settimeout(timeout)
+ data, _ = self.__read_response_from_socket()
+ if data['tag'] != 'recordingResponse':
+ raise error_util.CameraItsError(f'Invalid response for command: {cmd[cmdName]}')
+ logging.debug('VideoRecordingObject: %s' % data)
+ return data['objValue']
+
def get_supported_video_qualities(self, camera_id):
"""Get all supported video qualities for this camera device.
Args:
camera_id: device id
Returns:
- List of all supported video qualities
+ List of all supported video qualities and corresponding profileIds.
+ Ex: ['480:4', '1080:6', '2160:8', '720:5', 'CIF:3', 'HIGH:1', 'LOW:0', 'QCIF:2', 'QVGA:7']
"""
cmd = {}
cmd['cmdName'] = 'getSupportedVideoQualities'
diff --git a/apps/CameraITS/utils/video_processing_utils.py b/apps/CameraITS/utils/video_processing_utils.py
index 9646003..967bb9e 100644
--- a/apps/CameraITS/utils/video_processing_utils.py
+++ b/apps/CameraITS/utils/video_processing_utils.py
@@ -17,30 +17,14 @@
# CamcorderProfile. For Video ITS, we will currently test below qualities
# only if supported by the camera device.
_ITS_SUPPORTED_QUALITIES = (
- "HIGH",
- "2160P",
- "1080P",
- "720P",
- "480P",
- "CIF",
- "QCIF",
- "QVGA",
- "LOW",
- "VGA"
+ 'HIGH',
+ '2160P',
+ '1080P',
+ '720P',
+ '480P',
+ 'CIF',
+ 'QCIF',
+ 'QVGA',
+ 'LOW',
+ 'VGA'
)
-
-
-def create_test_format_list(qualities):
- """Returns the video quality levels to be tested.
-
- Args:
- qualities: List of all the quality levels supported by the camera device.
- Returns:
- test_qualities: Subset of test qualities to be tested from the
- supported qualities.
- """
- test_qualities = []
- for s in _ITS_SUPPORTED_QUALITIES:
- if s in qualities:
- test_qualities.append(s)
- return test_qualities
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java
index 336c2bf..520d95f 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java
@@ -47,6 +47,7 @@
import android.hardware.SensorManager;
import android.media.AudioAttributes;
import android.media.CamcorderProfile;
+import android.media.MediaRecorder;
import android.media.Image;
import android.media.ImageReader;
import android.media.ImageWriter;
@@ -89,6 +90,7 @@
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
@@ -101,8 +103,10 @@
import java.nio.FloatBuffer;
import java.nio.charset.Charset;
import java.security.MessageDigest;
+import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@@ -219,6 +223,9 @@
private int mCaptureStatsGridWidth;
private int mCaptureStatsGridHeight;
private CaptureResult mCaptureResults[] = null;
+ private MediaRecorder mMediaRecorder;
+ private Surface mRecordSurface;
+ private CaptureRequest.Builder mCaptureRequestBuilder;
private volatile ConditionVariable mInterlock3A = new ConditionVariable(true);
@@ -238,6 +245,21 @@
public float values[];
}
+ class VideoRecordingObject {
+ public String recordedOutputPath;
+ public String quality;
+ public Size videoSize;
+ public int videoFrameRate;
+
+ public VideoRecordingObject(String recordedOutputPath,
+ String quality, Size videoSize, int videoFrameRate) {
+ this.recordedOutputPath = recordedOutputPath;
+ this.quality = quality;
+ this.videoSize = videoSize;
+ this.videoFrameRate = videoFrameRate;
+ }
+ }
+
// For capturing motion sensor traces.
private SensorManager mSensorManager = null;
private Sensor mAccelSensor = null;
@@ -752,6 +774,12 @@
} else if ("getSupportedVideoQualities".equals(cmdObj.getString("cmdName"))) {
String cameraId = cmdObj.getString("cameraId");
doGetSupportedVideoQualities(cameraId);
+ } else if ("doBasicRecording".equals(cmdObj.getString("cmdName"))) {
+ String cameraId = cmdObj.getString("cameraId");
+ int profileId = cmdObj.getInt("profileId");
+ String quality = cmdObj.getString("quality");
+ int recordingDuration = cmdObj.getInt("recordingDuration");
+ doBasicRecording(cameraId, profileId, quality, recordingDuration);
} else {
throw new ItsException("Unknown command: " + cmd);
}
@@ -852,6 +880,20 @@
}
}
+ public void sendVideoRecordingObject(VideoRecordingObject obj)
+ throws ItsException {
+ try {
+ JSONObject videoJson = new JSONObject();
+ videoJson.put("recordedOutputPath", obj.recordedOutputPath);
+ videoJson.put("quality", obj.quality);
+ videoJson.put("videoFrameRate", obj.videoFrameRate);
+ videoJson.put("videoSize", obj.videoSize);
+ sendResponse("recordingResponse", null, videoJson, null);
+ } catch (org.json.JSONException e) {
+ throw new ItsException("JSON error: ", e);
+ }
+ }
+
public void sendResponseCaptureResult(CameraCharacteristics props,
CaptureRequest request,
TotalCaptureResult result,
@@ -1681,10 +1723,131 @@
private void appendSupportProfile(StringBuilder profiles, String name, int profile,
int cameraId) {
if (CamcorderProfile.hasProfile(cameraId, profile)) {
- profiles.append(name).append(';');
+ profiles.append(name).append(':').append(profile).append(';');
}
}
+ private void doBasicRecording(String cameraId, int profileId, String quality,
+ int recordingDuration) throws ItsException {
+ int cameraDeviceId = Integer.parseInt(cameraId);
+ mMediaRecorder = new MediaRecorder();
+ CamcorderProfile camcorderProfile = getCamcorderProfile(cameraDeviceId, profileId);
+ assert(camcorderProfile != null);
+ Size videoSize = new Size(camcorderProfile.videoFrameWidth,
+ camcorderProfile.videoFrameHeight);
+ String outputFilePath = getOutputMediaFile(cameraDeviceId, videoSize, quality);
+ assert(outputFilePath != null);
+ Log.i(TAG, "Video recording outputFilePath:"+ outputFilePath);
+ setupMediaRecorderWithProfile(cameraDeviceId, camcorderProfile, outputFilePath);
+ // Prepare MediaRecorder
+ try {
+ mMediaRecorder.prepare();
+ } catch (IOException e) {
+ throw new ItsException("Error preparing the MediaRecorder.");
+ }
+
+ mRecordSurface = mMediaRecorder.getSurface();
+ // Configure and create capture session.
+ try {
+ configureAndCreateCaptureSession(mRecordSurface);
+ } catch (android.hardware.camera2.CameraAccessException e) {
+ throw new ItsException("Access error: ", e);
+ }
+ // Start Recording
+ if (mMediaRecorder != null) {
+ Log.i(TAG, "Now recording video for quality: " + quality + " profile id: " +
+ profileId + " cameraId: " + cameraDeviceId + " size: " + videoSize);
+ mMediaRecorder.start();
+ try {
+ Thread.sleep(recordingDuration*1000); // recordingDuration is in seconds
+ } catch (InterruptedException e) {
+ throw new ItsException("Unexpected InterruptedException: ", e);
+ }
+ // Stop MediaRecorder
+ mMediaRecorder.stop();
+ mSession.close();
+ mMediaRecorder.reset();
+ mMediaRecorder.release();
+ mMediaRecorder = null;
+ if (mRecordSurface != null) {
+ mRecordSurface.release();
+ mRecordSurface = null;
+ }
+ }
+
+ Log.i(TAG, "Recording Done for quality: " + quality);
+
+ // Send VideoRecordingObject for further processing.
+ VideoRecordingObject obj = new VideoRecordingObject(outputFilePath,
+ quality, videoSize, camcorderProfile.videoFrameRate);
+ mSocketRunnableObj.sendVideoRecordingObject(obj);
+ }
+
+ private void configureAndCreateCaptureSession(Surface recordSurface)
+ throws CameraAccessException{
+ assert(recordSurface != null);
+ // Create capture request builder
+ mCaptureRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
+ mCaptureRequestBuilder.addTarget(recordSurface);
+ // Create capture session
+ mCamera.createCaptureSession(Arrays.asList(recordSurface),
+ new CameraCaptureSession.StateCallback() {
+ @Override
+ public void onConfigured(CameraCaptureSession session) {
+ mSession = session;
+ try {
+ mSession.setRepeatingRequest(mCaptureRequestBuilder.build(), null, null);
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onConfigureFailed(CameraCaptureSession session) {
+ Log.i(TAG, "CameraCaptureSession configuration failed.");
+ }
+ }, mCameraHandler);
+ }
+
+ // Returns the default camcorder profile for the given camera at the given quality level
+ // Each CamcorderProfile has duration, quality, fileFormat, videoCodec, videoBitRate,
+ // videoFrameRate,videoWidth, videoHeight, audioCodec, audioBitRate, audioSampleRate
+ // and audioChannels.
+ private CamcorderProfile getCamcorderProfile(int cameraId, int profileId) {
+ CamcorderProfile camcorderProfile = CamcorderProfile.get(cameraId, profileId);
+ return camcorderProfile;
+ }
+
+ // This method should be called before preparing MediaRecorder.
+ // Set video and audio source should be done before setting the CamcorderProfile.
+ // Output file path should be set after setting the CamcorderProfile.
+ // These events should always be done in this particular order.
+ private void setupMediaRecorderWithProfile(int cameraId, CamcorderProfile camcorderProfile,
+ String outputFilePath) {
+ mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
+ mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
+ mMediaRecorder.setProfile(camcorderProfile);
+ mMediaRecorder.setOutputFile(outputFilePath);
+ }
+
+ private String getOutputMediaFile(int cameraId, Size videoSize, String quality ) {
+ // All the video recordings will be available in VideoITS directory on device.
+ File mediaStorageDir = new File(getExternalFilesDir(null), "VideoITS");
+ if (mediaStorageDir == null) {
+ Log.e(TAG, "Failed to retrieve external files directory.");
+ return null;
+ }
+ if (!mediaStorageDir.exists()) {
+ if (!mediaStorageDir.mkdirs()) {
+ Log.d(TAG, "Failed to create media storage directory.");
+ return null;
+ }
+ }
+ String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
+ File mediaFile = new File(mediaStorageDir.getPath() + File.separator +
+ "VID_" + timestamp + '_' + cameraId + '_' + quality + '_' + videoSize);
+ return mediaFile + ".mp4";
+ }
private void doCapture(JSONObject params) throws ItsException {
try {