blob: 1ac4be417a9b5e17e8ac809df003808254d7f6dc [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 Portmannfb236352024-06-10 10:13:40 -070068def get_largest_common_preview_video_size(cam, camera_id):
69 """Returns the largest, common size between preview and video.
70
71 Args:
72 cam: camera object.
73 camera_id: str; camera ID.
74
75 Returns:
76 largest_common_size: str; largest common size between preview & video.
77 """
78 supported_preview_sizes = cam.get_all_supported_preview_sizes(camera_id)
79 supported_video_qualities = cam.get_supported_video_qualities(camera_id)
80 logging.debug('Supported video profiles & IDs: %s', supported_video_qualities)
81
82 # Make a list of supported_video_sizes from video qualities
83 supported_video_sizes = []
84 for quality in supported_video_qualities:
85 video_quality = quality.split(':')[0] # form is ['CIF:3', '480P:4', ...]
86 if video_quality in VIDEO_QUALITY_SIZE:
87 supported_video_sizes.append(VIDEO_QUALITY_SIZE[video_quality])
88 logging.debug(
89 'Supported video sizes: %s', supported_video_sizes)
90
91 # Use areas of video sizes to find the largest common size
92 size_to_area = lambda s: int(s.split('x')[0])*int(s.split('x')[1])
93 largest_common_size = ''
94 largest_area = 0
95 common_sizes = list(set(supported_preview_sizes) & set(supported_video_sizes))
96 for size in common_sizes:
97 area = size_to_area(size)
98 if area > largest_area:
99 largest_area = area
100 largest_common_size = size
101 if not largest_common_size:
102 raise AssertionError('No common size between Preview and Video!')
103 logging.debug('Largest common size: %s', largest_common_size)
104 return largest_common_size
105
106
Clemenz Portmannba0de522024-04-30 08:01:20 -0700107def get_lowest_common_preview_video_size(
leslieshaw541e2972023-02-28 11:06:07 -0800108 supported_preview_sizes, supported_video_qualities, min_area):
109 """Returns the common, smallest size above minimum in preview and video.
110
111 Args:
112 supported_preview_sizes: str; preview size (ex. '1920x1080')
113 supported_video_qualities: str; video recording quality and id pair
114 (ex. '480P:4', '720P:5'')
115 min_area: int; filter to eliminate smaller sizes (ex. 640*480)
116 Returns:
117 smallest_common_size: str; smallest, common size between preview and video
118 smallest_common_video_quality: str; video recording quality such as 480P
119 """
120
121 # Make dictionary on video quality and size according to compatibility
122 supported_video_size_to_quality = {}
123 for quality in supported_video_qualities:
124 video_quality = quality.split(':')[0]
125 if video_quality in VIDEO_QUALITY_SIZE:
126 video_size = VIDEO_QUALITY_SIZE[video_quality]
127 supported_video_size_to_quality[video_size] = video_quality
128 logging.debug(
129 'Supported video size to quality: %s', supported_video_size_to_quality)
130
131 # Use areas of video sizes to find the smallest, common size
132 size_to_area = lambda s: int(s.split('x')[0])*int(s.split('x')[1])
133 smallest_common_size = ''
134 smallest_area = float('inf')
135 for size in supported_preview_sizes:
136 if size in supported_video_size_to_quality:
137 area = size_to_area(size)
138 if smallest_area > area >= min_area:
139 smallest_area = area
140 smallest_common_size = size
141 logging.debug('Lowest common size: %s', smallest_common_size)
142
143 # Find video quality of resolution with resolution as key
144 smallest_common_video_quality = (
145 supported_video_size_to_quality[smallest_common_size])
146 logging.debug(
147 'Lowest common size video quality: %s', smallest_common_video_quality)
148
149 return smallest_common_size, smallest_common_video_quality
150
151
Clemenz Portmann6e00e052023-02-14 16:39:57 -0800152def log_ffmpeg_version():
153 """Logs the ffmpeg version being used."""
Rucha Katakwar31a54502022-08-10 16:32:30 -0700154
155 ffmpeg_version_cmd = ('ffmpeg -version')
156 p = subprocess.Popen(ffmpeg_version_cmd, shell=True, stdout=subprocess.PIPE)
157 output, _ = p.communicate()
158 if p.poll() != 0:
159 raise error_util.CameraItsError('Error running ffmpeg version cmd.')
160 decoded_output = output.decode('utf-8')
Clemenz Portmann6e00e052023-02-14 16:39:57 -0800161 logging.debug('ffmpeg version: %s', decoded_output.split(' ')[2])
Rucha Katakwar31a54502022-08-10 16:32:30 -0700162
163
Rucha Katakwar932bded2022-04-04 14:29:52 -0700164def extract_key_frames_from_video(log_path, video_file_name):
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700165 """Returns a list of extracted key frames.
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700166
167 Ffmpeg tool is used to extract key frames from the video at path
Rucha Katakwar932bded2022-04-04 14:29:52 -0700168 os.path.join(log_path, video_file_name).
169 The extracted key frames will have the name video_file_name with "_key_frame"
leslieshaw3e0fb3c2024-03-31 23:56:04 -0700170 suffix to identify the frames for video of each quality. Since there can be
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700171 multiple key frames, each key frame image will be differentiated with it's
leslieshaw3e0fb3c2024-03-31 23:56:04 -0700172 frame index. All the extracted key frames will be available in jpeg format
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700173 at the same path as the video file.
174
Rucha Katakwar05167b72022-05-25 13:42:32 -0700175 The run time flag '-loglevel quiet' hides the information from terminal.
176 In order to see the detailed output of ffmpeg command change the loglevel
177 option to 'info'.
178
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700179 Args:
leslieshaw3e0fb3c2024-03-31 23:56:04 -0700180 log_path: path for video file directory.
Rucha Katakwar932bded2022-04-04 14:29:52 -0700181 video_file_name: name of the video file.
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700182 Returns:
leslieshaw3e0fb3c2024-03-31 23:56:04 -0700183 key_frame_files: a sorted list of files which contains a name per key
184 frame. Ex: VID_20220325_050918_0_preview_1920x1440_key_frame_0001.png
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700185 """
Clemenz Portmann787a70c2024-05-06 13:44:32 -0700186 ffmpeg_image_name = f'{os.path.splitext(video_file_name)[0]}_key_frame'
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700187 ffmpeg_image_file_path = os.path.join(
leslieshaw3e0fb3c2024-03-31 23:56:04 -0700188 log_path, ffmpeg_image_name + '_%04d.png')
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700189 cmd = ['ffmpeg',
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700190 '-skip_frame',
191 'nokey',
192 '-i',
193 os.path.join(log_path, video_file_name),
194 '-vsync',
195 'vfr',
196 '-frame_pts',
197 'true',
198 ffmpeg_image_file_path,
Rucha Katakwar05167b72022-05-25 13:42:32 -0700199 '-loglevel',
200 'quiet',
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700201 ]
202 logging.debug('Extracting key frames from: %s', video_file_name)
Jonathan Liu98b99812023-07-14 13:20:38 -0700203 _ = subprocess.call(cmd,
204 stdin=subprocess.DEVNULL,
205 stdout=subprocess.DEVNULL,
206 stderr=subprocess.DEVNULL)
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700207 arr = os.listdir(os.path.join(log_path))
208 key_frame_files = []
209 for file in arr:
210 if '.png' in file and not os.path.isdir(file) and ffmpeg_image_name in file:
211 key_frame_files.append(file)
leslieshaw3e0fb3c2024-03-31 23:56:04 -0700212 key_frame_files.sort()
Rucha Katakwar2e8ab092022-05-26 10:31:40 -0700213 logging.debug('Extracted key frames: %s', key_frame_files)
214 logging.debug('Length of key_frame_files: %d', len(key_frame_files))
Clemenz Portmann00c6ef32022-06-02 16:21:19 -0700215 if not key_frame_files:
Rucha Katakwar2c49c452022-05-19 14:29:03 -0700216 raise AssertionError('No key frames extracted. Check source video.')
217
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700218 return key_frame_files
219
220
221def get_key_frame_to_process(key_frame_files):
Clemenz Portmann8d40e422022-04-28 10:29:01 -0700222 """Returns the key frame file from the list of key_frame_files.
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700223
224 If the size of the list is 1 then the file in the list will be returned else
225 the file with highest frame_index will be returned for further processing.
226
227 Args:
228 key_frame_files: A list of key frame files.
229 Returns:
230 key_frame_file to be used for further processing.
231 """
Rucha Katakwar312d64e2022-05-25 10:24:25 -0700232 if not key_frame_files:
233 raise AssertionError('key_frame_files list is empty.')
Rucha Katakwarb6847db2022-03-24 16:49:13 -0700234 key_frame_files.sort()
235 return key_frame_files[-1]
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700236
237
Leslie Shawca6c7632024-07-10 18:41:10 -0700238def extract_all_frames_from_video(
239 log_path, video_file_name, img_format, video_fps=None):
240 """Extracts and returns a list of frames from a video using FFmpeg.
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700241
Leslie Shawca6c7632024-07-10 18:41:10 -0700242 Extract all frames from the video at path <log_path>/<video_file_name>.
243 The extracted frames will have the name video_file_name with "_frame"
244 suffix to identify the frames for video of each size. Each frame image
245 will be differentiated with its frame index. All extracted rames will be
246 available in the provided img_format format at the same path as the video.
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700247
Rucha Katakwar05167b72022-05-25 13:42:32 -0700248 The run time flag '-loglevel quiet' hides the information from terminal.
249 In order to see the detailed output of ffmpeg command change the loglevel
250 option to 'info'.
251
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700252 Args:
Leslie Shawca6c7632024-07-10 18:41:10 -0700253 log_path: str; directory containing video file.
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700254 video_file_name: str; name of the video file.
Leslie Shawca6c7632024-07-10 18:41:10 -0700255 img_format: str; desired image format for export frames. ex. 'png'
256 video_fps: str; fps of imported video.
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700257 Returns:
Leslie Shawca6c7632024-07-10 18:41:10 -0700258 an ordered list of paths to the extracted frame images.
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700259 """
260 logging.debug('Extracting all frames')
261 ffmpeg_image_name = f"{video_file_name.split('.')[0]}_frame"
262 logging.debug('ffmpeg_image_name: %s', ffmpeg_image_name)
263 ffmpeg_image_file_names = (
Dipen Patelc4c10e92024-05-07 16:35:35 -0700264 f'{os.path.join(log_path, ffmpeg_image_name)}_%04d.{img_format}')
Leslie Shawca6c7632024-07-10 18:41:10 -0700265 if video_fps:
266 cmd = [
267 'ffmpeg', '-i', os.path.join(log_path, video_file_name),
268 '-r', video_fps, # force a constant frame rate for reliability
269 ffmpeg_image_file_names, '-loglevel', 'quiet'
270 ]
271 else:
272 cmd = [
273 'ffmpeg', '-i', os.path.join(log_path, video_file_name),
274 '-vsync', 'passthrough', # prevents frame drops during decoding
275 ffmpeg_image_file_names, '-loglevel', 'quiet'
276 ]
277 subprocess.call(cmd,
278 stdin=subprocess.DEVNULL,
279 stdout=subprocess.DEVNULL,
280 stderr=subprocess.DEVNULL)
Avichal Rakeshf82d5262022-04-22 16:24:18 -0700281
Leslie Shawca6c7632024-07-10 18:41:10 -0700282 files = sorted(
283 [file for file in os.listdir(log_path) if
284 (file.endswith(img_format) and ffmpeg_image_name in file)])
285 if not files:
Rucha Katakwar2c49c452022-05-19 14:29:03 -0700286 raise AssertionError('No frames extracted. Check source video.')
287
Leslie Shawca6c7632024-07-10 18:41:10 -0700288 return files
Jonathan Liu76f51682022-11-12 00:27:47 +0000289
290
leslieshaw21c49e512023-08-25 13:47:28 -0700291def extract_last_key_frame_from_recording(log_path, file_name):
leslieshaw5b54ee52023-08-29 13:22:30 -0700292 """Extract last key frame from recordings.
leslieshaw21c49e512023-08-25 13:47:28 -0700293
294 Args:
295 log_path: str; file location
296 file_name: str file name for saved video
297
298 Returns:
Clemenz Portmann25529eb2023-09-17 21:13:15 -0700299 numpy image of last key frame
leslieshaw21c49e512023-08-25 13:47:28 -0700300 """
301 key_frame_files = extract_key_frames_from_video(log_path, file_name)
302 logging.debug('key_frame_files: %s', key_frame_files)
303
304 # Get the last_key_frame file to process.
305 last_key_frame_file = get_key_frame_to_process(key_frame_files)
306 logging.debug('last_key_frame: %s', last_key_frame_file)
307
Clemenz Portmann25529eb2023-09-17 21:13:15 -0700308 # Convert last_key_frame to numpy array
leslieshaw21c49e512023-08-25 13:47:28 -0700309 np_image = image_processing_utils.convert_image_to_numpy_array(
310 os.path.join(log_path, last_key_frame_file))
311 logging.debug('last key frame image shape: %s', np_image.shape)
312
313 return np_image
314
315
Shuzhen Wang9140e452024-08-01 13:41:52 -0700316def get_avg_frame_rate(video_file_name_with_path):
Jonathan Liu76f51682022-11-12 00:27:47 +0000317 """Get average frame rate assuming variable frame rate video.
318
319 Args:
320 video_file_name_with_path: path to the video to be analyzed
321 Returns:
322 Float. average frames per second.
323 """
324
Jonathan Liufb3f6032023-08-21 11:02:40 -0700325 cmd = ['ffprobe',
326 '-v',
327 'quiet',
328 '-show_streams',
329 '-select_streams',
330 'v:0', # first video stream
331 video_file_name_with_path
Jonathan Liu76f51682022-11-12 00:27:47 +0000332 ]
333 logging.debug('Getting frame rate')
334 raw_output = ''
335 try:
Jonathan Liu98b99812023-07-14 13:20:38 -0700336 raw_output = subprocess.check_output(cmd,
337 stdin=subprocess.DEVNULL,
338 stderr=subprocess.STDOUT)
Jonathan Liu76f51682022-11-12 00:27:47 +0000339 except subprocess.CalledProcessError as e:
340 raise AssertionError(str(e.output)) from e
341 if raw_output:
342 output = str(raw_output.decode('utf-8')).strip()
Jonathan Liufb3f6032023-08-21 11:02:40 -0700343 logging.debug('ffprobe command %s output: %s', ' '.join(cmd), output)
344 average_frame_rate_data = (
Kailiang Chen6526b3a2024-01-25 11:28:58 -0800345 re.search(r'avg_frame_rate=*([0-9]+/[0-9]+)', output)
346 .group(INDEX_FIRST_SUBGROUP)
Jonathan Liufb3f6032023-08-21 11:02:40 -0700347 )
348 average_frame_rate = (int(average_frame_rate_data.split('/')[0]) /
349 int(average_frame_rate_data.split('/')[1]))
350 logging.debug('Average FPS: %.4f', average_frame_rate)
351 return average_frame_rate
Jonathan Liu76f51682022-11-12 00:27:47 +0000352 else:
Jonathan Liufb3f6032023-08-21 11:02:40 -0700353 raise AssertionError('ffprobe failed to provide frame rate data')
Jonathan Liu76f51682022-11-12 00:27:47 +0000354
355
356def get_frame_deltas(video_file_name_with_path, timestamp_type='pts'):
357 """Get list of time diffs between frames.
358
359 Args:
360 video_file_name_with_path: path to the video to be analyzed
361 timestamp_type: 'pts' or 'dts'
362 Returns:
363 List of floats. Time diffs between frames in seconds.
364 """
365
366 cmd = ['ffprobe',
367 '-show_entries',
368 f'frame=pkt_{timestamp_type}_time',
369 '-select_streams',
370 'v',
371 video_file_name_with_path
372 ]
373 logging.debug('Getting frame deltas')
374 raw_output = ''
375 try:
Jonathan Liu98b99812023-07-14 13:20:38 -0700376 raw_output = subprocess.check_output(cmd,
377 stdin=subprocess.DEVNULL,
378 stderr=subprocess.STDOUT)
Jonathan Liu76f51682022-11-12 00:27:47 +0000379 except subprocess.CalledProcessError as e:
380 raise AssertionError(str(e.output)) from e
381 if raw_output:
382 output = str(raw_output.decode('utf-8')).strip().split('\n')
383 deltas = []
Jonathan Liu61b36cf2023-06-14 10:30:06 -0700384 prev_time = None
Jonathan Liu76f51682022-11-12 00:27:47 +0000385 for line in output:
386 if timestamp_type not in line:
387 continue
Kailiang Chen6526b3a2024-01-25 11:28:58 -0800388 curr_time = float(re.search(r'time= *([0-9][0-9\.]*)', line)
389 .group(INDEX_FIRST_SUBGROUP))
Jonathan Liu61b36cf2023-06-14 10:30:06 -0700390 if prev_time is not None:
391 deltas.append(curr_time - prev_time)
Jonathan Liu76f51682022-11-12 00:27:47 +0000392 prev_time = curr_time
393 logging.debug('Frame deltas: %s', deltas)
394 return deltas
395 else:
396 raise AssertionError('ffprobe failed to provide frame delta data')
Kailiang Chen6526b3a2024-01-25 11:28:58 -0800397
398
399def get_video_colorspace(log_path, video_file_name):
400 """Get the video colorspace.
401
402 Args:
403 log_path: path for video file directory
404 video_file_name: name of the video file
405 Returns:
406 video colorspace, e.g. BT.2020 or BT.709
407 """
408
409 cmd = ['ffprobe',
410 '-show_streams',
411 '-select_streams',
412 'v:0',
413 '-of',
414 'json',
415 '-i',
416 os.path.join(log_path, video_file_name)
417 ]
418 logging.debug('Get the video colorspace')
419 raw_output = ''
420 try:
421 raw_output = subprocess.check_output(cmd,
422 stdin=subprocess.DEVNULL,
423 stderr=subprocess.STDOUT)
424 except subprocess.CalledProcessError as e:
425 raise AssertionError(str(e.output)) from e
426
427 logging.debug('raw_output: %s', raw_output)
428 if raw_output:
429 colorspace = ''
430 output = str(raw_output.decode('utf-8')).strip().split('\n')
431 logging.debug('output: %s', output)
432 for line in output:
433 logging.debug('line: %s', line)
434 metadata = re.search(r'"color_space": ("[a-z0-9]*")', line)
435 if metadata:
436 colorspace = metadata.group(INDEX_FIRST_SUBGROUP)
437 logging.debug('Colorspace: %s', colorspace)
438 return colorspace
439 else:
440 raise AssertionError('ffprobe failed to provide color space')