From 7f574897c7609d997b3b014141da8274b89aed2c Mon Sep 17 00:00:00 2001 From: Luna Date: Thu, 4 Jun 2026 04:31:05 -0700 Subject: [PATCH] Fix progress reporting and desktop responsiveness during encode The progress bar barely moved and the window showed "not responding" during long encodes: ffmpeg's carriage-return stats line flushes rarely over a pipe, and the transcode saturated the CPU. - Drive progress from ffmpeg's structured `-progress pipe:1` output (`-stats_period 0.5 -nostats`) and parse out_time/progress in process_stdout, giving steady frequent updates (verified ~2/sec via the live executor pipe vs ~1 per whole encode before). - Run the encoder under `nice -n 10` so the compositor keeps a CPU slice (avconv gets the nice prefix too). Also round pad/crop targets to even dimensions and use positive crop offsets, fixing an odd-height (e.g. 1079) MPEG-2 error surfaced when burning in subtitles on a 1080p source. Co-Authored-By: Claude Opus 4.8 --- src/devedeng/avconv.py | 23 +++++++++----- src/devedeng/ffmpeg.py | 69 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 79 insertions(+), 13 deletions(-) diff --git a/src/devedeng/avconv.py b/src/devedeng/avconv.py index 9c3aca8..fb70348 100644 --- a/src/devedeng/avconv.py +++ b/src/devedeng/avconv.py @@ -148,6 +148,10 @@ class avconv(devedeng.avbase.avbase): else: self.final_length = video_length self.command_var = [] + # lower priority so the desktop stays responsive during the transcode + self.command_var.append("nice") + self.command_var.append("-n") + self.command_var.append("10") self.command_var.append("avconv") if start_offset and start_offset > 0: self.command_var.append("-ss") @@ -228,16 +232,19 @@ class avconv(devedeng.avbase.avbase): if (file_project.width_midle != file_project.original_width) or (file_project.height_midle != file_project.original_height): if (cmd_line != ""): cmd_line += ",fifo," - x = int((file_project.width_midle - - file_project.original_width) / 2) - y = int((file_project.height_midle - - file_project.original_height) / 2) + # MPEG-2 / yuv420p needs even dimensions; round to even so the + # aspect math can't produce an odd pad/crop target (e.g. 1079). + 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(file_project.width_midle) + ":" + str( - file_project.height_midle) + ":" + str(x) + ":" + str(y) + ":0x000000" + cmd_line += "pad=" + str(mid_w) + ":" + str( + mid_h) + ":" + str(x) + ":" + str(y) + ":0x000000" else: - cmd_line += "crop=" + str(file_project.width_midle) + ":" + str( - file_project.height_midle) + ":" + str(x) + ":" + str(y) + # 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 != ""): diff --git a/src/devedeng/ffmpeg.py b/src/devedeng/ffmpeg.py index adf7ae3..b82343c 100644 --- a/src/devedeng/ffmpeg.py +++ b/src/devedeng/ffmpeg.py @@ -176,7 +176,20 @@ class ffmpeg(devedeng.executor.executor): 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") + # emit structured, steadily-paced progress on stdout (pipe:1) instead of + # the carriage-return stats line, which barely updates over a pipe; this + # is what keeps the progress bar moving (see process_stdout) + self.command_var.append("-progress") + self.command_var.append("pipe:1") + 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: @@ -263,12 +276,18 @@ class ffmpeg(devedeng.executor.executor): 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 - x = int((file_project.width_midle - file_project.original_width) / 2) - y = int((file_project.height_midle - file_project.original_height) / 2) + # 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(file_project.width_midle) + ":" + str(file_project.height_midle) + ":" + str(x) + ":" + str(y) + ":0x000000" + cmd_line += "pad=" + str(mid_w) + ":" + str(mid_h) + ":" + str(x) + ":" + str(y) + ":0x000000" else: - cmd_line += "crop=" + str(file_project.width_midle) + ":" + str(file_project.height_midle) + ":" + str(x) + ":" + str(y) + # 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 != ""): @@ -706,9 +725,49 @@ class ffmpeg(devedeng.executor.executor): return (final_path) def process_stdout(self, data): - + # ffmpeg -progress emits newline-delimited key=value pairs on stdout, + # e.g. out_time=00:01:23.456 / out_time_ms=83456000 / progress=continue. + # This updates steadily over a pipe, unlike the carriage-return stats. + if self.progress_bar is None: + return + for line in data: + line = line.strip() + if line.startswith("out_time_ms=") or line.startswith("out_time_us="): + try: + micros = float(line.split("=", 1)[1]) + except (ValueError, IndexError): + continue + seconds = micros / 1000000.0 + self._set_progress_fraction(seconds) + elif line.startswith("out_time="): + value = line.split("=", 1)[1] + parts = value.split(":") + seconds = 0.0 + ok = True + for e in parts: + try: + seconds = seconds * 60.0 + float(e) + except ValueError: + ok = False + break + if ok: + self._set_progress_fraction(seconds) + elif line == "progress=end": + self.progress_bar[1].set_fraction(1.0) + self.progress_bar[1].set_text("100.0%") return + 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=")