blob: 66da1c13114293a0577941265ed329a3ed4832ff [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.
leslieshaw5b54ee52023-08-29 13:22:30 -070019
20
Rucha Katakwarb6847db2022-03-24 16:49:13 -070021import logging
22import os.path
Jonathan Liu76f51682022-11-12 00:27:47 +000023import re
Rucha Katakwarb6847db2022-03-24 16:49:13 -070024import subprocess
Rucha Katakwar31a54502022-08-10 16:32:30 -070025import error_util
leslieshaw21c49e512023-08-25 13:47:28 -070026import image_processing_utils
Rucha Katakwarb6847db2022-03-24 16:49:13 -070027
28
leslieshaw5b54ee52023-08-29 13:22:30 -070029AREA_720P_VIDEO = 1280*720
Jonathan Liu76f51682022-11-12 00:27:47 +000030HR_TO_SEC = 3600
31MIN_TO_SEC = 60
32
Rucha Katakwar089e6092022-03-30 11:19:26 -070033ITS_SUPPORTED_QUALITIES = (
Rucha Katakwar41133f92022-03-22 13:44:09 -070034 'HIGH',
35 '2160P',
36 '1080P',
37 '720P',
38 '480P',
39 'CIF',
40 'QCIF',
41 'QVGA',
42 'LOW',
43 'VGA'
Rucha Katakwar222c5312022-03-21 14:17:01 -070044)
Rucha Katakwar0ced2c32022-08-01 14:54:42 -070045
leslieshaw6fb2c072023-04-06 16:38:54 -070046LOW_RESOLUTION_SIZES = (
47 '176x144',
48 '192x144',
49 '352x288',
50 '384x288',
51 '320x240',
52)
Rucha Katakwarb6847db2022-03-24 16:49:13 -070053
leslieshaw6fb2c072023-04-06 16:38:54 -070054LOWEST_RES_TESTED_AREA = 640*360
leslieshaw99fd75b2023-01-09 15:01:19 -080055
leslieshaw541e2972023-02-28 11:06:07 -080056VIDEO_QUALITY_SIZE = {
Clemenz Portmann703010e2023-07-21 16:39:18 -070057 # '480P', '1080P', HIGH' & 'LOW' are not included as they are DUT-dependent
leslieshaw541e2972023-02-28 11:06:07 -080058 '2160P': '3840x2160',
leslieshaw541e2972023-02-28 11:06:07 -080059 '720P': '1280x720',
leslieshaw541e2972023-02-28 11:06:07 -080060 'VGA': '640x480',
61 'CIF': '352x288',
62 'QVGA': '320x240',
63 'QCIF': '176x144',
64}
65
66
leslieshaw5b54ee52023-08-29 13:22:30 -070067def get_720p_or_above_size(supported_preview_sizes):
68 """Returns the smallest size above or equal to 720p in preview and video.
69
70 If the largest preview size is under 720P, returns the largest value.
71
72 Args:
73 supported_preview_sizes: list; preview sizes.
74 e.g. ['1920x960', '1600x1200', '1920x1080']
75 Returns:
76 smallest size >= 720p video format
77 """
78
79 size_to_area = lambda s: int(s.split('x')[0])*int(s.split('x')[1])
80 smallest_area = float('inf')
81 smallest_720p_or_above_size = ''
82 largest_supported_preview_size = ''
83 largest_area = 0
84 for size in supported_preview_sizes:
85 area = size_to_area(size)
86 if smallest_area > area >= AREA_720P_VIDEO:
87 smallest_area = area
88 smallest_720p_or_above_size = size
89 else:
90 if area > largest_area:
91 largest_area = area
92 largest_supported_preview_size = size
93
94 if largest_area > AREA_720P_VIDEO:
95 logging.debug('Smallest 720p or above size: %s',
96 smallest_720p_or_above_size)
97 return smallest_720p_or_above_size
98 else:
99 logging.debug('Largest supported preview size: %s',
100 largest_supported_preview_size)
101 return largest_supported_preview_size
102
103
leslieshaw541e2972023-02-28 11:06:07 -0800104def get_lowest_preview_video_size(
105 supported_preview_sizes, supported_video_qualities, min_area):
106 """Returns the common, smallest size above minimum in preview and video.
107
108 Args:
109 supported_preview_sizes: str; preview size (ex. '1920x1080')
110 supported_video_qualities: str; video recording quality and id pair
111 (ex. '480P:4', '720P:5'')
112 min_area: int; filter to eliminate smaller sizes (ex. 640*480)
113 Returns:
114 smallest_common_size: str; smallest, common size between preview and video
115 smallest_common_video_quality: str; video recording quality such as 480P
116 """
117
118 # Make dictionary on video quality and size according to compatibility
119 supported_video_size_to_quality = {}
120 for quality in supported_video_qualities:
121 video_quality = quality.split(':')[0]
122 if video_quality in VIDEO_QUALITY_SIZE:
123 video_size = VIDEO_QUALITY_SIZE[video_quality]
124 supported_video_size_to_quality[video_size] = video_quality
125 logging.debug(
126 'Supported video size to quality: %s', supported_video_size_to_quality)
127
128 # Use areas of video sizes to find the smallest, common size
129 size_to_area = lambda s: int(s.split('x')[0])*int(s.split('x')[1])
130 smallest_common_size = ''
131 smallest_area = float('inf')
132 for size in supported_preview_sizes:
133 if size in supported_video_size_to_quality:
134 area = size_to_area(size)
135 if smallest_area > area >= min_area:
136 smallest_area = area
137 smallest_common_size = size
138 logging.debug('Lowest common size: %s', smallest_common_size)
139
140 # Find video quality of resolution with resolution as key
141 smallest_common_video_quality = (
142 supported_video_size_to_quality[smallest_common_size])
143 logging.debug(
144 'Lowest common size video quality: %s', smallest_common_video_quality)
145
146 return smallest_common_size, smallest_common_video_quality
147
148
Clemenz Portmann6e00e052023-02-14 16:39:57 -0800149def log_ffmpeg_version():
150 """Logs the ffmpeg version being used."""
Rucha Katakwar31a54502022-08-10 16:32:30 -0700151
152 ffmpeg_version_cmd = ('ffmpeg -version')
153 p = subprocess.Popen(ffmpeg_version_cmd, shell=True, stdout=subprocess.PIPE)
154 output, _ = p.communicate()
155 if p.poll() != 0:
156 raise error_util.CameraItsError('Error running ffmpeg version cmd.')
157 decoded_output = output.decode('utf-8')
Clemenz Portmann6e00e052023-02-14 16:39:57 -0800158 logging.debug('ffmpeg version: %s', decoded_output.split(' ')[2])
Rucha Katakwar31a54502022-08-10 16:32:30 -0700159
160
Rucha Katakwar932bded2022-04-04 14:29:52 -0700161def extract_key_frames_from_video(log_path, video_file_name):
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700162 """Returns a list of extracted key frames.
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700163
164 Ffmpeg tool is used to extract key frames from the video at path
Rucha Katakwar932bded2022-04-04 14:29:52 -0700165 os.path.join(log_path, video_file_name).
166 The extracted key frames will have the name video_file_name with "_key_frame"
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700167 suffix to identify the frames for video of each quality.Since there can be
168 multiple key frames, each key frame image will be differentiated with it's
169 frame index.All the extracted key frames will be available in jpeg format
170 at the same path as the video file.
171
Rucha Katakwar05167b72022-05-25 13:42:32 -0700172 The run time flag '-loglevel quiet' hides the information from terminal.
173 In order to see the detailed output of ffmpeg command change the loglevel
174 option to 'info'.
175
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700176 Args:
177 log_path: path for video file directory
Rucha Katakwar932bded2022-04-04 14:29:52 -0700178 video_file_name: name of the video file.
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700179 Returns:
180 key_frame_files: A list of paths for each key frame extracted from the
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700181 video. Ex: VID_20220325_050918_0_CIF_352x288.mp4
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700182 """
Rucha Katakwar932bded2022-04-04 14:29:52 -0700183 ffmpeg_image_name = f"{video_file_name.split('.')[0]}_key_frame"
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700184 ffmpeg_image_file_path = os.path.join(
185 log_path, ffmpeg_image_name + '_%02d.png')
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700186 cmd = ['ffmpeg',
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700187 '-skip_frame',
188 'nokey',
189 '-i',
190 os.path.join(log_path, video_file_name),
191 '-vsync',
192 'vfr',
193 '-frame_pts',
194 'true',
195 ffmpeg_image_file_path,
Rucha Katakwar05167b72022-05-25 13:42:32 -0700196 '-loglevel',
197 'quiet',
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700198 ]
199 logging.debug('Extracting key frames from: %s', video_file_name)
Jonathan Liu98b99812023-07-14 13:20:38 -0700200 _ = subprocess.call(cmd,
201 stdin=subprocess.DEVNULL,
202 stdout=subprocess.DEVNULL,
203 stderr=subprocess.DEVNULL)
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700204 arr = os.listdir(os.path.join(log_path))
205 key_frame_files = []
206 for file in arr:
207 if '.png' in file and not os.path.isdir(file) and ffmpeg_image_name in file:
208 key_frame_files.append(file)
Rucha Katakwar2c49c452022-05-19 14:29:03 -0700209
Rucha Katakwar2e8ab092022-05-26 10:31:40 -0700210 logging.debug('Extracted key frames: %s', key_frame_files)
211 logging.debug('Length of key_frame_files: %d', len(key_frame_files))
Clemenz Portmann00c6ef32022-06-02 16:21:19 -0700212 if not key_frame_files:
Rucha Katakwar2c49c452022-05-19 14:29:03 -0700213 raise AssertionError('No key frames extracted. Check source video.')
214
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700215 return key_frame_files
216
217
218def get_key_frame_to_process(key_frame_files):
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700219 """Returns the key frame file from the list of key_frame_files.
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700220
221 If the size of the list is 1 then the file in the list will be returned else
222 the file with highest frame_index will be returned for further processing.
223
224 Args:
225 key_frame_files: A list of key frame files.
226 Returns:
227 key_frame_file to be used for further processing.
228 """
Rucha Katakwar312d64e2022-05-25 10:24:25 -0700229 if not key_frame_files:
230 raise AssertionError('key_frame_files list is empty.')
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700231 key_frame_files.sort()
232 return key_frame_files[-1]
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700233
234
235def extract_all_frames_from_video(log_path, video_file_name, img_format):
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700236 """Extracts and returns a list of all extracted frames.
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700237
238 Ffmpeg tool is used to extract all frames from the video at path
239 <log_path>/<video_file_name>. The extracted key frames will have the name
240 video_file_name with "_frame" suffix to identify the frames for video of each
241 size. Each frame image will be differentiated with its frame index. All
242 extracted key frames will be available in the provided img_format format at
243 the same path as the video file.
244
Rucha Katakwar05167b72022-05-25 13:42:32 -0700245 The run time flag '-loglevel quiet' hides the information from terminal.
246 In order to see the detailed output of ffmpeg command change the loglevel
247 option to 'info'.
248
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700249 Args:
250 log_path: str; path for video file directory
251 video_file_name: str; name of the video file.
252 img_format: str; type of image to export frames into. ex. 'png'
253 Returns:
254 key_frame_files: An ordered list of paths for each frame extracted from the
255 video
256 """
257 logging.debug('Extracting all frames')
258 ffmpeg_image_name = f"{video_file_name.split('.')[0]}_frame"
259 logging.debug('ffmpeg_image_name: %s', ffmpeg_image_name)
260 ffmpeg_image_file_names = (
261 f'{os.path.join(log_path, ffmpeg_image_name)}_%03d.{img_format}')
262 cmd = [
263 'ffmpeg', '-i', os.path.join(log_path, video_file_name),
Clemenz Portmann714b9302023-06-30 13:53:53 -0700264 '-vsync', 'vfr', # force ffmpeg to use video fps instead of inferred fps
Rucha Katakwar05167b72022-05-25 13:42:32 -0700265 ffmpeg_image_file_names, '-loglevel', 'quiet'
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700266 ]
Jonathan Liu98b99812023-07-14 13:20:38 -0700267 _ = subprocess.call(cmd,
268 stdin=subprocess.DEVNULL,
269 stdout=subprocess.DEVNULL,
270 stderr=subprocess.DEVNULL)
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700271
272 file_list = sorted(
273 [_ for _ in os.listdir(log_path) if (_.endswith(img_format)
274 and ffmpeg_image_name in _)])
Clemenz Portmann00c6ef32022-06-02 16:21:19 -0700275 if not file_list:
Rucha Katakwar2c49c452022-05-19 14:29:03 -0700276 raise AssertionError('No frames extracted. Check source video.')
277
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700278 return file_list
Jonathan Liu76f51682022-11-12 00:27:47 +0000279
280
leslieshaw21c49e512023-08-25 13:47:28 -0700281def extract_last_key_frame_from_recording(log_path, file_name):
leslieshaw5b54ee52023-08-29 13:22:30 -0700282 """Extract last key frame from recordings.
leslieshaw21c49e512023-08-25 13:47:28 -0700283
284 Args:
285 log_path: str; file location
286 file_name: str file name for saved video
287
288 Returns:
leslieshaw5b54ee52023-08-29 13:22:30 -0700289 numpy image of last key frame
leslieshaw21c49e512023-08-25 13:47:28 -0700290 """
291 key_frame_files = extract_key_frames_from_video(log_path, file_name)
292 logging.debug('key_frame_files: %s', key_frame_files)
293
294 # Get the last_key_frame file to process.
295 last_key_frame_file = get_key_frame_to_process(key_frame_files)
296 logging.debug('last_key_frame: %s', last_key_frame_file)
297
298 # Convert lastKeyFrame to numpy array
299 np_image = image_processing_utils.convert_image_to_numpy_array(
300 os.path.join(log_path, last_key_frame_file))
301 logging.debug('last key frame image shape: %s', np_image.shape)
302
303 return np_image
304
305
Jonathan Liu76f51682022-11-12 00:27:47 +0000306def get_average_frame_rate(video_file_name_with_path):
307 """Get average frame rate assuming variable frame rate video.
308
309 Args:
310 video_file_name_with_path: path to the video to be analyzed
311 Returns:
312 Float. average frames per second.
313 """
314
Jonathan Liufb3f6032023-08-21 11:02:40 -0700315 cmd = ['ffprobe',
316 '-v',
317 'quiet',
318 '-show_streams',
319 '-select_streams',
320 'v:0', # first video stream
321 video_file_name_with_path
Jonathan Liu76f51682022-11-12 00:27:47 +0000322 ]
323 logging.debug('Getting frame rate')
324 raw_output = ''
325 try:
Jonathan Liu98b99812023-07-14 13:20:38 -0700326 raw_output = subprocess.check_output(cmd,
327 stdin=subprocess.DEVNULL,
328 stderr=subprocess.STDOUT)
Jonathan Liu76f51682022-11-12 00:27:47 +0000329 except subprocess.CalledProcessError as e:
330 raise AssertionError(str(e.output)) from e
331 if raw_output:
332 output = str(raw_output.decode('utf-8')).strip()
Jonathan Liufb3f6032023-08-21 11:02:40 -0700333 logging.debug('ffprobe command %s output: %s', ' '.join(cmd), output)
334 average_frame_rate_data = (
335 re.search(r'avg_frame_rate=*([0-9]+/[0-9]+)', output).group(1)
336 )
337 average_frame_rate = (int(average_frame_rate_data.split('/')[0]) /
338 int(average_frame_rate_data.split('/')[1]))
339 logging.debug('Average FPS: %.4f', average_frame_rate)
340 return average_frame_rate
Jonathan Liu76f51682022-11-12 00:27:47 +0000341 else:
Jonathan Liufb3f6032023-08-21 11:02:40 -0700342 raise AssertionError('ffprobe failed to provide frame rate data')
Jonathan Liu76f51682022-11-12 00:27:47 +0000343
344
345def get_frame_deltas(video_file_name_with_path, timestamp_type='pts'):
346 """Get list of time diffs between frames.
347
348 Args:
349 video_file_name_with_path: path to the video to be analyzed
350 timestamp_type: 'pts' or 'dts'
351 Returns:
352 List of floats. Time diffs between frames in seconds.
353 """
354
355 cmd = ['ffprobe',
356 '-show_entries',
357 f'frame=pkt_{timestamp_type}_time',
358 '-select_streams',
359 'v',
360 video_file_name_with_path
361 ]
362 logging.debug('Getting frame deltas')
363 raw_output = ''
364 try:
Jonathan Liu98b99812023-07-14 13:20:38 -0700365 raw_output = subprocess.check_output(cmd,
366 stdin=subprocess.DEVNULL,
367 stderr=subprocess.STDOUT)
Jonathan Liu76f51682022-11-12 00:27:47 +0000368 except subprocess.CalledProcessError as e:
369 raise AssertionError(str(e.output)) from e
370 if raw_output:
371 output = str(raw_output.decode('utf-8')).strip().split('\n')
372 deltas = []
Jonathan Liu61b36cf2023-06-14 10:30:06 -0700373 prev_time = None
Jonathan Liu76f51682022-11-12 00:27:47 +0000374 for line in output:
375 if timestamp_type not in line:
376 continue
377 curr_time = float(re.search(r'time= *([0-9][0-9\.]*)', line).group(1))
Jonathan Liu61b36cf2023-06-14 10:30:06 -0700378 if prev_time is not None:
379 deltas.append(curr_time - prev_time)
Jonathan Liu76f51682022-11-12 00:27:47 +0000380 prev_time = curr_time
381 logging.debug('Frame deltas: %s', deltas)
382 return deltas
383 else:
384 raise AssertionError('ffprobe failed to provide frame delta data')