#!/usr/bin/env python3 # Copyright 2014 (C) Raster Software Vigo (Sergio Costas) # # This file is part of DeVeDe-NG # # DeVeDe-NG is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3 # as published by the Free Software Foundation. # # DeVeDe-NG is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see import subprocess import os import re from gi.repository import GLib import devedeng.configuration_data import devedeng.executor import devedeng.mux_dvd_menu import devedeng.project class ffmpeg(devedeng.executor.executor): supports_analize = False supports_play = False supports_convert = True supports_menu = True supports_mkiso = False supports_burn = False display_name = "FFMPEG" disc_types = [] @staticmethod def check_is_installed(): try: handle = subprocess.Popen( ["ffmpeg", "-codecs"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) (stdout, stderr) = handle.communicate() if 0 == handle.wait(): mp2 = False mp3 = False ac3 = False mpeg1 = False mpeg2 = False divx = False h264 = False hevc = False for line in stdout.decode("latin-1").split("\n"): parts = line.strip().split(" ") if len(parts) < 2: continue if len(parts[0]) != 6: continue capabilities = parts[0] codec = parts[1] if capabilities[1] != 'E': continue if (codec == "mpeg1video"): mpeg1 = True continue if (codec == "mpeg2video"): mpeg2 = True continue if (codec == "mp2"): mp2 = True continue if (codec == "mp3"): mp3 = True continue if (codec == "ac3"): ac3 = True continue if (codec == "h264") or (codec == "H264"): h264 = True continue if (codec == "hevc") or (codec == "HEVC"): hevc = True continue if (codec == "mpeg4"): divx = True continue if (mpeg1 and mp2): devedeng.ffmpeg.ffmpeg.disc_types.append("vcd") if (mpeg2 and mp2): devedeng.ffmpeg.ffmpeg.disc_types.append("svcd") devedeng.ffmpeg.ffmpeg.disc_types.append("cvd") if (mpeg2 and mp2 and ac3): devedeng.ffmpeg.ffmpeg.disc_types.append("dvd") if (divx and mp3): devedeng.ffmpeg.ffmpeg.disc_types.append("divx") if (h264 and mp3): devedeng.ffmpeg.ffmpeg.disc_types.append("mkv") if (hevc and mp3): devedeng.ffmpeg.ffmpeg.disc_types.append("hevc") return True else: return False except (OSError, subprocess.SubprocessError): return False def __init__(self): devedeng.executor.executor.__init__(self) self.config = devedeng.configuration_data.configuration.get_config() try: version_number = 7 handle = subprocess.Popen( ["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) (stdout, stderr) = handle.communicate() if 0 == handle.wait(): for line in stdout.decode("latin-1").split("\n"): line = line.strip() if line.startswith("ffmpeg version "): version_number = int(re.search(r"[0-9]+", line).group()) break except (OSError, subprocess.SubprocessError, ValueError, AttributeError): version_number = 7 pass print(f"Detected ffmpeg version number: {version_number}") if version_number <= 6: self._filter_sep = ",fifo," else: self._filter_sep = "," def convert_file(self, file_project, output_file, video_length, pass2=False, start_offset=0): self.start_offset = start_offset if file_project.two_pass_encoding: if pass2: self.text = _("Converting %(X)s (pass 2)") % { "X": file_project.title_name} else: self.text = _("Converting %(X)s (pass 1)") % { "X": file_project.title_name} # Prepare the converting process for the second pass tmp = devedeng.ffmpeg.ffmpeg() tmp.convert_file(file_project, output_file, video_length, True, start_offset) # it deppends of this process tmp.add_dependency(self) # add it as a child process of this one self.add_child_process(tmp) else: self.text = _("Converting %(X)s") % {"X": file_project.title_name} if (pass2 == False) and (file_project.two_pass_encoding == True): # this is the first pass in a 2-pass codification second_pass = False else: # second_pass is TRUE in the second pass of a 2-pass codification, and also when not doing 2-pass codification # It is used to remove unnecessary steps during the first pass, but # that are needed on the second pass, or when not using 2-pass # codification second_pass = True # whether this encode burns in an image-based (PGS/VOBSUB) subtitle via # an overlay filtergraph; only applies on the (final) encoding pass and # when we are actually re-encoding the video overlay_subs = (second_pass and (not file_project.no_reencode_audio_video) and self._image_overlay_active(file_project)) if (video_length == 0): self.final_length = file_project.original_length else: self.final_length = video_length self.command_var = [] # run the encoder at a lower priority so the desktop/GUI stays # responsive while a heavy transcode saturates the CPU self.command_var.append("nice") self.command_var.append("-n") self.command_var.append("10") self.command_var.append("ffmpeg") # Write structured, steadily-paced progress to a FILE (never a pipe): # a file write can't block the encoder, so a wedged or crashed GUI can # never deadlock ffmpeg on backpressure. The app polls this file on a # timer to update the progress bar (see start_progress_polling). self.progress_file = output_file + ".progress" try: # start clean so we never read a previous run's position if os.path.exists(self.progress_file): os.remove(self.progress_file) except OSError: pass self.command_var.append("-progress") self.command_var.append(self.progress_file) self.command_var.append("-stats_period") self.command_var.append("0.5") self.command_var.append("-nostats") # input seek (for splitting a long movie across discs); placed before # -i so ffmpeg seeks quickly and the output starts at offset 0 if start_offset and start_offset > 0: self.command_var.append("-ss") self.command_var.append(str(start_offset)) self.command_var.append("-i") self.command_var.append(file_project.file_name) if (file_project.volume != 100) and second_pass: self.command_var.append("-vol") self.command_var.append( str(int((256 * file_project.volume) / 100))) if (file_project.audio_delay != 0.0) and (file_project.copy_sound == False) and (file_project.no_reencode_audio_video == False) and second_pass: self.command_var.append("-itsoffset") self.command_var.append(str(file_project.audio_delay)) if start_offset and start_offset > 0: self.command_var.append("-ss") self.command_var.append(str(start_offset)) self.command_var.append("-i") self.command_var.append(file_project.file_name) if overlay_subs: # video comes from the overlay filtergraph output, mapped later self.command_var.append("-map") self.command_var.append("[vsub]") else: self.command_var.append("-map") self.command_var.append("1:" + str(file_project.video_list[0])) if (not file_project.copy_sound) and (not file_project.no_reencode_audio_video): for l in file_project.audio_list: self.command_var.append("-map") self.command_var.append("0:" + str(l)) if (file_project.no_reencode_audio_video == False) and second_pass: cmd_line = "" if file_project.deinterlace == "deinterlace_yadif": cmd_line += "yadif" vflip = False hflip = False if (file_project.prerotation == 90): if (cmd_line != ""): cmd_line += self._filter_sep cmd_line += "transpose=1" elif (file_project.prerotation == -90) or (file_project.prerotation == 270): if (cmd_line != ""): cmd_line += self._filter_sep cmd_line += "transpose=2" elif (file_project.prerotation == 180): if (cmd_line != ""): cmd_line += self._filter_sep vflip = not vflip hflip = not hflip if (file_project.rotation == "rotation_90"): if (cmd_line != ""): cmd_line += self._filter_sep cmd_line += "transpose=1" elif (file_project.rotation == "rotation_270"): if (cmd_line != ""): cmd_line += self._filter_sep cmd_line += "transpose=2" elif (file_project.rotation == "rotation_180"): vflip = not vflip hflip = not hflip if (file_project.mirror_vertical): vflip = not vflip if (file_project.mirror_horizontal): hflip = not hflip if (vflip): if (cmd_line != ""): cmd_line += self._filter_sep cmd_line += "vflip" if (hflip): if (cmd_line != ""): cmd_line += self._filter_sep cmd_line += "hflip" if (file_project.width_midle != file_project.original_width) or (file_project.height_midle != file_project.original_height): if (cmd_line != ""): cmd_line += self._filter_sep # MPEG-2 / yuv420p needs even dimensions; the aspect math can # yield an odd width/height (e.g. 1079), which makes ffmpeg # error or corrupt a frame. Round the pad/crop target to even. mid_w = file_project.width_midle - (file_project.width_midle % 2) mid_h = file_project.height_midle - (file_project.height_midle % 2) x = int((mid_w - file_project.original_width) / 2) y = int((mid_h - file_project.original_height) / 2) if (x > 0) or (y > 0): cmd_line += "pad=" + str(mid_w) + ":" + str(mid_h) + ":" + str(x) + ":" + str(y) + ":0x000000" else: # crop x/y are the (positive) top-left offset into the source cmd_line += "crop=" + str(mid_w) + ":" + str(mid_h) + ":" + str(abs(x)) + ":" + str(abs(y)) if (file_project.width_final != file_project.width_midle) or (file_project.height_final != file_project.height_midle): if (cmd_line != ""): cmd_line += self._filter_sep cmd_line += "scale=" + \ str(file_project.width_final) + ":" + \ str(file_project.height_final) # --- subtitle burn-in (hardsub) --------------------------------- # Image-based subs (PGS/VOBSUB) must be overlaid via filter_complex # BEFORE scaling, so they stay aligned with the source frame; text # subs are rendered by libass AFTER scaling, as the last -vf entry. if overlay_subs: self._apply_image_overlay(file_project, cmd_line) cmd_line = "" # consumed by filter_complex above else: if second_pass: text_filter = self.get_hardsub_text_filter(file_project) if text_filter is not None: if (cmd_line != ""): cmd_line += self._filter_sep cmd_line += text_filter if cmd_line != "": self.command_var.append("-vf") self.command_var.append(cmd_line) self.command_var.append("-y") vcd = False maxrate = int(file_project.video_rate_final * 1500) minrate = int(file_project.video_rate_final * 666.66) if (self.config.disc_type == "divx"): self.command_var.append("-vcodec") self.command_var.append("mpeg4") self.command_var.append("-acodec") self.command_var.append("libmp3lame") self.command_var.append("-f") self.command_var.append("avi") if not pass2: self.command_var.append("-maxrate:v") self.command_var.append(str(maxrate)) self.command_var.append("-minrate:v") if minrate < (devedeng.project.devede_project.divx_min_bps * 1000): minrate = devedeng.project.devede_project.divx_min_bps * 1000 self.command_var.append(str(minrate)) elif (self.config.disc_type == "mkv") or (self.config.disc_type == "hevc"): self.command_var.append("-vcodec") self.command_var.append("h264" if self.config.disc_type == "mkv" else "hevc") self.command_var.append("-acodec") self.command_var.append("libmp3lame") self.command_var.append("-f") self.command_var.append("matroska") if not pass2: self.command_var.append("-maxrate:v") self.command_var.append(str(maxrate)) self.command_var.append("-minrate:v") if minrate < (devedeng.project.devede_project.mkv_min_bps * 1000): minrate = devedeng.project.devede_project.mkv_min_bps * 1000 self.command_var.append(str(minrate)) elif (self.config.disc_type == "dvd"): if not file_project.no_reencode_audio_video: self.command_var.append("-c:v") self.command_var.append("mpeg2video") if not file_project.copy_sound: if file_project.sound5_1: self.command_var.append("-c:a") self.command_var.append("ac3") else: self.command_var.append("-c:a") # PAL always uses AC3; for NTSC the original used MP2, # but the DVD-safe profiles use AC3 (matches the known- # good baseline and is more broadly player-compatible) profile = getattr(self.config, "encode_profile", "compatibility") if file_project.format_pal or profile in ( "compatibility", "high", "custom"): self.command_var.append("ac3") else: self.command_var.append("mp2") self.command_var.append("-f") self.command_var.append("dvd") self.command_var.append("-r") if file_project.format_pal: self.command_var.append("25") else: if (file_project.original_fps == 24): self.command_var.append("24000/1001") else: self.command_var.append("30000/1001") self.command_var.append("-pix_fmt") self.command_var.append("yuv420p") self.command_var.append("-maxrate:v") if maxrate > (devedeng.project.devede_project.dvd_max_bps_base[0] * 1000): maxrate = devedeng.project.devede_project.dvd_max_bps_base[0] * 1000 self.command_var.append(str(maxrate)) if not pass2: self.command_var.append("-minrate:v") if minrate < (devedeng.project.devede_project.dvd_min_bps * 1000): minrate = devedeng.project.devede_project.dvd_min_bps * 1000 self.command_var.append(str(minrate)) self.command_var.append("-bufsize") self.command_var.append("1835008") self.command_var.append("-packetsize") self.command_var.append("2048") self.command_var.append("-muxrate") self.command_var.append("10080000") self.command_var.append("-ar") self.command_var.append("48000") elif (self.config.disc_type == "vcd"): vcd = True if not file_project.no_reencode_audio_video: self.command_var.append("-c:v") self.command_var.append("mpeg1video") if not file_project.copy_sound: self.command_var.append("-c:a") self.command_var.append("mp2") self.command_var.append("-b:v") self.command_var.append("1150000") self.command_var.append("-b:a") self.command_var.append("224000") self.command_var.append("-f") self.command_var.append("vcd") self.command_var.append("-r") if file_project.format_pal: self.command_var.append("25") else: if (file_project.original_fps == 24): self.command_var.append("24000/1001") else: self.command_var.append("30000/1001") self.command_var.append("-g") if file_project.format_pal: self.command_var.append("15") else: self.command_var.append("18") self.command_var.append("-s") if file_project.format_pal: self.command_var.append("352x288") else: self.command_var.append("352x240") self.command_var.append("-maxrate:v") self.command_var.append("1150000") self.command_var.append("-minrate:v") self.command_var.append("1150000") self.command_var.append("-bufsize") self.command_var.append("327680") self.command_var.append("-packetsize") self.command_var.append("2324") self.command_var.append("-muxrate") self.command_var.append("1411200") self.command_var.append("-ar") self.command_var.append("44100") elif (self.config.disc_type == "svcd") or (self.config.disc_type == "cvd"): if not file_project.no_reencode_audio_video: self.command_var.append("-c:v") self.command_var.append("mpeg2video") if not file_project.copy_sound: self.command_var.append("-c:a") self.command_var.append("mp2") self.command_var.append("-f") self.command_var.append("svcd") self.command_var.append("-r") if file_project.format_pal: self.command_var.append("25") else: if (file_project.original_fps == 24): self.command_var.append("24000/1001") else: self.command_var.append("30000/1001") self.command_var.append("-g") if file_project.format_pal: self.command_var.append("12") else: self.command_var.append("15") self.command_var.append("-s") if self.config.disc_type == "cvd": if file_project.format_pal: self.command_var.append("352x576") else: self.command_var.append("352x480") else: if file_project.format_pal: self.command_var.append("480x576") else: self.command_var.append("480x480") self.command_var.append("-pix_fmt") self.command_var.append("yuv420p") self.command_var.append("-maxrate:v") if maxrate > (devedeng.project.devede_project.svcd_max_bps_base[0] * 1000): maxrate = devedeng.project.devede_project.svcd_max_bps_base[0] * 1000 self.command_var.append(str(maxrate)) if not pass2: self.command_var.append("-minrate:v") if minrate < (devedeng.project.devede_project.svcd_min_bps * 1000): minrate = (devedeng.project.devede_project.svcd_min_bps * 1000) self.command_var.append(str(minrate)) self.command_var.append("-bufsize") self.command_var.append("1835008") self.command_var.append("-packetsize") self.command_var.append("2324") self.command_var.append("-ar") self.command_var.append("44100") self.command_var.append("-scan_offset") self.command_var.append("1") if (not file_project.no_reencode_audio_video): self.command_var.append("-sn") # no subtitles if file_project.copy_sound or file_project.no_reencode_audio_video: self.command_var.append("-c:a") self.command_var.append("copy") if file_project.no_reencode_audio_video: self.command_var.append("-c:v") self.command_var.append("copy") if (not vcd): if not file_project.format_pal: if (file_project.original_fps == 24) and ((self.config.disc_type == "dvd")): keyintv = 15 else: keyintv = 18 else: keyintv = 15 if (not file_project.gop12) and (not(self.config.disc_type == "divx")) and (not(self.config.disc_type == "mkv") and (not(self.config.disc_type == "hevc"))): self.command_var.append("-g") self.command_var.append(str(keyintv)) if (self.config.disc_type == "divx") or (self.config.disc_type == "mkv") or (self.config.disc_type == "hevc"): self.command_var.append("-g") self.command_var.append("300") elif file_project.gop12 and (not file_project.no_reencode_audio_video): self.command_var.append("-g") self.command_var.append("12") self.command_var.append("-bf") self.command_var.append("2") self.command_var.append("-strict") self.command_var.append("1") if video_length != 0: self.command_var.append("-t") self.command_var.append(str(video_length)) self.command_var.append("-ac") if (file_project.sound5_1) and ((self.config.disc_type == "dvd") or (self.config.disc_type == "divx") or (self.config.disc_type == "mkv") or (self.config.disc_type == "hevc")): self.command_var.append("6") else: self.command_var.append("2") self.command_var.append("-aspect") self.command_var.append(str(file_project.aspect_ratio_final)) if self.config.disc_type == "divx": self.command_var.append("-vtag") self.command_var.append("DX50") if (file_project.deinterlace == "deinterlace_ffmpeg") and (file_project.no_reencode_audio_video == False) and second_pass: self.command_var.append("-deinterlace") if (file_project.no_reencode_audio_video == False) and (vcd == False) and second_pass: self.command_var.append("-s") self.command_var.append( str(file_project.width_final) + "x" + str(file_project.height_final)) if second_pass: self.command_var.append("-trellis") self.command_var.append("1") self.command_var.append("-mbd") self.command_var.append("2") else: self.command_var.append("-trellis") self.command_var.append("0") self.command_var.append("-mbd") self.command_var.append("0") if (vcd == False) and (file_project.no_reencode_audio_video == False): self.command_var.append("-b:a") self.command_var.append(str(self._adjust_audio_bitrate(file_project.audio_rate_final)*1000)) self.command_var.append("-b:v") self.command_var.append(str(file_project.video_rate_final*1000)) if file_project.two_pass_encoding == True: self.command_var.append("-passlogfile") self.command_var.append(output_file + ".passlog") self.command_var.append("-pass") if pass2: self.command_var.append("2") else: self.command_var.append("1") self.command_var.append(output_file) print(self.command_var) def _adjust_audio_bitrate(self, bitrate): # includes AC3 rates above the MP2 ceiling (384) so the High profile's # 448k AC3 isn't silently clamped down br = [32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 640] for a in br: if a >= bitrate: return a return br[-1] @staticmethod def _escape_subtitle_path(path): """ Escape a file path for use inside an ffmpeg filtergraph argument. The `subtitles`/`ass` filters parse ':' and '\\' specially, and the whole option may be wrapped in single quotes by the caller. """ # backslash first, then the filtergraph-special characters path = path.replace("\\", "\\\\") path = path.replace(":", "\\:") path = path.replace("'", "\\'") path = path.replace("[", "\\[").replace("]", "\\]") path = path.replace(",", "\\,") return path def _hardsub_relative_index(self, file_project): """ Returns the 0-based position of the chosen hardsub track among the file's subtitle streams (what ffmpeg's `subtitles` filter `si=` expects), or None if no track is selected / found. """ index = getattr(file_project, "hardsub_index", -1) if index is None or index < 0: return None subs = getattr(file_project, "embedded_subtitles", []) or [] for rel, track in enumerate(subs): if track["index"] == index: return rel return None def _image_overlay_active(self, file_project): """ True when an image-based (PGS/VOBSUB) subtitle track is selected for burn-in, which requires an overlay filtergraph rather than -vf. """ if getattr(file_project, "hardsub_kind", "text") != "image": return False return self._hardsub_relative_index(file_project) is not None def _apply_image_overlay(self, file_project, pre_chain): """ Replaces the plain -vf path with a -filter_complex that overlays the bitmap subtitle stream onto the video and then applies the same scaling/padding chain. The chosen subtitle stream is taken from the primary input (index 1, the one used for -map 1:video). Overlay happens at source resolution; `pre_chain` (crop/pad/scale, possibly empty) is applied to the overlaid result so the final size and aspect are identical to the no-subtitle path. """ rel = self._hardsub_relative_index(file_project) video_idx = file_project.video_list[0] # input 1 is the one mapped for video (see -map 1:video above) graph = "[1:%d][1:s:%d]overlay" % (video_idx, rel) if pre_chain: graph += self._filter_sep + pre_chain graph += "[vsub]" self.command_var.append("-filter_complex") self.command_var.append(graph) self._overlay_video_label = "[vsub]" def get_hardsub_text_filter(self, file_project): """ Builds the `subtitles=` filtergraph entry to burn a text-based embedded subtitle track into the picture, or None if not applicable. Must run AFTER scaling so the rendered text matches the DVD size. """ if getattr(file_project, "hardsub_kind", "text") != "text": return None rel = self._hardsub_relative_index(file_project) if rel is None: return None path = self._escape_subtitle_path(file_project.file_name) # original_size used so libass renders at source res; ffmpeg then maps # onto the already-scaled frame. The filter is placed last in the chain. return "subtitles='%s':si=%d" % (path, rel) def create_menu_mpeg(self, n_page, background_music, sound_length, pal, video_rate, audio_rate, output_path, use_mp2, widescreen): self.n_page = n_page self.final_length = float(sound_length) self.text = ("Creating menu %(X)d") % {"X": self.n_page} self.command_var = [] self.command_var.append("ffmpeg") self.command_var.append("-loop") self.command_var.append("1") self.command_var.append("-f") self.command_var.append("image2") self.command_var.append("-i") self.command_var.append(os.path.join( output_path, "menu_" + str(n_page) + "_bg.png")) self.command_var.append("-i") self.command_var.append(background_music) self.command_var.append("-y") self.command_var.append("-target") if pal: self.command_var.append("pal-dvd") else: self.command_var.append("ntsc-dvd") self.command_var.append("-acodec") if use_mp2: self.command_var.append("mp2") if (audio_rate > 384): audio_rate = 384 # max bitrate for mp2 else: self.command_var.append("ac3") self.command_var.append("-s") if pal: self.command_var.append("720x576") else: self.command_var.append("720x480") self.command_var.append("-g") self.command_var.append("12") self.command_var.append("-b:v") self.command_var.append(str(video_rate) + "k") self.command_var.append("-b:a") self.command_var.append(str(self._adjust_audio_bitrate(audio_rate)) + "k") self.command_var.append("-aspect") if widescreen: self.command_var.append("16:9") else: self.command_var.append("4:3") self.command_var.append("-t") self.command_var.append(str(1 + sound_length)) movie_path = os.path.join(output_path, "menu_" + str(n_page) + ".mpg") self.command_var.append(movie_path) muxer = devedeng.mux_dvd_menu.mux_dvd_menu() final_path = muxer.create_mpg(n_page, output_path, movie_path) # the muxer process depends of the converter process muxer.add_dependency(self) self.add_child_process(muxer) return (final_path) def process_stdout(self, data): # progress is read from the -progress file (see start_progress_polling), # not stdout, so nothing to do here return def start_progress_polling(self): """ Poll ffmpeg's -progress file on a GLib timer to drive the progress bar. Reading a file never blocks the encoder, so a wedged or crashed GUI can't deadlock ffmpeg on pipe backpressure. """ self._progress_timer = None if getattr(self, "progress_file", None) is None: return self._progress_timer = GLib.timeout_add(500, self._poll_progress_file) def stop_progress_polling(self): timer = getattr(self, "_progress_timer", None) if timer is not None: GLib.source_remove(timer) self._progress_timer = None # tidy up the temp progress file pf = getattr(self, "progress_file", None) if pf is not None: try: os.remove(pf) except OSError: pass def _poll_progress_file(self): # returning True keeps the GLib timer alive if self.progress_bar is None: return True pf = getattr(self, "progress_file", None) if (pf is None) or (not os.path.exists(pf)): return True try: with open(pf, "r") as f: content = f.read() except OSError: return True # ffmpeg appends progress blocks; the last out_time/progress is current seconds = None ended = False for line in content.splitlines(): line = line.strip() if line.startswith("out_time_ms=") or line.startswith("out_time_us="): try: seconds = float(line.split("=", 1)[1]) / 1000000.0 except (ValueError, IndexError): pass elif line.startswith("out_time="): parts = line.split("=", 1)[1].split(":") acc = 0.0 ok = True for e in parts: try: acc = acc * 60.0 + float(e) except ValueError: ok = False break if ok: seconds = acc elif line == "progress=end": ended = True if seconds is not None: self._set_progress_fraction(seconds) if ended: self.progress_bar[1].set_fraction(1.0) self.progress_bar[1].set_text("100.0%") return True def _set_progress_fraction(self, seconds): if (self.progress_bar is None) or (not self.final_length): return t = seconds / float(self.final_length) if t < 0.0: t = 0.0 if t > 1.0: t = 1.0 self.progress_bar[1].set_fraction(t) self.progress_bar[1].set_text("%.1f%%" % (100.0 * t)) def process_stderr(self, data): pos = data[0].find("time=") if (pos != -1): pos += 5 pos2 = data[0].find(" ", pos) if (pos2 != -1): parts = data[0][pos:pos2].split(":") t = 0.0 for e in parts: try: v = float(e) except ValueError: continue t = t * 60.0 + v t /= self.final_length self.progress_bar[1].set_fraction(t) self.progress_bar[1].set_text("%.1f%%" % (100.0 * t))