embeded stream opengl
This commit is contained in:
@@ -3,6 +3,8 @@ Thin wrapper around ffmpeg-python for building and running ffmpeg pipelines.
|
||||
|
||||
All ffmpeg command construction goes through this module so manager.py
|
||||
and other consumers never build raw CLI arg lists.
|
||||
|
||||
Uses ffmpeg-python's own run/run_async for subprocess management.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -15,28 +17,51 @@ import ffmpeg
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
GLOBAL_ARGS = ("-hide_banner", "-loglevel", "warning")
|
||||
|
||||
def receive_and_segment(stream_url, segment_dir, segment_duration=60):
|
||||
"""Receive mpegts stream and save as segmented .ts files.
|
||||
|
||||
Returns an ffmpeg-python output node (not yet running).
|
||||
def receive_to_pipe(stream_url, segment_dir=None, segment_duration=60):
|
||||
"""Receive mpegts stream and pipe to stdout for mpv.
|
||||
|
||||
If segment_dir is provided, also saves segments to disk.
|
||||
Uses pipe (not fifo) so OS kernel buffers prevent blocking.
|
||||
"""
|
||||
stream = ffmpeg.input(stream_url, fflags="nobuffer", flags="low_delay")
|
||||
return ffmpeg.output(
|
||||
stream,
|
||||
str(segment_dir / "segment_%04d.ts"),
|
||||
c="copy",
|
||||
f="segment",
|
||||
segment_time=segment_duration,
|
||||
reset_timestamps=1,
|
||||
|
||||
out_pipe = ffmpeg.output(stream, "pipe:", c="copy", f="mpegts")
|
||||
|
||||
if segment_dir:
|
||||
out_segments = ffmpeg.output(
|
||||
stream,
|
||||
str(segment_dir / "segment_%04d.ts"),
|
||||
c="copy",
|
||||
f="segment",
|
||||
segment_time=segment_duration,
|
||||
reset_timestamps=1,
|
||||
)
|
||||
return ffmpeg.merge_outputs(out_pipe, out_segments).global_args(*GLOBAL_ARGS)
|
||||
|
||||
return out_pipe.global_args(*GLOBAL_ARGS)
|
||||
|
||||
|
||||
def receive_and_segment(stream_url, segment_dir, segment_duration=60):
|
||||
"""Receive mpegts stream and save as segmented .ts files."""
|
||||
stream = ffmpeg.input(stream_url, fflags="nobuffer", flags="low_delay")
|
||||
return (
|
||||
ffmpeg.output(
|
||||
stream,
|
||||
str(segment_dir / "segment_%04d.ts"),
|
||||
c="copy",
|
||||
f="segment",
|
||||
segment_time=segment_duration,
|
||||
reset_timestamps=1,
|
||||
)
|
||||
.global_args(*GLOBAL_ARGS)
|
||||
)
|
||||
|
||||
|
||||
def receive_and_segment_with_monitor(stream_url, segment_dir, fifo_path, segment_duration=60):
|
||||
"""Receive stream, save segments AND tee to a named pipe for monitoring.
|
||||
|
||||
Returns an ffmpeg-python merged output node.
|
||||
"""
|
||||
"""Receive stream, save segments AND tee to a named pipe for monitoring."""
|
||||
if not fifo_path.exists():
|
||||
os.mkfifo(str(fifo_path))
|
||||
|
||||
@@ -58,7 +83,7 @@ def receive_and_segment_with_monitor(stream_url, segment_dir, fifo_path, segment
|
||||
f="mpegts",
|
||||
)
|
||||
|
||||
return ffmpeg.merge_outputs(out_segments, out_monitor)
|
||||
return ffmpeg.merge_outputs(out_segments, out_monitor).global_args(*GLOBAL_ARGS)
|
||||
|
||||
|
||||
def extract_scene_frames(input_path, output_dir, scene_threshold=0.3,
|
||||
@@ -66,78 +91,57 @@ def extract_scene_frames(input_path, output_dir, scene_threshold=0.3,
|
||||
"""Extract frames from a file on scene change.
|
||||
|
||||
Uses ffmpeg select filter with scene detection and a max-interval fallback.
|
||||
Returns (process_result, stderr) for timestamp parsing.
|
||||
Returns (stdout bytes, stderr bytes) for timestamp parsing.
|
||||
"""
|
||||
select_expr = (
|
||||
f"gt(scene\\,{scene_threshold})"
|
||||
f"+gte(t-prev_selected_t\\,{max_interval})"
|
||||
f"gt(scene,{scene_threshold})"
|
||||
f"+gte(t-prev_selected_t,{max_interval})"
|
||||
)
|
||||
stream = ffmpeg.input(str(input_path))
|
||||
stream = stream.filter("select", select_expr).filter("showinfo")
|
||||
|
||||
output = ffmpeg.output(
|
||||
stream,
|
||||
str(output_dir / "F%04d.jpg"),
|
||||
vsync="vfr",
|
||||
**{"q:v": "2"},
|
||||
start_number=start_number,
|
||||
output = (
|
||||
ffmpeg.output(
|
||||
stream,
|
||||
str(output_dir / "F%04d.jpg"),
|
||||
vsync="vfr",
|
||||
**{"q:v": "2"},
|
||||
start_number=start_number,
|
||||
)
|
||||
.global_args(*GLOBAL_ARGS)
|
||||
)
|
||||
|
||||
return run_sync(output, timeout=120)
|
||||
log.info("extract_scene_frames: %s", " ".join(output.compile()))
|
||||
stdout, stderr = output.run(capture_stdout=True, capture_stderr=True)
|
||||
return stdout.decode("utf-8", errors="replace"), stderr.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def extract_audio_pcm(input_path):
|
||||
"""Extract audio as 16kHz mono PCM wav, returning an output node for piping.
|
||||
|
||||
Use run_async with pipe_stdout=True to stream PCM data.
|
||||
"""
|
||||
"""Extract audio as 16kHz mono PCM wav, returning an output node for piping."""
|
||||
stream = ffmpeg.input(str(input_path))
|
||||
return ffmpeg.output(
|
||||
stream.audio,
|
||||
"pipe:",
|
||||
vn=None,
|
||||
acodec="pcm_s16le",
|
||||
ar=16000,
|
||||
ac=1,
|
||||
f="wav",
|
||||
return (
|
||||
ffmpeg.output(
|
||||
stream.audio,
|
||||
"pipe:",
|
||||
vn=None,
|
||||
acodec="pcm_s16le",
|
||||
ar=16000,
|
||||
ac=1,
|
||||
f="wav",
|
||||
)
|
||||
.global_args(*GLOBAL_ARGS)
|
||||
)
|
||||
|
||||
|
||||
def run_async(output_node, pipe_stdout=False, pipe_stderr=False):
|
||||
"""Start an ffmpeg pipeline asynchronously. Returns subprocess.Popen."""
|
||||
cmd = compile_cmd(output_node)
|
||||
log.info("run_async: %s", " ".join(str(c) for c in cmd))
|
||||
return subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE if pipe_stdout else subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE if pipe_stderr else subprocess.DEVNULL,
|
||||
"""Start an ffmpeg pipeline asynchronously via ffmpeg-python's run_async."""
|
||||
log.info("run_async: %s", " ".join(output_node.compile()))
|
||||
return output_node.run_async(
|
||||
pipe_stdout=pipe_stdout,
|
||||
pipe_stderr=pipe_stderr,
|
||||
)
|
||||
|
||||
|
||||
def run_sync(output_node, timeout=None):
|
||||
"""Run an ffmpeg pipeline synchronously. Returns (stdout, stderr) as strings."""
|
||||
cmd = compile_cmd(output_node)
|
||||
log.info("run_sync: %s", " ".join(str(c) for c in cmd))
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
return result.stdout, result.stderr
|
||||
|
||||
|
||||
def compile_cmd(output_node):
|
||||
"""Compile an ffmpeg-python node to a command list, adding global flags."""
|
||||
cmd = output_node.compile()
|
||||
# Insert global flags after 'ffmpeg'
|
||||
idx = 1
|
||||
for flag in ["-hide_banner", "-loglevel", "warning"]:
|
||||
cmd.insert(idx, flag)
|
||||
idx += 1
|
||||
return cmd
|
||||
|
||||
|
||||
def stop_proc(proc, timeout=5):
|
||||
"""Gracefully stop an ffmpeg subprocess."""
|
||||
if proc and proc.poll() is None:
|
||||
|
||||
@@ -75,6 +75,20 @@ class StreamManager:
|
||||
log.info("Recorder started: pid=%s url=%s", proc.pid, self.stream_url)
|
||||
self._start_stderr_reader("recorder", proc)
|
||||
|
||||
def start_receiver_pipe(self):
|
||||
"""Receive stream, pipe stdout to mpv, save segments to disk."""
|
||||
self.setup_dirs()
|
||||
node = ff.receive_to_pipe(
|
||||
self.stream_url,
|
||||
segment_dir=self.stream_dir,
|
||||
segment_duration=SEGMENT_DURATION,
|
||||
)
|
||||
proc = ff.run_async(node, pipe_stdout=True, pipe_stderr=True)
|
||||
self._procs["receiver"] = proc
|
||||
log.info("Receiver started: pid=%s url=%s (pipe + segments)", proc.pid, self.stream_url)
|
||||
self._start_stderr_reader("receiver", proc)
|
||||
return proc
|
||||
|
||||
def start_recorder_with_monitor(self):
|
||||
self.setup_dirs()
|
||||
fifo_path = self.session_dir / "monitor.pipe"
|
||||
@@ -104,6 +118,30 @@ class StreamManager:
|
||||
t = Thread(target=_read, daemon=True, name=f"{name}_stderr")
|
||||
t.start()
|
||||
|
||||
def start_frame_extractor_on_recording(self, recording_path):
|
||||
"""Extract frames periodically from a growing recording file."""
|
||||
log.info("Starting frame extractor on recording: %s", recording_path)
|
||||
self._recording_path = recording_path
|
||||
self._start_recording_frame_watcher()
|
||||
|
||||
def _start_recording_frame_watcher(self):
|
||||
def _watch():
|
||||
last_size = 0
|
||||
log.info("Recording frame watcher running, watching %s", self._recording_path)
|
||||
while "stop" not in self._stop_flags:
|
||||
if self._recording_path.exists():
|
||||
size = self._recording_path.stat().st_size
|
||||
if size > last_size and size > 100_000: # wait for some data
|
||||
log.info("Recording grew: %d -> %d bytes, extracting frames", last_size, size)
|
||||
last_size = size
|
||||
self._extract_frames_from_file(self._recording_path)
|
||||
time.sleep(10) # check every 10s
|
||||
log.info("Recording frame watcher stopped")
|
||||
|
||||
t = Thread(target=_watch, daemon=True, name="recording_frame_watcher")
|
||||
t.start()
|
||||
self._threads["recording_frame_watcher"] = t
|
||||
|
||||
def start_frame_extractor(self):
|
||||
log.info("Starting frame watcher...")
|
||||
self._start_frame_watcher()
|
||||
|
||||
Reference in New Issue
Block a user