| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677 |
- """The external live view must not cap ffmpeg's stream probing (#3082).
- An external camera passed the connection test, played in VLC, and showed a
- black live view that gave up after a few seconds. The two RTSP paths in
- ``external_camera`` were not asking ffmpeg for the same thing: the one-shot
- ``_capture_rtsp_frame`` passed no probe settings and got ffmpeg's defaults,
- while ``_stream_rtsp`` hard-coded ``-probesize 32 -analyzeduration 0``.
- 32 bytes is enough for a camera that puts SPS/PPS in its SDP. It is not enough
- for one that sends them in-band a moment later — a WebRTC source republished
- through go2rtc, in @M1XZG's report — and without them ffmpeg never starts an
- H.264 decoder, so the stream yields no frames at all. Those settings were never
- chosen for external cameras: they came in with the P2S TLS proxy (#661) as
- fast-start tuning for the *printer* camera path, where the source is a known
- Bambu model, and were copied across to this one in the same commit. The printer
- path keeps its per-model tuning in ``camera_profiles.py``; this path has no
- model to tune against and belongs on the defaults.
- """
- import pytest
- from backend.app.services.external_camera import _capture_rtsp_frame, _stream_rtsp
- from backend.tests._fixtures.external_camera import fake_ffmpeg, spawn_spy
- CAMERA = "rtsp://admin:hunter2@192.168.1.50:554/live"
- async def _stream_argv() -> tuple[str, ...]:
- with fake_ffmpeg(), spawn_spy(returncode=None) as spawn:
- [frame async for frame in _stream_rtsp(CAMERA, fps=5)]
- return spawn.await_args.args
- async def _capture_argv() -> tuple[str, ...]:
- with fake_ffmpeg(), spawn_spy() as spawn:
- await _capture_rtsp_frame(CAMERA, timeout=5)
- return spawn.await_args.args
- class TestTheLiveStreamDoesNotCapProbing:
- @pytest.mark.asyncio
- @pytest.mark.parametrize("flag", ["-probesize", "-analyzeduration"])
- async def test_no_probe_ceiling_is_imposed(self, flag):
- """Re-adding either of these is the regression, and it is silent.
- Nothing fails, no error is logged, the connection test still passes —
- the live view just stops producing frames on the subset of cameras
- that need longer than a 32-byte probe to describe themselves.
- """
- argv = await _stream_argv()
- assert flag not in argv, f"{flag} is back in the external live stream: {argv!r}"
- @pytest.mark.asyncio
- async def test_the_low_latency_flags_are_kept(self):
- """The probe cap went; the rest of the fast-start tuning did not.
- ``-fflags nobuffer`` and ``-flags low_delay`` ask ffmpeg not to sit on
- frames it already has, which is a different question from how long it
- may look before it has any. @M1XZG re-ran the A/B with both retained
- and the stream still came up, so latency is no reason to reach for
- ``-probesize`` again.
- """
- argv = await _stream_argv()
- assert argv[argv.index("-fflags") + 1] == "nobuffer"
- assert argv[argv.index("-flags") + 1] == "low_delay"
- @pytest.mark.asyncio
- async def test_both_rtsp_paths_probe_alike(self):
- """The asymmetry itself is the bug, whichever way it is reintroduced.
- A camera that answers the test button has demonstrated nothing about
- the live view unless both paths ask ffmpeg to look at the stream the
- same way.
- """
- stream, capture = await _stream_argv(), await _capture_argv()
- probe_flags = ("-probesize", "-analyzeduration")
- assert [f for f in probe_flags if f in stream] == [f for f in probe_flags if f in capture]
|