working the player
This commit is contained in:
83
cht/stream/tracker.py
Normal file
83
cht/stream/tracker.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user