""" RecordingTracker: monitors the growing recording file and estimates duration. Polls file size periodically. Uses ffprobe occasionally for accurate duration calibration. Feeds duration updates to the Timeline. """ import json import logging import subprocess import time from pathlib import Path from threading import Thread import ffmpeg as ffmpeg_lib log = logging.getLogger(__name__) class RecordingTracker: """Tracks a growing recording file and estimates its duration.""" def __init__(self, recording_path, on_duration_update=None): self._path = recording_path self._on_duration = on_duration_update self._duration = 0.0 self._avg_bitrate = None # bytes per second, calibrated by ffprobe self._stop = False self._thread = None @property def duration(self): return self._duration def start(self): self._stop = False self._thread = Thread(target=self._poll_loop, daemon=True, name="rec_tracker") self._thread.start() log.info("RecordingTracker started: %s", self._path) def stop(self): self._stop = True log.info("RecordingTracker stopped") def _poll_loop(self): probe_interval = 0 # probe on first data cycles = 0 while not self._stop: time.sleep(2) if not self._path.exists(): continue size = self._path.stat().st_size if size < 10_000: continue # Calibrate with ffprobe every ~30s or on first data cycles += 1 if self._avg_bitrate is None or cycles % 15 == 0: probed = self._probe_duration() if probed and probed > 0 and size > 0: self._avg_bitrate = size / probed self._duration = probed log.info("Probed duration: %.1fs (bitrate: %.0f B/s)", probed, self._avg_bitrate) elif self._avg_bitrate: # Estimate from file size between probes self._duration = size / self._avg_bitrate if self._on_duration and self._duration > 0: self._on_duration(self._duration) def _probe_duration(self): """Use ffprobe to get accurate duration of the recording.""" try: info = ffmpeg_lib.probe(str(self._path)) duration = float(info.get("format", {}).get("duration", 0)) return duration except Exception as e: log.debug("ffprobe failed (file still growing): %s", e) return None