This commit is contained in:
2026-04-01 21:14:30 -03:00
parent 0f7e4424bc
commit 172f21a845
9 changed files with 139 additions and 106 deletions

View File

@@ -1,15 +1,13 @@
"""
RecordingTracker: monitors the growing recording file and estimates duration.
RecordingTracker: monitors the growing recording file and reports duration.
Polls file size periodically. Uses ffprobe occasionally for accurate
duration calibration. Feeds duration updates to the Timeline.
Probes with ffprobe every cycle. No bitrate estimation — initial burst frames
make calibration unreliable. Falls back to file-size heuristic only when
ffprobe returns nothing (e.g. file too new).
"""
import json
import logging
import subprocess
import time
from pathlib import Path
from threading import Thread
import ffmpeg as ffmpeg_lib
@@ -18,13 +16,12 @@ log = logging.getLogger(__name__)
class RecordingTracker:
"""Tracks a growing recording file and estimates its duration."""
"""Tracks a growing recording file and reports 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
@@ -43,9 +40,6 @@ class RecordingTracker:
log.info("RecordingTracker stopped")
def _poll_loop(self):
probe_interval = 0 # probe on first data
cycles = 0
while not self._stop:
time.sleep(2)
@@ -56,28 +50,31 @@ class RecordingTracker:
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)
duration = self._probe_duration()
if duration and duration > self._duration:
self._duration = duration
log.info("Duration: %.1fs", duration)
if self._on_duration:
self._on_duration(self._duration)
def _probe_duration(self):
"""Use ffprobe to get accurate duration of the recording."""
"""Probe recording duration via ffprobe."""
try:
info = ffmpeg_lib.probe(str(self._path))
duration = float(info.get("format", {}).get("duration", 0))
return duration
# Format-level duration is 0 for fragmented MP4 (empty_moov)
dur = float(info.get("format", {}).get("duration", 0))
if dur > 0:
return dur
# Fragmented MP4: check video stream duration
for stream in info.get("streams", []):
sdur = float(stream.get("duration", 0))
if sdur > 0:
return sdur
except Exception as e:
log.debug("ffprobe failed (file still growing): %s", e)
log.debug("ffprobe failed: %s", e)
# Last resort: file size heuristic (~500kbps for this stream type)
try:
return self._path.stat().st_size / 65_000
except Exception:
return None