1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-24 12:59:34 +00:00

Implement an rtsp to webrtc registry in camera (#62962)

* Implement a webrtc to rtsp support in camera as a registry

Allow integrations to register a provider that can convert an RTSP stream and WebRTC offer to a WebRTC answer. This is
planned to be used by the RTSPtoWebRTC server integration as an initial pass, but could
support other server implementations as well (or even native implementationf or that matter).

* Fix test bug to improve test covergae and restructure statements

* Add missing call to refresh webrtc providers

* Run provider refresh in parallel since it may send RPCs

* Replace for loop with any

* Fix pylint warning to use a generator
This commit is contained in:
Allen Porter
2021-12-31 13:44:33 -08:00
committed by GitHub
parent 0de3a299d6
commit 0dee4f85f0
3 changed files with 299 additions and 17 deletions

View File

@@ -28,6 +28,11 @@ from .common import EMPTY_8_6_JPEG, mock_turbo_jpeg
from tests.components.camera import common
STREAM_SOURCE = "rtsp://127.0.0.1/stream"
HLS_STREAM_SOURCE = "http://127.0.0.1/example.m3u"
WEBRTC_OFFER = "v=0\r\n"
WEBRTC_ANSWER = "a=sendonly"
@pytest.fixture(name="mock_camera")
async def mock_camera_fixture(hass):
@@ -57,7 +62,7 @@ async def mock_camera_web_rtc_fixture(hass):
new_callable=PropertyMock(return_value=STREAM_TYPE_WEB_RTC),
), patch(
"homeassistant.components.camera.Camera.async_handle_web_rtc_offer",
return_value="a=sendonly",
return_value=WEBRTC_ANSWER,
):
yield
@@ -85,6 +90,50 @@ async def image_mock_url_fixture(hass):
await hass.async_block_till_done()
@pytest.fixture(name="mock_stream_source")
async def mock_stream_source_fixture():
"""Fixture to create an RTSP stream source."""
with patch(
"homeassistant.components.camera.Camera.stream_source",
return_value=STREAM_SOURCE,
) as mock_stream_source, patch(
"homeassistant.components.camera.Camera.supported_features",
return_value=camera.SUPPORT_STREAM,
):
yield mock_stream_source
@pytest.fixture(name="mock_hls_stream_source")
async def mock_hls_stream_source_fixture():
"""Fixture to create an HLS stream source."""
with patch(
"homeassistant.components.camera.Camera.stream_source",
return_value=HLS_STREAM_SOURCE,
) as mock_hls_stream_source, patch(
"homeassistant.components.camera.Camera.supported_features",
return_value=camera.SUPPORT_STREAM,
):
yield mock_hls_stream_source
async def provide_web_rtc_answer(stream_source: str, offer: str) -> str:
"""Simulate an rtsp to webrtc provider."""
assert stream_source == STREAM_SOURCE
assert offer == WEBRTC_OFFER
return WEBRTC_ANSWER
@pytest.fixture(name="mock_rtsp_to_web_rtc")
async def mock_rtsp_to_web_rtc_fixture(hass):
"""Fixture that registers a mock rtsp to web_rtc provider."""
mock_provider = Mock(side_effect=provide_web_rtc_answer)
unsub = camera.async_register_rtsp_to_web_rtc_provider(
hass, "mock_domain", mock_provider
)
yield mock_provider
unsub()
async def test_get_image_from_camera(hass, image_mock_url):
"""Grab an image from camera entity."""
@@ -189,17 +238,13 @@ async def test_get_image_from_camera_not_jpeg(hass, image_mock_url):
assert image.content == b"png"
async def test_get_stream_source_from_camera(hass, mock_camera):
async def test_get_stream_source_from_camera(hass, mock_camera, mock_stream_source):
"""Fetch stream source from camera entity."""
with patch(
"homeassistant.components.camera.Camera.stream_source",
return_value="rtsp://127.0.0.1/stream",
) as mock_camera_stream_source:
stream_source = await camera.async_get_stream_source(hass, "camera.demo_camera")
stream_source = await camera.async_get_stream_source(hass, "camera.demo_camera")
assert mock_camera_stream_source.called
assert stream_source == "rtsp://127.0.0.1/stream"
assert mock_stream_source.called
assert stream_source == STREAM_SOURCE
async def test_get_image_without_exists_camera(hass, image_mock_url):
@@ -503,7 +548,7 @@ async def test_websocket_web_rtc_offer(
"id": 9,
"type": "camera/web_rtc_offer",
"entity_id": "camera.demo_camera",
"offer": "v=0\r\n",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
@@ -511,7 +556,7 @@ async def test_websocket_web_rtc_offer(
assert response["id"] == 9
assert response["type"] == TYPE_RESULT
assert response["success"]
assert response["result"]["answer"] == "a=sendonly"
assert response["result"]["answer"] == WEBRTC_ANSWER
async def test_websocket_web_rtc_offer_invalid_entity(
@@ -526,7 +571,7 @@ async def test_websocket_web_rtc_offer_invalid_entity(
"id": 9,
"type": "camera/web_rtc_offer",
"entity_id": "camera.does_not_exist",
"offer": "v=0\r\n",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
@@ -575,7 +620,7 @@ async def test_websocket_web_rtc_offer_failure(
"id": 9,
"type": "camera/web_rtc_offer",
"entity_id": "camera.demo_camera",
"offer": "v=0\r\n",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
@@ -604,7 +649,7 @@ async def test_websocket_web_rtc_offer_timeout(
"id": 9,
"type": "camera/web_rtc_offer",
"entity_id": "camera.demo_camera",
"offer": "v=0\r\n",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
@@ -628,7 +673,7 @@ async def test_websocket_web_rtc_offer_invalid_stream_type(
"id": 9,
"type": "camera/web_rtc_offer",
"entity_id": "camera.demo_camera",
"offer": "v=0\r\n",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
@@ -690,3 +735,145 @@ async def test_stream_unavailable(hass, hass_ws_client, mock_camera, mock_stream
demo_camera = hass.states.get("camera.demo_camera")
assert demo_camera is not None
assert demo_camera.state == camera.STATE_STREAMING
async def test_rtsp_to_web_rtc_offer(
hass,
hass_ws_client,
mock_camera,
mock_stream_source,
mock_rtsp_to_web_rtc,
):
"""Test creating a web_rtc offer from an rstp provider."""
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 9,
"type": "camera/web_rtc_offer",
"entity_id": "camera.demo_camera",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
assert response.get("id") == 9
assert response.get("type") == TYPE_RESULT
assert response.get("success")
assert "result" in response
assert response["result"] == {"answer": WEBRTC_ANSWER}
assert mock_rtsp_to_web_rtc.called
async def test_unsupported_rtsp_to_web_rtc_stream_type(
hass,
hass_ws_client,
mock_camera,
mock_hls_stream_source, # Not an RTSP stream source
mock_rtsp_to_web_rtc,
):
"""Test rtsp-to-webrtc is not registered for non-RTSP streams."""
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 10,
"type": "camera/web_rtc_offer",
"entity_id": "camera.demo_camera",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
assert response.get("id") == 10
assert response.get("type") == TYPE_RESULT
assert "success" in response
assert not response["success"]
async def test_rtsp_to_web_rtc_provider_unregistered(
hass,
hass_ws_client,
mock_camera,
mock_stream_source,
):
"""Test creating a web_rtc offer from an rstp provider."""
mock_provider = Mock(side_effect=provide_web_rtc_answer)
unsub = camera.async_register_rtsp_to_web_rtc_provider(
hass, "mock_domain", mock_provider
)
client = await hass_ws_client(hass)
# Registered provider can handle the WebRTC offer
await client.send_json(
{
"id": 11,
"type": "camera/web_rtc_offer",
"entity_id": "camera.demo_camera",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
assert response["id"] == 11
assert response["type"] == TYPE_RESULT
assert response["success"]
assert response["result"]["answer"] == WEBRTC_ANSWER
assert mock_provider.called
mock_provider.reset_mock()
# Unregister provider, then verify the WebRTC offer cannot be handled
unsub()
await client.send_json(
{
"id": 12,
"type": "camera/web_rtc_offer",
"entity_id": "camera.demo_camera",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
assert response.get("id") == 12
assert response.get("type") == TYPE_RESULT
assert "success" in response
assert not response["success"]
assert not mock_provider.called
async def test_rtsp_to_web_rtc_offer_not_accepted(
hass,
hass_ws_client,
mock_camera,
mock_stream_source,
):
"""Test a provider that can't satisfy the rtsp to webrtc offer."""
async def provide_none(stream_source: str, offer: str) -> str:
"""Simulate a provider that can't accept the offer."""
return None
mock_provider = Mock(side_effect=provide_none)
unsub = camera.async_register_rtsp_to_web_rtc_provider(
hass, "mock_domain", mock_provider
)
client = await hass_ws_client(hass)
# Registered provider can handle the WebRTC offer
await client.send_json(
{
"id": 11,
"type": "camera/web_rtc_offer",
"entity_id": "camera.demo_camera",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
assert response["id"] == 11
assert response.get("type") == TYPE_RESULT
assert "success" in response
assert not response["success"]
assert mock_provider.called
unsub()