diff --git a/README.rst b/README.rst index 15194bd..fa4be67 100755 --- a/README.rst +++ b/README.rst @@ -1558,6 +1558,12 @@ Release Notes - Changed (BREAKING): How offsets are calculated has been changed and can impact result if negative values were being provided for start_offset and/or end_offset! - Changed: Updated Docketfile. Contributed by magicalyak - Fixed: Resolved issue on Linux with default device initialization failure when using Intel GPU acceleration and multiple usable devices by adding new qsv gpu_type (https://trac.ffmpeg.org/ticket/7649). Contributed by timrettop. +0.1.21: + - New: Option --camera_position to provide coordinates where the camera clip should be positioned in the video providing very granular custom layout options. Using this option results in ignoring the layout option. + - New: Option --camera_order to define the order the cameras should be processed. This allows to overlay one camera over another one and define which one should be on top. + - Fixed: Issue with GPU type check of qsv for Linux. Contributed by cjwang18 + - Fixed: ffmpeg error when swapping front/rear and excluding front or rear + - Fixed: ffmpeg error when swapping left/right and excluding left or right TODO diff --git a/tesla_dashcam/tesla_dashcam.py b/tesla_dashcam/tesla_dashcam.py index 9ef3c00..a6ab28a 100644 --- a/tesla_dashcam/tesla_dashcam.py +++ b/tesla_dashcam/tesla_dashcam.py @@ -37,7 +37,7 @@ # different ones to be created based on where it should go to (stdout, # log file, ...). -VERSION = {"major": 0, "minor": 1, "patch": 20, "beta": -1} +VERSION = {"major": 0, "minor": 1, "patch": 21, "beta": 2} VERSION_STR = f"v{VERSION['major']}.{VERSION['minor']}.{VERSION['patch']}" if VERSION["beta"] > -1: @@ -148,8 +148,7 @@ class Camera_Clip(object): - """ Camera Clip Class - """ + """Camera Clip Class""" def __init__(self, filename, timestamp, duration=0, include=False): self._filename = filename @@ -199,8 +198,7 @@ def end_timestamp(self): class Clip(object): - """ Clip Class - """ + """Clip Class""" def __init__(self, timestamp=None, filename=None): self._timestamp = timestamp @@ -300,7 +298,7 @@ def sorted(self): class Event(object): - """ Event Class """ + """Event Class""" def __init__(self, folder, isfile=False, filename=None): self._folder = folder @@ -470,7 +468,7 @@ def template(self, template, timestamp_format, video_settings): class Movie(object): - """ Movie Class """ + """Movie Class""" def __init__(self, filename=None): self._filename = filename @@ -574,8 +572,7 @@ def sorted(self): class Font(object): - """ Font Class - """ + """Font Class""" def __init__(self, layout, font=None, size=None, color=None): self._layout = layout @@ -656,8 +653,7 @@ def ypos(self, value): class Camera(object): - """ Camera Class - """ + """Camera Class""" def __init__(self, layout, camera): self._layout = layout @@ -666,7 +662,9 @@ def __init__(self, layout, camera): self._width = 1280 self._height = 960 self._xpos = 0 + self._xpos_override = False self._ypos = 0 + self._ypos_override = False self._scale = 0 self._options = "" @@ -712,25 +710,37 @@ def height(self, value): @property def xpos(self): - if hasattr(self._layout, "_" + self._camera + "_xpos"): + if not self._xpos_override and hasattr( + self._layout, "_" + self._camera + "_xpos" + ): return getattr(self._layout, "_" + self._camera + "_xpos")() * self.include return self._xpos * self.include @xpos.setter def xpos(self, value): - self._xpos = value + if value is not None: + self._xpos = int(value) + self._xpos_override = True + else: + self._xpos_override = False @property def ypos(self): - if hasattr(self._layout, "_" + self._camera + "_ypos"): + if not self._ypos_override and hasattr( + self._layout, "_" + self._camera + "_ypos" + ): return getattr(self._layout, "_" + self._camera + "_ypos")() * self.include return self._ypos * self.include @ypos.setter def ypos(self, value): - self._ypos = value + if value is not None: + self._ypos = int(value) + self._ypos_override = True + else: + self._ypos_override = False @property def scale(self): @@ -759,16 +769,16 @@ def options(self, value): class MovieLayout(object): - """ Main Layout class - """ + """Main Layout class""" def __init__(self): self._cameras = { - "Front": Camera(layout=self, camera="front"), - "Left": Camera(layout=self, camera="left"), - "Right": Camera(layout=self, camera="right"), - "Rear": Camera(layout=self, camera="rear"), + "front": Camera(layout=self, camera="front"), + "left": Camera(layout=self, camera="left"), + "right": Camera(layout=self, camera="right"), + "rear": Camera(layout=self, camera="rear"), } + self._clip_order = ["left", "right", "front", "rear"] self._font = Font(layout=self) self._swap_left_right = False @@ -782,6 +792,28 @@ def __init__(self): def cameras(self, camera): return self._cameras.get(camera, self._cameras) + @property + def clip_order(self) -> list: + return self._clip_order + + @clip_order.setter + def clip_order(self, value: list): + self._clip_order = [] + for camera in value: + camera = camera.lower().strip() + if camera in ["front", "left", "right", "rear"]: + self._clip_order.append(camera) + + # Make sure we have all of them, if not then add based on default order. + if "left" not in self._clip_order: + self._clip_order.append("left") + if "right" not in self._clip_order: + self._clip_order.append("right") + if "front" not in self._clip_order: + self._clip_order.append("front") + if "rear" not in self._clip_order: + self._clip_order.append("rear") + @property def font(self): return self._font @@ -815,19 +847,19 @@ def perspective(self, new_perspective): self._perspective = new_perspective if self._perspective: - self.cameras("Left").options = ( + self.cameras("left").options = ( ", pad=iw+4:3/2*ih:-1:ih/8:0x00000000, " "perspective=x0=0:y0=1*H/5:x1=W:y1=-3/44*H:" "x2=0:y2=6*H/5:x3=7/8*W:y3=5*H/6:sense=destination" ) - self.cameras("Right").options = ( + self.cameras("right").options = ( ", pad=iw+4:3/2*ih:-1:ih/8:0x00000000," "perspective=x0=0:y1=1*H/5:x1=W:y0=-3/44*H:" "x2=1/8*W:y3=6*H/5:x3=W:y2=5*H/6:sense=destination" ) else: - self.cameras("Left").options = "" - self.cameras("Right").options = "" + self.cameras("left").options = "" + self.cameras("right").options = "" @property def scale(self): @@ -836,19 +868,19 @@ def scale(self): @scale.setter def scale(self, scale): - self.cameras("Front").scale = scale - self.cameras("Left").scale = scale - self.cameras("Right").scale = scale - self.cameras("Rear").scale = scale + self.cameras("front").scale = scale + self.cameras("left").scale = scale + self.cameras("right").scale = scale + self.cameras("rear").scale = scale @property def video_width(self): return int( max( - self.cameras("Front").xpos + self.cameras("Front").width, - self.cameras("Left").xpos + self.cameras("Left").width, - self.cameras("Right").xpos + self.cameras("Right").width, - self.cameras("Rear").xpos + self.cameras("Rear").width, + self.cameras("front").xpos + self.cameras("front").width, + self.cameras("left").xpos + self.cameras("left").width, + self.cameras("right").xpos + self.cameras("right").width, + self.cameras("rear").xpos + self.cameras("rear").width, ) ) @@ -857,12 +889,12 @@ def video_height(self): perspective_adjustement = 3 / 2 if self.perspective else 1 return int( max( - self.cameras("Front").ypos + self.cameras("Front").height, - perspective_adjustement * self.cameras("Left").ypos - + self.cameras("Left").height, - perspective_adjustement * self.cameras("Right").ypos - + self.cameras("Right").height, - self.cameras("Rear").ypos + self.cameras("Rear").height, + self.cameras("front").ypos + self.cameras("front").height, + perspective_adjustement * self.cameras("left").ypos + + self.cameras("left").height, + perspective_adjustement * self.cameras("right").ypos + + self.cameras("right").height, + self.cameras("rear").ypos + self.cameras("rear").height, ) ) @@ -874,12 +906,34 @@ def center_xpos(self): def center_ypos(self): return int(self.video_height / 2) + @property + def _rear_xpos(self): + return self.cameras("front").xpos + self.cameras("front").width + + @property + def _left_ypos(self): + return max( + self.cameras("front").ypos + self.cameras("front").height, + self.cameras("rear").ypos + self.cameras("rear").height, + ) + + @property + def _right_xpos(self): + return self.cameras("left").xpos + self.cameras("left").width + + @property + def _right_ypos(self): + return max( + self.cameras("front").ypos + self.cameras("front").height, + self.cameras("rear").ypos + self.cameras("rear").height, + ) + class FullScreen(MovieLayout): - """ FullScreen Movie Layout + """FullScreen Movie Layout - [FRONT_CAMERA] - [LEFT_CAMERA][REAR_CAMERA][RIGHT_CAMERA] + [FRONT_CAMERA] + [LEFT_CAMERA][REAR_CAMERA][RIGHT_CAMERA] """ def __init__(self): @@ -890,10 +944,10 @@ def __init__(self): def video_width(self): return int( max( - self.cameras("Front").width, - self.cameras("Left").width - + self.cameras("Rear").width - + self.cameras("Right").width, + self.cameras("front").width, + self.cameras("left").width + + self.cameras("rear").width + + self.cameras("right").width, ) ) @@ -901,17 +955,17 @@ def video_width(self): def video_height(self): perspective_adjustement = 3 / 2 if self.perspective else 1 return int( - self.cameras("Front").height + self.cameras("front").height + max( - perspective_adjustement * self.cameras("Left").height, - self.cameras("Rear").height, - perspective_adjustement * self.cameras("Right").height, + perspective_adjustement * self.cameras("left").height, + self.cameras("rear").height, + perspective_adjustement * self.cameras("right").height, ) ) def _front_height(self): # For height keep same ratio of 4/3 - return int(self.cameras("Front").width / 4 * 3) + return int(self.cameras("front").width / 4 * 3) def _front_xpos(self): # Make sure that front is placed in the middle @@ -921,15 +975,15 @@ def _front_xpos(self): self.center_xpos - int( ( - self.cameras("Left").width - + self.cameras("Front").width - + self.cameras("Right").width + self.cameras("left").width + + self.cameras("front").width + + self.cameras("right").width ) / 2 ) - + self.cameras("Left").width, + + self.cameras("left").width, ) - * self.cameras("Front").include + * self.cameras("front").include ) def _left_xpos(self): @@ -939,20 +993,20 @@ def _left_xpos(self): self.center_xpos - int( ( - self.cameras("Left").width - + self.cameras("Rear").width - + self.cameras("Right").width + self.cameras("left").width + + self.cameras("rear").width + + self.cameras("right").width ) / 2 ), ) - * self.cameras("Left").include + * self.cameras("left").include ) def _left_ypos(self): return ( - self.cameras("Front").ypos + self.cameras("Front").height - ) * self.cameras("Left").include + self.cameras("front").ypos + self.cameras("front").height + ) * self.cameras("left").include def _rear_xpos(self): return ( @@ -961,21 +1015,21 @@ def _rear_xpos(self): self.center_xpos - int( ( - self.cameras("Left").width - + self.cameras("Rear").width - + self.cameras("Right").width + self.cameras("left").width + + self.cameras("rear").width + + self.cameras("right").width ) / 2 ) - + self.cameras("Left").width, + + self.cameras("left").width, ) - * self.cameras("Rear").include + * self.cameras("rear").include ) def _rear_ypos(self): return ( - self.cameras("Front").ypos + self.cameras("Front").height - ) * self.cameras("Rear").include + self.cameras("front").ypos + self.cameras("front").height + ) * self.cameras("rear").include def _right_xpos(self): return ( @@ -984,27 +1038,27 @@ def _right_xpos(self): self.center_xpos - int( ( - self.cameras("Left").width - + self.cameras("Rear").width - + self.cameras("Right").width + self.cameras("left").width + + self.cameras("rear").width + + self.cameras("right").width ) / 2 ) - + self.cameras("Left").width - + self.cameras("Rear").width, + + self.cameras("left").width + + self.cameras("rear").width, ) - * self.cameras("Right").include + * self.cameras("right").include ) def _right_ypos(self): return ( - self.cameras("Front").ypos + self.cameras("Front").height - ) * self.cameras("Right").include + self.cameras("front").ypos + self.cameras("front").height + ) * self.cameras("right").include # noinspection PyProtectedMember class WideScreen(FullScreen): - """ WideScreen Movie Layout + """WideScreen Movie Layout [ FRONT_CAMERA ] [LEFT_CAMERA][REAR_CAMERA][RIGHT_CAMERA] @@ -1014,34 +1068,34 @@ def __init__(self): super().__init__() self.scale = 1 / 2 # Set front scale to None so we know if it was overriden or not. - self.cameras("Front").scale = None + self.cameras("front").scale = None # Only front_width has to be adjusted as by default width would be left+rear+right instead of normal scale. def _front_width(self): return ( ( - self.cameras("Left").width - + self.cameras("Rear").width - + self.cameras("Right").width + self.cameras("left").width + + self.cameras("rear").width + + self.cameras("right").width ) - * self.cameras("Front").include - if self.cameras("Front").scale is None + * self.cameras("front").include + if self.cameras("front").scale is None else int( ( - self.cameras("Front")._width - * self.cameras("Front").scale - * self.cameras("Front").include + self.cameras("front")._width + * self.cameras("front").scale + * self.cameras("front").include ) ) ) class Cross(FullScreen): - """ Cross Movie Layout + """Cross Movie Layout - [FRONT_CAMERA] - [LEFT_CAMERA][RIGHT_CAMERA] - [REAR_CAMERA] + [FRONT_CAMERA] + [LEFT_CAMERA][RIGHT_CAMERA] + [REAR_CAMERA] """ def __init__(self): @@ -1051,9 +1105,9 @@ def __init__(self): @property def video_width(self): return max( - self.cameras("Front").width, - self.cameras("Left").width + self.cameras("Right").width, - self.cameras("Rear").width, + self.cameras("front").width, + self.cameras("left").width + self.cameras("right").width, + self.cameras("rear").width, ) @property @@ -1061,31 +1115,31 @@ def video_height(self): if self.perspective: height = int( max( - 3 / 2 * self.cameras("Left").height, - 3 / 2 * self.cameras("Right").height, + 3 / 2 * self.cameras("left").height, + 3 / 2 * self.cameras("right").height, ) ) if ( - self.cameras("Left").include - and self.cameras("Left").scale >= self.cameras("Rear").scale - and self.cameras("Right").include - and self.cameras("Right").scale >= self.cameras("Rear").scale - and self.cameras("Rear").include + self.cameras("left").include + and self.cameras("left").scale >= self.cameras("rear").scale + and self.cameras("right").include + and self.cameras("right").scale >= self.cameras("rear").scale + and self.cameras("rear").include ): height = int(height / 3 * 2) - height += self.cameras("Rear").height + height += self.cameras("rear").height else: height = ( - max(self.cameras("Left").height, self.cameras("Right").height) - + self.cameras("Rear").height + max(self.cameras("left").height, self.cameras("right").height) + + self.cameras("rear").height ) - return int(height + self.cameras("Front").height) + return int(height + self.cameras("front").height) def _front_xpos(self): return ( - int(max(0, self.center_xpos - (self.cameras("Front").width / 2))) - * self.cameras("Front").include + int(max(0, self.center_xpos - (self.cameras("front").width / 2))) + * self.cameras("front").include ) def _left_xpos(self): @@ -1093,63 +1147,63 @@ def _left_xpos(self): max( 0, self.center_xpos - - int((self.cameras("Left").width + self.cameras("Right").width) / 2), + - int((self.cameras("left").width + self.cameras("right").width) / 2), ) - * self.cameras("Left").include + * self.cameras("left").include ) def _left_ypos(self): return ( - self.cameras("Front").height + self.cameras("front").height + int( ( - max(self.cameras("Left").height, self.cameras("Right").height) - - self.cameras("Left").height + max(self.cameras("left").height, self.cameras("right").height) + - self.cameras("left").height ) / 2 ) - ) * self.cameras("Left").include + ) * self.cameras("left").include def _right_xpos(self): return ( max( 0, self.center_xpos - - int((self.cameras("Left").width + self.cameras("Right").width) / 2) - + self.cameras("Left").width, + - int((self.cameras("left").width + self.cameras("right").width) / 2) + + self.cameras("left").width, ) - * self.cameras("Right").include + * self.cameras("right").include ) def _right_ypos(self): return ( - self.cameras("Front").height + self.cameras("front").height + int( ( - max(self.cameras("Left").height, self.cameras("Right").height) - - self.cameras("Right").height + max(self.cameras("left").height, self.cameras("right").height) + - self.cameras("right").height ) / 2 ) - ) * self.cameras("Right").include + ) * self.cameras("right").include def _rear_xpos(self): return ( - int(max(0, self.center_xpos - (self.cameras("Rear").width / 2))) - * self.cameras("Rear").include + int(max(0, self.center_xpos - (self.cameras("rear").width / 2))) + * self.cameras("rear").include ) def _rear_ypos(self): - return int(max(0, self.video_height - self.cameras("Rear").height)) + return int(max(0, self.video_height - self.cameras("rear").height)) # noinspection PyProtectedMember class Diamond(Cross): - """ Diamond Movie Layout + """Diamond Movie Layout - [FRONT_CAMERA] - [LEFT_CAMERA] [RIGHT_CAMERA] - [REAR_CAMERA] + [FRONT_CAMERA] + [LEFT_CAMERA] [RIGHT_CAMERA] + [REAR_CAMERA] """ def __init__(self): @@ -1161,13 +1215,13 @@ def __init__(self): def _font_halign(self): if self._font._halign == "CENTER": # Change alignment to left or right if one of the left/right cameras is excluded. - if (self.cameras("Left").include and not self.cameras("Right").include) or ( - self.cameras("Right").include and not self.cameras("Left").include + if (self.cameras("left").include and not self.cameras("right").include) or ( + self.cameras("right").include and not self.cameras("left").include ): x_pos = int( max( - self.cameras("Front").xpos + self.cameras("Front").width / 2, - self.cameras("Rear").xpos + self.cameras("Rear").width / 2, + self.cameras("front").xpos + self.cameras("front").width / 2, + self.cameras("rear").xpos + self.cameras("rear").width / 2, ) ) return f"({x_pos} - text_w / 2)" @@ -1176,12 +1230,12 @@ def _font_halign(self): def _font_valign(self): if self._font._valign == "MIDDLE": - if self.cameras("Front").include: + if self.cameras("front").include: return ( - f'({self.cameras("Front").ypos + self.cameras("Front").height} + 5)' + f'({self.cameras("front").ypos + self.cameras("front").height} + 5)' ) - elif self.cameras("Rear").include: - return f'({self.cameras("Rear").ypos} - 5 - text_h)' + elif self.cameras("rear").include: + return f'({self.cameras("rear").ypos} - 5 - text_h)' return VALIGN.get(self._font._valign, self._font._valign) @@ -1202,9 +1256,9 @@ def _font_size(self): @property def video_width(self): return ( - max(self.cameras("Front").width, self.cameras("Rear").width) - + self.cameras("Left").width - + self.cameras("Right").width + max(self.cameras("front").width, self.cameras("rear").width) + + self.cameras("left").width + + self.cameras("right").width ) def _video_height(self, include_fontsize=True): @@ -1214,8 +1268,8 @@ def _video_height(self, include_fontsize=True): return int( max( perspective - * max(self.cameras("Left").height, self.cameras("Right").height), - self.cameras("Front").height + self.cameras("Rear").height + fontsize, + * max(self.cameras("left").height, self.cameras("right").height), + self.cameras("front").height + self.cameras("rear").height + fontsize, ) ) @@ -1225,42 +1279,42 @@ def video_height(self): def _front_xpos(self): return ( - self.cameras("Left").width + self.cameras("left").width + int( ( - max(self.cameras("Front").width, self.cameras("Rear").width) - - self.cameras("Front").width + max(self.cameras("front").width, self.cameras("rear").width) + - self.cameras("front").width ) / 2 ) - ) * self.cameras("Front").include + ) * self.cameras("front").include def _left_xpos(self): return 0 def _left_ypos(self): - return max(0, self.center_ypos - int(self.cameras("Left").height / 2)) + return max(0, self.center_ypos - int(self.cameras("left").height / 2)) def _right_xpos(self): return max( - self.cameras("Front").xpos + self.cameras("Front").width, - self.cameras("Rear").xpos + self.cameras("Rear").width, + self.cameras("front").xpos + self.cameras("front").width, + self.cameras("rear").xpos + self.cameras("rear").width, ) def _right_ypos(self): - return max(0, self.center_ypos - int(self.cameras("Right").height / 2)) + return max(0, self.center_ypos - int(self.cameras("right").height / 2)) def _rear_xpos(self): return ( - self.cameras("Left").width + self.cameras("left").width + int( ( - max(self.cameras("Front").width, self.cameras("Rear").width) - - self.cameras("Rear").width + max(self.cameras("front").width, self.cameras("rear").width) + - self.cameras("rear").width ) / 2 ) - ) * self.cameras("Rear").include + ) * self.cameras("rear").include class MyArgumentParser(argparse.ArgumentParser): @@ -1295,17 +1349,17 @@ def args_to_dict(self, arguments, default): # noinspection PyCallByClass,PyProtectedMember class SmartFormatter(argparse.HelpFormatter): - """ Formatter for argument help. """ + """Formatter for argument help.""" def _split_lines(self, text, width): - """ Provide raw output allowing for prettier help output """ + """Provide raw output allowing for prettier help output""" if text.startswith("R|"): return text[2:].splitlines() # this is the RawTextHelpFormatter._split_lines return argparse.HelpFormatter._split_lines(self, text, width) def _get_help_string(self, action): - """ Call default help string """ + """Call default help string""" return argparse.ArgumentDefaultsHelpFormatter._get_help_string(self, action) @@ -1349,7 +1403,7 @@ def get_current_timestamp(): def check_latest_release(include_beta): - """ Checks GitHub for latest release """ + """Checks GitHub for latest release""" url = f"{GITHUB['URL']}/repos/{GITHUB['owner']}/{GITHUB['repo']}/releases" @@ -1371,7 +1425,7 @@ def check_latest_release(include_beta): def get_tesladashcam_folder(): - """ Check if there is a drive mounted with the Tesla DashCam folder.""" + """Check if there is a drive mounted with the Tesla DashCam folder.""" for partition in disk_partitions(all=False): if "cdrom" in partition.opts or partition.fstype == "": continue @@ -1385,10 +1439,7 @@ def get_tesladashcam_folder(): def get_movie_files(source_folder, video_settings): - """ Find all the clip files within folder (and subfolder if requested) - - - """ + """Find all the clip files within folder (and subfolder if requested)""" # Making as a set to ensure uniqueness. folder_list = set() @@ -1469,13 +1520,25 @@ def get_movie_files(source_folder, video_settings): for item in metadata: _, filename = os.path.split(item["filename"]) if filename == front_filename: - camera = "Front" + if video_settings["video_layout"].swap_front_rear: + camera = "rear" + else: + camera = "front" elif filename == left_filename: - camera = "Left" + if video_settings["video_layout"].swap_left_right: + camera = "right" + else: + camera = "left" elif filename == right_filename: - camera = "Right" + if video_settings["video_layout"].swap_left_right: + camera = "left" + else: + camera = "right" elif filename == rear_filename: - camera = "Rear" + if video_settings["video_layout"].swap_front_rear: + camera = "front" + else: + camera = "rear" else: continue @@ -1486,16 +1549,16 @@ def get_movie_files(source_folder, video_settings): clip_starting_timestamp = datetime.strptime( clip_timestamp, "%Y-%m-%d_%H-%M" ) - clip_starting_timestamp = clip_starting_timestamp.astimezone( - get_localzone() + clip_starting_timestamp = ( + clip_starting_timestamp.astimezone(get_localzone()) ) else: # This is for version 2019.16 and later clip_starting_timestamp = datetime.strptime( clip_timestamp, "%Y-%m-%d_%H-%M-%S" ) - clip_starting_timestamp = clip_starting_timestamp.astimezone( - timezone.utc + clip_starting_timestamp = ( + clip_starting_timestamp.astimezone(timezone.utc) ) clip_info = Clip(timestamp=clip_starting_timestamp) @@ -1642,7 +1705,7 @@ def get_movie_files(source_folder, video_settings): def get_metadata(ffmpeg, filenames): - """ Retrieve the meta data for the clip (i.e. timestamp, duration) """ + """Retrieve the meta data for the clip (i.e. timestamp, duration)""" # Get meta data for each video to determine creation time and duration. ffmpeg_command = [ffmpeg] @@ -1727,46 +1790,25 @@ def get_metadata(ffmpeg, filenames): def create_intermediate_movie( event_info: Event, clip_info: Clip, folder_timestamps, video_settings, clip_number ): - """ Create intermediate movie files. This is the merging of the 3 camera + """Create intermediate movie files. This is the merging of the 3 camera - video files into 1 video file. """ + video files into 1 video file.""" # We first stack (combine the 3 different camera video files into 1 # and then we concatenate. - front_camera = None - left_camera = None - right_camera = None - rear_camera = None + clip_filenames = {} for camera_name, camera_info in clip_info.cameras: if camera_info.include: camera_filename = os.path.join(event_info.folder, camera_info.filename) - if camera_name == "Front": - front_camera = camera_filename - elif camera_name == "Left": - left_camera = camera_filename - elif camera_name == "Right": - right_camera = camera_filename - elif camera_name == "Rear": - rear_camera = camera_filename + clip_filenames.update({camera_name: camera_filename}) - if ( - front_camera is None - and left_camera is None - and right_camera is None - and rear_camera is None - ): + if len(clip_filenames) == 0: _LOGGER.debug( f"No valid front, left, right, and rear camera clip exist for " f'{clip_info.timestamp.astimezone(get_localzone()).strftime("%Y-%m-%dT%H-%M-%S")}' ) return True - if video_settings["video_layout"].swap_left_right: - left_camera, right_camera = right_camera, left_camera - - if video_settings["video_layout"].swap_front_rear: - front_camera, rear_camera = rear_camera, front_camera - # Determine if this clip is to be included based on potential start and end timestamp/offsets that were provided. # Clip starting time is between the start&end times we're looking for # or Clip end time is between the start&end time we're looking for. @@ -1822,87 +1864,33 @@ def create_intermediate_movie( ) return True - # Confirm if files exist, if not replace with nullsrc - input_count = 0 - if left_camera is not None: - ffmpeg_left_command = ffmpeg_offset_command + ["-i", left_camera] - ffmpeg_left_camera = ( - ";[" + str(input_count) + ":v] " + video_settings["left_camera"] - ) - input_count += 1 - else: - ffmpeg_left_command = [] - ffmpeg_left_camera = ( - video_settings["background"].format( - duration=clip_duration, - speed=video_settings["movie_speed"], - width=video_settings["video_layout"].cameras("Left").width, - height=video_settings["video_layout"].cameras("Left").height, - ) - + "[left]" - if video_settings["video_layout"].cameras("Left").include - else "" - ) - - if front_camera is not None: - ffmpeg_front_command = ffmpeg_offset_command + ["-i", front_camera] - ffmpeg_front_camera = ( - ";[" + str(input_count) + ":v] " + video_settings["front_camera"] - ) - input_count += 1 - else: - ffmpeg_front_command = [] - ffmpeg_front_camera = ( - video_settings["background"].format( - duration=clip_duration, - speed=video_settings["movie_speed"], - width=video_settings["video_layout"].cameras("Front").width, - height=video_settings["video_layout"].cameras("Front").height, - ) - + "[front]" - if video_settings["video_layout"].cameras("Front").include - else "" - ) - - if right_camera is not None: - ffmpeg_right_command = ffmpeg_offset_command + ["-i", right_camera] - ffmpeg_right_camera = ( - ";[" + str(input_count) + ":v] " + video_settings["right_camera"] - ) - input_count += 1 - else: - ffmpeg_right_command = [] - ffmpeg_right_camera = ( - video_settings["background"].format( - duration=clip_duration, - speed=video_settings["movie_speed"], - width=video_settings["video_layout"].cameras("Right").width, - height=video_settings["video_layout"].cameras("Right").height, + ffmpeg_camera_commands = [] + ffmpeg_camera_filters = [] + _exclude = object() + for camera in video_settings["video_layout"].clip_order: + if clip_filename := clip_filenames.get(camera, _exclude): + # If camera is not in dictionary then it is excluded. + if clip_filename is not _exclude: + # Got a valid clip for this camera and to be included + ffmpeg_camera_commands.append( + ffmpeg_offset_command + ["-i", clip_filename] + ) + ffmpeg_camera_filters.append( + ";[" + + str(len(ffmpeg_camera_commands) - 1) + + ":v] " + + video_settings["cameras"][camera] + ) + else: + ffmpeg_camera_filters.append( + video_settings["background"].format( + duration=clip_duration, + speed=video_settings["movie_speed"], + width=video_settings["video_layout"].cameras(camera).width, + height=video_settings["video_layout"].cameras(camera).height, + ) + + f"[{camera}]" ) - + "[right]" - if video_settings["video_layout"].cameras("Right").include - else "" - ) - - if rear_camera is not None: - ffmpeg_rear_command = ffmpeg_offset_command + ["-i", rear_camera] - ffmpeg_rear_camera = ( - ";[" + str(input_count) + ":v] " + video_settings["rear_camera"] - ) - input_count += 1 - else: - ffmpeg_rear_command = [] - ffmpeg_rear_camera = ( - video_settings["background"].format( - duration=clip_duration, - speed=video_settings["movie_speed"], - width=video_settings["video_layout"].cameras("Rear").width, - height=video_settings["video_layout"].cameras("Rear").height, - ) - + "[rear]" - if video_settings["video_layout"].cameras("Rear").include - else "" - ) local_timestamp = clip_info.timestamp.astimezone(get_localzone()) @@ -2024,15 +2012,16 @@ def create_intermediate_movie( ffmpeg_text = ffmpeg_text.replace("__USERTEXT__", user_formatted_text) - ffmpeg_filter = ( - video_settings["base"].format( - duration=clip_duration, speed=video_settings["movie_speed"] - ) - + ffmpeg_left_camera - + ffmpeg_front_camera - + ffmpeg_right_camera - + ffmpeg_rear_camera - + video_settings["clip_positions"] + ffmpeg_filter = video_settings["base"].format( + duration=clip_duration, speed=video_settings["movie_speed"] + ) + + # Add the respective camera filters. + for ffmpeg_camera_filter in ffmpeg_camera_filters: + ffmpeg_filter += ffmpeg_camera_filter + + ffmpeg_filter += ( + video_settings["clip_positions"] + ffmpeg_text + video_settings["ffmpeg_speed"] + video_settings["ffmpeg_motiononly"] @@ -2065,11 +2054,13 @@ def create_intermediate_movie( + ["-loglevel", "info"] + video_settings["ffmpeg_hwdev"] + video_settings["ffmpeg_hwout"] - + ffmpeg_left_command - + ffmpeg_front_command - + ffmpeg_right_command - + ffmpeg_rear_command - + ["-filter_complex", ffmpeg_filter] + ) + + for ffmpeg_camera_command in ffmpeg_camera_commands: + ffmpeg_command += ffmpeg_camera_command + + ffmpeg_command += ( + ["-filter_complex", ffmpeg_filter] + ["-map", f"[{video_settings['input_clip']}]"] + video_settings["other_params"] + ffmpeg_metadata @@ -2077,6 +2068,7 @@ def create_intermediate_movie( ffmpeg_command = ffmpeg_command + ["-y", temp_movie_name] _LOGGER.debug(f"FFMPEG Command: {ffmpeg_command}") + print(f"FFMPEG Command: {ffmpeg_command}") # Run the command. try: ffmpeg_output = run( @@ -2105,7 +2097,7 @@ def create_intermediate_movie( def create_title_screen(events, video_settings): - """ Create a map centered around the event """ + """Create a map centered around the event""" _LOGGER.debug(f"Creating map based on {len(events)}") if events == None or len(events) == 0: _LOGGER.debug("No events provided to create map for.") @@ -2168,7 +2160,7 @@ def create_title_screen(events, video_settings): def create_movie( movie, event_info, movie_filename, video_settings, chapter_offset, title_screen_map ): - """ Concatenate provided movie files into 1.""" + """Concatenate provided movie files into 1.""" # Just return if there are no clips. if movie.count <= 0: _LOGGER.debug(f"Movie list is empty") @@ -2537,7 +2529,7 @@ def make_folder(parameter, folder): def delete_intermediate(movie_files): - """ Delete the files provided in list """ + """Delete the files provided in list""" for file in movie_files: if file is not None: if os.path.isfile(file): @@ -2569,7 +2561,7 @@ def delete_intermediate(movie_files): def process_folders(source_folders, video_settings, delete_source): - """ Process all clips found within folders. """ + """Process all clips found within folders.""" # Retrieve all the video files within the folders provided. event_list = get_movie_files(source_folders, video_settings) @@ -3048,7 +3040,7 @@ def process_folders(source_folders, video_settings, delete_source): def resource_path(relative_path): - """ Return absolute path for provided relative item based on location + """Return absolute path for provided relative item based on location of program. """ @@ -3059,7 +3051,7 @@ def resource_path(relative_path): def notify_macos(title, subtitle, message): - """ Notification on MacOS """ + """Notification on MacOS""" try: run( [ @@ -3074,7 +3066,7 @@ def notify_macos(title, subtitle, message): def notify_windows(title, subtitle, message): - """ Notification on Windows """ + """Notification on Windows""" # Section commented out, waiting to see if it really does not work on Windows 7 # This works only on Windows 10 9r Windows Server 2016/2019. Skipping for everything else @@ -3105,7 +3097,7 @@ def notify_windows(title, subtitle, message): def notify_linux(title, subtitle, message): - """ Notification on Linux """ + """Notification on Linux""" try: run(["notify-send", f'"{title} {subtitle}"', f'"{message}"']) except Exception as exc: @@ -3113,7 +3105,7 @@ def notify_linux(title, subtitle, message): def notify(title, subtitle, message): - """ Call function to send notification based on OS """ + """Call function to send notification based on OS""" if PLATFORM == "darwin": notify_macos(title, subtitle, message) elif PLATFORM == "win32": @@ -3123,7 +3115,7 @@ def notify(title, subtitle, message): def main() -> int: - """ Main function """ + """Main function""" loglevels = dict( (logging.getLevelName(level), level) for level in [10, 20, 30, 40, 50] @@ -3257,6 +3249,43 @@ def main() -> int: " DIAMOND: Front camera center top, side cameras below front camera left and right of front, " "and rear camera center bottom.\n", ) + layout_group.add_argument( + "--camera_position", + dest="clip_pos", + type=str.lower, + nargs="+", + action="append", + help="R|Set camera clip position within video. Selecting this will override the layout selected!\n" + "The camera clip scale will be set to 1280x960, use scale to adjust accordingly.\n" + "Default layout is:\n" + " Front: 0x0\n" + " Rear: x0\n" + " Left: 0xmax(, \n" + " Right: xmax(, \n" + "Using this together with argument camera_order allows one to completely customize the layout\n" + "Note that layout chosen also determines camera clip size and thus default position. See scale for respective sizing.\n" + "Further, changing the scale of a camera clip would further impact potential positioning." + "for example:\n" + " --camera_position camera=left 640x480 Position left camera at 640x480\n" + " --camera_position camera=right x_pos=640 Position right camera at x-position 640, y-position based on layout\n" + " --camera_position camera=front y_pos=480 Position front camera at x-position based on layout, y-position at 480\n" + " --camera_position camera=rear 1280x960 Position rear camera at 1280x960\n", + ) + + layout_group.add_argument( + "--camera_order", + dest="clip_order", + type=str.lower, + help="R|Determines the order of processing the camera. Normally this is not required unless there is overlap.\n" + "When using argument camera_position it is possible to overlap cameras partially or completely, by then\n" + "leveraging this argument one can determine which camera will be on top and which one will be bottom." + " Default order is: left, front, right, rear. If there is no overlap then the order does not matter.\n" + " If not all cameras are specified then default order will be followed for those not specified, and thus be more on top." + "for example:\n" + " --camera_order front,rear,left,right Makes it that right will be on top, then left, then rear, and front at the bottom.\n", + ) + layout_group.set_defaults(clip_order="front,rear,left,right") + layout_group.add_argument( "--perspective", dest="perspective", @@ -3888,7 +3917,10 @@ def main() -> int: "and providing it leveraging the --ffmpeg parameter." ) - if args.layout == "PERSPECTIVE": + if args.clip_pos: + # If clip positions have been provided it is custom. + layout_settings = MovieLayout() + elif args.layout == "PERSPECTIVE": layout_settings = FullScreen() layout_settings.perspective = True else: @@ -3905,19 +3937,49 @@ def main() -> int: layout_settings.perspective = args.perspective - layout_settings.cameras("Front").include = not args.no_front - layout_settings.cameras("Left").include = not args.no_left - layout_settings.cameras("Right").include = not args.no_right - layout_settings.cameras("Rear").include = not args.no_rear + # Determine if left and right cameras should be swapped or not. + # No more arguments related to cameras (i.e .scale, include or not) can be processed from now on. + # Up till now left means left camera and right means right camera. + # From this point forward left can mean right camera if we're swapping output. + layout_settings.swap_front_rear = args.swap_frontrear + + layout_settings.cameras("front").include = not args.no_front + layout_settings.cameras("left").include = not args.no_left + if layout_settings.swap_front_rear: + layout_settings.cameras("front").include = not args.no_rear + layout_settings.cameras("rear").include = not args.no_front + else: + layout_settings.cameras("front").include = not args.no_front + layout_settings.cameras("rear").include = not args.no_rear # Check if either rear or mirror argument has been provided. # If front camera then default to mirror, if no front camera then default to rear. side_camera_as_mirror = ( - layout_settings.cameras("Front").include - if args.rear_or_mirror is None - else args.rear_or_mirror + not args.no_front if args.rear_or_mirror is None else args.rear_or_mirror + ) + mirror = { + "left": ", hflip" if side_camera_as_mirror else "", + "right": ", hflip" if side_camera_as_mirror else "", + } + mirror.update( + { + "front": mirror["left"] if layout_settings.swap_front_rear else "", + "rear": mirror["left"] if not layout_settings.swap_front_rear else "", + } ) - mirror_sides = ", hflip" if side_camera_as_mirror else "" + + layout_settings.swap_left_right = ( + not side_camera_as_mirror + if args.swap_leftright is None + else args.swap_leftright + ) + + if layout_settings.swap_left_right: + layout_settings.cameras("left").include = not args.no_right + layout_settings.cameras("right").include = not args.no_left + else: + layout_settings.cameras("left").include = not args.no_left + layout_settings.cameras("right").include = not args.no_right # For scale first set the main clip one if provided, this than allows camera specific ones to override for # that camera. @@ -3928,12 +3990,31 @@ def main() -> int: layout_settings.scale = main_scale.get("scale", layout_settings.scale) for scale in scaling: - if scale.get("camera", "").lower() in ["front", "left", "right", "rear"]: - camera_scale = scale.get("scale") - if camera_scale is not None: - layout_settings.cameras( - scale["camera"].lower().capitalize() - ).scale = camera_scale + camera = scale.get("camera", "").lower() + if camera in ["front", "left", "right", "rear"]: + if camera_scale := scale.get("scale"): + layout_settings.cameras(camera).scale = camera_scale + + for pos in parser.args_to_dict(args.clip_pos, "x_y_pos"): + camera = pos.get("camera", "").lower() + if camera in ["front", "left", "right", "rear"]: + x_pos, y_pos = None, None + if x_y_pos := pos.get("x_y_pos"): + x_y_pos = x_y_pos.split("x") + x_pos = x_y_pos[0] + if len(x_y_pos) == 2: + y_pos = x_y_pos[1] + + x_pos = pos.get("x_pos", x_pos) + y_pos = pos.get("y_pos", y_pos) + + if x_pos is not None and x_pos.isnumeric(): + layout_settings.cameras(camera).xpos = x_pos + + if y_pos is not None and y_pos.isnumeric(): + layout_settings.cameras(camera).ypos = y_pos + + layout_settings.clip_order = args.clip_order.split(",") layout_settings.font.halign = ( args.halign if args.halign is not None else layout_settings.font.halign @@ -3942,17 +4023,6 @@ def main() -> int: args.valign if args.valign is not None else layout_settings.font.valign ) - # Determine if left and right cameras should be swapped or not. - # No more arguments related to cameras (i.e .scale, include or not) can be processed from now on. - # Up till now Left means left camera and Right means Right camera. - # From this point forward Left can mean Right camera if we're swapping output. - layout_settings.swap_left_right = ( - not side_camera_as_mirror - if args.swap_leftright is None - else args.swap_leftright - ) - - layout_settings.swap_front_rear = args.swap_frontrear layout_settings.title_screen_map = args.title_screen_map layout_settings.font.font = args.font @@ -3975,103 +4045,33 @@ def main() -> int: input_clip = "base" ffmpeg_video_position = "" + ffmpeg_camera = {} - ffmpeg_left_camera = "" - camera = "Left" - if layout_settings.cameras(camera).include: - ffmpeg_left_camera = ( - "setpts=PTS-STARTPTS, " - "scale={clip_width}x{clip_height} {mirror}{options}" - " [left]".format( - clip_width=layout_settings.cameras(camera).width, - clip_height=layout_settings.cameras(camera).height, - mirror=mirror_sides, - options=layout_settings.cameras(camera).options, + for camera in layout_settings.clip_order: + if layout_settings.cameras(camera).include: + ffmpeg_camera.update( + { + camera: ( + "setpts=PTS-STARTPTS, " + "scale={clip_width}x{clip_height} {mirror}{options}" + " [{camera}]".format( + clip_width=layout_settings.cameras(camera).width, + clip_height=layout_settings.cameras(camera).height, + mirror=mirror[camera], + options=layout_settings.cameras(camera).options, + camera=camera, + ) + ) + } ) - ) - ffmpeg_video_position = ( - ffmpeg_video_position - + ";[{input_clip}][left] overlay=eof_action=pass:repeatlast=0:" - "x={x_pos}:y={y_pos} [left1]".format( - input_clip=input_clip, - x_pos=layout_settings.cameras(camera).xpos, - y_pos=layout_settings.cameras(camera).ypos, - ) - ) - input_clip = "left1" - - ffmpeg_front_camera = "" - camera = "Front" - if layout_settings.cameras(camera).include: - ffmpeg_front_camera = ( - "setpts=PTS-STARTPTS, " - "scale={clip_width}x{clip_height} {options}" - " [front]".format( - clip_width=layout_settings.cameras(camera).width, - clip_height=layout_settings.cameras(camera).height, - options=layout_settings.cameras(camera).options, - ) - ) - ffmpeg_video_position = ( - ffmpeg_video_position - + ";[{input_clip}][front] overlay=eof_action=pass:repeatlast=0:" - "x={x_pos}:y={y_pos} [front1]".format( - input_clip=input_clip, - x_pos=layout_settings.cameras(camera).xpos, - y_pos=layout_settings.cameras(camera).ypos, - ) - ) - input_clip = "front1" - - ffmpeg_right_camera = "" - camera = "Right" - if layout_settings.cameras(camera).include: - ffmpeg_right_camera = ( - "setpts=PTS-STARTPTS, " - "scale={clip_width}x{clip_height} {mirror}{options}" - " [right]".format( - clip_width=layout_settings.cameras(camera).width, - clip_height=layout_settings.cameras(camera).height, - mirror=mirror_sides, - options=layout_settings.cameras(camera).options, - ) - ) - ffmpeg_video_position = ( - ffmpeg_video_position - + ";[{input_clip}][right] overlay=eof_action=pass:repeatlast=0:" - "x={x_pos}:y={y_pos} [right1]".format( + ffmpeg_video_position += ";[{input_clip}][{camera}] overlay=eof_action=pass:repeatlast=0:x={x_pos}:y={y_pos} [{camera}1]".format( input_clip=input_clip, + camera=camera, x_pos=layout_settings.cameras(camera).xpos, y_pos=layout_settings.cameras(camera).ypos, ) - ) - input_clip = "right1" - - ffmpeg_rear_camera = "" - camera = "Rear" - if layout_settings.cameras(camera).include: - ffmpeg_rear_camera = ( - "setpts=PTS-STARTPTS, " - # "crop=512:798:225:26, " - "scale={clip_width}x{clip_height} {mirror}{options}" - " [rear]".format( - clip_width=layout_settings.cameras(camera).width, - clip_height=layout_settings.cameras(camera).height, - mirror=mirror_sides, - options=layout_settings.cameras(camera).options, - ) - ) - ffmpeg_video_position = ( - ffmpeg_video_position - + ";[{input_clip}][rear] overlay=eof_action=pass:repeatlast=0:" - "x={x_pos}:y={y_pos} [rear1]".format( - input_clip=input_clip, - x_pos=layout_settings.cameras(camera).xpos, - y_pos=layout_settings.cameras(camera).ypos, - ) - ) - input_clip = "rear1" + input_clip = f"{camera}1" # Text Overlay text_overlay_format = ( @@ -4356,10 +4356,7 @@ def main() -> int: "movflags_faststart": not args.faststart, "input_clip": input_clip, "other_params": ffmpeg_params, - "left_camera": ffmpeg_left_camera, - "front_camera": ffmpeg_front_camera, - "right_camera": ffmpeg_right_camera, - "rear_camera": ffmpeg_rear_camera, + "cameras": ffmpeg_camera, "start_timestamp": start_timestamp, "end_timestamp": end_timestamp, "start_offset": getattr(args, "start_offset", None),