try 2
This commit is contained in:
@@ -12,6 +12,7 @@ import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from queue import Queue, Empty
|
||||
from threading import Thread
|
||||
|
||||
from cht.config import (
|
||||
@@ -196,14 +197,13 @@ class StreamManager:
|
||||
start_number = self._next_frame_number()
|
||||
node = ff.receive_record_relay_and_detect(
|
||||
self.stream_url, self.recording_path, self.relay_url,
|
||||
self.frames_dir, scene_threshold=self.scene_threshold,
|
||||
start_number=start_number,
|
||||
scene_threshold=self.scene_threshold,
|
||||
)
|
||||
proc = ff.run_async(node, pipe_stderr=True)
|
||||
proc = ff.run_async(node, pipe_stdout=True, pipe_stderr=True)
|
||||
self._procs["recorder"] = proc
|
||||
log.info("Recorder+scene: pid=%s → %s (threshold=%.2f, start_number=%d)",
|
||||
proc.pid, self.recording_path, self.scene_threshold, start_number)
|
||||
self._start_scene_stderr_reader(proc, start_number)
|
||||
self._start_scene_readers(proc, start_number)
|
||||
|
||||
# -- Scene Detection --
|
||||
|
||||
@@ -236,16 +236,20 @@ class StreamManager:
|
||||
index.append(entry)
|
||||
index_path.write_text(json.dumps(index, indent=2))
|
||||
|
||||
def _start_scene_stderr_reader(self, proc, start_number):
|
||||
"""Read stderr continuously, parsing showinfo lines for scene frames.
|
||||
def _start_scene_readers(self, proc, start_number):
|
||||
"""Read scene frames from stdout (MJPEG pipe) and timestamps from stderr.
|
||||
|
||||
Each showinfo line corresponds to a JPEG that ffmpeg writes. We wait
|
||||
briefly for the file to appear on disk (showinfo fires before the
|
||||
muxer flushes), then update the index and fire the callback.
|
||||
Two threads:
|
||||
- stderr: parses showinfo lines, queues pts_time values
|
||||
- stdout: reads JPEG frames from pipe, pairs with queued timestamps,
|
||||
writes files to disk, fires callbacks immediately
|
||||
|
||||
showinfo fires before the JPEG encoder, so timestamps are always
|
||||
queued before the corresponding JPEG data arrives on stdout.
|
||||
"""
|
||||
def _read():
|
||||
frame_num = start_number
|
||||
offset = self.current_global_offset
|
||||
ts_queue = Queue()
|
||||
|
||||
def _read_stderr():
|
||||
for raw in proc.stderr:
|
||||
line = raw.decode("utf-8", errors="replace").rstrip()
|
||||
if not line:
|
||||
@@ -254,38 +258,59 @@ class StreamManager:
|
||||
log.debug("[recorder] %s", line)
|
||||
continue
|
||||
pts_match = re.search(r"pts_time:\s*([\d.]+)", line)
|
||||
if not pts_match:
|
||||
continue
|
||||
pts_time = float(pts_match.group(1))
|
||||
frame_id = f"F{frame_num:04d}"
|
||||
frame_path = self.frames_dir / f"{frame_id}.jpg"
|
||||
|
||||
# Wait for ffmpeg to flush the JPEG (showinfo fires before mux)
|
||||
for _ in range(20): # up to ~200ms
|
||||
if frame_path.exists() and frame_path.stat().st_size > 0:
|
||||
break
|
||||
time.sleep(0.01)
|
||||
|
||||
if not frame_path.exists():
|
||||
log.warning("Scene frame %s not found on disk", frame_id)
|
||||
continue
|
||||
|
||||
entry = {
|
||||
"id": frame_id,
|
||||
"timestamp": pts_time + offset,
|
||||
"path": str(frame_path),
|
||||
"sent_to_agent": False,
|
||||
}
|
||||
self._append_frame_index(entry)
|
||||
log.info("Scene frame: %s at %.1fs (pts=%.1f + offset=%.1f)",
|
||||
frame_id, entry["timestamp"], pts_time, offset)
|
||||
if self._on_new_frames:
|
||||
self._on_new_frames([entry])
|
||||
frame_num += 1
|
||||
|
||||
if pts_match:
|
||||
ts_queue.put(float(pts_match.group(1)))
|
||||
log.info("[recorder] stderr closed, exit=%s", proc.poll())
|
||||
|
||||
Thread(target=_read, daemon=True, name="recorder_stderr").start()
|
||||
def _read_stdout():
|
||||
frame_num = start_number
|
||||
offset = self.current_global_offset
|
||||
buf = b""
|
||||
while True:
|
||||
chunk = proc.stdout.read(4096)
|
||||
if not chunk:
|
||||
break
|
||||
buf += chunk
|
||||
# Split JPEG frames by SOI (0xFFD8) and EOI (0xFFD9) markers
|
||||
while True:
|
||||
soi = buf.find(b"\xff\xd8")
|
||||
if soi < 0:
|
||||
buf = b""
|
||||
break
|
||||
eoi = buf.find(b"\xff\xd9", soi + 2)
|
||||
if eoi < 0:
|
||||
buf = buf[soi:] # keep from SOI, need more data
|
||||
break
|
||||
jpeg_data = buf[soi:eoi + 2]
|
||||
buf = buf[eoi + 2:]
|
||||
|
||||
# Get timestamp (showinfo fires before encode, so it's queued)
|
||||
try:
|
||||
pts_time = ts_queue.get(timeout=2.0)
|
||||
except Empty:
|
||||
log.warning("No timestamp for scene frame %d", frame_num)
|
||||
pts_time = 0.0
|
||||
|
||||
frame_id = f"F{frame_num:04d}"
|
||||
frame_path = self.frames_dir / f"{frame_id}.jpg"
|
||||
frame_path.write_bytes(jpeg_data)
|
||||
|
||||
entry = {
|
||||
"id": frame_id,
|
||||
"timestamp": pts_time + offset,
|
||||
"path": str(frame_path),
|
||||
"sent_to_agent": False,
|
||||
}
|
||||
self._append_frame_index(entry)
|
||||
log.info("Scene frame: %s at %.1fs (pts=%.1f + offset=%.1f)",
|
||||
frame_id, entry["timestamp"], pts_time, offset)
|
||||
if self._on_new_frames:
|
||||
self._on_new_frames([entry])
|
||||
frame_num += 1
|
||||
log.info("[recorder] stdout closed")
|
||||
|
||||
Thread(target=_read_stderr, daemon=True, name="recorder_stderr").start()
|
||||
Thread(target=_read_stdout, daemon=True, name="recorder_stdout").start()
|
||||
|
||||
def _probe_safe_duration(self):
|
||||
"""Probe current recording duration via ffprobe. Returns seconds or None."""
|
||||
|
||||
Reference in New Issue
Block a user