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 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-06-04 04:31:05 -07:00
parent 6cddd72792
commit 7f574897c7
2 changed files with 79 additions and 13 deletions

View file

@ -148,6 +148,10 @@ class avconv(devedeng.avbase.avbase):
else: else:
self.final_length = video_length self.final_length = video_length
self.command_var = [] 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") self.command_var.append("avconv")
if start_offset and start_offset > 0: if start_offset and start_offset > 0:
self.command_var.append("-ss") 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 (file_project.width_midle != file_project.original_width) or (file_project.height_midle != file_project.original_height):
if (cmd_line != ""): if (cmd_line != ""):
cmd_line += ",fifo," cmd_line += ",fifo,"
x = int((file_project.width_midle - # MPEG-2 / yuv420p needs even dimensions; round to even so the
file_project.original_width) / 2) # aspect math can't produce an odd pad/crop target (e.g. 1079).
y = int((file_project.height_midle - mid_w = file_project.width_midle - (file_project.width_midle % 2)
file_project.original_height) / 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): if (x > 0) or (y > 0):
cmd_line += "pad=" + str(file_project.width_midle) + ":" + str( cmd_line += "pad=" + str(mid_w) + ":" + str(
file_project.height_midle) + ":" + str(x) + ":" + str(y) + ":0x000000" mid_h) + ":" + str(x) + ":" + str(y) + ":0x000000"
else: else:
cmd_line += "crop=" + str(file_project.width_midle) + ":" + str( # crop x/y are the (positive) top-left offset into the source
file_project.height_midle) + ":" + str(x) + ":" + str(y) 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 (file_project.width_final != file_project.width_midle) or (file_project.height_final != file_project.height_midle):
if (cmd_line != ""): if (cmd_line != ""):

View file

@ -176,7 +176,20 @@ class ffmpeg(devedeng.executor.executor):
else: else:
self.final_length = video_length self.final_length = video_length
self.command_var = [] 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") 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 # input seek (for splitting a long movie across discs); placed before
# -i so ffmpeg seeks quickly and the output starts at offset 0 # -i so ffmpeg seeks quickly and the output starts at offset 0
if start_offset and start_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 (file_project.width_midle != file_project.original_width) or (file_project.height_midle != file_project.original_height):
if (cmd_line != ""): if (cmd_line != ""):
cmd_line += self._filter_sep cmd_line += self._filter_sep
x = int((file_project.width_midle - file_project.original_width) / 2) # MPEG-2 / yuv420p needs even dimensions; the aspect math can
y = int((file_project.height_midle - file_project.original_height) / 2) # 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): 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: 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 (file_project.width_final != file_project.width_midle) or (file_project.height_final != file_project.height_midle):
if (cmd_line != ""): if (cmd_line != ""):
@ -706,8 +725,48 @@ class ffmpeg(devedeng.executor.executor):
return (final_path) return (final_path)
def process_stdout(self, data): 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 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): def process_stderr(self, data):