blob: f5e80ee042884a70140a27bf0beca5f84db7762c [file] [log] [blame]
Rucha Katakwar222c5312022-03-21 14:17:01 -07001# 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.
Rucha Katakwarb6847db2022-03-24 16:49:13 -070019import logging
20import os.path
Jonathan Liu76f51682022-11-12 00:27:47 +000021import re
Rucha Katakwarb6847db2022-03-24 16:49:13 -070022import subprocess
Rucha Katakwar31a54502022-08-10 16:32:30 -070023import error_util
Rucha Katakwarb6847db2022-03-24 16:49:13 -070024
25
Jonathan Liu76f51682022-11-12 00:27:47 +000026HR_TO_SEC = 3600
27MIN_TO_SEC = 60
28
Rucha Katakwar089e6092022-03-30 11:19:26 -070029ITS_SUPPORTED_QUALITIES = (
Rucha Katakwar41133f92022-03-22 13:44:09 -070030 'HIGH',
31 '2160P',
32 '1080P',
33 '720P',
34 '480P',
35 'CIF',
36 'QCIF',
37 'QVGA',
38 'LOW',
39 'VGA'
Rucha Katakwar222c5312022-03-21 14:17:01 -070040)
Rucha Katakwar0ced2c32022-08-01 14:54:42 -070041
Clemenz Portmannd781bc12022-11-22 10:29:36 -080042LOW_RESOLUTION_SIZES = {
43 'W': ('176x144', '192x144'),
44 'UW': ('176x144', '192x144', '352x288', '384x288', '320x240'),
45}
Rucha Katakwarb6847db2022-03-24 16:49:13 -070046
leslieshaw99fd75b2023-01-09 15:01:19 -080047LOWEST_RES_TESTED_AREA = {
Jonathan Liu60fedbc2023-01-12 23:13:55 +000048 'W': 320*180,
49 'UW': 640*360,
leslieshaw99fd75b2023-01-09 15:01:19 -080050}
51
Jonathan Liu60fedbc2023-01-12 23:13:55 +000052
leslieshaw541e2972023-02-28 11:06:07 -080053VIDEO_QUALITY_SIZE = {
54 # 'HIGH' and 'LOW' not included as they are DUT-dependent
55 '2160P': '3840x2160',
56 '1080P': '1920x1080',
57 '720P': '1280x720',
58 '480P': '720x480',
59 'VGA': '640x480',
60 'CIF': '352x288',
61 'QVGA': '320x240',
62 'QCIF': '176x144',
63}
64
65
66def get_lowest_preview_video_size(
67 supported_preview_sizes, supported_video_qualities, min_area):
68 """Returns the common, smallest size above minimum in preview and video.
69
70 Args:
71 supported_preview_sizes: str; preview size (ex. '1920x1080')
72 supported_video_qualities: str; video recording quality and id pair
73 (ex. '480P:4', '720P:5'')
74 min_area: int; filter to eliminate smaller sizes (ex. 640*480)
75 Returns:
76 smallest_common_size: str; smallest, common size between preview and video
77 smallest_common_video_quality: str; video recording quality such as 480P
78 """
79
80 # Make dictionary on video quality and size according to compatibility
81 supported_video_size_to_quality = {}
82 for quality in supported_video_qualities:
83 video_quality = quality.split(':')[0]
84 if video_quality in VIDEO_QUALITY_SIZE:
85 video_size = VIDEO_QUALITY_SIZE[video_quality]
86 supported_video_size_to_quality[video_size] = video_quality
87 logging.debug(
88 'Supported video size to quality: %s', supported_video_size_to_quality)
89
90 # Use areas of video sizes to find the smallest, common size
91 size_to_area = lambda s: int(s.split('x')[0])*int(s.split('x')[1])
92 smallest_common_size = ''
93 smallest_area = float('inf')
94 for size in supported_preview_sizes:
95 if size in supported_video_size_to_quality:
96 area = size_to_area(size)
97 if smallest_area > area >= min_area:
98 smallest_area = area
99 smallest_common_size = size
100 logging.debug('Lowest common size: %s', smallest_common_size)
101
102 # Find video quality of resolution with resolution as key
103 smallest_common_video_quality = (
104 supported_video_size_to_quality[smallest_common_size])
105 logging.debug(
106 'Lowest common size video quality: %s', smallest_common_video_quality)
107
108 return smallest_common_size, smallest_common_video_quality
109
110
Clemenz Portmann6e00e052023-02-14 16:39:57 -0800111def log_ffmpeg_version():
112 """Logs the ffmpeg version being used."""
Rucha Katakwar31a54502022-08-10 16:32:30 -0700113
114 ffmpeg_version_cmd = ('ffmpeg -version')
115 p = subprocess.Popen(ffmpeg_version_cmd, shell=True, stdout=subprocess.PIPE)
116 output, _ = p.communicate()
117 if p.poll() != 0:
118 raise error_util.CameraItsError('Error running ffmpeg version cmd.')
119 decoded_output = output.decode('utf-8')
Clemenz Portmann6e00e052023-02-14 16:39:57 -0800120 logging.debug('ffmpeg version: %s', decoded_output.split(' ')[2])
Rucha Katakwar31a54502022-08-10 16:32:30 -0700121
122
Rucha Katakwar932bded2022-04-04 14:29:52 -0700123def extract_key_frames_from_video(log_path, video_file_name):
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700124 """Returns a list of extracted key frames.
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700125
126 Ffmpeg tool is used to extract key frames from the video at path
Rucha Katakwar932bded2022-04-04 14:29:52 -0700127 os.path.join(log_path, video_file_name).
128 The extracted key frames will have the name video_file_name with "_key_frame"
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700129 suffix to identify the frames for video of each quality.Since there can be
130 multiple key frames, each key frame image will be differentiated with it's
131 frame index.All the extracted key frames will be available in jpeg format
132 at the same path as the video file.
133
Rucha Katakwar05167b72022-05-25 13:42:32 -0700134 The run time flag '-loglevel quiet' hides the information from terminal.
135 In order to see the detailed output of ffmpeg command change the loglevel
136 option to 'info'.
137
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700138 Args:
139 log_path: path for video file directory
Rucha Katakwar932bded2022-04-04 14:29:52 -0700140 video_file_name: name of the video file.
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700141 Returns:
142 key_frame_files: A list of paths for each key frame extracted from the
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700143 video. Ex: VID_20220325_050918_0_CIF_352x288.mp4
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700144 """
Rucha Katakwar932bded2022-04-04 14:29:52 -0700145 ffmpeg_image_name = f"{video_file_name.split('.')[0]}_key_frame"
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700146 ffmpeg_image_file_path = os.path.join(
147 log_path, ffmpeg_image_name + '_%02d.png')
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700148 cmd = ['ffmpeg',
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700149 '-skip_frame',
150 'nokey',
151 '-i',
152 os.path.join(log_path, video_file_name),
153 '-vsync',
154 'vfr',
155 '-frame_pts',
156 'true',
157 ffmpeg_image_file_path,
Rucha Katakwar05167b72022-05-25 13:42:32 -0700158 '-loglevel',
159 'quiet',
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700160 ]
161 logging.debug('Extracting key frames from: %s', video_file_name)
162 _ = subprocess.call(cmd)
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700163 arr = os.listdir(os.path.join(log_path))
164 key_frame_files = []
165 for file in arr:
166 if '.png' in file and not os.path.isdir(file) and ffmpeg_image_name in file:
167 key_frame_files.append(file)
Rucha Katakwar2c49c452022-05-19 14:29:03 -0700168
Rucha Katakwar2e8ab092022-05-26 10:31:40 -0700169 logging.debug('Extracted key frames: %s', key_frame_files)
170 logging.debug('Length of key_frame_files: %d', len(key_frame_files))
Clemenz Portmann00c6ef32022-06-02 16:21:19 -0700171 if not key_frame_files:
Rucha Katakwar2c49c452022-05-19 14:29:03 -0700172 raise AssertionError('No key frames extracted. Check source video.')
173
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700174 return key_frame_files
175
176
177def get_key_frame_to_process(key_frame_files):
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700178 """Returns the key frame file from the list of key_frame_files.
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700179
180 If the size of the list is 1 then the file in the list will be returned else
181 the file with highest frame_index will be returned for further processing.
182
183 Args:
184 key_frame_files: A list of key frame files.
185 Returns:
186 key_frame_file to be used for further processing.
187 """
Rucha Katakwar312d64e2022-05-25 10:24:25 -0700188 if not key_frame_files:
189 raise AssertionError('key_frame_files list is empty.')
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700190 key_frame_files.sort()
191 return key_frame_files[-1]
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700192
193
194def extract_all_frames_from_video(log_path, video_file_name, img_format):
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700195 """Extracts and returns a list of all extracted frames.
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700196
197 Ffmpeg tool is used to extract all frames from the video at path
198 <log_path>/<video_file_name>. The extracted key frames will have the name
199 video_file_name with "_frame" suffix to identify the frames for video of each
200 size. Each frame image will be differentiated with its frame index. All
201 extracted key frames will be available in the provided img_format format at
202 the same path as the video file.
203
Rucha Katakwar05167b72022-05-25 13:42:32 -0700204 The run time flag '-loglevel quiet' hides the information from terminal.
205 In order to see the detailed output of ffmpeg command change the loglevel
206 option to 'info'.
207
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700208 Args:
209 log_path: str; path for video file directory
210 video_file_name: str; name of the video file.
211 img_format: str; type of image to export frames into. ex. 'png'
212 Returns:
213 key_frame_files: An ordered list of paths for each frame extracted from the
214 video
215 """
216 logging.debug('Extracting all frames')
217 ffmpeg_image_name = f"{video_file_name.split('.')[0]}_frame"
218 logging.debug('ffmpeg_image_name: %s', ffmpeg_image_name)
219 ffmpeg_image_file_names = (
220 f'{os.path.join(log_path, ffmpeg_image_name)}_%03d.{img_format}')
221 cmd = [
222 'ffmpeg', '-i', os.path.join(log_path, video_file_name),
Rucha Katakwar05167b72022-05-25 13:42:32 -0700223 ffmpeg_image_file_names, '-loglevel', 'quiet'
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700224 ]
225 _ = subprocess.call(cmd)
226
227 file_list = sorted(
228 [_ for _ in os.listdir(log_path) if (_.endswith(img_format)
229 and ffmpeg_image_name in _)])
Clemenz Portmann00c6ef32022-06-02 16:21:19 -0700230 if not file_list:
Rucha Katakwar2c49c452022-05-19 14:29:03 -0700231 raise AssertionError('No frames extracted. Check source video.')
232
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700233 return file_list
Jonathan Liu76f51682022-11-12 00:27:47 +0000234
235
236def get_average_frame_rate(video_file_name_with_path):
237 """Get average frame rate assuming variable frame rate video.
238
239 Args:
240 video_file_name_with_path: path to the video to be analyzed
241 Returns:
242 Float. average frames per second.
243 """
244
245 cmd = ['ffmpeg',
246 '-i',
247 video_file_name_with_path,
248 '-vf',
249 'vfrdet',
250 '-f',
251 'null',
252 '-',
253 ]
254 logging.debug('Getting frame rate')
255 raw_output = ''
256 try:
257 raw_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
258 except subprocess.CalledProcessError as e:
259 raise AssertionError(str(e.output)) from e
260 if raw_output:
261 output = str(raw_output.decode('utf-8')).strip()
262 logging.debug('FFmpeg command %s output: %s', ' '.join(cmd), output)
263 fps_data = output.splitlines()[-3] # frames printed on third to last line
264 frames = int(re.search(r'frame= *([0-9]+)', fps_data).group(1))
265 duration = re.search(r'time= *([0-9][0-9:\.]*)', fps_data).group(1)
266 time_parts = [float(t) for t in duration.split(':')]
267 seconds = time_parts[0] * HR_TO_SEC + time_parts[
268 1] * MIN_TO_SEC + time_parts[2]
269 logging.debug('Average FPS: %d / %d = %.4f',
270 frames, seconds, frames / seconds)
271 return frames / seconds
272 else:
273 raise AssertionError('ffmpeg failed to provide frame rate data')
274
275
276def get_frame_deltas(video_file_name_with_path, timestamp_type='pts'):
277 """Get list of time diffs between frames.
278
279 Args:
280 video_file_name_with_path: path to the video to be analyzed
281 timestamp_type: 'pts' or 'dts'
282 Returns:
283 List of floats. Time diffs between frames in seconds.
284 """
285
286 cmd = ['ffprobe',
287 '-show_entries',
288 f'frame=pkt_{timestamp_type}_time',
289 '-select_streams',
290 'v',
291 video_file_name_with_path
292 ]
293 logging.debug('Getting frame deltas')
294 raw_output = ''
295 try:
296 raw_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
297 except subprocess.CalledProcessError as e:
298 raise AssertionError(str(e.output)) from e
299 if raw_output:
300 output = str(raw_output.decode('utf-8')).strip().split('\n')
301 deltas = []
302 prev_time = 0
303 for line in output:
304 if timestamp_type not in line:
305 continue
306 curr_time = float(re.search(r'time= *([0-9][0-9\.]*)', line).group(1))
307 deltas.append(curr_time - prev_time)
308 prev_time = curr_time
309 logging.debug('Frame deltas: %s', deltas)
310 return deltas
311 else:
312 raise AssertionError('ffprobe failed to provide frame delta data')