Read ffmpeg progress from a file, not a pipe, to avoid deadlock
Driving -progress over pipe:1 made ffmpeg block on the progress write whenever the GUI stopped draining the pipe (e.g. the parent terminal or the app's event loop wedging), deadlocking every running encode. Write -progress to a temp file instead: a file write never blocks, so the encoder runs to completion regardless of GUI state. The executor calls optional start_/stop_progress_polling hooks; ffmpeg polls the file on a 500ms GLib timer and removes it when done. Verified an encode completes with no reader at all (previously a hard deadlock). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7f574897c7
commit
480ec59ba4
2 changed files with 78 additions and 21 deletions
|
|
@ -109,6 +109,12 @@ class executor(GObject.GObject):
|
|||
self.launch_process(self.command_var)
|
||||
if self.use_pulse_mode != self.pulse_mode:
|
||||
self.set_pulse_mode(self.use_pulse_mode)
|
||||
# let subclasses start any timer-based progress polling (e.g. ffmpeg
|
||||
# reads its -progress file on a GLib timer). No-op by default.
|
||||
try:
|
||||
self.start_progress_polling()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def remove_ansi(self, line):
|
||||
|
||||
|
|
@ -308,6 +314,11 @@ class executor(GObject.GObject):
|
|||
retval = -1
|
||||
|
||||
self.set_pulse_mode(False)
|
||||
# stop any subclass progress polling timer
|
||||
try:
|
||||
self.stop_progress_polling()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# call, if it exists, the post-function
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
import subprocess
|
||||
import os
|
||||
import re
|
||||
from gi.repository import GLib
|
||||
import devedeng.configuration_data
|
||||
import devedeng.executor
|
||||
import devedeng.mux_dvd_menu
|
||||
|
|
@ -182,11 +183,19 @@ class ffmpeg(devedeng.executor.executor):
|
|||
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)
|
||||
# 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("pipe:1")
|
||||
self.command_var.append(self.progress_file)
|
||||
self.command_var.append("-stats_period")
|
||||
self.command_var.append("0.5")
|
||||
self.command_var.append("-nostats")
|
||||
|
|
@ -725,37 +734,74 @@ 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:
|
||||
# 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
|
||||
for line in data:
|
||||
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:
|
||||
micros = float(line.split("=", 1)[1])
|
||||
seconds = float(line.split("=", 1)[1]) / 1000000.0
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
seconds = micros / 1000000.0
|
||||
self._set_progress_fraction(seconds)
|
||||
pass
|
||||
elif line.startswith("out_time="):
|
||||
value = line.split("=", 1)[1]
|
||||
parts = value.split(":")
|
||||
seconds = 0.0
|
||||
parts = line.split("=", 1)[1].split(":")
|
||||
acc = 0.0
|
||||
ok = True
|
||||
for e in parts:
|
||||
try:
|
||||
seconds = seconds * 60.0 + float(e)
|
||||
acc = acc * 60.0 + float(e)
|
||||
except ValueError:
|
||||
ok = False
|
||||
break
|
||||
if ok:
|
||||
self._set_progress_fraction(seconds)
|
||||
seconds = acc
|
||||
elif line == "progress=end":
|
||||
self.progress_bar[1].set_fraction(1.0)
|
||||
self.progress_bar[1].set_text("100.0%")
|
||||
return
|
||||
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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue