saving status before scene frame fix after rust change
This commit is contained in:
@@ -47,6 +47,7 @@ class SessionProcessor:
|
||||
self._threads: dict[str, Thread] = {}
|
||||
self._on_new_frames = None
|
||||
self._on_new_audio = None
|
||||
self._last_scene_capture = 0.0
|
||||
|
||||
self._get_recording_path = None
|
||||
self._get_current_global_offset = None
|
||||
@@ -86,6 +87,98 @@ class SessionProcessor:
|
||||
"""Receive a manually captured frame. Write and index it."""
|
||||
self.on_raw_frame(jpeg_bytes, global_ts)
|
||||
|
||||
def capture_now_from_file(self):
|
||||
"""Extract the current frame from the growing fMP4 (Rust transport mode)."""
|
||||
import tempfile, os as _os
|
||||
|
||||
def _capture():
|
||||
seg = self._get_recording_path() if self._get_recording_path else None
|
||||
if not seg or not seg.exists():
|
||||
log.warning("capture_now: no recording file")
|
||||
return
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
["ffprobe", "-v", "quiet", "-show_entries", "format=duration",
|
||||
"-of", "default=noprint_wrappers=1:nokey=1", str(seg)],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
duration = float(result.stdout.strip())
|
||||
except Exception as e:
|
||||
log.warning("capture_now: could not probe duration: %s", e)
|
||||
return
|
||||
if duration < 1:
|
||||
log.warning("capture_now: recording too short")
|
||||
return
|
||||
timestamp = max(0, duration - 0.5)
|
||||
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
|
||||
tmp_path = Path(tmp.name)
|
||||
try:
|
||||
ff.extract_frame_at(seg, tmp_path, timestamp)
|
||||
if not tmp_path.exists():
|
||||
log.warning("capture_now: frame not written")
|
||||
return
|
||||
jpeg_bytes = tmp_path.read_bytes()
|
||||
except Exception as e:
|
||||
log.error("capture_now failed: %s", e)
|
||||
return
|
||||
finally:
|
||||
try:
|
||||
_os.unlink(tmp_path)
|
||||
except Exception:
|
||||
pass
|
||||
offset = self._get_current_global_offset() if self._get_current_global_offset else 0.0
|
||||
self.on_raw_frame(jpeg_bytes, timestamp + offset)
|
||||
|
||||
Thread(target=_capture, daemon=True, name="capture_now").start()
|
||||
|
||||
def _capture_current_frame(self):
|
||||
"""Capture a fresh frame from the recording file's current tip.
|
||||
|
||||
Called when scene detection triggers. The scene filter's own JPEG
|
||||
is stale (buffered in the encoder), so we extract directly from
|
||||
the fMP4 which is always near-current.
|
||||
"""
|
||||
seg = self._get_recording_path() if self._get_recording_path else None
|
||||
if not seg or not seg.exists():
|
||||
return
|
||||
duration = self._probe_safe_duration(seg)
|
||||
if not duration or duration < 0.5:
|
||||
return
|
||||
local_ts = max(0, duration - 0.3)
|
||||
offset = self._get_current_global_offset() if self._get_current_global_offset else 0.0
|
||||
|
||||
import tempfile, os as _os
|
||||
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
|
||||
tmp_path = Path(tmp.name)
|
||||
try:
|
||||
ff.extract_frame_at(seg, tmp_path, local_ts)
|
||||
if not tmp_path.exists() or tmp_path.stat().st_size == 0:
|
||||
return
|
||||
jpeg_bytes = tmp_path.read_bytes()
|
||||
except Exception as e:
|
||||
log.debug("Scene capture failed: %s", e)
|
||||
return
|
||||
finally:
|
||||
try:
|
||||
_os.unlink(tmp_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.on_raw_frame(jpeg_bytes, local_ts + offset)
|
||||
|
||||
def restart_scene_detector(self, threshold):
|
||||
"""Restart scene detector with a new threshold.
|
||||
|
||||
Kills the running ffmpeg — the detector thread reconnects automatically
|
||||
and picks up the new threshold on the next call to start_scene_detector.
|
||||
"""
|
||||
if "scene_detector" in self._procs:
|
||||
ff.stop_proc(self._procs.pop("scene_detector"), timeout=2)
|
||||
# Spawn a fresh thread with the new threshold; old thread will exit
|
||||
# when its ffmpeg proc dies.
|
||||
self.start_scene_detector(threshold=threshold)
|
||||
|
||||
# -- Frame index --
|
||||
|
||||
@property
|
||||
@@ -134,7 +227,8 @@ class SessionProcessor:
|
||||
Retries on failure (e.g. ffmpeg dies from bad initial frames).
|
||||
The server buffers the latest keyframe so reconnects start clean.
|
||||
"""
|
||||
socket_path = self.session_dir / "stream" / "scene.sock"
|
||||
from cht.config import DATA_DIR
|
||||
socket_path = DATA_DIR / "scene.sock"
|
||||
|
||||
# Wait for the socket to appear (server creates it on session start).
|
||||
while "stop" not in self._stop_flags:
|
||||
@@ -151,6 +245,10 @@ class SessionProcessor:
|
||||
log.exception("Scene detector error")
|
||||
if "stop" in self._stop_flags:
|
||||
break
|
||||
# If the socket is gone, the session ended — don't retry.
|
||||
if not socket_path.exists():
|
||||
log.info("Scene detector: socket gone, session ended")
|
||||
break
|
||||
log.info("Scene detector: reconnecting in 2s...")
|
||||
time.sleep(2.0)
|
||||
|
||||
@@ -163,7 +261,7 @@ class SessionProcessor:
|
||||
try:
|
||||
sock.connect(str(socket_path))
|
||||
except OSError as e:
|
||||
log.error("Scene detector: connect failed: %s", e)
|
||||
log.debug("Scene detector: connect failed: %s", e)
|
||||
return
|
||||
|
||||
log.info("Scene detector: connected, starting ffmpeg")
|
||||
@@ -197,7 +295,7 @@ class SessionProcessor:
|
||||
stdin_t = Thread(target=_feed_stdin, daemon=True, name="scene_stdin")
|
||||
stdin_t.start()
|
||||
|
||||
# Thread: ffmpeg stderr → parse showinfo timestamps
|
||||
# Thread: ffmpeg stderr → parse showinfo timestamps → queue
|
||||
ts_queue = Queue()
|
||||
offset = self._get_current_global_offset() if self._get_current_global_offset else 0.0
|
||||
|
||||
@@ -217,7 +315,9 @@ class SessionProcessor:
|
||||
stderr_t = Thread(target=_read_stderr, daemon=True, name="scene_stderr")
|
||||
stderr_t.start()
|
||||
|
||||
# Main: ffmpeg stdout → extract JPEG frames
|
||||
# Main: read JPEG frames from stdout, pair with stderr timestamps,
|
||||
# skip flush frames. Same proven pattern as StreamRecorder._read_stdout.
|
||||
flush_window = (SCENE_FLUSH_FRAMES + 1) / 30.0
|
||||
last_pts = 0.0
|
||||
buf = b""
|
||||
raw_fd = proc.stdout.fileno()
|
||||
@@ -241,16 +341,16 @@ class SessionProcessor:
|
||||
try:
|
||||
pts_time = ts_queue.get(timeout=2.0)
|
||||
except Empty:
|
||||
log.warning("No timestamp for scene frame")
|
||||
log.warning("No timestamp for scene frame, using 0")
|
||||
pts_time = 0.0
|
||||
|
||||
# Skip flush frames (within 100ms of previous = duplicate)
|
||||
if pts_time - last_pts < 0.1:
|
||||
if pts_time - last_pts < flush_window:
|
||||
log.debug("Skipping flush frame at pts=%.3f", pts_time)
|
||||
continue
|
||||
last_pts = pts_time
|
||||
|
||||
global_ts = pts_time + offset
|
||||
log.debug("Scene frame at pts=%.3f (global=%.3f)", pts_time, global_ts)
|
||||
self.on_raw_frame(jpeg_data, global_ts)
|
||||
|
||||
ff.stop_proc(proc, timeout=3)
|
||||
|
||||
Reference in New Issue
Block a user