test_external_live_view_defers_captures.py 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. """One-shot captures wait for a live view of an external MJPEG camera (SP-C7).
  2. An ESP32-style MJPEG camera serves one client. Its live view ran no process
  3. the stream registry tracks, so ``is_stream_active`` said no viewer was
  4. attached, and timelapse frames, finish photos, Obico and the plate check
  5. opened a second connection the camera refused. A camera with a separate
  6. snapshot URL is left out: that URL serves captures beside the live view, at
  7. full quality, and the plate check's references come from it (#1177).
  8. """
  9. import asyncio
  10. from unittest.mock import patch
  11. import pytest
  12. from httpx import AsyncClient
  13. from backend.app.api.routes import camera
  14. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  15. FRAME = b"\xff\xd8frame\xff\xd9"
  16. @pytest.fixture(autouse=True)
  17. def _forget_frames():
  18. yield
  19. for state in (camera._last_frames, camera._last_frame_times, camera._stream_start_times):
  20. state.clear()
  21. async def _watch(async_client: AsyncClient, printer_id: int) -> dict:
  22. seen: dict = {}
  23. async def _stream(*_args, on_frame=None, **_kwargs):
  24. on_frame(FRAME)
  25. yield b"--frame\r\nContent-Type: image/jpeg\r\n\r\n\xff\xd8\xff\xd9\r\n"
  26. await asyncio.sleep(0.05)
  27. seen["active"] = camera.is_stream_active(printer_id)
  28. seen["defer"] = camera.live_frame_for_capture(printer_id)[0]
  29. yield b"--frame\r\nContent-Type: image/jpeg\r\n\r\n\xff\xd8\xff\xd9\r\n"
  30. with patch("backend.app.services.external_camera.generate_mjpeg_stream", _stream):
  31. response = await async_client.get(f"/api/v1/printers/{printer_id}/camera/stream")
  32. assert response.status_code == 200
  33. await asyncio.sleep(0.1)
  34. seen["after"] = camera.is_stream_active(printer_id)
  35. seen["frame_after"] = camera._last_frames.get(printer_id)
  36. return seen
  37. async def test_a_watched_mjpeg_camera_is_not_dialled_again(async_client: AsyncClient, printer_factory):
  38. printer = await printer_factory(
  39. external_camera_enabled=True,
  40. external_camera_url="http://192.0.2.50:81/stream",
  41. external_camera_type="mjpeg",
  42. )
  43. seen = await _watch(async_client, printer.id)
  44. assert seen["active"] is True
  45. assert seen["defer"] is True
  46. assert seen["after"] is False
  47. # The camera's upstream ended while the viewer was still counted: its last
  48. # frame must not outlive it, or a plate check or the next viewer's start
  49. # takes an old picture for a current one.
  50. assert seen["frame_after"] is None
  51. async def test_a_camera_with_a_snapshot_url_keeps_using_it(async_client: AsyncClient, printer_factory):
  52. printer = await printer_factory(
  53. external_camera_enabled=True,
  54. # Its own URL: a fan-out key of its own, not the first test's upstream
  55. # still in its grace.
  56. external_camera_url="http://192.0.2.51:81/stream",
  57. external_camera_type="mjpeg",
  58. external_camera_snapshot_url="http://192.0.2.51/capture",
  59. )
  60. seen = await _watch(async_client, printer.id)
  61. assert seen["active"] is False