import threading import time class ProgressHandler: def __init__(self, callback, duration=None, phase=None): self.callback = callback self.duration = duration self.phase = phase def __call__(self, *args): if len(args) == 2: # for ffmpeg -progress output k, v = args if k != "out_time_us" or not v.isnumeric() or self.duration is None: return p = float(v) / (1.0e6 * self.duration) elif len(args) == 1: (p,) = args else: raise ValueError("Invalid number of arguments") if self.phase is not None: p = {"text": self.phase, "progress": p} self.callback(p) class OpProgress: def __init__(self, total, rate): self.total = total # total number of steps self.rate = rate # number of steps per second self.progress = 0 # number of steps completed self.set = None def __call__(self, p): self.progress = min(self.total, max(0, p)) if self.set is not None: self.set.update() def finish(self): self.progress = self.total class OpProgressFfmpeg(OpProgress): def __call__(self, k, v): if k != "out_time_us" or not v.isnumeric(): return super().__call__(float(v) / 1e6) class ProgressBox: def __init__(self, value): self.lock = threading.Lock() self.condition = threading.Condition(self.lock) self.value = value self.was_updated = False def set(self, value): with self.lock: if self.value is None or value is None or value > self.value: self.value = value self.was_updated = True self.condition.notify_all() def wait(self): with self.lock: while not self.was_updated: self.condition.wait() self.was_updated = False return self.value class OpProgressSet: def __init__(self, callback, debounce=0.1): self._ops = [] self._callback = callback self._debounce = debounce self._last_progress = ProgressBox(None) def _reporter_thread_entry(b, callback, dt): while True: next_progress = b.wait() t0 = time.time() if next_progress is None: return try: callback(100.0 * next_progress) except Exception as e: print(f"Error in progress callback: {e}") remaining_wait = dt - (time.time() - t0) if remaining_wait > 0: time.sleep(remaining_wait) self._reporter_thread = threading.Thread( target=_reporter_thread_entry, args=(self._last_progress, callback, debounce), daemon=True, ) self._reporter_thread.start() def __del__(self): self._last_progress.set(None) self._reporter_thread.join() def add(self, op): self._ops.append(op) op.set = self def update(self): op_times = [op.total / op.rate for op in self._ops] total_time = sum(op_times) progress = sum( op.progress / op.total * t / total_time for op, t in zip(self._ops, op_times) ) self._last_progress.set(progress) class OpProgressMinSet(OpProgressSet): def __init__(self): super().__init__(None) self.set = None @property def progress(self): return min(op.progress for op in self._ops) @property def total(self): return max(op.total for op in self._ops) @total.setter def total(self, value): for op in self._ops: op.total = value @property def rate(self): return min(op.rate for op in self._ops) def finish(self): for op in self._ops: op.finish() def update(self): self.set.update()