xref: /aosp_15_r20/cts/apps/CameraITS/utils/video_processing_utils.py (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1*b7c941bbSAndroid Build Coastguard Worker# Copyright 2022 The Android Open Source Project
2*b7c941bbSAndroid Build Coastguard Worker#
3*b7c941bbSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
4*b7c941bbSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
5*b7c941bbSAndroid Build Coastguard Worker# You may obtain a copy of the License at
6*b7c941bbSAndroid Build Coastguard Worker#
7*b7c941bbSAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
8*b7c941bbSAndroid Build Coastguard Worker#
9*b7c941bbSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
10*b7c941bbSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
11*b7c941bbSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*b7c941bbSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
13*b7c941bbSAndroid Build Coastguard Worker# limitations under the License.
14*b7c941bbSAndroid Build Coastguard Worker"""Utility functions for processing video recordings.
15*b7c941bbSAndroid Build Coastguard Worker"""
16*b7c941bbSAndroid Build Coastguard Worker# Each item in this list corresponds to quality levels defined per
17*b7c941bbSAndroid Build Coastguard Worker# CamcorderProfile. For Video ITS, we will currently test below qualities
18*b7c941bbSAndroid Build Coastguard Worker# only if supported by the camera device.
19*b7c941bbSAndroid Build Coastguard Worker
20*b7c941bbSAndroid Build Coastguard Worker
21*b7c941bbSAndroid Build Coastguard Workerimport dataclasses
22*b7c941bbSAndroid Build Coastguard Workerimport logging
23*b7c941bbSAndroid Build Coastguard Workerimport math
24*b7c941bbSAndroid Build Coastguard Workerimport os.path
25*b7c941bbSAndroid Build Coastguard Workerimport re
26*b7c941bbSAndroid Build Coastguard Workerimport subprocess
27*b7c941bbSAndroid Build Coastguard Workerimport error_util
28*b7c941bbSAndroid Build Coastguard Workerimport image_processing_utils
29*b7c941bbSAndroid Build Coastguard Worker
30*b7c941bbSAndroid Build Coastguard Worker
31*b7c941bbSAndroid Build Coastguard WorkerCOLORSPACE_HDR = 'bt2020'
32*b7c941bbSAndroid Build Coastguard WorkerHR_TO_SEC = 3600
33*b7c941bbSAndroid Build Coastguard WorkerINDEX_FIRST_SUBGROUP = 1
34*b7c941bbSAndroid Build Coastguard WorkerMIN_TO_SEC = 60
35*b7c941bbSAndroid Build Coastguard Worker
36*b7c941bbSAndroid Build Coastguard WorkerITS_SUPPORTED_QUALITIES = (
37*b7c941bbSAndroid Build Coastguard Worker    'HIGH',
38*b7c941bbSAndroid Build Coastguard Worker    '2160P',
39*b7c941bbSAndroid Build Coastguard Worker    '1080P',
40*b7c941bbSAndroid Build Coastguard Worker    '720P',
41*b7c941bbSAndroid Build Coastguard Worker    '480P',
42*b7c941bbSAndroid Build Coastguard Worker    'CIF',
43*b7c941bbSAndroid Build Coastguard Worker    'QCIF',
44*b7c941bbSAndroid Build Coastguard Worker    'QVGA',
45*b7c941bbSAndroid Build Coastguard Worker    'LOW',
46*b7c941bbSAndroid Build Coastguard Worker    'VGA'
47*b7c941bbSAndroid Build Coastguard Worker)
48*b7c941bbSAndroid Build Coastguard Worker
49*b7c941bbSAndroid Build Coastguard WorkerLOW_RESOLUTION_SIZES = (
50*b7c941bbSAndroid Build Coastguard Worker    '176x144',
51*b7c941bbSAndroid Build Coastguard Worker    '192x144',
52*b7c941bbSAndroid Build Coastguard Worker    '352x288',
53*b7c941bbSAndroid Build Coastguard Worker    '384x288',
54*b7c941bbSAndroid Build Coastguard Worker    '320x240',
55*b7c941bbSAndroid Build Coastguard Worker)
56*b7c941bbSAndroid Build Coastguard Worker
57*b7c941bbSAndroid Build Coastguard WorkerLOWEST_RES_TESTED_AREA = 640*360
58*b7c941bbSAndroid Build Coastguard Worker
59*b7c941bbSAndroid Build Coastguard WorkerVIDEO_QUALITY_SIZE = {
60*b7c941bbSAndroid Build Coastguard Worker    # '480P', '1080P', HIGH' & 'LOW' are not included as they are DUT-dependent
61*b7c941bbSAndroid Build Coastguard Worker    '2160P': '3840x2160',
62*b7c941bbSAndroid Build Coastguard Worker    '720P': '1280x720',
63*b7c941bbSAndroid Build Coastguard Worker    'VGA': '640x480',
64*b7c941bbSAndroid Build Coastguard Worker    'CIF': '352x288',
65*b7c941bbSAndroid Build Coastguard Worker    'QVGA': '320x240',
66*b7c941bbSAndroid Build Coastguard Worker    'QCIF': '176x144',
67*b7c941bbSAndroid Build Coastguard Worker}
68*b7c941bbSAndroid Build Coastguard Worker
69*b7c941bbSAndroid Build Coastguard Worker
70*b7c941bbSAndroid Build Coastguard Worker@dataclasses.dataclass
71*b7c941bbSAndroid Build Coastguard Workerclass CommonPreviewSizeData:
72*b7c941bbSAndroid Build Coastguard Worker  """Class to store smallest and largest common sizes of preview and video."""
73*b7c941bbSAndroid Build Coastguard Worker  smallest_size: str
74*b7c941bbSAndroid Build Coastguard Worker  smallest_quality: str
75*b7c941bbSAndroid Build Coastguard Worker  largest_size: str
76*b7c941bbSAndroid Build Coastguard Worker  largest_quality: str
77*b7c941bbSAndroid Build Coastguard Worker
78*b7c941bbSAndroid Build Coastguard Worker
79*b7c941bbSAndroid Build Coastguard Workerdef get_preview_video_sizes_union(cam, camera_id, min_area=0):
80*b7c941bbSAndroid Build Coastguard Worker  """Returns largest and smallest common size and quality of preview and video.
81*b7c941bbSAndroid Build Coastguard Worker
82*b7c941bbSAndroid Build Coastguard Worker  Args:
83*b7c941bbSAndroid Build Coastguard Worker    cam: camera object.
84*b7c941bbSAndroid Build Coastguard Worker    camera_id: str; camera ID.
85*b7c941bbSAndroid Build Coastguard Worker    min_area: int; Optional filter to eliminate smaller sizes (ex. 640*480).
86*b7c941bbSAndroid Build Coastguard Worker
87*b7c941bbSAndroid Build Coastguard Worker  Returns:
88*b7c941bbSAndroid Build Coastguard Worker    common_size_quality, CommonPreviewSizeData class
89*b7c941bbSAndroid Build Coastguard Worker  """
90*b7c941bbSAndroid Build Coastguard Worker  supported_preview_sizes = set(cam.get_all_supported_preview_sizes(camera_id))
91*b7c941bbSAndroid Build Coastguard Worker  supported_video_qualities = cam.get_supported_video_qualities(camera_id)
92*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Supported video profiles & IDs: %s', supported_video_qualities)
93*b7c941bbSAndroid Build Coastguard Worker
94*b7c941bbSAndroid Build Coastguard Worker  # Make dictionary on video quality and size according to compatibility
95*b7c941bbSAndroid Build Coastguard Worker  supported_video_size_to_quality = {}
96*b7c941bbSAndroid Build Coastguard Worker  for quality in supported_video_qualities:
97*b7c941bbSAndroid Build Coastguard Worker    video_quality = quality.split(':')[0]
98*b7c941bbSAndroid Build Coastguard Worker    if video_quality in VIDEO_QUALITY_SIZE:
99*b7c941bbSAndroid Build Coastguard Worker      video_size = VIDEO_QUALITY_SIZE[video_quality]
100*b7c941bbSAndroid Build Coastguard Worker      supported_video_size_to_quality[video_size] = video_quality
101*b7c941bbSAndroid Build Coastguard Worker  logging.debug(
102*b7c941bbSAndroid Build Coastguard Worker      'Supported video size to quality: %s', supported_video_size_to_quality
103*b7c941bbSAndroid Build Coastguard Worker  )
104*b7c941bbSAndroid Build Coastguard Worker  # Find the intersection of supported preview sizes and video sizes
105*b7c941bbSAndroid Build Coastguard Worker  common_sizes = supported_preview_sizes.intersection(
106*b7c941bbSAndroid Build Coastguard Worker      supported_video_size_to_quality.keys()
107*b7c941bbSAndroid Build Coastguard Worker  )
108*b7c941bbSAndroid Build Coastguard Worker  if not common_sizes:
109*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('No common size between Preview and Video!')
110*b7c941bbSAndroid Build Coastguard Worker  # Filter common sizes based on min_area
111*b7c941bbSAndroid Build Coastguard Worker  size_to_area = lambda s: int(s.split('x')[0])*int(s.split('x')[1])
112*b7c941bbSAndroid Build Coastguard Worker  common_sizes = (
113*b7c941bbSAndroid Build Coastguard Worker      [size for size in common_sizes if size_to_area(size) >= min_area]
114*b7c941bbSAndroid Build Coastguard Worker  )
115*b7c941bbSAndroid Build Coastguard Worker  if not common_sizes:
116*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(
117*b7c941bbSAndroid Build Coastguard Worker        'No common size above min_area between Preview and Video!'
118*b7c941bbSAndroid Build Coastguard Worker    )
119*b7c941bbSAndroid Build Coastguard Worker  # Use areas of video sizes to find the smallest and largest common size
120*b7c941bbSAndroid Build Coastguard Worker  smallest_common_size = min(common_sizes, key=size_to_area)
121*b7c941bbSAndroid Build Coastguard Worker  largest_common_size = max(common_sizes, key=size_to_area)
122*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Smallest common size: %s', smallest_common_size)
123*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Largest common size: %s', largest_common_size)
124*b7c941bbSAndroid Build Coastguard Worker  # Find video quality of resolution with resolution as key
125*b7c941bbSAndroid Build Coastguard Worker  smallest_common_quality = (
126*b7c941bbSAndroid Build Coastguard Worker      supported_video_size_to_quality[smallest_common_size]
127*b7c941bbSAndroid Build Coastguard Worker  )
128*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Smallest common quality: %s', smallest_common_quality)
129*b7c941bbSAndroid Build Coastguard Worker  largest_common_quality = supported_video_size_to_quality[largest_common_size]
130*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Largest common quality: %s', largest_common_quality)
131*b7c941bbSAndroid Build Coastguard Worker  common_size_quality = CommonPreviewSizeData(
132*b7c941bbSAndroid Build Coastguard Worker      smallest_size=smallest_common_size,
133*b7c941bbSAndroid Build Coastguard Worker      smallest_quality=smallest_common_quality,
134*b7c941bbSAndroid Build Coastguard Worker      largest_size=largest_common_size,
135*b7c941bbSAndroid Build Coastguard Worker      largest_quality=largest_common_quality
136*b7c941bbSAndroid Build Coastguard Worker  )
137*b7c941bbSAndroid Build Coastguard Worker  return common_size_quality
138*b7c941bbSAndroid Build Coastguard Worker
139*b7c941bbSAndroid Build Coastguard Worker
140*b7c941bbSAndroid Build Coastguard Workerdef clamp_preview_sizes(preview_sizes, min_area=0, max_area=math.inf):
141*b7c941bbSAndroid Build Coastguard Worker  """Returns a list of preview_sizes with areas between min/max_area.
142*b7c941bbSAndroid Build Coastguard Worker
143*b7c941bbSAndroid Build Coastguard Worker  Args:
144*b7c941bbSAndroid Build Coastguard Worker    preview_sizes: list; sizes to be filtered (ex. "1280x720")
145*b7c941bbSAndroid Build Coastguard Worker    min_area: int; optional filter to eliminate sizes <= to the specified
146*b7c941bbSAndroid Build Coastguard Worker        area (ex. 640*480).
147*b7c941bbSAndroid Build Coastguard Worker    max_area: int; optional filter to eliminate sizes >= to the specified
148*b7c941bbSAndroid Build Coastguard Worker        area (ex. 3840*2160).
149*b7c941bbSAndroid Build Coastguard Worker  Returns:
150*b7c941bbSAndroid Build Coastguard Worker    preview_sizes: list; filtered preview sizes clamped by min/max_area.
151*b7c941bbSAndroid Build Coastguard Worker  """
152*b7c941bbSAndroid Build Coastguard Worker  size_to_area = lambda size: int(size.split('x')[0])*int(size.split('x')[1])
153*b7c941bbSAndroid Build Coastguard Worker  filtered_preview_sizes = [
154*b7c941bbSAndroid Build Coastguard Worker      size for size in preview_sizes
155*b7c941bbSAndroid Build Coastguard Worker      if max_area >= size_to_area(size) >= min_area]
156*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Filtered preview sizes: %s', filtered_preview_sizes)
157*b7c941bbSAndroid Build Coastguard Worker  if not filtered_preview_sizes:
158*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(f'No preview sizes between {min_area} and {max_area}')
159*b7c941bbSAndroid Build Coastguard Worker  return filtered_preview_sizes
160*b7c941bbSAndroid Build Coastguard Worker
161*b7c941bbSAndroid Build Coastguard Worker
162*b7c941bbSAndroid Build Coastguard Workerdef log_ffmpeg_version():
163*b7c941bbSAndroid Build Coastguard Worker  """Logs the ffmpeg version being used."""
164*b7c941bbSAndroid Build Coastguard Worker
165*b7c941bbSAndroid Build Coastguard Worker  ffmpeg_version_cmd = ('ffmpeg -version')
166*b7c941bbSAndroid Build Coastguard Worker  p = subprocess.Popen(ffmpeg_version_cmd, shell=True, stdout=subprocess.PIPE)
167*b7c941bbSAndroid Build Coastguard Worker  output, _ = p.communicate()
168*b7c941bbSAndroid Build Coastguard Worker  if p.poll() != 0:
169*b7c941bbSAndroid Build Coastguard Worker    raise error_util.CameraItsError('Error running ffmpeg version cmd.')
170*b7c941bbSAndroid Build Coastguard Worker  decoded_output = output.decode('utf-8')
171*b7c941bbSAndroid Build Coastguard Worker  logging.debug('ffmpeg version: %s', decoded_output.split(' ')[2])
172*b7c941bbSAndroid Build Coastguard Worker
173*b7c941bbSAndroid Build Coastguard Worker
174*b7c941bbSAndroid Build Coastguard Workerdef extract_key_frames_from_video(log_path, video_file_name):
175*b7c941bbSAndroid Build Coastguard Worker  """Returns a list of extracted key frames.
176*b7c941bbSAndroid Build Coastguard Worker
177*b7c941bbSAndroid Build Coastguard Worker  Ffmpeg tool is used to extract key frames from the video at path
178*b7c941bbSAndroid Build Coastguard Worker  os.path.join(log_path, video_file_name).
179*b7c941bbSAndroid Build Coastguard Worker  The extracted key frames will have the name video_file_name with "_key_frame"
180*b7c941bbSAndroid Build Coastguard Worker  suffix to identify the frames for video of each quality. Since there can be
181*b7c941bbSAndroid Build Coastguard Worker  multiple key frames, each key frame image will be differentiated with its
182*b7c941bbSAndroid Build Coastguard Worker  frame index. All the extracted key frames will be available in jpeg format
183*b7c941bbSAndroid Build Coastguard Worker  at the same path as the video file.
184*b7c941bbSAndroid Build Coastguard Worker
185*b7c941bbSAndroid Build Coastguard Worker  The run time flag '-loglevel quiet' hides the information from terminal.
186*b7c941bbSAndroid Build Coastguard Worker  In order to see the detailed output of ffmpeg command change the loglevel
187*b7c941bbSAndroid Build Coastguard Worker  option to 'info'.
188*b7c941bbSAndroid Build Coastguard Worker
189*b7c941bbSAndroid Build Coastguard Worker  Args:
190*b7c941bbSAndroid Build Coastguard Worker    log_path: path for video file directory.
191*b7c941bbSAndroid Build Coastguard Worker    video_file_name: name of the video file.
192*b7c941bbSAndroid Build Coastguard Worker  Returns:
193*b7c941bbSAndroid Build Coastguard Worker    key_frame_files: a sorted list of files which contains a name per key
194*b7c941bbSAndroid Build Coastguard Worker      frame. Ex: VID_20220325_050918_0_preview_1920x1440_key_frame_0001.png
195*b7c941bbSAndroid Build Coastguard Worker  """
196*b7c941bbSAndroid Build Coastguard Worker  ffmpeg_image_name = f'{os.path.splitext(video_file_name)[0]}_key_frame'
197*b7c941bbSAndroid Build Coastguard Worker  ffmpeg_image_file_path = os.path.join(
198*b7c941bbSAndroid Build Coastguard Worker      log_path, ffmpeg_image_name + '_%04d.png')
199*b7c941bbSAndroid Build Coastguard Worker  cmd = ['ffmpeg',
200*b7c941bbSAndroid Build Coastguard Worker         '-skip_frame',
201*b7c941bbSAndroid Build Coastguard Worker         'nokey',
202*b7c941bbSAndroid Build Coastguard Worker         '-i',
203*b7c941bbSAndroid Build Coastguard Worker         os.path.join(log_path, video_file_name),
204*b7c941bbSAndroid Build Coastguard Worker         '-vsync',
205*b7c941bbSAndroid Build Coastguard Worker         'vfr',
206*b7c941bbSAndroid Build Coastguard Worker         '-frame_pts',
207*b7c941bbSAndroid Build Coastguard Worker         'true',
208*b7c941bbSAndroid Build Coastguard Worker         ffmpeg_image_file_path,
209*b7c941bbSAndroid Build Coastguard Worker         '-loglevel',
210*b7c941bbSAndroid Build Coastguard Worker         'quiet',
211*b7c941bbSAndroid Build Coastguard Worker        ]
212*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Extracting key frames from: %s', video_file_name)
213*b7c941bbSAndroid Build Coastguard Worker  _ = subprocess.call(cmd,
214*b7c941bbSAndroid Build Coastguard Worker                      stdin=subprocess.DEVNULL,
215*b7c941bbSAndroid Build Coastguard Worker                      stdout=subprocess.DEVNULL,
216*b7c941bbSAndroid Build Coastguard Worker                      stderr=subprocess.DEVNULL)
217*b7c941bbSAndroid Build Coastguard Worker  arr = os.listdir(os.path.join(log_path))
218*b7c941bbSAndroid Build Coastguard Worker  key_frame_files = []
219*b7c941bbSAndroid Build Coastguard Worker  for file in arr:
220*b7c941bbSAndroid Build Coastguard Worker    if '.png' in file and not os.path.isdir(file) and ffmpeg_image_name in file:
221*b7c941bbSAndroid Build Coastguard Worker      key_frame_files.append(file)
222*b7c941bbSAndroid Build Coastguard Worker  key_frame_files.sort()
223*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Extracted key frames: %s', key_frame_files)
224*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Length of key_frame_files: %d', len(key_frame_files))
225*b7c941bbSAndroid Build Coastguard Worker  if not key_frame_files:
226*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('No key frames extracted. Check source video.')
227*b7c941bbSAndroid Build Coastguard Worker
228*b7c941bbSAndroid Build Coastguard Worker  return key_frame_files
229*b7c941bbSAndroid Build Coastguard Worker
230*b7c941bbSAndroid Build Coastguard Worker
231*b7c941bbSAndroid Build Coastguard Workerdef get_key_frame_to_process(key_frame_files):
232*b7c941bbSAndroid Build Coastguard Worker  """Returns the key frame file from the list of key_frame_files.
233*b7c941bbSAndroid Build Coastguard Worker
234*b7c941bbSAndroid Build Coastguard Worker  If the size of the list is 1 then the file in the list will be returned else
235*b7c941bbSAndroid Build Coastguard Worker  the file with highest frame_index will be returned for further processing.
236*b7c941bbSAndroid Build Coastguard Worker
237*b7c941bbSAndroid Build Coastguard Worker  Args:
238*b7c941bbSAndroid Build Coastguard Worker    key_frame_files: A list of key frame files.
239*b7c941bbSAndroid Build Coastguard Worker  Returns:
240*b7c941bbSAndroid Build Coastguard Worker    key_frame_file to be used for further processing.
241*b7c941bbSAndroid Build Coastguard Worker  """
242*b7c941bbSAndroid Build Coastguard Worker  if not key_frame_files:
243*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('key_frame_files list is empty.')
244*b7c941bbSAndroid Build Coastguard Worker  key_frame_files.sort()
245*b7c941bbSAndroid Build Coastguard Worker  return key_frame_files[-1]
246*b7c941bbSAndroid Build Coastguard Worker
247*b7c941bbSAndroid Build Coastguard Worker
248*b7c941bbSAndroid Build Coastguard Workerdef extract_all_frames_from_video(
249*b7c941bbSAndroid Build Coastguard Worker    log_path, video_file_name, img_format, video_fps=None):
250*b7c941bbSAndroid Build Coastguard Worker  """Extracts and returns a list of frames from a video using FFmpeg.
251*b7c941bbSAndroid Build Coastguard Worker
252*b7c941bbSAndroid Build Coastguard Worker  Extract all frames from the video at path <log_path>/<video_file_name>.
253*b7c941bbSAndroid Build Coastguard Worker  The extracted frames will have the name video_file_name with "_frame"
254*b7c941bbSAndroid Build Coastguard Worker  suffix to identify the frames for video of each size. Each frame image
255*b7c941bbSAndroid Build Coastguard Worker  will be differentiated with its frame index. All extracted rames will be
256*b7c941bbSAndroid Build Coastguard Worker  available in the provided img_format format at the same path as the video.
257*b7c941bbSAndroid Build Coastguard Worker
258*b7c941bbSAndroid Build Coastguard Worker  The run time flag '-loglevel quiet' hides the information from terminal.
259*b7c941bbSAndroid Build Coastguard Worker  In order to see the detailed output of ffmpeg command change the loglevel
260*b7c941bbSAndroid Build Coastguard Worker  option to 'info'.
261*b7c941bbSAndroid Build Coastguard Worker
262*b7c941bbSAndroid Build Coastguard Worker  Args:
263*b7c941bbSAndroid Build Coastguard Worker    log_path: str; directory containing video file.
264*b7c941bbSAndroid Build Coastguard Worker    video_file_name: str; name of the video file.
265*b7c941bbSAndroid Build Coastguard Worker    img_format: str; desired image format for export frames. ex. 'png'
266*b7c941bbSAndroid Build Coastguard Worker    video_fps: str; fps of imported video.
267*b7c941bbSAndroid Build Coastguard Worker  Returns:
268*b7c941bbSAndroid Build Coastguard Worker    an ordered list of paths to the extracted frame images.
269*b7c941bbSAndroid Build Coastguard Worker  """
270*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Extracting all frames')
271*b7c941bbSAndroid Build Coastguard Worker  ffmpeg_image_name = f"{video_file_name.split('.')[0]}_frame"
272*b7c941bbSAndroid Build Coastguard Worker  logging.debug('ffmpeg_image_name: %s', ffmpeg_image_name)
273*b7c941bbSAndroid Build Coastguard Worker  ffmpeg_image_file_names = (
274*b7c941bbSAndroid Build Coastguard Worker      f'{os.path.join(log_path, ffmpeg_image_name)}_%04d.{img_format}')
275*b7c941bbSAndroid Build Coastguard Worker  if video_fps:
276*b7c941bbSAndroid Build Coastguard Worker    cmd = [
277*b7c941bbSAndroid Build Coastguard Worker        'ffmpeg', '-i', os.path.join(log_path, video_file_name),
278*b7c941bbSAndroid Build Coastguard Worker        '-r', video_fps,  # force a constant frame rate for reliability
279*b7c941bbSAndroid Build Coastguard Worker        ffmpeg_image_file_names, '-loglevel', 'quiet'
280*b7c941bbSAndroid Build Coastguard Worker    ]
281*b7c941bbSAndroid Build Coastguard Worker  else:
282*b7c941bbSAndroid Build Coastguard Worker    cmd = [
283*b7c941bbSAndroid Build Coastguard Worker        'ffmpeg', '-i', os.path.join(log_path, video_file_name),
284*b7c941bbSAndroid Build Coastguard Worker        '-vsync', 'passthrough',  # prevents frame drops during decoding
285*b7c941bbSAndroid Build Coastguard Worker        ffmpeg_image_file_names, '-loglevel', 'quiet'
286*b7c941bbSAndroid Build Coastguard Worker    ]
287*b7c941bbSAndroid Build Coastguard Worker  subprocess.call(cmd,
288*b7c941bbSAndroid Build Coastguard Worker                  stdin=subprocess.DEVNULL,
289*b7c941bbSAndroid Build Coastguard Worker                  stdout=subprocess.DEVNULL,
290*b7c941bbSAndroid Build Coastguard Worker                  stderr=subprocess.DEVNULL)
291*b7c941bbSAndroid Build Coastguard Worker
292*b7c941bbSAndroid Build Coastguard Worker  files = sorted(
293*b7c941bbSAndroid Build Coastguard Worker      [file for file in os.listdir(log_path) if
294*b7c941bbSAndroid Build Coastguard Worker       (file.endswith(img_format) and ffmpeg_image_name in file)])
295*b7c941bbSAndroid Build Coastguard Worker  if not files:
296*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('No frames extracted. Check source video.')
297*b7c941bbSAndroid Build Coastguard Worker
298*b7c941bbSAndroid Build Coastguard Worker  return files
299*b7c941bbSAndroid Build Coastguard Worker
300*b7c941bbSAndroid Build Coastguard Worker
301*b7c941bbSAndroid Build Coastguard Workerdef extract_last_key_frame_from_recording(log_path, file_name):
302*b7c941bbSAndroid Build Coastguard Worker  """Extract last key frame from recordings.
303*b7c941bbSAndroid Build Coastguard Worker
304*b7c941bbSAndroid Build Coastguard Worker  Args:
305*b7c941bbSAndroid Build Coastguard Worker    log_path: str; file location
306*b7c941bbSAndroid Build Coastguard Worker    file_name: str file name for saved video
307*b7c941bbSAndroid Build Coastguard Worker
308*b7c941bbSAndroid Build Coastguard Worker  Returns:
309*b7c941bbSAndroid Build Coastguard Worker    numpy image of last key frame
310*b7c941bbSAndroid Build Coastguard Worker  """
311*b7c941bbSAndroid Build Coastguard Worker  key_frame_files = extract_key_frames_from_video(log_path, file_name)
312*b7c941bbSAndroid Build Coastguard Worker  logging.debug('key_frame_files: %s', key_frame_files)
313*b7c941bbSAndroid Build Coastguard Worker
314*b7c941bbSAndroid Build Coastguard Worker  # Get the last_key_frame file to process.
315*b7c941bbSAndroid Build Coastguard Worker  last_key_frame_file = get_key_frame_to_process(key_frame_files)
316*b7c941bbSAndroid Build Coastguard Worker  logging.debug('last_key_frame: %s', last_key_frame_file)
317*b7c941bbSAndroid Build Coastguard Worker
318*b7c941bbSAndroid Build Coastguard Worker  # Convert last_key_frame to numpy array
319*b7c941bbSAndroid Build Coastguard Worker  np_image = image_processing_utils.convert_image_to_numpy_array(
320*b7c941bbSAndroid Build Coastguard Worker      os.path.join(log_path, last_key_frame_file))
321*b7c941bbSAndroid Build Coastguard Worker  logging.debug('last key frame image shape: %s', np_image.shape)
322*b7c941bbSAndroid Build Coastguard Worker
323*b7c941bbSAndroid Build Coastguard Worker  return np_image
324*b7c941bbSAndroid Build Coastguard Worker
325*b7c941bbSAndroid Build Coastguard Worker
326*b7c941bbSAndroid Build Coastguard Workerdef get_avg_frame_rate(video_file_name_with_path):
327*b7c941bbSAndroid Build Coastguard Worker  """Get average frame rate assuming variable frame rate video.
328*b7c941bbSAndroid Build Coastguard Worker
329*b7c941bbSAndroid Build Coastguard Worker  Args:
330*b7c941bbSAndroid Build Coastguard Worker    video_file_name_with_path: path to the video to be analyzed
331*b7c941bbSAndroid Build Coastguard Worker  Returns:
332*b7c941bbSAndroid Build Coastguard Worker    Float. average frames per second.
333*b7c941bbSAndroid Build Coastguard Worker  """
334*b7c941bbSAndroid Build Coastguard Worker
335*b7c941bbSAndroid Build Coastguard Worker  cmd = ['ffprobe',
336*b7c941bbSAndroid Build Coastguard Worker         '-v',
337*b7c941bbSAndroid Build Coastguard Worker         'quiet',
338*b7c941bbSAndroid Build Coastguard Worker         '-show_streams',
339*b7c941bbSAndroid Build Coastguard Worker         '-select_streams',
340*b7c941bbSAndroid Build Coastguard Worker         'v:0',  # first video stream
341*b7c941bbSAndroid Build Coastguard Worker         video_file_name_with_path
342*b7c941bbSAndroid Build Coastguard Worker        ]
343*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Getting frame rate')
344*b7c941bbSAndroid Build Coastguard Worker  raw_output = ''
345*b7c941bbSAndroid Build Coastguard Worker  try:
346*b7c941bbSAndroid Build Coastguard Worker    raw_output = subprocess.check_output(cmd,
347*b7c941bbSAndroid Build Coastguard Worker                                         stdin=subprocess.DEVNULL,
348*b7c941bbSAndroid Build Coastguard Worker                                         stderr=subprocess.STDOUT)
349*b7c941bbSAndroid Build Coastguard Worker  except subprocess.CalledProcessError as e:
350*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(str(e.output)) from e
351*b7c941bbSAndroid Build Coastguard Worker  if raw_output:
352*b7c941bbSAndroid Build Coastguard Worker    output = str(raw_output.decode('utf-8')).strip()
353*b7c941bbSAndroid Build Coastguard Worker    logging.debug('ffprobe command %s output: %s', ' '.join(cmd), output)
354*b7c941bbSAndroid Build Coastguard Worker    average_frame_rate_data = (
355*b7c941bbSAndroid Build Coastguard Worker        re.search(r'avg_frame_rate=*([0-9]+/[0-9]+)', output)
356*b7c941bbSAndroid Build Coastguard Worker        .group(INDEX_FIRST_SUBGROUP)
357*b7c941bbSAndroid Build Coastguard Worker    )
358*b7c941bbSAndroid Build Coastguard Worker    average_frame_rate = (int(average_frame_rate_data.split('/')[0]) /
359*b7c941bbSAndroid Build Coastguard Worker                          int(average_frame_rate_data.split('/')[1]))
360*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Average FPS: %.4f', average_frame_rate)
361*b7c941bbSAndroid Build Coastguard Worker    return average_frame_rate
362*b7c941bbSAndroid Build Coastguard Worker  else:
363*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('ffprobe failed to provide frame rate data')
364*b7c941bbSAndroid Build Coastguard Worker
365*b7c941bbSAndroid Build Coastguard Worker
366*b7c941bbSAndroid Build Coastguard Workerdef get_frame_deltas(video_file_name_with_path, timestamp_type='pts'):
367*b7c941bbSAndroid Build Coastguard Worker  """Get list of time diffs between frames.
368*b7c941bbSAndroid Build Coastguard Worker
369*b7c941bbSAndroid Build Coastguard Worker  Args:
370*b7c941bbSAndroid Build Coastguard Worker    video_file_name_with_path: path to the video to be analyzed
371*b7c941bbSAndroid Build Coastguard Worker    timestamp_type: 'pts' or 'dts'
372*b7c941bbSAndroid Build Coastguard Worker  Returns:
373*b7c941bbSAndroid Build Coastguard Worker    List of floats. Time diffs between frames in seconds.
374*b7c941bbSAndroid Build Coastguard Worker  """
375*b7c941bbSAndroid Build Coastguard Worker
376*b7c941bbSAndroid Build Coastguard Worker  cmd = ['ffprobe',
377*b7c941bbSAndroid Build Coastguard Worker         '-show_entries',
378*b7c941bbSAndroid Build Coastguard Worker         f'frame=pkt_{timestamp_type}_time',
379*b7c941bbSAndroid Build Coastguard Worker         '-select_streams',
380*b7c941bbSAndroid Build Coastguard Worker         'v',
381*b7c941bbSAndroid Build Coastguard Worker         video_file_name_with_path
382*b7c941bbSAndroid Build Coastguard Worker         ]
383*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Getting frame deltas')
384*b7c941bbSAndroid Build Coastguard Worker  raw_output = ''
385*b7c941bbSAndroid Build Coastguard Worker  try:
386*b7c941bbSAndroid Build Coastguard Worker    raw_output = subprocess.check_output(cmd,
387*b7c941bbSAndroid Build Coastguard Worker                                         stdin=subprocess.DEVNULL,
388*b7c941bbSAndroid Build Coastguard Worker                                         stderr=subprocess.STDOUT)
389*b7c941bbSAndroid Build Coastguard Worker  except subprocess.CalledProcessError as e:
390*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(str(e.output)) from e
391*b7c941bbSAndroid Build Coastguard Worker  if raw_output:
392*b7c941bbSAndroid Build Coastguard Worker    output = str(raw_output.decode('utf-8')).strip().split('\n')
393*b7c941bbSAndroid Build Coastguard Worker    deltas = []
394*b7c941bbSAndroid Build Coastguard Worker    prev_time = None
395*b7c941bbSAndroid Build Coastguard Worker    for line in output:
396*b7c941bbSAndroid Build Coastguard Worker      if timestamp_type not in line:
397*b7c941bbSAndroid Build Coastguard Worker        continue
398*b7c941bbSAndroid Build Coastguard Worker      curr_time = float(re.search(r'time= *([0-9][0-9\.]*)', line)
399*b7c941bbSAndroid Build Coastguard Worker                        .group(INDEX_FIRST_SUBGROUP))
400*b7c941bbSAndroid Build Coastguard Worker      if prev_time is not None:
401*b7c941bbSAndroid Build Coastguard Worker        deltas.append(curr_time - prev_time)
402*b7c941bbSAndroid Build Coastguard Worker      prev_time = curr_time
403*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Frame deltas: %s', deltas)
404*b7c941bbSAndroid Build Coastguard Worker    return deltas
405*b7c941bbSAndroid Build Coastguard Worker  else:
406*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('ffprobe failed to provide frame delta data')
407*b7c941bbSAndroid Build Coastguard Worker
408*b7c941bbSAndroid Build Coastguard Worker
409*b7c941bbSAndroid Build Coastguard Workerdef get_video_colorspace(log_path, video_file_name):
410*b7c941bbSAndroid Build Coastguard Worker  """Get the video colorspace.
411*b7c941bbSAndroid Build Coastguard Worker
412*b7c941bbSAndroid Build Coastguard Worker  Args:
413*b7c941bbSAndroid Build Coastguard Worker    log_path: path for video file directory
414*b7c941bbSAndroid Build Coastguard Worker    video_file_name: name of the video file
415*b7c941bbSAndroid Build Coastguard Worker  Returns:
416*b7c941bbSAndroid Build Coastguard Worker    video colorspace, e.g. BT.2020 or BT.709
417*b7c941bbSAndroid Build Coastguard Worker  """
418*b7c941bbSAndroid Build Coastguard Worker
419*b7c941bbSAndroid Build Coastguard Worker  cmd = ['ffprobe',
420*b7c941bbSAndroid Build Coastguard Worker         '-show_streams',
421*b7c941bbSAndroid Build Coastguard Worker         '-select_streams',
422*b7c941bbSAndroid Build Coastguard Worker         'v:0',
423*b7c941bbSAndroid Build Coastguard Worker         '-of',
424*b7c941bbSAndroid Build Coastguard Worker         'json',
425*b7c941bbSAndroid Build Coastguard Worker         '-i',
426*b7c941bbSAndroid Build Coastguard Worker         os.path.join(log_path, video_file_name)
427*b7c941bbSAndroid Build Coastguard Worker         ]
428*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Get the video colorspace')
429*b7c941bbSAndroid Build Coastguard Worker  raw_output = ''
430*b7c941bbSAndroid Build Coastguard Worker  try:
431*b7c941bbSAndroid Build Coastguard Worker    raw_output = subprocess.check_output(cmd,
432*b7c941bbSAndroid Build Coastguard Worker                                         stdin=subprocess.DEVNULL,
433*b7c941bbSAndroid Build Coastguard Worker                                         stderr=subprocess.STDOUT)
434*b7c941bbSAndroid Build Coastguard Worker  except subprocess.CalledProcessError as e:
435*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(str(e.output)) from e
436*b7c941bbSAndroid Build Coastguard Worker
437*b7c941bbSAndroid Build Coastguard Worker  logging.debug('raw_output: %s', raw_output)
438*b7c941bbSAndroid Build Coastguard Worker  if raw_output:
439*b7c941bbSAndroid Build Coastguard Worker    colorspace = ''
440*b7c941bbSAndroid Build Coastguard Worker    output = str(raw_output.decode('utf-8')).strip().split('\n')
441*b7c941bbSAndroid Build Coastguard Worker    logging.debug('output: %s', output)
442*b7c941bbSAndroid Build Coastguard Worker    for line in output:
443*b7c941bbSAndroid Build Coastguard Worker      logging.debug('line: %s', line)
444*b7c941bbSAndroid Build Coastguard Worker      metadata = re.search(r'"color_space": ("[a-z0-9]*")', line)
445*b7c941bbSAndroid Build Coastguard Worker      if metadata:
446*b7c941bbSAndroid Build Coastguard Worker        colorspace = metadata.group(INDEX_FIRST_SUBGROUP)
447*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Colorspace: %s', colorspace)
448*b7c941bbSAndroid Build Coastguard Worker    return colorspace
449*b7c941bbSAndroid Build Coastguard Worker  else:
450*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('ffprobe failed to provide color space')
451