blob: 1665dbf8e570503cf9bcd029320643f2bb1f2c2d [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
leslieshaw6fb2c072023-04-06 16:38:54 -070042LOW_RESOLUTION_SIZES = (
43 '176x144',
44 '192x144',
45 '352x288',
46 '384x288',
47 '320x240',
48)
Rucha Katakwarb6847db2022-03-24 16:49:13 -070049
leslieshaw6fb2c072023-04-06 16:38:54 -070050LOWEST_RES_TESTED_AREA = 640*360
leslieshaw99fd75b2023-01-09 15:01:19 -080051
Jonathan Liu60fedbc2023-01-12 23:13:55 +000052
leslieshaw541e2972023-02-28 11:06:07 -080053VIDEO_QUALITY_SIZE = {
leslieshaw8e9ea9e2023-06-07 12:59:44 -070054 # '480P', '1080P', HIGH' and 'LOW' are not included as they are DUT-dependent
leslieshaw541e2972023-02-28 11:06:07 -080055 '2160P': '3840x2160',
leslieshaw541e2972023-02-28 11:06:07 -080056 '720P': '1280x720',
leslieshaw541e2972023-02-28 11:06:07 -080057 '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
Clemenz Portmann6e00e052023-02-14 16:39:57 -0800109def log_ffmpeg_version():
110 """Logs the ffmpeg version being used."""
Rucha Katakwar31a54502022-08-10 16:32:30 -0700111
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')
Clemenz Portmann6e00e052023-02-14 16:39:57 -0800118 logging.debug('ffmpeg version: %s', decoded_output.split(' ')[2])
Rucha Katakwar31a54502022-08-10 16:32:30 -0700119
120
Rucha Katakwar932bded2022-04-04 14:29:52 -0700121def extract_key_frames_from_video(log_path, video_file_name):
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700122 """Returns a list of extracted key frames.
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700123
124 Ffmpeg tool is used to extract key frames from the video at path
Rucha Katakwar932bded2022-04-04 14:29:52 -0700125 os.path.join(log_path, video_file_name).
126 The extracted key frames will have the name video_file_name with "_key_frame"
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700127 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
Rucha Katakwar05167b72022-05-25 13:42:32 -0700132 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
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700136 Args:
137 log_path: path for video file directory
Rucha Katakwar932bded2022-04-04 14:29:52 -0700138 video_file_name: name of the video file.
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700139 Returns:
140 key_frame_files: A list of paths for each key frame extracted from the
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700141 video. Ex: VID_20220325_050918_0_CIF_352x288.mp4
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700142 """
Rucha Katakwar932bded2022-04-04 14:29:52 -0700143 ffmpeg_image_name = f"{video_file_name.split('.')[0]}_key_frame"
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700144 ffmpeg_image_file_path = os.path.join(
145 log_path, ffmpeg_image_name + '_%02d.png')
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700146 cmd = ['ffmpeg',
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700147 '-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,
Rucha Katakwar05167b72022-05-25 13:42:32 -0700156 '-loglevel',
157 'quiet',
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700158 ]
159 logging.debug('Extracting key frames from: %s', video_file_name)
Jonathan Liu98b99812023-07-14 13:20:38 -0700160 _ = subprocess.call(cmd,
161 stdin=subprocess.DEVNULL,
162 stdout=subprocess.DEVNULL,
163 stderr=subprocess.DEVNULL)
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700164 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)
Rucha Katakwar2c49c452022-05-19 14:29:03 -0700169
Rucha Katakwar2e8ab092022-05-26 10:31:40 -0700170 logging.debug('Extracted key frames: %s', key_frame_files)
171 logging.debug('Length of key_frame_files: %d', len(key_frame_files))
Clemenz Portmann00c6ef32022-06-02 16:21:19 -0700172 if not key_frame_files:
Rucha Katakwar2c49c452022-05-19 14:29:03 -0700173 raise AssertionError('No key frames extracted. Check source video.')
174
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700175 return key_frame_files
176
177
178def get_key_frame_to_process(key_frame_files):
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700179 """Returns the key frame file from the list of key_frame_files.
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700180
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 """
Rucha Katakwar312d64e2022-05-25 10:24:25 -0700189 if not key_frame_files:
190 raise AssertionError('key_frame_files list is empty.')
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700191 key_frame_files.sort()
192 return key_frame_files[-1]
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700193
194
195def extract_all_frames_from_video(log_path, video_file_name, img_format):
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700196 """Extracts and returns a list of all extracted frames.
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700197
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
Rucha Katakwar05167b72022-05-25 13:42:32 -0700205 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
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700209 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),
leslieshaw32d2d4d2023-06-08 16:59:08 -0700224 '-vsync', 'vfr', # force ffmpeg to use video fps instead of inferred fps
Rucha Katakwar05167b72022-05-25 13:42:32 -0700225 ffmpeg_image_file_names, '-loglevel', 'quiet'
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700226 ]
Jonathan Liu98b99812023-07-14 13:20:38 -0700227 _ = subprocess.call(cmd,
228 stdin=subprocess.DEVNULL,
229 stdout=subprocess.DEVNULL,
230 stderr=subprocess.DEVNULL)
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700231
232 file_list = sorted(
233 [_ for _ in os.listdir(log_path) if (_.endswith(img_format)
234 and ffmpeg_image_name in _)])
Clemenz Portmann00c6ef32022-06-02 16:21:19 -0700235 if not file_list:
Rucha Katakwar2c49c452022-05-19 14:29:03 -0700236 raise AssertionError('No frames extracted. Check source video.')
237
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700238 return file_list
Jonathan Liu76f51682022-11-12 00:27:47 +0000239
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 = ['ffmpeg',
251 '-i',
252 video_file_name_with_path,
253 '-vf',
254 'vfrdet',
255 '-f',
256 'null',
257 '-',
258 ]
259 logging.debug('Getting frame rate')
260 raw_output = ''
261 try:
Jonathan Liu98b99812023-07-14 13:20:38 -0700262 raw_output = subprocess.check_output(cmd,
263 stdin=subprocess.DEVNULL,
264 stderr=subprocess.STDOUT)
Jonathan Liu76f51682022-11-12 00:27:47 +0000265 except subprocess.CalledProcessError as e:
266 raise AssertionError(str(e.output)) from e
267 if raw_output:
268 output = str(raw_output.decode('utf-8')).strip()
269 logging.debug('FFmpeg command %s output: %s', ' '.join(cmd), output)
270 fps_data = output.splitlines()[-3] # frames printed on third to last line
271 frames = int(re.search(r'frame= *([0-9]+)', fps_data).group(1))
272 duration = re.search(r'time= *([0-9][0-9:\.]*)', fps_data).group(1)
273 time_parts = [float(t) for t in duration.split(':')]
274 seconds = time_parts[0] * HR_TO_SEC + time_parts[
275 1] * MIN_TO_SEC + time_parts[2]
276 logging.debug('Average FPS: %d / %d = %.4f',
277 frames, seconds, frames / seconds)
278 return frames / seconds
279 else:
280 raise AssertionError('ffmpeg failed to provide frame rate data')
281
282
283def get_frame_deltas(video_file_name_with_path, timestamp_type='pts'):
284 """Get list of time diffs between frames.
285
286 Args:
287 video_file_name_with_path: path to the video to be analyzed
288 timestamp_type: 'pts' or 'dts'
289 Returns:
290 List of floats. Time diffs between frames in seconds.
291 """
292
293 cmd = ['ffprobe',
294 '-show_entries',
295 f'frame=pkt_{timestamp_type}_time',
296 '-select_streams',
297 'v',
298 video_file_name_with_path
299 ]
300 logging.debug('Getting frame deltas')
301 raw_output = ''
302 try:
Jonathan Liu98b99812023-07-14 13:20:38 -0700303 raw_output = subprocess.check_output(cmd,
304 stdin=subprocess.DEVNULL,
305 stderr=subprocess.STDOUT)
Jonathan Liu76f51682022-11-12 00:27:47 +0000306 except subprocess.CalledProcessError as e:
307 raise AssertionError(str(e.output)) from e
308 if raw_output:
309 output = str(raw_output.decode('utf-8')).strip().split('\n')
310 deltas = []
Jonathan Liu61b36cf2023-06-14 10:30:06 -0700311 prev_time = None
Jonathan Liu76f51682022-11-12 00:27:47 +0000312 for line in output:
313 if timestamp_type not in line:
314 continue
315 curr_time = float(re.search(r'time= *([0-9][0-9\.]*)', line).group(1))
Jonathan Liu61b36cf2023-06-14 10:30:06 -0700316 if prev_time is not None:
317 deltas.append(curr_time - prev_time)
Jonathan Liu76f51682022-11-12 00:27:47 +0000318 prev_time = curr_time
319 logging.debug('Frame deltas: %s', deltas)
320 return deltas
321 else:
322 raise AssertionError('ffprobe failed to provide frame delta data')