1# Copyright 2022 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""Utility functions for processing video recordings. 15""" 16# Each item in this list corresponds to quality levels defined per 17# CamcorderProfile. For Video ITS, we will currently test below qualities 18# only if supported by the camera device. 19import logging 20import os.path 21import re 22import subprocess 23import error_util 24 25 26HR_TO_SEC = 3600 27MIN_TO_SEC = 60 28 29ITS_SUPPORTED_QUALITIES = ( 30 'HIGH', 31 '2160P', 32 '1080P', 33 '720P', 34 '480P', 35 'CIF', 36 'QCIF', 37 'QVGA', 38 'LOW', 39 'VGA' 40) 41 42LOW_RESOLUTION_SIZES = ( 43 '176x144', 44 '192x144', 45 '352x288', 46 '384x288', 47 '320x240', 48) 49 50LOWEST_RES_TESTED_AREA = 640*360 51 52 53VIDEO_QUALITY_SIZE = { 54 # '480P', '1080P', HIGH' and 'LOW' are not included as they are DUT-dependent 55 '2160P': '3840x2160', 56 '720P': '1280x720', 57 'VGA': '640x480', 58 'CIF': '352x288', 59 'QVGA': '320x240', 60 'QCIF': '176x144', 61} 62 63 64def get_lowest_preview_video_size( 65 supported_preview_sizes, supported_video_qualities, min_area): 66 """Returns the common, smallest size above minimum in preview and video. 67 68 Args: 69 supported_preview_sizes: str; preview size (ex. '1920x1080') 70 supported_video_qualities: str; video recording quality and id pair 71 (ex. '480P:4', '720P:5'') 72 min_area: int; filter to eliminate smaller sizes (ex. 640*480) 73 Returns: 74 smallest_common_size: str; smallest, common size between preview and video 75 smallest_common_video_quality: str; video recording quality such as 480P 76 """ 77 78 # Make dictionary on video quality and size according to compatibility 79 supported_video_size_to_quality = {} 80 for quality in supported_video_qualities: 81 video_quality = quality.split(':')[0] 82 if video_quality in VIDEO_QUALITY_SIZE: 83 video_size = VIDEO_QUALITY_SIZE[video_quality] 84 supported_video_size_to_quality[video_size] = video_quality 85 logging.debug( 86 'Supported video size to quality: %s', supported_video_size_to_quality) 87 88 # Use areas of video sizes to find the smallest, common size 89 size_to_area = lambda s: int(s.split('x')[0])*int(s.split('x')[1]) 90 smallest_common_size = '' 91 smallest_area = float('inf') 92 for size in supported_preview_sizes: 93 if size in supported_video_size_to_quality: 94 area = size_to_area(size) 95 if smallest_area > area >= min_area: 96 smallest_area = area 97 smallest_common_size = size 98 logging.debug('Lowest common size: %s', smallest_common_size) 99 100 # Find video quality of resolution with resolution as key 101 smallest_common_video_quality = ( 102 supported_video_size_to_quality[smallest_common_size]) 103 logging.debug( 104 'Lowest common size video quality: %s', smallest_common_video_quality) 105 106 return smallest_common_size, smallest_common_video_quality 107 108 109def log_ffmpeg_version(): 110 """Logs the ffmpeg version being used.""" 111 112 ffmpeg_version_cmd = ('ffmpeg -version') 113 p = subprocess.Popen(ffmpeg_version_cmd, shell=True, stdout=subprocess.PIPE) 114 output, _ = p.communicate() 115 if p.poll() != 0: 116 raise error_util.CameraItsError('Error running ffmpeg version cmd.') 117 decoded_output = output.decode('utf-8') 118 logging.debug('ffmpeg version: %s', decoded_output.split(' ')[2]) 119 120 121def extract_key_frames_from_video(log_path, video_file_name): 122 """Returns a list of extracted key frames. 123 124 Ffmpeg tool is used to extract key frames from the video at path 125 os.path.join(log_path, video_file_name). 126 The extracted key frames will have the name video_file_name with "_key_frame" 127 suffix to identify the frames for video of each quality.Since there can be 128 multiple key frames, each key frame image will be differentiated with it's 129 frame index.All the extracted key frames will be available in jpeg format 130 at the same path as the video file. 131 132 The run time flag '-loglevel quiet' hides the information from terminal. 133 In order to see the detailed output of ffmpeg command change the loglevel 134 option to 'info'. 135 136 Args: 137 log_path: path for video file directory 138 video_file_name: name of the video file. 139 Returns: 140 key_frame_files: A list of paths for each key frame extracted from the 141 video. Ex: VID_20220325_050918_0_CIF_352x288.mp4 142 """ 143 ffmpeg_image_name = f"{video_file_name.split('.')[0]}_key_frame" 144 ffmpeg_image_file_path = os.path.join( 145 log_path, ffmpeg_image_name + '_%02d.png') 146 cmd = ['ffmpeg', 147 '-skip_frame', 148 'nokey', 149 '-i', 150 os.path.join(log_path, video_file_name), 151 '-vsync', 152 'vfr', 153 '-frame_pts', 154 'true', 155 ffmpeg_image_file_path, 156 '-loglevel', 157 'quiet', 158 ] 159 logging.debug('Extracting key frames from: %s', video_file_name) 160 _ = subprocess.call(cmd, 161 stdin=subprocess.DEVNULL, 162 stdout=subprocess.DEVNULL, 163 stderr=subprocess.DEVNULL) 164 arr = os.listdir(os.path.join(log_path)) 165 key_frame_files = [] 166 for file in arr: 167 if '.png' in file and not os.path.isdir(file) and ffmpeg_image_name in file: 168 key_frame_files.append(file) 169 170 logging.debug('Extracted key frames: %s', key_frame_files) 171 logging.debug('Length of key_frame_files: %d', len(key_frame_files)) 172 if not key_frame_files: 173 raise AssertionError('No key frames extracted. Check source video.') 174 175 return key_frame_files 176 177 178def get_key_frame_to_process(key_frame_files): 179 """Returns the key frame file from the list of key_frame_files. 180 181 If the size of the list is 1 then the file in the list will be returned else 182 the file with highest frame_index will be returned for further processing. 183 184 Args: 185 key_frame_files: A list of key frame files. 186 Returns: 187 key_frame_file to be used for further processing. 188 """ 189 if not key_frame_files: 190 raise AssertionError('key_frame_files list is empty.') 191 key_frame_files.sort() 192 return key_frame_files[-1] 193 194 195def extract_all_frames_from_video(log_path, video_file_name, img_format): 196 """Extracts and returns a list of all extracted frames. 197 198 Ffmpeg tool is used to extract all frames from the video at path 199 <log_path>/<video_file_name>. The extracted key frames will have the name 200 video_file_name with "_frame" suffix to identify the frames for video of each 201 size. Each frame image will be differentiated with its frame index. All 202 extracted key frames will be available in the provided img_format format at 203 the same path as the video file. 204 205 The run time flag '-loglevel quiet' hides the information from terminal. 206 In order to see the detailed output of ffmpeg command change the loglevel 207 option to 'info'. 208 209 Args: 210 log_path: str; path for video file directory 211 video_file_name: str; name of the video file. 212 img_format: str; type of image to export frames into. ex. 'png' 213 Returns: 214 key_frame_files: An ordered list of paths for each frame extracted from the 215 video 216 """ 217 logging.debug('Extracting all frames') 218 ffmpeg_image_name = f"{video_file_name.split('.')[0]}_frame" 219 logging.debug('ffmpeg_image_name: %s', ffmpeg_image_name) 220 ffmpeg_image_file_names = ( 221 f'{os.path.join(log_path, ffmpeg_image_name)}_%03d.{img_format}') 222 cmd = [ 223 'ffmpeg', '-i', os.path.join(log_path, video_file_name), 224 '-vsync', 'vfr', # force ffmpeg to use video fps instead of inferred fps 225 ffmpeg_image_file_names, '-loglevel', 'quiet' 226 ] 227 _ = subprocess.call(cmd, 228 stdin=subprocess.DEVNULL, 229 stdout=subprocess.DEVNULL, 230 stderr=subprocess.DEVNULL) 231 232 file_list = sorted( 233 [_ for _ in os.listdir(log_path) if (_.endswith(img_format) 234 and ffmpeg_image_name in _)]) 235 if not file_list: 236 raise AssertionError('No frames extracted. Check source video.') 237 238 return file_list 239 240 241def get_average_frame_rate(video_file_name_with_path): 242 """Get average frame rate assuming variable frame rate video. 243 244 Args: 245 video_file_name_with_path: path to the video to be analyzed 246 Returns: 247 Float. average frames per second. 248 """ 249 250 cmd = ['ffprobe', 251 '-v', 252 'quiet', 253 '-show_streams', 254 '-select_streams', 255 'v:0', # first video stream 256 video_file_name_with_path 257 ] 258 logging.debug('Getting frame rate') 259 raw_output = '' 260 try: 261 raw_output = subprocess.check_output(cmd, 262 stdin=subprocess.DEVNULL, 263 stderr=subprocess.STDOUT) 264 except subprocess.CalledProcessError as e: 265 raise AssertionError(str(e.output)) from e 266 if raw_output: 267 output = str(raw_output.decode('utf-8')).strip() 268 logging.debug('ffprobe command %s output: %s', ' '.join(cmd), output) 269 average_frame_rate_data = ( 270 re.search(r'avg_frame_rate=*([0-9]+/[0-9]+)', output).group(1) 271 ) 272 average_frame_rate = (int(average_frame_rate_data.split('/')[0]) / 273 int(average_frame_rate_data.split('/')[1])) 274 logging.debug('Average FPS: %.4f', average_frame_rate) 275 return average_frame_rate 276 else: 277 raise AssertionError('ffprobe failed to provide frame rate data') 278 279 280def get_frame_deltas(video_file_name_with_path, timestamp_type='pts'): 281 """Get list of time diffs between frames. 282 283 Args: 284 video_file_name_with_path: path to the video to be analyzed 285 timestamp_type: 'pts' or 'dts' 286 Returns: 287 List of floats. Time diffs between frames in seconds. 288 """ 289 290 cmd = ['ffprobe', 291 '-show_entries', 292 f'frame=pkt_{timestamp_type}_time', 293 '-select_streams', 294 'v', 295 video_file_name_with_path 296 ] 297 logging.debug('Getting frame deltas') 298 raw_output = '' 299 try: 300 raw_output = subprocess.check_output(cmd, 301 stdin=subprocess.DEVNULL, 302 stderr=subprocess.STDOUT) 303 except subprocess.CalledProcessError as e: 304 raise AssertionError(str(e.output)) from e 305 if raw_output: 306 output = str(raw_output.decode('utf-8')).strip().split('\n') 307 deltas = [] 308 prev_time = None 309 for line in output: 310 if timestamp_type not in line: 311 continue 312 curr_time = float(re.search(r'time= *([0-9][0-9\.]*)', line).group(1)) 313 if prev_time is not None: 314 deltas.append(curr_time - prev_time) 315 prev_time = curr_time 316 logging.debug('Frame deltas: %s', deltas) 317 return deltas 318 else: 319 raise AssertionError('ffprobe failed to provide frame delta data') 320