| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- """Snapshots and the external camera test stand down for a live view (SP-C2).
- A USB camera allows one V4L2 handle, so an external snapshot taken while a
- viewer was watching failed with 503 on every Home Assistant or camera-wall
- poll, and the Test button failed the same way. Finish photos and timelapse
- frames already reuse the live frame (``live_frame_for_capture``); the
- snapshot route and the Test button now do too.
- """
- from unittest.mock import AsyncMock, MagicMock, patch
- import pytest
- from httpx import AsyncClient
- # 640x480 baseline JPEG header: SOI, then SOF0 with height 480 and width 640.
- LIVE_JPEG = b"\xff\xd8\xff\xc0\x00\x11\x08\x01\xe0\x02\x80\x03" + b"\x00" * 120 + b"\xff\xd9"
- USB = {"external_camera_enabled": True, "external_camera_url": "/dev/video0", "external_camera_type": "usb"}
- def _live_view(printer_id: int, frame: bytes | None):
- streams = {f"{printer_id}-fanout": MagicMock()}
- frames = {printer_id: frame} if frame is not None else {}
- return (
- patch("backend.app.api.routes.camera._active_streams", streams),
- patch("backend.app.api.routes.camera._last_frames", frames),
- )
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_external_snapshot_uses_the_live_frame(async_client: AsyncClient, printer_factory):
- printer = await printer_factory(**USB)
- streams, frames = _live_view(printer.id, LIVE_JPEG)
- with (
- streams,
- frames,
- patch("backend.app.services.external_camera.capture_frame", new_callable=AsyncMock) as capture,
- ):
- response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/snapshot")
- assert response.status_code == 200
- assert response.content == LIVE_JPEG
- capture.assert_not_called()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_external_snapshot_waits_for_a_starting_live_view(async_client: AsyncClient, printer_factory):
- """No frame yet: opening the camera now would knock the viewer off (#1348)."""
- printer = await printer_factory(**USB)
- streams, frames = _live_view(printer.id, None)
- with (
- streams,
- frames,
- patch("backend.app.services.external_camera.capture_frame", new_callable=AsyncMock) as capture,
- ):
- response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/snapshot")
- assert response.status_code == 503
- assert response.headers.get("retry-after") == "2"
- capture.assert_not_called()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_bambu_snapshot_waits_for_a_starting_live_view(async_client: AsyncClient, printer_factory):
- printer = await printer_factory()
- streams, frames = _live_view(printer.id, None)
- with (
- streams,
- frames,
- patch("backend.app.api.routes.camera.capture_camera_frame", new_callable=AsyncMock) as capture,
- ):
- response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/snapshot")
- assert response.status_code == 503
- capture.assert_not_called()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_testing_the_watched_camera_reports_it_working(async_client: AsyncClient, printer_factory):
- printer = await printer_factory(**USB)
- streams, frames = _live_view(printer.id, LIVE_JPEG)
- with (
- streams,
- frames,
- patch("backend.app.services.external_camera.test_connection", new_callable=AsyncMock) as probe,
- ):
- response = await async_client.post(
- f"/api/v1/printers/{printer.id}/camera/external/test",
- json={"url": "/dev/video0", "camera_type": "usb"},
- )
- assert response.status_code == 200
- assert response.json() == {"success": True, "resolution": "640x480", "coalesced": True, "live_view": True}
- probe.assert_not_called()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_testing_the_watched_camera_while_the_view_starts(async_client: AsyncClient, printer_factory):
- printer = await printer_factory(**USB)
- streams, frames = _live_view(printer.id, None)
- with (
- streams,
- frames,
- patch("backend.app.services.external_camera.test_connection", new_callable=AsyncMock) as probe,
- ):
- response = await async_client.post(
- f"/api/v1/printers/{printer.id}/camera/external/test",
- json={"url": "/dev/video0", "camera_type": "usb"},
- )
- body = response.json()
- assert body["success"] is False
- assert body["live_view"] is True
- probe.assert_not_called()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_testing_another_camera_still_opens_it(async_client: AsyncClient, printer_factory):
- """The form may hold a device that is not the one being watched."""
- printer = await printer_factory(**USB)
- streams, frames = _live_view(printer.id, LIVE_JPEG)
- with (
- streams,
- frames,
- patch(
- "backend.app.services.external_camera.test_connection",
- new_callable=AsyncMock,
- return_value={"success": True, "resolution": "1280x720", "coalesced": False},
- ) as probe,
- ):
- response = await async_client.post(
- f"/api/v1/printers/{printer.id}/camera/external/test",
- json={"url": "/dev/video2", "camera_type": "usb"},
- )
- assert response.json()["resolution"] == "1280x720"
- probe.assert_awaited_once_with("/dev/video2", "usb")
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_a_dedicated_snapshot_url_is_still_used_during_a_live_view(async_client: AsyncClient, printer_factory):
- """#1177: an HTTP snapshot endpoint has no reader to compete with, and gives
- the full-quality frame the live view does not."""
- printer = await printer_factory(
- external_camera_enabled=True,
- external_camera_url="rtsp://10.0.0.9/live",
- external_camera_type="rtsp",
- external_camera_snapshot_url="http://10.0.0.9/snapshot.jpg",
- )
- streams, frames = _live_view(printer.id, None)
- with (
- streams,
- frames,
- patch(
- "backend.app.services.external_camera.capture_frame", new_callable=AsyncMock, return_value=LIVE_JPEG
- ) as capture,
- ):
- response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/snapshot")
- assert response.status_code == 200
- capture.assert_awaited_once()
- assert capture.await_args.kwargs["snapshot_url"] == "http://10.0.0.9/snapshot.jpg"
|