blob: d6caf18eb261d5da5fa95dd9f594828e162e4089 [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
leslieshaw21c49e512023-08-25 13:47:28 -070024import image_processing_utils
Rucha Katakwarb6847db2022-03-24 16:49:13 -070025
26
Jonathan Liu76f51682022-11-12 00:27:47 +000027HR_TO_SEC = 3600
28MIN_TO_SEC = 60
29
Rucha Katakwar089e6092022-03-30 11:19:26 -070030ITS_SUPPORTED_QUALITIES = (
Rucha Katakwar41133f92022-03-22 13:44:09 -070031 'HIGH',
32 '2160P',
33 '1080P',
34 '720P',
35 '480P',
36 'CIF',
37 'QCIF',
38 'QVGA',
39 'LOW',
40 'VGA'
Rucha Katakwar222c5312022-03-21 14:17:01 -070041)
Rucha Katakwar0ced2c32022-08-01 14:54:42 -070042
leslieshaw6fb2c072023-04-06 16:38:54 -070043LOW_RESOLUTION_SIZES = (
44 '176x144',
45 '192x144',
46 '352x288',
47 '384x288',
48 '320x240',
49)
Rucha Katakwarb6847db2022-03-24 16:49:13 -070050
leslieshaw6fb2c072023-04-06 16:38:54 -070051LOWEST_RES_TESTED_AREA = 640*360
leslieshaw99fd75b2023-01-09 15:01:19 -080052
leslieshaw541e2972023-02-28 11:06:07 -080053VIDEO_QUALITY_SIZE = {
Clemenz Portmann703010e2023-07-21 16:39:18 -070054 # '480P', '1080P', HIGH' & '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),
Clemenz Portmann714b9302023-06-30 13:53:53 -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
leslieshaw21c49e512023-08-25 13:47:28 -0700241def extract_last_key_frame_from_recording(log_path, file_name):
242 """Extract key frames from recordings.
243
244 Args:
245 log_path: str; file location
246 file_name: str file name for saved video
247
248 Returns:
Clemenz Portmann25529eb2023-09-17 21:13:15 -0700249 numpy image of last key frame
leslieshaw21c49e512023-08-25 13:47:28 -0700250 """
251 key_frame_files = extract_key_frames_from_video(log_path, file_name)
252 logging.debug('key_frame_files: %s', key_frame_files)
253
254 # Get the last_key_frame file to process.
255 last_key_frame_file = get_key_frame_to_process(key_frame_files)
256 logging.debug('last_key_frame: %s', last_key_frame_file)
257
Clemenz Portmann25529eb2023-09-17 21:13:15 -0700258 # Convert last_key_frame to numpy array
leslieshaw21c49e512023-08-25 13:47:28 -0700259 np_image = image_processing_utils.convert_image_to_numpy_array(
260 os.path.join(log_path, last_key_frame_file))
261 logging.debug('last key frame image shape: %s', np_image.shape)
262
263 return np_image
264
265
Jonathan Liu76f51682022-11-12 00:27:47 +0000266def get_average_frame_rate(video_file_name_with_path):
267 """Get average frame rate assuming variable frame rate video.
268
269 Args:
270 video_file_name_with_path: path to the video to be analyzed
271 Returns:
272 Float. average frames per second.
273 """
274
Jonathan Liufb3f6032023-08-21 11:02:40 -0700275 cmd = ['ffprobe',
276 '-v',
277 'quiet',
278 '-show_streams',
279 '-select_streams',
280 'v:0', # first video stream
281 video_file_name_with_path
Jonathan Liu76f51682022-11-12 00:27:47 +0000282 ]
283 logging.debug('Getting frame rate')
284 raw_output = ''
285 try:
Jonathan Liu98b99812023-07-14 13:20:38 -0700286 raw_output = subprocess.check_output(cmd,
287 stdin=subprocess.DEVNULL,
288 stderr=subprocess.STDOUT)
Jonathan Liu76f51682022-11-12 00:27:47 +0000289 except subprocess.CalledProcessError as e:
290 raise AssertionError(str(e.output)) from e
291 if raw_output:
292 output = str(raw_output.decode('utf-8')).strip()
Jonathan Liufb3f6032023-08-21 11:02:40 -0700293 logging.debug('ffprobe command %s output: %s', ' '.join(cmd), output)
294 average_frame_rate_data = (
295 re.search(r'avg_frame_rate=*([0-9]+/[0-9]+)', output).group(1)
296 )
297 average_frame_rate = (int(average_frame_rate_data.split('/')[0]) /
298 int(average_frame_rate_data.split('/')[1]))
299 logging.debug('Average FPS: %.4f', average_frame_rate)
300 return average_frame_rate
Jonathan Liu76f51682022-11-12 00:27:47 +0000301 else:
Jonathan Liufb3f6032023-08-21 11:02:40 -0700302 raise AssertionError('ffprobe failed to provide frame rate data')
Jonathan Liu76f51682022-11-12 00:27:47 +0000303
304
305def get_frame_deltas(video_file_name_with_path, timestamp_type='pts'):
306 """Get list of time diffs between frames.
307
308 Args:
309 video_file_name_with_path: path to the video to be analyzed
310 timestamp_type: 'pts' or 'dts'
311 Returns:
312 List of floats. Time diffs between frames in seconds.
313 """
314
315 cmd = ['ffprobe',
316 '-show_entries',
317 f'frame=pkt_{timestamp_type}_time',
318 '-select_streams',
319 'v',
320 video_file_name_with_path
321 ]
322 logging.debug('Getting frame deltas')
323 raw_output = ''
324 try:
Jonathan Liu98b99812023-07-14 13:20:38 -0700325 raw_output = subprocess.check_output(cmd,
326 stdin=subprocess.DEVNULL,
327 stderr=subprocess.STDOUT)
Jonathan Liu76f51682022-11-12 00:27:47 +0000328 except subprocess.CalledProcessError as e:
329 raise AssertionError(str(e.output)) from e
330 if raw_output:
331 output = str(raw_output.decode('utf-8')).strip().split('\n')
332 deltas = []
Jonathan Liu61b36cf2023-06-14 10:30:06 -0700333 prev_time = None
Jonathan Liu76f51682022-11-12 00:27:47 +0000334 for line in output:
335 if timestamp_type not in line:
336 continue
337 curr_time = float(re.search(r'time= *([0-9][0-9\.]*)', line).group(1))
Jonathan Liu61b36cf2023-06-14 10:30:06 -0700338 if prev_time is not None:
339 deltas.append(curr_time - prev_time)
Jonathan Liu76f51682022-11-12 00:27:47 +0000340 prev_time = curr_time
341 logging.debug('Frame deltas: %s', deltas)
342 return deltas
343 else:
344 raise AssertionError('ffprobe failed to provide frame delta data')