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 {