• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 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.
19import logging
20import os.path
21import re
22import subprocess
23import error_util
24
25
26HR_TO_SEC = 3600
27MIN_TO_SEC = 60
28
29ITS_SUPPORTED_QUALITIES = (
30    'HIGH',
31    '2160P',
32    '1080P',
33    '720P',
34    '480P',
35    'CIF',
36    'QCIF',
37    'QVGA',
38    'LOW',
39    'VGA'
40)
41
42LOW_RESOLUTION_SIZES = (
43    '176x144',
44    '192x144',
45    '352x288',
46    '384x288',
47    '320x240',
48)
49
50LOWEST_RES_TESTED_AREA = 640*360
51
52
53VIDEO_QUALITY_SIZE = {
54    # '480P', '1080P', HIGH' and 'LOW' are not included as they are DUT-dependent
55    '2160P': '3840x2160',
56    '720P': '1280x720',
57    '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
109def log_ffmpeg_version():
110  """Logs the ffmpeg version being used."""
111
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')
118  logging.debug('ffmpeg version: %s', decoded_output.split(' ')[2])
119
120
121def extract_key_frames_from_video(log_path, video_file_name):
122  """Returns a list of extracted key frames.
123
124  Ffmpeg tool is used to extract key frames from the video at path
125  os.path.join(log_path, video_file_name).
126  The extracted key frames will have the name video_file_name with "_key_frame"
127  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
132  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
136  Args:
137    log_path: path for video file directory
138    video_file_name: name of the video file.
139  Returns:
140    key_frame_files: A list of paths for each key frame extracted from the
141    video. Ex: VID_20220325_050918_0_CIF_352x288.mp4
142  """
143  ffmpeg_image_name = f"{video_file_name.split('.')[0]}_key_frame"
144  ffmpeg_image_file_path = os.path.join(
145      log_path, ffmpeg_image_name + '_%02d.png')
146  cmd = ['ffmpeg',
147         '-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,
156         '-loglevel',
157         'quiet',
158        ]
159  logging.debug('Extracting key frames from: %s', video_file_name)
160  _ = subprocess.call(cmd,
161                      stdin=subprocess.DEVNULL,
162                      stdout=subprocess.DEVNULL,
163                      stderr=subprocess.DEVNULL)
164  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)
169
170  logging.debug('Extracted key frames: %s', key_frame_files)
171  logging.debug('Length of key_frame_files: %d', len(key_frame_files))
172  if not key_frame_files:
173    raise AssertionError('No key frames extracted. Check source video.')
174
175  return key_frame_files
176
177
178def get_key_frame_to_process(key_frame_files):
179  """Returns the key frame file from the list of key_frame_files.
180
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  """
189  if not key_frame_files:
190    raise AssertionError('key_frame_files list is empty.')
191  key_frame_files.sort()
192  return key_frame_files[-1]
193
194
195def extract_all_frames_from_video(log_path, video_file_name, img_format):
196  """Extracts and returns a list of all extracted frames.
197
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
205  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
209  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),
224      '-vsync', 'vfr', # force ffmpeg to use video fps instead of inferred fps
225      ffmpeg_image_file_names, '-loglevel', 'quiet'
226  ]
227  _ = subprocess.call(cmd,
228                      stdin=subprocess.DEVNULL,
229                      stdout=subprocess.DEVNULL,
230                      stderr=subprocess.DEVNULL)
231
232  file_list = sorted(
233      [_ for _ in os.listdir(log_path) if (_.endswith(img_format)
234                                           and ffmpeg_image_name in _)])
235  if not file_list:
236    raise AssertionError('No frames extracted. Check source video.')
237
238  return file_list
239
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 = ['ffprobe',
251         '-v',
252         'quiet',
253         '-show_streams',
254         '-select_streams',
255         'v:0',  # first video stream
256         video_file_name_with_path
257        ]
258  logging.debug('Getting frame rate')
259  raw_output = ''
260  try:
261    raw_output = subprocess.check_output(cmd,
262                                         stdin=subprocess.DEVNULL,
263                                         stderr=subprocess.STDOUT)
264  except subprocess.CalledProcessError as e:
265    raise AssertionError(str(e.output)) from e
266  if raw_output:
267    output = str(raw_output.decode('utf-8')).strip()
268    logging.debug('ffprobe command %s output: %s', ' '.join(cmd), output)
269    average_frame_rate_data = (
270        re.search(r'avg_frame_rate=*([0-9]+/[0-9]+)', output).group(1)
271    )
272    average_frame_rate = (int(average_frame_rate_data.split('/')[0]) /
273                          int(average_frame_rate_data.split('/')[1]))
274    logging.debug('Average FPS: %.4f', average_frame_rate)
275    return average_frame_rate
276  else:
277    raise AssertionError('ffprobe failed to provide frame rate data')
278
279
280def get_frame_deltas(video_file_name_with_path, timestamp_type='pts'):
281  """Get list of time diffs between frames.
282
283  Args:
284    video_file_name_with_path: path to the video to be analyzed
285    timestamp_type: 'pts' or 'dts'
286  Returns:
287    List of floats. Time diffs between frames in seconds.
288  """
289
290  cmd = ['ffprobe',
291         '-show_entries',
292         f'frame=pkt_{timestamp_type}_time',
293         '-select_streams',
294         'v',
295         video_file_name_with_path
296         ]
297  logging.debug('Getting frame deltas')
298  raw_output = ''
299  try:
300    raw_output = subprocess.check_output(cmd,
301                                         stdin=subprocess.DEVNULL,
302                                         stderr=subprocess.STDOUT)
303  except subprocess.CalledProcessError as e:
304    raise AssertionError(str(e.output)) from e
305  if raw_output:
306    output = str(raw_output.decode('utf-8')).strip().split('\n')
307    deltas = []
308    prev_time = None
309    for line in output:
310      if timestamp_type not in line:
311        continue
312      curr_time = float(re.search(r'time= *([0-9][0-9\.]*)', line).group(1))
313      if prev_time is not None:
314        deltas.append(curr_time - prev_time)
315      prev_time = curr_time
316    logging.debug('Frame deltas: %s', deltas)
317    return deltas
318  else:
319    raise AssertionError('ffprobe failed to provide frame delta data')
320