test_external_camera_rtsp_probe_3082.py 3.5 KB

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