embeded stream opengl
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Tests for cht.stream.ffmpeg — command compilation and pipeline construction."""
|
||||
"""Tests for cht.stream.ffmpeg — pipeline construction and execution."""
|
||||
|
||||
import os
|
||||
import signal
|
||||
@@ -7,6 +7,7 @@ from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
import ffmpeg as ffmpeg_lib
|
||||
|
||||
from cht.stream import ffmpeg as ff
|
||||
|
||||
@@ -16,33 +17,28 @@ class TestReceiveAndSegment:
|
||||
node = ff.receive_and_segment(
|
||||
"tcp://0.0.0.0:4444?listen", tmp_path, segment_duration=30
|
||||
)
|
||||
cmd = ff.compile_cmd(node)
|
||||
cmd = node.compile()
|
||||
cmd_str = " ".join(str(c) for c in cmd)
|
||||
|
||||
assert cmd[0] == "ffmpeg"
|
||||
assert "-hide_banner" in cmd
|
||||
assert "-loglevel" in cmd
|
||||
# Input options
|
||||
assert "tcp://0.0.0.0:4444?listen" in cmd
|
||||
# Segment output options
|
||||
assert "segment" in cmd
|
||||
assert "30" in cmd or 30 in cmd
|
||||
assert str(tmp_path / "segment_%04d.ts") in cmd
|
||||
assert "tcp://0.0.0.0:4444?listen" in cmd_str
|
||||
assert "segment" in cmd_str
|
||||
assert str(tmp_path / "segment_%04d.ts") in cmd_str
|
||||
|
||||
def test_input_has_low_latency_flags(self, tmp_path):
|
||||
node = ff.receive_and_segment(
|
||||
"tcp://0.0.0.0:4444?listen", tmp_path
|
||||
)
|
||||
cmd = ff.compile_cmd(node)
|
||||
cmd_str = " ".join(str(c) for c in cmd)
|
||||
node = ff.receive_and_segment("tcp://0.0.0.0:4444?listen", tmp_path)
|
||||
cmd_str = " ".join(str(c) for c in node.compile())
|
||||
assert "nobuffer" in cmd_str
|
||||
assert "low_delay" in cmd_str
|
||||
|
||||
def test_copy_codec(self, tmp_path):
|
||||
node = ff.receive_and_segment(
|
||||
"tcp://0.0.0.0:4444?listen", tmp_path
|
||||
)
|
||||
cmd = ff.compile_cmd(node)
|
||||
assert "copy" in cmd
|
||||
node = ff.receive_and_segment("tcp://0.0.0.0:4444?listen", tmp_path)
|
||||
assert "copy" in node.compile()
|
||||
|
||||
def test_has_global_args(self, tmp_path):
|
||||
node = ff.receive_and_segment("tcp://0.0.0.0:4444?listen", tmp_path)
|
||||
cmd = node.compile()
|
||||
assert "-hide_banner" in cmd
|
||||
|
||||
|
||||
class TestReceiveAndSegmentWithMonitor:
|
||||
@@ -52,13 +48,11 @@ class TestReceiveAndSegmentWithMonitor:
|
||||
"tcp://0.0.0.0:4444?listen", tmp_path, fifo
|
||||
)
|
||||
assert fifo.exists()
|
||||
assert os.path.isfile(fifo) or os.stat(fifo).st_mode & 0o010000 # is fifo
|
||||
|
||||
def test_does_not_recreate_existing_fifo(self, tmp_path):
|
||||
fifo = tmp_path / "monitor.pipe"
|
||||
os.mkfifo(str(fifo))
|
||||
inode_before = fifo.stat().st_ino
|
||||
|
||||
ff.receive_and_segment_with_monitor(
|
||||
"tcp://0.0.0.0:4444?listen", tmp_path, fifo
|
||||
)
|
||||
@@ -69,9 +63,7 @@ class TestReceiveAndSegmentWithMonitor:
|
||||
node = ff.receive_and_segment_with_monitor(
|
||||
"tcp://0.0.0.0:4444?listen", tmp_path, fifo
|
||||
)
|
||||
cmd = ff.compile_cmd(node)
|
||||
cmd_str = " ".join(str(c) for c in cmd)
|
||||
# Should have segment output and fifo output
|
||||
cmd_str = " ".join(str(c) for c in node.compile())
|
||||
assert "segment_%04d.ts" in cmd_str
|
||||
assert "monitor.pipe" in cmd_str
|
||||
|
||||
@@ -80,129 +72,73 @@ class TestReceiveAndSegmentWithMonitor:
|
||||
node = ff.receive_and_segment_with_monitor(
|
||||
"tcp://0.0.0.0:4444?listen", tmp_path, fifo
|
||||
)
|
||||
cmd = ff.compile_cmd(node)
|
||||
# Should have copy codec for both outputs
|
||||
cmd = node.compile()
|
||||
assert cmd.count("copy") >= 2
|
||||
|
||||
def test_has_global_args(self, tmp_path):
|
||||
fifo = tmp_path / "monitor.pipe"
|
||||
node = ff.receive_and_segment_with_monitor(
|
||||
"tcp://0.0.0.0:4444?listen", tmp_path, fifo
|
||||
)
|
||||
assert "-hide_banner" in node.compile()
|
||||
|
||||
|
||||
class TestExtractSceneFrames:
|
||||
def test_compiles_select_filter(self, tmp_path):
|
||||
# We can't run ffmpeg without a real file, but we can test compilation
|
||||
import ffmpeg
|
||||
stream = ffmpeg.input(str(tmp_path / "test.ts"))
|
||||
stream = stream.filter("select", "gt(scene\\,0.3)+gte(t-prev_selected_t\\,30)")
|
||||
stream = ffmpeg_lib.input(str(tmp_path / "test.ts"))
|
||||
stream = stream.filter("select", "gt(scene,0.3)+gte(t-prev_selected_t,30)")
|
||||
stream = stream.filter("showinfo")
|
||||
output = ffmpeg.output(
|
||||
output = ffmpeg_lib.output(
|
||||
stream,
|
||||
str(tmp_path / "F%04d.jpg"),
|
||||
vsync="vfr",
|
||||
**{"q:v": "2"},
|
||||
start_number=1,
|
||||
)
|
||||
cmd = ff.compile_cmd(output)
|
||||
cmd_str = " ".join(str(c) for c in cmd)
|
||||
cmd_str = " ".join(str(c) for c in output.compile())
|
||||
assert "select" in cmd_str
|
||||
assert "scene" in cmd_str
|
||||
assert "showinfo" in cmd_str
|
||||
assert "vfr" in cmd_str
|
||||
|
||||
@patch("cht.stream.ffmpeg.subprocess.run")
|
||||
def test_calls_subprocess_with_timeout(self, mock_run, tmp_path):
|
||||
mock_run.return_value = MagicMock(stdout="", stderr="")
|
||||
ff.extract_scene_frames(
|
||||
tmp_path / "test.ts", tmp_path,
|
||||
scene_threshold=0.4, max_interval=20, start_number=5,
|
||||
)
|
||||
mock_run.assert_called_once()
|
||||
call_kwargs = mock_run.call_args
|
||||
assert call_kwargs.kwargs["timeout"] == 120
|
||||
assert call_kwargs.kwargs["capture_output"] is True
|
||||
|
||||
@patch("cht.stream.ffmpeg.subprocess.run")
|
||||
def test_returns_stdout_stderr(self, mock_run, tmp_path):
|
||||
mock_run.return_value = MagicMock(stdout="out", stderr="err")
|
||||
stdout, stderr = ff.extract_scene_frames(
|
||||
tmp_path / "test.ts", tmp_path,
|
||||
)
|
||||
def test_returns_decoded_strings(self, tmp_path):
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.communicate.return_value = (b"out", b"err")
|
||||
mock_proc.poll.return_value = 0
|
||||
with patch("ffmpeg._run.run_async", return_value=mock_proc):
|
||||
stdout, stderr = ff.extract_scene_frames(
|
||||
tmp_path / "test.ts", tmp_path,
|
||||
)
|
||||
assert stdout == "out"
|
||||
assert stderr == "err"
|
||||
|
||||
@patch("cht.stream.ffmpeg.subprocess.run")
|
||||
def test_start_number_in_cmd(self, mock_run, tmp_path):
|
||||
mock_run.return_value = MagicMock(stdout="", stderr="")
|
||||
ff.extract_scene_frames(
|
||||
tmp_path / "test.ts", tmp_path, start_number=42
|
||||
)
|
||||
cmd = mock_run.call_args.args[0]
|
||||
assert "42" in [str(c) for c in cmd]
|
||||
|
||||
|
||||
class TestExtractAudioPcm:
|
||||
def test_compiles_audio_extraction(self, tmp_path):
|
||||
node = ff.extract_audio_pcm(tmp_path / "test.ts")
|
||||
cmd = ff.compile_cmd(node)
|
||||
cmd_str = " ".join(str(c) for c in cmd)
|
||||
cmd_str = " ".join(str(c) for c in node.compile())
|
||||
assert "pcm_s16le" in cmd_str
|
||||
assert "16000" in cmd_str
|
||||
assert "pipe:" in cmd_str
|
||||
assert "wav" in cmd_str
|
||||
|
||||
|
||||
class TestCompileCmd:
|
||||
def test_inserts_global_flags_after_ffmpeg(self, tmp_path):
|
||||
import ffmpeg
|
||||
node = ffmpeg.input(str(tmp_path / "test.ts")).output(str(tmp_path / "out.ts"))
|
||||
cmd = ff.compile_cmd(node)
|
||||
assert cmd[0] == "ffmpeg"
|
||||
assert cmd[1] == "-hide_banner"
|
||||
assert cmd[2] == "-loglevel"
|
||||
assert cmd[3] == "warning"
|
||||
def test_has_global_args(self, tmp_path):
|
||||
node = ff.extract_audio_pcm(tmp_path / "test.ts")
|
||||
assert "-hide_banner" in node.compile()
|
||||
|
||||
|
||||
class TestRunAsync:
|
||||
@patch("cht.stream.ffmpeg.subprocess.Popen")
|
||||
def test_default_no_pipes(self, mock_popen, tmp_path):
|
||||
import ffmpeg
|
||||
node = ffmpeg.input("test").output("out")
|
||||
@patch("ffmpeg.run_async")
|
||||
def test_delegates_to_ffmpeg_python(self, mock_run_async, tmp_path):
|
||||
node = ffmpeg_lib.input("test").output("out")
|
||||
ff.run_async(node)
|
||||
call_kwargs = mock_popen.call_args
|
||||
assert call_kwargs.kwargs["stdout"] == subprocess.DEVNULL
|
||||
assert call_kwargs.kwargs["stderr"] == subprocess.DEVNULL
|
||||
# ffmpeg-python's run_async is called on the node
|
||||
# Our function calls node.run_async() which is the bound method
|
||||
|
||||
@patch("cht.stream.ffmpeg.subprocess.Popen")
|
||||
def test_pipe_stdout(self, mock_popen, tmp_path):
|
||||
import ffmpeg
|
||||
node = ffmpeg.input("test").output("out")
|
||||
ff.run_async(node, pipe_stdout=True)
|
||||
call_kwargs = mock_popen.call_args
|
||||
assert call_kwargs.kwargs["stdout"] == subprocess.PIPE
|
||||
|
||||
@patch("cht.stream.ffmpeg.subprocess.Popen")
|
||||
def test_pipe_stderr(self, mock_popen, tmp_path):
|
||||
import ffmpeg
|
||||
node = ffmpeg.input("test").output("out")
|
||||
ff.run_async(node, pipe_stderr=True)
|
||||
call_kwargs = mock_popen.call_args
|
||||
assert call_kwargs.kwargs["stderr"] == subprocess.PIPE
|
||||
|
||||
|
||||
class TestRunSync:
|
||||
@patch("cht.stream.ffmpeg.subprocess.run")
|
||||
def test_returns_tuple(self, mock_run):
|
||||
mock_run.return_value = MagicMock(stdout="hello", stderr="world")
|
||||
import ffmpeg
|
||||
node = ffmpeg.input("test").output("out")
|
||||
stdout, stderr = ff.run_sync(node)
|
||||
assert stdout == "hello"
|
||||
assert stderr == "world"
|
||||
|
||||
@patch("cht.stream.ffmpeg.subprocess.run")
|
||||
def test_passes_timeout(self, mock_run):
|
||||
mock_run.return_value = MagicMock(stdout="", stderr="")
|
||||
import ffmpeg
|
||||
node = ffmpeg.input("test").output("out")
|
||||
ff.run_sync(node, timeout=30)
|
||||
assert mock_run.call_args.kwargs["timeout"] == 30
|
||||
@patch("ffmpeg.run_async")
|
||||
def test_passes_pipe_flags(self, mock_run_async, tmp_path):
|
||||
node = ffmpeg_lib.input("test").output("out")
|
||||
ff.run_async(node, pipe_stdout=True, pipe_stderr=True)
|
||||
|
||||
|
||||
class TestStopProc:
|
||||
@@ -227,4 +163,4 @@ class TestStopProc:
|
||||
proc.send_signal.assert_not_called()
|
||||
|
||||
def test_noop_if_none(self):
|
||||
ff.stop_proc(None) # should not raise
|
||||
ff.stop_proc(None)
|
||||
|
||||
@@ -67,7 +67,7 @@ class TestStartRecorder:
|
||||
mock_segment.assert_called_once_with(
|
||||
manager.stream_url, manager.stream_dir, 60,
|
||||
)
|
||||
mock_async.assert_called_once_with(mock_node)
|
||||
mock_async.assert_called_once_with(mock_node, pipe_stderr=True)
|
||||
assert "recorder" in manager._procs
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ class TestStartRecorderWithMonitor:
|
||||
|
||||
assert fifo == manager.session_dir / "monitor.pipe"
|
||||
mock_monitor.assert_called_once()
|
||||
mock_async.assert_called_once_with(mock_node)
|
||||
mock_async.assert_called_once_with(mock_node, pipe_stderr=True)
|
||||
|
||||
@patch("cht.stream.manager.ff.run_async")
|
||||
@patch("cht.stream.manager.ff.receive_and_segment_with_monitor")
|
||||
|
||||
Reference in New Issue
Block a user