def posterize(files, posterfile=None, background_color="black", margin=5): min_h = max_height(files) min_w = max_width(files) util.logger.debug("Max W x H = %d x %d", min_w, min_h) gap = (min_w * margin) // 100 nb_files = len(files) root = math.sqrt(nb_files) rows = int(round(root)) if rows < root: rows += 1 cols = (nb_files + rows-1) // rows full_w = (cols*min_w) + (cols+1)*gap full_h = (rows*min_h) + (rows+1)*gap util.logger.debug("W x H = %d x %d / Gap = %d / c,r = %d, %d => Full W x H = %d x %d", min_w, min_h, gap, cols, rows, full_w, full_h) bgfile = "white-square.jpg" if background_color == "white" else "black-square.jpg" tmpbg = "bg.tmp.jpg" rescale(bgfile, full_w, full_h, tmpbg) file_list = util.build_ffmpeg_file_list(files) cmplx = util.build_ffmpeg_complex_prep(files) cmplx = cmplx + __build_poster_fcomplex(rows, cols, gap, min_w, min_h, len(files)) posterfile = util.automatic_output_file_name(posterfile, files[0], "poster") util.run_ffmpeg('-i "%s" %s -filter_complex "%s" "%s"' % (tmpbg, file_list, cmplx, posterfile)) util.logger.info("Generated %s", posterfile) util.delete_files(tmpbg) return posterfile
def shake_horizontal(self, nbr_slices = 10 , shake_pct = 3, background_color = "black", out_file = None): w, h = self.get_dimensions() w_jitter = w * shake_pct // 100 slice_height = max(h // nbr_slices, 16) slices = self.slice_horizontal(nbr_slices) tmpbg = get_rectangle(background_color, w + w_jitter, slice_height * len(slices)) filelist = util.build_ffmpeg_file_list(slices) + ' -i "%s"' % tmpbg cmplx = util.build_ffmpeg_complex_prep(slices) step = 0 n_slices = len(slices) cmplx = cmplx + "[%d][pip0]overlay=0:0[step%d]; " % (n_slices, step) first_slice = slices.pop(0) for j in range(n_slices): x = random.randint(1, w_jitter) y = (j+1) * slice_height cmplx = cmplx + "[step%d][pip%d]overlay=%d:%d" % (j, j+1, x, y) if j < n_slices-1: cmplx = cmplx + '[step%d]; ' % (j+1) j = j+1 out_file = util.automatic_output_file_name(out_file, self.filename, "shake") util.run_ffmpeg(' %s -filter_complex "%s" %s' % (filelist, cmplx, out_file)) util.delete_files(*slices, first_slice, tmpbg) return out_file
def encode(self, target_file, profile, **kwargs): '''Encodes a file - target_file is the name of the output file. Optional - Profile is the encoding profile as per the VideoTools.properties config file - **kwargs accepts at large panel of other ptional options''' util.logger.debug("Encoding %s with profile %s and args %s", self.filename, profile, str(kwargs)) if target_file is None: target_file = media.build_target_file(self.filename, profile) media_opts = self.get_properties() util.logger.debug("Input file settings = %s", str(media_opts)) media_opts.update( util.get_ffmpeg_cmdline_params( util.get_conf_property(profile + '.cmdline'))) util.logger.debug("After profile settings(%s) = %s", profile, str(media_opts)) media_opts.update(kwargs) util.logger.debug("After cmd line settings(%s) = %s", str(kwargs), str(media_opts)) ffopts = opt.media2ffmpeg(media_opts) util.logger.debug("FFOPTS = %s", str(ffopts)) util.run_ffmpeg('-i "%s" %s "%s"' % (self.filename, util.dict2str(ffopts), target_file)) util.logger.info("File %s encoded", target_file) return target_file
def crop(self, width, height, top, left, out_file, **kwargs): ''' Applies crop video filter for width x height pixels ''' if width is str: width = int(width) if height is str: height = int(height) i_bitrate = self.get_video_bitrate() i_w, i_h = self.get_dimensions() media_opts = self.get_properties() media_opts[opt.media.ACODEC] = 'copy' # Target bitrate proportional to crop level (+ 20%) media_opts[opt.media.VBITRATE] = int( int(i_bitrate) * (width * height) / (int(i_w) * int(i_h)) * 1.2) media_opts.update(util.cleanup_options(kwargs)) util.logger.info("Cmd line settings = %s", str(media_opts)) out_file = util.automatic_output_file_name(out_file, self.filename, \ "crop_{0}x{1}-{2}x{3}".format(width, height, top, left)) aspect = __get_aspect_ratio__(width, height, **kwargs) cmd = '-i "%s" %s %s -aspect %s "%s"' % (self.filename, \ media.build_ffmpeg_options(media_opts), media.get_crop_filter_options(width, height, top, left), \ aspect, out_file) util.run_ffmpeg(cmd) return out_file
def add_stream_property(self, stream_index, prop, value=None): direct_copy = '-vcodec copy -c:a copy -map 0' output_file = util.add_postfix(self.filename, "meta") if value is None: stream_index, value = stream_index.split(':') util.run_ffmpeg('-i "{0}" {1} -metadata:s:a:{2} {3}="{4}" "{5}"'.format( \ self.filename, direct_copy, stream_index, prop, value, output_file)) return output_file
def set_tracks_property(self, prop, **props): util.logger.debug("Set tracks properties: %s-->%s", prop, str(props)) meta = VideoFile.AV_PASSTHROUGH for idx, propval in props.items(): meta += '-metadata:s:a:{0} {1}="{2}" '.format(idx, prop, propval) output_file = util.add_postfix(self.filename, prop) util.run_ffmpeg('-i "{0}" {1} "{2}"'.format(self.filename, meta.strip(), output_file)) return output_file
def encode(self, target_file, profile, **kwargs): '''Encodes a file - target_file is the name of the output file. Optional - Profile is the encoding profile as per the VideoTools.properties config file - **kwargs accepts at large panel of other ptional options''' util.logger.debug("Encoding %s with profile %s and args %s", self.filename, profile, str(kwargs)) if target_file is None: target_file = media.build_target_file(self.filename, profile) media_opts = {} video_filters = [] media_opts = self.get_properties() util.logger.debug("File settings(%s) = %s", self.filename, str(media_opts)) media_opts.update( util.get_ffmpeg_cmdline_params( util.get_conf_property(profile + '.cmdline'))) util.logger.debug("After profile settings(%s) = %s", profile, str(media_opts)) media_opts.update(kwargs) util.logger.debug("After cmd line settings(%s) = %s", str(kwargs), str(media_opts)) media_opts['input_params'] = '' if 'hw_accel' in kwargs and kwargs['hw_accel'] is True: if re.search(r'[xh]264', media_opts[opt.media.VCODEC]): util.logger.debug("Patching settings for H264 hw acceleration") media_opts[opt.media.VCODEC] = 'h264_nvenc' media_opts['input_params'] = '-hwaccel cuvid -c:v h264_cuvid' elif re.search(r'[xh]265', media_opts[opt.media.VCODEC]): util.logger.debug("Patching settings for H265 hw acceleration") media_opts[opt.media.VCODEC] = 'hevc_nvenc' media_opts['input_params'] = '-hwaccel cuvid -c:v h264_cuvid' if media_opts[ 'input_params'] != '' and opt.media.SIZE in media_opts and media_opts[ opt.media.SIZE] is not None: media_opts['input_params'] += ' -resize ' + media_opts[ opt.media.SIZE] del media_opts[opt.media.SIZE] util.logger.debug("After hw acceleration = %s", str(media_opts)) ffopts = opt.media2ffmpeg(media_opts) util.logger.debug("After converting to ffmpeg params = %s", str(ffopts)) # Hack for channels selection mapping = __get_audio_channel_mapping__(**kwargs) video_filters.append(self.__get_fader_filter__(**kwargs)) util.run_ffmpeg('%s -i "%s" %s %s %s "%s"' % (media_opts['input_params'], self.filename, util.dict2str(ffopts), \ media.build_video_filters_options(video_filters), mapping, target_file)) util.logger.info("File %s encoded", target_file) return target_file
def concat(target_file, file_list): # ffmpeg -i opening.mkv -i episode.mkv -i ending.mkv \ # -filter_complex "[0:v] [0:a] [1:v] [1:a] [2:v] [2:a] concat=n=3:v=1:a=1 [v] [a]" \ # -map "[v]" -map "[a]" output.mkv util.logger.info("Concatenating %s", str(file_list)) cmd = util.build_ffmpeg_file_list(file_list) cmd = cmd + '-filter_complex "' for i in range(len(file_list)): cmd = cmd + ('[%d:v] [%d:a] ' % (i, i)) cmd = cmd + 'concat=n=%d:v=1:a=1 [v] [a]" -map "[v]" -map "[a]" %s' % (len(file_list), target_file) util.run_ffmpeg(cmd)
def blindify(self, out_file = None, **kwargs): nbr_slices = int(kwargs.pop('blinds', 10)) blinds_size_pct = int(kwargs.pop('blinds_ratio', 3)) background_color = kwargs.pop('background_color', 'black') direction = kwargs.pop('direction', 'vertical') w, h = self.get_dimensions() w_gap = w * blinds_size_pct // 100 h_gap = h * blinds_size_pct // 100 if direction == 'horizontal': tmpbg = get_rectangle(background_color, w, (h//nbr_slices*nbr_slices) + h_gap*(nbr_slices-1)) else: tmpbg = get_rectangle(background_color, (w//nbr_slices*nbr_slices) + w_gap*(nbr_slices-1), h) # ffmpeg -i file1.jpg -i file2.jpg -i bg.tmp.jpg \ # -filter_complex "[0]scale=iw:-1:flags=lanczos[pip0]; \ # [1]scale=iw:-1:flags=lanczos[pip1]; \ # [8]scale=iw:-1:flags=lanczos[pip8]; \ # [9][pip0]overlay=204:204[step0] ; \ # [step0][pip1]overlay=2456:204[step1]; \ # [step7][pip8]overlay=4708:3374" outfile.jpg slices = self.slice(nbr_slices, direction) filelist = util.build_ffmpeg_file_list(slices) filelist = filelist + ' -i "%s"' % tmpbg cmplx = util.build_ffmpeg_complex_prep(slices) i = 0 cmplx = '' for slicefile in slices: cmplx = cmplx + "[%d]scale=iw:-1:flags=lanczos[pip%d]; " % (i, i) i = i + 1 step = 0 cmplx = cmplx + "[%d][pip0]overlay=0:0[step%d]; " % (i, step) first_slice = slices.pop(0) j = 0 x = 0 y = 0 for slicefile in slices: if direction == 'horizontal': y = (j+1) * (h // nbr_slices + h_gap) else: x = (j+1) * (w // nbr_slices + w_gap) cmplx = cmplx + "[step%d][pip%d]overlay=%d:%d" % (j, j+1, x, y) if slicefile != slices[len(slices)-1]: cmplx = cmplx + '[step%d]; ' % (j+1) j = j+1 out_file = util.automatic_output_file_name(out_file, self.filename, "blind") util.run_ffmpeg('%s -filter_complex "%s" %s' % (filelist, cmplx, out_file)) util.delete_files(*slices, first_slice, tmpbg)
def add_audio_tracks(self, *audio_files): inputs = '-i "{0}"'.format(self.filename) maps = '-map 0' i = 1 for audio_file in audio_files: inputs += ' -i "{0}"'.format(audio_file) maps += ' -map {0}'.format(i) i += 1 output_file = util.add_postfix(self.filename, "muxed") util.run_ffmpeg('{0} {1} -dn -codec copy "{2}"'.format( inputs, maps, output_file)) return output_file
def add_metadata(self, **metadatas): # ffmpeg -i in.mp4 -vcodec copy -c:a copy -map 0 -metadata year=<year> # -metadata copyright="(c) O. Korach <year>" -metadata author="Olivier Korach" # -metadata:s:a:0 language=fre -metadata:s:a:0 title="Avec musique" # -metadata:s:v:0 language=fre -disposition:a:0 default -disposition:a:1 none "%~1.meta.mp4" util.logger.debug("Add metadata: %s", str(metadatas)) opts = VideoFile.AV_PASSTHROUGH for key, value in metadatas.items(): opts += '-metadata {0}="{1}" '.format(key, value) output_file = util.add_postfix(self.filename, "meta") util.run_ffmpeg('-i "{0}" {1} "{2}"'.format(self.filename, opts.strip(), output_file)) return output_file
def set_default_track(self, track): # ffmpeg -i in.mp4 -vcodec copy -c:a copy -map 0 # -disposition:a:0 default -disposition:a:1 none out.mp4 util.logger.debug("Set default track: %s", track) disp = VideoFile.AV_PASSTHROUGH for i in range(self.__get_number_of_audio_tracks() + 1): util.logger.debug("i = %d, nb tracks = %d", i, self.__get_number_of_audio_tracks()) is_default = "default" if i == track else "none" disp += "-disposition:a:{0} {1} ".format(i, is_default) output_file = util.add_postfix(self.filename, "track") util.run_ffmpeg('-i "{0}" {1} "{2}"'.format(self.filename, disp.strip(), output_file)) return output_file
def concat(target_file, file_list): '''Concatenates several video files - They must have same video+audio format and bitrate''' util.logger.info("%s = %s", target_file, ' + '.join(file_list)) cmd = '' for file in file_list: cmd += (' -i "%s" ' % file) count = 0 cmd += '-filter_complex "' for file in file_list: cmd += ("[%d:v][%d:a]" % (count, count)) count += 1 cmd += 'concat=n=%d:v=1:a=1[outv][outa]" -map "[outv]" -map "[outa]" "%s"' % ( count, target_file) util.run_ffmpeg(cmd.strip())
def encode_album_art(source_file, album_art_file, **kwargs): """Encodes album art image in an audio file after optionally resizing""" # profile = 'album_art' - # For the future, we'll use the cmd line associated to the profile in the config file album_art_std_settings = '-metadata:s:v title="Album cover" -metadata:s:v comment="Cover (Front)"' target_file = util.add_postfix(source_file, 'album_art') if kwargs['scale'] is not None: w, h = re.split("x", kwargs['scale']) album_art_file = image.rescale(album_art_file, int(w), int(h)) delete_aa_file = True # ffmpeg -i %1 -i %2 -map 0:0 -map 1:0 -c copy -id3v2_version 3 -metadata:s:v title="Album cover" # -metadata:s:v comment="Cover (Front)" %1.mp3 util.run_ffmpeg('-i "%s" -i "%s" -map 0:0 -map 1:0 -c copy -id3v2_version 3 %s "%s"' % \ (source_file, album_art_file, album_art_std_settings, target_file)) shutil.copy(target_file, source_file) os.remove(target_file) if delete_aa_file: os.remove(album_art_file)
def resize(self, width = None, height = None, out_file = None): '''Resizes an image file If one of width or height is None, then it is calculated to preserve the image aspect ratio''' if width is None and height is None: util.logger.error("Resize requested with neither width not height") return None if isinstance(width, str): width = int(width) if isinstance(height, str): height = int(height) if width is None: w, h = self.get_dimensions() width = w * height // h elif height is None: w, h = self.get_dimensions() height = h * width // w util.logger.debug("Resizing %s to %d x %d into %s", self.filename, width, height, out_file) out_file = util.automatic_output_file_name(out_file, self.filename, "resized-%dx%d" % (width, height)) util.run_ffmpeg('-i "%s" -vf scale=%d:%d "%s"' % (self.filename, width, height, out_file)) return out_file
def stack(file1, file2, direction, out_file = None): util.logger.debug("stack(%s, %s, %s, _)", file1, file2, direction) if not util.is_image_file(file1): raise media.FileTypeError('File %s is not an image file' % file1) if not util.is_image_file(file2): raise media.FileTypeError('File %s is not an image file' % file2) out_file = util.automatic_output_file_name(out_file, file1, "stacked") w1, h1 = ImageFile(file1).get_dimensions() w2, h2 = ImageFile(file2).get_dimensions() tmpfile1 = file1 tmpfile2 = file2 util.logger.debug("Images dimensions: %d x %d and %d x %d", w1, h1, w2, h2) if direction == 'horizontal': filter_name = 'hstack' if h1 > h2: new_w2 = w2 * h1 // h2 tmpfile2 = rescale(file2, new_w2, h1) elif h2 > h1: new_w1 = w1 * h2 // h1 tmpfile1 = rescale(file1, new_w1, h2) else: filter_name = 'vstack' if w1 > w2: new_h2 = h2 * w1 // w2 tmpfile2 = rescale(file2, w1, new_h2) elif w2 > w1: new_h1 = h1 * w2 // w1 tmpfile1 = rescale(file1, w2, new_h1) # ffmpeg -i a.jpg -i b.jpg -filter_complex hstack output util.run_ffmpeg('-i "%s" -i "%s" -filter_complex %s "%s"' % (tmpfile1, tmpfile2, filter_name, out_file)) if tmpfile1 is not file1: util.delete_files(tmpfile1) if tmpfile2 is not file2: util.delete_files(tmpfile2) return out_file
def cut(self, start, stop, out_file=None, **kwargs): if out_file is None: out_file = util.automatic_output_file_name( out_file, self.filename, "cut_%s-to-%s" % (start, stop)) util.logger.debug("Cutting %s from %s to %s into %s", self.filename, start, stop, out_file) media_opts = self.get_properties() kwargs['start'] = start kwargs['stop'] = stop video_filters = [] if 'fade' in kwargs and kwargs['fade'] is not None: fade_d = int(kwargs['fade']) fmt = "fade=type={0}:duration={1}:start_time={2}" fader = fmt.format('in', fade_d, start) + "," + fmt.format( 'out', fade_d, stop - fade_d) video_filters.append(fader) util.run_ffmpeg( '-i "%s" %s %s "%s"' % (self.filename, opt.media2ffmpeg(media_opts), media.build_video_filters_options(video_filters), out_file)) return out_file
def deshake(self, width, height, out_file, **kwargs): ''' Applies deshake video filter for width x height pixels ''' media_opts = self.get_properties() media_opts.update({ opt.media.DEINTERLACE: None, opt.media.ASPECT: self.get_aspect_ratio() }) media_opts.update(util.cleanup_options(kwargs)) if out_file is None or 'nocrop' in kwargs: output_file = util.add_postfix(self.filename, "deshake_%dx%d" % (width, height)) else: output_file = out_file ffopts = opt.media2ffmpeg(media_opts) cmd = '-i "%s" %s %s "%s"' % (self.filename, \ util.dict2str(ffopts), get_deshake_filter_options(width, height), output_file) util.run_ffmpeg(cmd) if 'nocrop' not in kwargs: return output_file new_w = self.get_width() - width new_h = self.get_height() - height if out_file is None: output_file2 = util.add_postfix( self.filename, "deshake_crop_%dx%d" % (new_w, new_h)) else: output_file2 = out_file deshake_file_o = VideoFile(output_file) kwargs.update({opt.media.ASPECT: self.get_aspect_ratio()}) deshake_file_o.crop(new_w, new_h, width // 2, height // 2, output_file2, **kwargs) os.remove(output_file) return output_file2
def rescale(image_file, width, height, out_file = None): util.logger.debug("Rescaling %s to %d x %d into %s", image_file, width, height, out_file) # ffmpeg -i input.jpg -vf scale=320:240 output_320x240.png out_file = util.automatic_output_file_name(out_file, image_file, "scale-%dx%d" % (width, height)) util.run_ffmpeg('-i "%s" -vf scale=%d:%d "%s"' % (image_file, width, height, out_file)) return out_file
def crop(self, w, h, x, y, out_file = None): util.logger.debug("%s(->%s, %d, %d, %d, %d)", 'crop', self.filename, w, h, x, y) out_file = util.automatic_output_file_name(out_file, self.filename, "crop.%dx%d" % (w, h)) # ffmpeg -i input.png -vf "crop=w:h:x:y" input_crop.png util.run_ffmpeg('-y -i "%s" -vf crop=%d:%d:%d:%d "%s"' % (self.filename, w, h, x, y, out_file))