blob: 1386f37f6722c929ff1f8def333d6bd05eb17d94 [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
Kailiang Chen6526b3a2024-01-25 11:28:58 -080029COLORSPACE_HDR = 'bt2020'
Jonathan Liu76f51682022-11-12 00:27:47 +000030HR_TO_SEC = 3600
Kailiang Chen6526b3a2024-01-25 11:28:58 -080031INDEX_FIRST_SUBGROUP = 1
Jonathan Liu76f51682022-11-12 00:27:47 +000032MIN_TO_SEC = 60
33
Rucha Katakwar089e6092022-03-30 11:19:26 -070034ITS_SUPPORTED_QUALITIES = (
Rucha Katakwar41133f92022-03-22 13:44:09 -070035 'HIGH',
36 '2160P',
37 '1080P',
38 '720P',
39 '480P',
40 'CIF',
41 'QCIF',
42 'QVGA',
43 'LOW',
44 'VGA'
Rucha Katakwar222c5312022-03-21 14:17:01 -070045)
Rucha Katakwar0ced2c32022-08-01 14:54:42 -070046
leslieshaw6fb2c072023-04-06 16:38:54 -070047LOW_RESOLUTION_SIZES = (
48 '176x144',
49 '192x144',
50 '352x288',
51 '384x288',
52 '320x240',
53)
Rucha Katakwarb6847db2022-03-24 16:49:13 -070054
leslieshaw6fb2c072023-04-06 16:38:54 -070055LOWEST_RES_TESTED_AREA = 640*360
leslieshaw99fd75b2023-01-09 15:01:19 -080056
leslieshaw541e2972023-02-28 11:06:07 -080057VIDEO_QUALITY_SIZE = {
Clemenz Portmann703010e2023-07-21 16:39:18 -070058 # '480P', '1080P', HIGH' & 'LOW' are not included as they are DUT-dependent
leslieshaw541e2972023-02-28 11:06:07 -080059 '2160P': '3840x2160',
leslieshaw541e2972023-02-28 11:06:07 -080060 '720P': '1280x720',
leslieshaw541e2972023-02-28 11:06:07 -080061 'VGA': '640x480',
62 'CIF': '352x288',
63 'QVGA': '320x240',
64 'QCIF': '176x144',
65}
66
67
Clemenz Portmannba0de522024-04-30 08:01:20 -070068def get_lowest_common_preview_video_size(
leslieshaw541e2972023-02-28 11:06:07 -080069 supported_preview_sizes, supported_video_qualities, min_area):
70 """Returns the common, smallest size above minimum in preview and video.
71
72 Args:
73 supported_preview_sizes: str; preview size (ex. '1920x1080')
74 supported_video_qualities: str; video recording quality and id pair
75 (ex. '480P:4', '720P:5'')
76 min_area: int; filter to eliminate smaller sizes (ex. 640*480)
77 Returns:
78 smallest_common_size: str; smallest, common size between preview and video
79 smallest_common_video_quality: str; video recording quality such as 480P
80 """
81
82 # Make dictionary on video quality and size according to compatibility
83 supported_video_size_to_quality = {}
84 for quality in supported_video_qualities:
85 video_quality = quality.split(':')[0]
86 if video_quality in VIDEO_QUALITY_SIZE:
87 video_size = VIDEO_QUALITY_SIZE[video_quality]
88 supported_video_size_to_quality[video_size] = video_quality
89 logging.debug(
90 'Supported video size to quality: %s', supported_video_size_to_quality)
91
92 # Use areas of video sizes to find the smallest, common size
93 size_to_area = lambda s: int(s.split('x')[0])*int(s.split('x')[1])
94 smallest_common_size = ''
95 smallest_area = float('inf')
96 for size in supported_preview_sizes:
97 if size in supported_video_size_to_quality:
98 area = size_to_area(size)
99 if smallest_area > area >= min_area:
100 smallest_area = area
101 smallest_common_size = size
102 logging.debug('Lowest common size: %s', smallest_common_size)
103
104 # Find video quality of resolution with resolution as key
105 smallest_common_video_quality = (
106 supported_video_size_to_quality[smallest_common_size])
107 logging.debug(
108 'Lowest common size video quality: %s', smallest_common_video_quality)
109
110 return smallest_common_size, smallest_common_video_quality
111
112
Clemenz Portmann6e00e052023-02-14 16:39:57 -0800113def log_ffmpeg_version():
114 """Logs the ffmpeg version being used."""
Rucha Katakwar31a54502022-08-10 16:32:30 -0700115
116 ffmpeg_version_cmd = ('ffmpeg -version')
117 p = subprocess.Popen(ffmpeg_version_cmd, shell=True, stdout=subprocess.PIPE)
118 output, _ = p.communicate()
119 if p.poll() != 0:
120 raise error_util.CameraItsError('Error running ffmpeg version cmd.')
121 decoded_output = output.decode('utf-8')
Clemenz Portmann6e00e052023-02-14 16:39:57 -0800122 logging.debug('ffmpeg version: %s', decoded_output.split(' ')[2])
Rucha Katakwar31a54502022-08-10 16:32:30 -0700123
124
Rucha Katakwar932bded2022-04-04 14:29:52 -0700125def extract_key_frames_from_video(log_path, video_file_name):
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700126 """Returns a list of extracted key frames.
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700127
128 Ffmpeg tool is used to extract key frames from the video at path
Rucha Katakwar932bded2022-04-04 14:29:52 -0700129 os.path.join(log_path, video_file_name).
130 The extracted key frames will have the name video_file_name with "_key_frame"
leslieshaw3e0fb3c2024-03-31 23:56:04 -0700131 suffix to identify the frames for video of each quality. Since there can be
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700132 multiple key frames, each key frame image will be differentiated with it's
leslieshaw3e0fb3c2024-03-31 23:56:04 -0700133 frame index. All the extracted key frames will be available in jpeg format
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700134 at the same path as the video file.
135
Rucha Katakwar05167b72022-05-25 13:42:32 -0700136 The run time flag '-loglevel quiet' hides the information from terminal.
137 In order to see the detailed output of ffmpeg command change the loglevel
138 option to 'info'.
139
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700140 Args:
leslieshaw3e0fb3c2024-03-31 23:56:04 -0700141 log_path: path for video file directory.
Rucha Katakwar932bded2022-04-04 14:29:52 -0700142 video_file_name: name of the video file.
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700143 Returns:
leslieshaw3e0fb3c2024-03-31 23:56:04 -0700144 key_frame_files: a sorted list of files which contains a name per key
145 frame. Ex: VID_20220325_050918_0_preview_1920x1440_key_frame_0001.png
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700146 """
Clemenz Portmann787a70c2024-05-06 13:44:32 -0700147 ffmpeg_image_name = f'{os.path.splitext(video_file_name)[0]}_key_frame'
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700148 ffmpeg_image_file_path = os.path.join(
leslieshaw3e0fb3c2024-03-31 23:56:04 -0700149 log_path, ffmpeg_image_name + '_%04d.png')
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700150 cmd = ['ffmpeg',
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700151 '-skip_frame',
152 'nokey',
153 '-i',
154 os.path.join(log_path, video_file_name),
155 '-vsync',
156 'vfr',
157 '-frame_pts',
158 'true',
159 ffmpeg_image_file_path,
Rucha Katakwar05167b72022-05-25 13:42:32 -0700160 '-loglevel',
161 'quiet',
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700162 ]
163 logging.debug('Extracting key frames from: %s', video_file_name)
Jonathan Liu98b99812023-07-14 13:20:38 -0700164 _ = subprocess.call(cmd,
165 stdin=subprocess.DEVNULL,
166 stdout=subprocess.DEVNULL,
167 stderr=subprocess.DEVNULL)
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700168 arr = os.listdir(os.path.join(log_path))
169 key_frame_files = []
170 for file in arr:
171 if '.png' in file and not os.path.isdir(file) and ffmpeg_image_name in file:
172 key_frame_files.append(file)
leslieshaw3e0fb3c2024-03-31 23:56:04 -0700173 key_frame_files.sort()
Rucha Katakwar2e8ab092022-05-26 10:31:40 -0700174 logging.debug('Extracted key frames: %s', key_frame_files)
175 logging.debug('Length of key_frame_files: %d', len(key_frame_files))
Clemenz Portmann00c6ef32022-06-02 16:21:19 -0700176 if not key_frame_files:
Rucha Katakwar2c49c452022-05-19 14:29:03 -0700177 raise AssertionError('No key frames extracted. Check source video.')
178
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700179 return key_frame_files
180
181
182def get_key_frame_to_process(key_frame_files):
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700183 """Returns the key frame file from the list of key_frame_files.
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700184
185 If the size of the list is 1 then the file in the list will be returned else
186 the file with highest frame_index will be returned for further processing.
187
188 Args:
189 key_frame_files: A list of key frame files.
190 Returns:
191 key_frame_file to be used for further processing.
192 """
Rucha Katakwar312d64e2022-05-25 10:24:25 -0700193 if not key_frame_files:
194 raise AssertionError('key_frame_files list is empty.')
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700195 key_frame_files.sort()
196 return key_frame_files[-1]
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700197
198
199def extract_all_frames_from_video(log_path, video_file_name, img_format):
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700200 """Extracts and returns a list of all extracted frames.
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700201
202 Ffmpeg tool is used to extract all frames from the video at path
203 <log_path>/<video_file_name>. The extracted key frames will have the name
204 video_file_name with "_frame" suffix to identify the frames for video of each
205 size. Each frame image will be differentiated with its frame index. All
206 extracted key frames will be available in the provided img_format format at
207 the same path as the video file.
208
Rucha Katakwar05167b72022-05-25 13:42:32 -0700209 The run time flag '-loglevel quiet' hides the information from terminal.
210 In order to see the detailed output of ffmpeg command change the loglevel
211 option to 'info'.
212
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700213 Args:
214 log_path: str; path for video file directory
215 video_file_name: str; name of the video file.
216 img_format: str; type of image to export frames into. ex. 'png'
217 Returns:
218 key_frame_files: An ordered list of paths for each frame extracted from the
219 video
220 """
221 logging.debug('Extracting all frames')
222 ffmpeg_image_name = f"{video_file_name.split('.')[0]}_frame"
223 logging.debug('ffmpeg_image_name: %s', ffmpeg_image_name)
224 ffmpeg_image_file_names = (
Dipen Patelc4c10e92024-05-07 16:35:35 -0700225 f'{os.path.join(log_path, ffmpeg_image_name)}_%04d.{img_format}')
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700226 cmd = [
227 'ffmpeg', '-i', os.path.join(log_path, video_file_name),
Dipen Patelc4c10e92024-05-07 16:35:35 -0700228 '-vsync', 'passthrough', # prevents frame drops during decoding
Rucha Katakwar05167b72022-05-25 13:42:32 -0700229 ffmpeg_image_file_names, '-loglevel', 'quiet'
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700230 ]
Jonathan Liu98b99812023-07-14 13:20:38 -0700231 _ = subprocess.call(cmd,
232 stdin=subprocess.DEVNULL,
233 stdout=subprocess.DEVNULL,
234 stderr=subprocess.DEVNULL)
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700235
236 file_list = sorted(
237 [_ for _ in os.listdir(log_path) if (_.endswith(img_format)
238 and ffmpeg_image_name in _)])
Clemenz Portmann00c6ef32022-06-02 16:21:19 -0700239 if not file_list:
Rucha Katakwar2c49c452022-05-19 14:29:03 -0700240 raise AssertionError('No frames extracted. Check source video.')
241
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700242 return file_list
Jonathan Liu76f51682022-11-12 00:27:47 +0000243
244
leslieshaw21c49e512023-08-25 13:47:28 -0700245def extract_last_key_frame_from_recording(log_path, file_name):
leslieshaw5b54ee52023-08-29 13:22:30 -0700246 """Extract last key frame from recordings.
leslieshaw21c49e512023-08-25 13:47:28 -0700247
248 Args:
249 log_path: str; file location
250 file_name: str file name for saved video
251
252 Returns:
Clemenz Portmann25529eb2023-09-17 21:13:15 -0700253 numpy image of last key frame
leslieshaw21c49e512023-08-25 13:47:28 -0700254 """
255 key_frame_files = extract_key_frames_from_video(log_path, file_name)
256 logging.debug('key_frame_files: %s', key_frame_files)
257
258 # Get the last_key_frame file to process.
259 last_key_frame_file = get_key_frame_to_process(key_frame_files)
260 logging.debug('last_key_frame: %s', last_key_frame_file)
261
Clemenz Portmann25529eb2023-09-17 21:13:15 -0700262 # Convert last_key_frame to numpy array
leslieshaw21c49e512023-08-25 13:47:28 -0700263 np_image = image_processing_utils.convert_image_to_numpy_array(
264 os.path.join(log_path, last_key_frame_file))
265 logging.debug('last key frame image shape: %s', np_image.shape)
266
267 return np_image
268
269
Jonathan Liu76f51682022-11-12 00:27:47 +0000270def get_average_frame_rate(video_file_name_with_path):
271 """Get average frame rate assuming variable frame rate video.
272
273 Args:
274 video_file_name_with_path: path to the video to be analyzed
275 Returns:
276 Float. average frames per second.
277 """
278
Jonathan Liufb3f6032023-08-21 11:02:40 -0700279 cmd = ['ffprobe',
280 '-v',
281 'quiet',
282 '-show_streams',
283 '-select_streams',
284 'v:0', # first video stream
285 video_file_name_with_path
Jonathan Liu76f51682022-11-12 00:27:47 +0000286 ]
287 logging.debug('Getting frame rate')
288 raw_output = ''
289 try:
Jonathan Liu98b99812023-07-14 13:20:38 -0700290 raw_output = subprocess.check_output(cmd,
291 stdin=subprocess.DEVNULL,
292 stderr=subprocess.STDOUT)
Jonathan Liu76f51682022-11-12 00:27:47 +0000293 except subprocess.CalledProcessError as e:
294 raise AssertionError(str(e.output)) from e
295 if raw_output:
296 output = str(raw_output.decode('utf-8')).strip()
Jonathan Liufb3f6032023-08-21 11:02:40 -0700297 logging.debug('ffprobe command %s output: %s', ' '.join(cmd), output)
298 average_frame_rate_data = (
Kailiang Chen6526b3a2024-01-25 11:28:58 -0800299 re.search(r'avg_frame_rate=*([0-9]+/[0-9]+)', output)
300 .group(INDEX_FIRST_SUBGROUP)
Jonathan Liufb3f6032023-08-21 11:02:40 -0700301 )
302 average_frame_rate = (int(average_frame_rate_data.split('/')[0]) /
303 int(average_frame_rate_data.split('/')[1]))
304 logging.debug('Average FPS: %.4f', average_frame_rate)
305 return average_frame_rate
Jonathan Liu76f51682022-11-12 00:27:47 +0000306 else:
Jonathan Liufb3f6032023-08-21 11:02:40 -0700307 raise AssertionError('ffprobe failed to provide frame rate data')
Jonathan Liu76f51682022-11-12 00:27:47 +0000308
309
310def get_frame_deltas(video_file_name_with_path, timestamp_type='pts'):
311 """Get list of time diffs between frames.
312
313 Args:
314 video_file_name_with_path: path to the video to be analyzed
315 timestamp_type: 'pts' or 'dts'
316 Returns:
317 List of floats. Time diffs between frames in seconds.
318 """
319
320 cmd = ['ffprobe',
321 '-show_entries',
322 f'frame=pkt_{timestamp_type}_time',
323 '-select_streams',
324 'v',
325 video_file_name_with_path
326 ]
327 logging.debug('Getting frame deltas')
328 raw_output = ''
329 try:
Jonathan Liu98b99812023-07-14 13:20:38 -0700330 raw_output = subprocess.check_output(cmd,
331 stdin=subprocess.DEVNULL,
332 stderr=subprocess.STDOUT)
Jonathan Liu76f51682022-11-12 00:27:47 +0000333 except subprocess.CalledProcessError as e:
334 raise AssertionError(str(e.output)) from e
335 if raw_output:
336 output = str(raw_output.decode('utf-8')).strip().split('\n')
337 deltas = []
Jonathan Liu61b36cf2023-06-14 10:30:06 -0700338 prev_time = None
Jonathan Liu76f51682022-11-12 00:27:47 +0000339 for line in output:
340 if timestamp_type not in line:
341 continue
Kailiang Chen6526b3a2024-01-25 11:28:58 -0800342 curr_time = float(re.search(r'time= *([0-9][0-9\.]*)', line)
343 .group(INDEX_FIRST_SUBGROUP))
Jonathan Liu61b36cf2023-06-14 10:30:06 -0700344 if prev_time is not None:
345 deltas.append(curr_time - prev_time)
Jonathan Liu76f51682022-11-12 00:27:47 +0000346 prev_time = curr_time
347 logging.debug('Frame deltas: %s', deltas)
348 return deltas
349 else:
350 raise AssertionError('ffprobe failed to provide frame delta data')
Kailiang Chen6526b3a2024-01-25 11:28:58 -0800351
352
353def get_video_colorspace(log_path, video_file_name):
354 """Get the video colorspace.
355
356 Args:
357 log_path: path for video file directory
358 video_file_name: name of the video file
359 Returns:
360 video colorspace, e.g. BT.2020 or BT.709
361 """
362
363 cmd = ['ffprobe',
364 '-show_streams',
365 '-select_streams',
366 'v:0',
367 '-of',
368 'json',
369 '-i',
370 os.path.join(log_path, video_file_name)
371 ]
372 logging.debug('Get the video colorspace')
373 raw_output = ''
374 try:
375 raw_output = subprocess.check_output(cmd,
376 stdin=subprocess.DEVNULL,
377 stderr=subprocess.STDOUT)
378 except subprocess.CalledProcessError as e:
379 raise AssertionError(str(e.output)) from e
380
381 logging.debug('raw_output: %s', raw_output)
382 if raw_output:
383 colorspace = ''
384 output = str(raw_output.decode('utf-8')).strip().split('\n')
385 logging.debug('output: %s', output)
386 for line in output:
387 logging.debug('line: %s', line)
388 metadata = re.search(r'"color_space": ("[a-z0-9]*")', line)
389 if metadata:
390 colorspace = metadata.group(INDEX_FIRST_SUBGROUP)
391 logging.debug('Colorspace: %s', colorspace)
392 return colorspace
393 else:
394 raise AssertionError('ffprobe failed to provide color space')