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