blob: ee40d3523918a4491c0006ac2e5c2559f50224e0 [file] [log] [blame] [edit]
# Copyright 2024 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Verify frames are not dropped during preview recording."""
import logging
import math
import os
import time
from mobly import test_runner
import numpy as np
import its_base_test
import camera_properties_utils
import its_session_utils
import video_processing_utils
_FPS_RTOL = 0.1 # Recording FPS must be within 10% of requested FPS
# Consecutive frames averaging >1.5x more than ideal frame rate -> FAIL
_FRAME_DELTA_MAXIMUM_FACTOR = 1.5
_FRAME_DELTA_WINDOW_SIZE = 30 # 0.5 second of 60FPS video -> 30 frames
_NAME = os.path.splitext(os.path.basename(__file__))[0]
_SCENE_DISPLAY_WAIT_TIME = 5 # seconds
_VIDEO_DURATION = 10 # seconds
def _get_local_maximum(values, window_size=1):
output = min(values)
for i in range(len(values)):
if i + window_size <= len(values):
output = max(output, np.average(values[i:i+window_size]))
return output
class PreviewFrameDropTest(its_base_test.ItsBaseTest):
"""Tests if frames are dropped during preview recording.
Takes a preview recording of a video scene, with circles moving
at different simulated frame rates. Verifies that the overall frame rate of
the recording matches the requested frame rate, and that there are no
significant groups of elevated frame deltas.
"""
def test_preview_frame_drop(self):
log_path = self.log_path
with its_session_utils.ItsSession(
device_id=self.dut.serial,
camera_id=self.camera_id,
hidden_physical_id=self.hidden_physical_id) as cam:
props = cam.get_camera_properties()
props = cam.override_with_hidden_physical_camera_props(props)
first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
camera_properties_utils.skip_unless(
first_api_level >= its_session_utils.ANDROID15_API_LEVEL)
its_session_utils.load_scene(
cam, props, self.scene, self.tablet, self.chart_distance,
lighting_check=False, log_path=self.log_path)
time.sleep(_SCENE_DISPLAY_WAIT_TIME)
# Log ffmpeg version being used
video_processing_utils.log_ffmpeg_version()
# Find largest minimum AE target FPS
fps_ranges = camera_properties_utils.get_ae_target_fps_ranges(props)
logging.debug('FPS ranges: %s', fps_ranges)
if not fps_ranges:
raise AssertionError('No FPS ranges found.')
video_fps = max(fps_ranges, key=lambda r: r[0])[0]
logging.debug('Recording FPS: %s', video_fps)
# Record preview at largest supported size
supported_preview_sizes = cam.get_supported_preview_sizes(self.camera_id)
supported_video_sizes = cam.get_supported_video_sizes_capped(
self.camera_id)
max_video_size = supported_video_sizes[-1]
logging.debug('Camera supported video sizes: %s',
supported_video_sizes)
# Change preview size depending on video size support
preview_size = supported_preview_sizes[-1]
if preview_size <= max_video_size:
logging.debug('preview_size is supported by video encoder')
else:
preview_size = max_video_size
recording_obj = cam.do_preview_recording(
preview_size, _VIDEO_DURATION, False,
ae_target_fps_min=video_fps, ae_target_fps_max=video_fps)
logging.debug('Recorded output path: %s',
recording_obj['recordedOutputPath'])
logging.debug('Tested quality: %s', recording_obj['quality'])
# Grab the video from the saved location on DUT
self.dut.adb.pull([recording_obj['recordedOutputPath'], log_path])
file_name = recording_obj['recordedOutputPath'].split('/')[-1]
logging.debug('Recorded file name: %s', file_name)
# Calculate average frame rate of recording
failure_messages = []
file_name_with_path = os.path.join(
self.log_path, file_name)
reported_frame_rate = video_processing_utils.get_avg_frame_rate(
file_name_with_path)
if not math.isclose(video_fps, reported_frame_rate, rel_tol=_FPS_RTOL):
failure_messages.append(
f'Requested FPS {video_fps} does not match '
f'recording FPS {reported_frame_rate}, RTOL: {_FPS_RTOL}'
)
else:
logging.debug('Reported preview frame rate: %s', reported_frame_rate)
# Calculate frame deltas, discarding first value
frame_deltas = np.array(video_processing_utils.get_frame_deltas(
file_name_with_path))[1:]
frame_delta_max = np.max(frame_deltas)
frame_delta_min = np.min(frame_deltas)
frame_delta_avg = np.average(frame_deltas)
frame_delta_var = np.var(frame_deltas)
logging.debug('Frame delta max: %.4f, min: %.4f, avg: %.4f, var: %.4f',
frame_delta_max, frame_delta_min,
frame_delta_avg, frame_delta_var)
frame_delta_local_max = _get_local_maximum(
frame_deltas, window_size=_FRAME_DELTA_WINDOW_SIZE)
logging.debug('Frame delta local maximum: %.4f', frame_delta_local_max)
# Below print statements are for metrics logging purpose.
# Do not replace with logging.debug().
print(f'{_NAME}_max_delta: {frame_delta_local_max:.4f}')
maximum_tolerable_frame_delta = _FRAME_DELTA_MAXIMUM_FACTOR / video_fps
if frame_delta_local_max > maximum_tolerable_frame_delta:
failure_messages.append(
f'Local maximum of frame deltas {frame_delta_local_max} was '
'greater than maximum tolerable '
f'frame delta {maximum_tolerable_frame_delta}. '
f'Window for local maximum: {_FRAME_DELTA_WINDOW_SIZE}. '
)
if failure_messages:
raise AssertionError('\n'.join(failure_messages))
def teardown_test(self):
its_session_utils.stop_video_playback(self.tablet)
if __name__ == '__main__':
test_runner.main()