Add Homekit cameras codecs (#35238)

* Homekit cameras - Add codecs support

* Add valid_codecs + move audio application parameter

* Increase video bufsize

* Increase audio bufsize

* Update config flow to be aware of the copy option

* Add tests for copy video and audio codec

* remove unused from test

* remove unused from test

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
stickpin 2020-05-07 07:55:09 -07:00 committed by GitHub
parent 5e33842ce0
commit a38bb5b33b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 377 additions and 149 deletions

View file

@ -186,6 +186,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
# If the previous instance hasn't cleaned up yet
# we need to wait a bit
if not await hass.async_add_executor_job(port_is_available, port):
_LOGGER.warning("The local port %s is in use.", port)
raise ConfigEntryNotReady
if CONF_ENTRY_INDEX in conf and conf[CONF_ENTRY_INDEX] == 0:
@ -202,8 +203,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
# These are yaml only
ip_address = conf.get(CONF_IP_ADDRESS)
advertise_ip = conf.get(CONF_ADVERTISE_IP)
entity_config = conf.get(CONF_ENTITY_CONFIG, {})
entity_config = options.get(CONF_ENTITY_CONFIG, {}).copy()
auto_start = options.get(CONF_AUTO_START, DEFAULT_AUTO_START)
safe_mode = options.get(CONF_SAFE_MODE, DEFAULT_SAFE_MODE)
entity_filter = convert_filter(

View file

@ -19,21 +19,26 @@ from homeassistant.helpers.entityfilter import (
from .const import (
CONF_AUTO_START,
CONF_ENTITY_CONFIG,
CONF_FILTER,
CONF_SAFE_MODE,
CONF_VIDEO_CODEC,
CONF_ZEROCONF_DEFAULT_INTERFACE,
DEFAULT_AUTO_START,
DEFAULT_CONFIG_FLOW_PORT,
DEFAULT_SAFE_MODE,
DEFAULT_ZEROCONF_DEFAULT_INTERFACE,
SHORT_BRIDGE_NAME,
VIDEO_CODEC_COPY,
)
from .const import DOMAIN # pylint:disable=unused-import
from .util import find_next_available_port
_LOGGER = logging.getLogger(__name__)
CONF_CAMERA_COPY = "camera_copy"
CONF_DOMAINS = "domains"
SUPPORTED_DOMAINS = [
"alarm_control_panel",
"automation",
@ -183,6 +188,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
"""Initialize options flow."""
self.config_entry = config_entry
self.homekit_options = {}
self.included_cameras = set()
async def async_step_yaml(self, user_input=None):
"""No options for yaml managed entries."""
@ -236,6 +242,38 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
step_id="advanced", data_schema=vol.Schema(schema_base)
)
async def async_step_cameras(self, user_input=None):
"""Choose camera config."""
if user_input is not None:
entity_config = self.homekit_options[CONF_ENTITY_CONFIG]
for entity_id in self.included_cameras:
if entity_id in user_input[CONF_CAMERA_COPY]:
entity_config.setdefault(entity_id, {})[
CONF_VIDEO_CODEC
] = VIDEO_CODEC_COPY
elif (
entity_id in entity_config
and CONF_VIDEO_CODEC in entity_config[entity_id]
):
del entity_config[entity_id][CONF_VIDEO_CODEC]
return await self.async_step_advanced()
cameras_with_copy = []
entity_config = self.homekit_options.setdefault(CONF_ENTITY_CONFIG, {})
for entity in self.included_cameras:
hk_entity_config = entity_config.get(entity, {})
if hk_entity_config.get(CONF_VIDEO_CODEC) == VIDEO_CODEC_COPY:
cameras_with_copy.append(entity)
data_schema = vol.Schema(
{
vol.Optional(
CONF_CAMERA_COPY, default=cameras_with_copy,
): cv.multi_select(self.included_cameras),
}
)
return self.async_show_form(step_id="cameras", data_schema=data_schema)
async def async_step_exclude(self, user_input=None):
"""Choose entities to exclude from the domain."""
if user_input is not None:
@ -249,6 +287,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
),
CONF_EXCLUDE_ENTITIES: user_input[CONF_EXCLUDE_ENTITIES],
}
for entity_id in user_input[CONF_EXCLUDE_ENTITIES]:
if entity_id in self.included_cameras:
self.included_cameras.remove(entity_id)
if self.included_cameras:
return await self.async_step_cameras()
return await self.async_step_advanced()
entity_filter = self.homekit_options.get(CONF_FILTER, {})
@ -257,6 +300,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
self.hass,
self.homekit_options[CONF_INCLUDE_DOMAINS],
)
self.included_cameras = {
entity_id
for entity_id in all_supported_entities
if entity_id.startswith("camera.")
}
data_schema = vol.Schema(
{
vol.Optional(

View file

@ -13,12 +13,20 @@ UNDO_UPDATE_LISTENER = "undo_update_listener"
SHUTDOWN_TIMEOUT = 30
CONF_ENTRY_INDEX = "index"
# ### Codecs ####
VIDEO_CODEC_COPY = "copy"
VIDEO_CODEC_LIBX264 = "libx264"
AUDIO_CODEC_OPUS = "libopus"
VIDEO_CODEC_H264_OMX = "h264_omx"
AUDIO_CODEC_COPY = "copy"
# #### Attributes ####
ATTR_DISPLAY_NAME = "display_name"
ATTR_VALUE = "value"
# #### Config ####
CONF_ADVERTISE_IP = "advertise_ip"
CONF_AUDIO_CODEC = "audio_codec"
CONF_AUDIO_MAP = "audio_map"
CONF_AUDIO_PACKET_SIZE = "audio_packet_size"
CONF_AUTO_START = "auto_start"
@ -37,10 +45,12 @@ CONF_ZEROCONF_DEFAULT_INTERFACE = "zeroconf_default_interface"
CONF_STREAM_ADDRESS = "stream_address"
CONF_STREAM_SOURCE = "stream_source"
CONF_SUPPORT_AUDIO = "support_audio"
CONF_VIDEO_CODEC = "video_codec"
CONF_VIDEO_MAP = "video_map"
CONF_VIDEO_PACKET_SIZE = "video_packet_size"
# #### Config Defaults ####
DEFAULT_AUDIO_CODEC = AUDIO_CODEC_OPUS
DEFAULT_AUDIO_MAP = "0:a:0"
DEFAULT_AUDIO_PACKET_SIZE = 188
DEFAULT_AUTO_START = True
@ -52,6 +62,7 @@ DEFAULT_PORT = 51827
DEFAULT_CONFIG_FLOW_PORT = 51828
DEFAULT_SAFE_MODE = False
DEFAULT_ZEROCONF_DEFAULT_INTERFACE = False
DEFAULT_VIDEO_CODEC = VIDEO_CODEC_LIBX264
DEFAULT_VIDEO_MAP = "0:v:0"
DEFAULT_VIDEO_PACKET_SIZE = 1316
@ -233,4 +244,5 @@ CONFIG_OPTIONS = [
CONF_AUTO_START,
CONF_ZEROCONF_DEFAULT_INTERFACE,
CONF_SAFE_MODE,
CONF_ENTITY_CONFIG,
]

View file

@ -20,6 +20,13 @@
"description": "Choose the entities that you do NOT want to be bridged.",
"title": "Exclude entities in selected domains from bridge"
},
"cameras": {
"data": {
"camera_copy": "Cameras that support native H.264 streams"
},
"description": "Check all cameras that support native H.264 streams. If the camera does not output a H.264 stream, the system will transcode the video to H.264 for HomeKit. Transcoding requires a performant CPU and is unlikely to work on single board computers.",
"title": "Select camera video codec."
},
"advanced": {
"data": {
"auto_start": "[%key:component::homekit::config::step::user::data::auto_start%]",

View file

@ -1,33 +1,17 @@
{
"config": {
"abort": {
"port_name_in_use": "A bridge with the same name or port is already configured."
},
"step": {
"pairing": {
"description": "As soon as the {name} bridge is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d.",
"title": "Pair HomeKit Bridge"
},
"user": {
"data": {
"auto_start": "Autostart (disable if using Z-Wave or other delayed start system)",
"include_domains": "Domains to include"
},
"description": "A HomeKit Bridge will allow you to access your Home Assistant entities in HomeKit. HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.",
"title": "Activate HomeKit Bridge"
}
}
},
"title": "HomeKit Bridge",
"options": {
"step": {
"advanced": {
"yaml": {
"title": "Adjust HomeKit Bridge Options",
"description": "This entry is controlled via YAML"
},
"init": {
"data": {
"auto_start": "Autostart (disable if using Z-Wave or other delayed start system)",
"safe_mode": "Safe Mode (enable only if pairing fails)",
"zeroconf_default_interface": "Use default zeroconf interface (enable if the bridge cannot be found in the Home app)"
"include_domains": "[%key:component::homekit::config::step::user::data::include_domains%]"
},
"description": "These settings only need to be adjusted if the HomeKit bridge is not functional.",
"title": "Advanced Configuration"
"description": "Entities in the \u201cDomains to include\u201d will be bridged to HomeKit. You will be able to select which entities to exclude from this list on the next screen.",
"title": "Select domains to bridge."
},
"exclude": {
"data": {
@ -36,18 +20,41 @@
"description": "Choose the entities that you do NOT want to be bridged.",
"title": "Exclude entities in selected domains from bridge"
},
"init": {
"cameras": {
"data": {
"include_domains": "Domains to include"
"camera_copy": "Cameras that support native H.264 streams"
},
"description": "Entities in the \u201cDomains to include\u201d will be bridged to HomeKit. You will be able to select which entities to exclude from this list on the next screen.",
"title": "Select domains to bridge."
"description": "Check all cameras that support native H.264 streams. If the camera does not output a H.264 stream, the system will transcode the video to H.264 for HomeKit. Transcoding requires a performant CPU and is unlikely to work on single board computers.",
"title": "Select camera video codec."
},
"yaml": {
"description": "This entry is controlled via YAML",
"title": "Adjust HomeKit Bridge Options"
"advanced": {
"data": {
"auto_start": "[%key:component::homekit::config::step::user::data::auto_start%]",
"safe_mode": "Safe Mode (enable only if pairing fails)",
"zeroconf_default_interface": "Use default zeroconf interface (enable if the bridge cannot be found in the Home app)"
},
"description": "These settings only need to be adjusted if the HomeKit bridge is not functional.",
"title": "Advanced Configuration"
}
}
},
"title": "HomeKit Bridge"
}
"config": {
"step": {
"user": {
"data": {
"auto_start": "Autostart (disable if using Z-Wave or other delayed start system)",
"include_domains": "Domains to include"
},
"description": "A HomeKit Bridge will allow you to access your Home Assistant entities in HomeKit. HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.",
"title": "Activate HomeKit Bridge"
},
"pairing": {
"title": "Pair HomeKit Bridge",
"description": "As soon as the {name} bridge is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d."
}
},
"abort": {
"port_name_in_use": "A bridge with the same name or port is already configured."
}
}
}

View file

@ -16,6 +16,7 @@ from homeassistant.util import get_local_ip
from .accessories import TYPES, HomeAccessory
from .const import (
CONF_AUDIO_CODEC,
CONF_AUDIO_MAP,
CONF_AUDIO_PACKET_SIZE,
CONF_MAX_FPS,
@ -24,6 +25,7 @@ from .const import (
CONF_STREAM_ADDRESS,
CONF_STREAM_SOURCE,
CONF_SUPPORT_AUDIO,
CONF_VIDEO_CODEC,
CONF_VIDEO_MAP,
CONF_VIDEO_PACKET_SIZE,
)
@ -33,7 +35,9 @@ _LOGGER = logging.getLogger(__name__)
VIDEO_OUTPUT = (
"-map {v_map} -an "
"-c:v libx264 -profile:v {v_profile} -tune zerolatency -pix_fmt yuv420p "
"-c:v {v_codec} "
"{v_profile}"
"-tune zerolatency -pix_fmt yuv420p "
"-r {fps} "
"-b:v {v_max_bitrate}k -bufsize {v_bufsize}k -maxrate {v_max_bitrate}k "
"-payload_type 99 "
@ -43,11 +47,10 @@ VIDEO_OUTPUT = (
"localrtcpport={v_port}&pkt_size={v_pkt_size}"
)
AUDIO_ENCODER_OPUS = "libopus -application lowdelay"
AUDIO_OUTPUT = (
"-map {a_map} -vn "
"-c:a {a_encoder} "
"{a_application}"
"-ac 1 -ar {a_sample_rate}k "
"-b:a {a_max_bitrate}k -bufsize {a_bufsize}k "
"-payload_type 110 "
@ -171,19 +174,31 @@ class Camera(HomeAccessory, PyhapCamera):
return False
if "-i " not in input_source:
input_source = "-i " + input_source
video_profile = ""
if self.config[CONF_VIDEO_CODEC] != "copy":
video_profile = (
"-profile:v "
+ VIDEO_PROFILE_NAMES[
int.from_bytes(stream_config["v_profile_id"], byteorder="big")
]
+ " "
)
audio_application = ""
if self.config[CONF_AUDIO_CODEC] == "libopus":
audio_application = "-application lowdelay "
output_vars = stream_config.copy()
output_vars.update(
{
"v_profile": VIDEO_PROFILE_NAMES[
int.from_bytes(stream_config["v_profile_id"], byteorder="big")
],
"v_bufsize": stream_config["v_max_bitrate"] * 2,
"v_profile": video_profile,
"v_bufsize": stream_config["v_max_bitrate"] * 4,
"v_map": self.config[CONF_VIDEO_MAP],
"v_pkt_size": self.config[CONF_VIDEO_PACKET_SIZE],
"a_bufsize": stream_config["a_max_bitrate"] * 2,
"v_codec": self.config[CONF_VIDEO_CODEC],
"a_bufsize": stream_config["a_max_bitrate"] * 4,
"a_map": self.config[CONF_AUDIO_MAP],
"a_pkt_size": self.config[CONF_AUDIO_PACKET_SIZE],
"a_encoder": AUDIO_ENCODER_OPUS,
"a_encoder": self.config[CONF_AUDIO_CODEC],
"a_application": audio_application,
}
)
output = VIDEO_OUTPUT.format(**output_vars)

View file

@ -24,6 +24,9 @@ from homeassistant.helpers.storage import STORAGE_DIR
import homeassistant.util.temperature as temp_util
from .const import (
AUDIO_CODEC_COPY,
AUDIO_CODEC_OPUS,
CONF_AUDIO_CODEC,
CONF_AUDIO_MAP,
CONF_AUDIO_PACKET_SIZE,
CONF_FEATURE,
@ -36,14 +39,17 @@ from .const import (
CONF_STREAM_ADDRESS,
CONF_STREAM_SOURCE,
CONF_SUPPORT_AUDIO,
CONF_VIDEO_CODEC,
CONF_VIDEO_MAP,
CONF_VIDEO_PACKET_SIZE,
DEFAULT_AUDIO_CODEC,
DEFAULT_AUDIO_MAP,
DEFAULT_AUDIO_PACKET_SIZE,
DEFAULT_LOW_BATTERY_THRESHOLD,
DEFAULT_MAX_FPS,
DEFAULT_MAX_HEIGHT,
DEFAULT_MAX_WIDTH,
DEFAULT_VIDEO_CODEC,
DEFAULT_VIDEO_MAP,
DEFAULT_VIDEO_PACKET_SIZE,
DOMAIN,
@ -60,11 +66,16 @@ from .const import (
TYPE_SPRINKLER,
TYPE_SWITCH,
TYPE_VALVE,
VIDEO_CODEC_COPY,
VIDEO_CODEC_H264_OMX,
VIDEO_CODEC_LIBX264,
)
_LOGGER = logging.getLogger(__name__)
MAX_PORT = 65535
VALID_VIDEO_CODECS = [VIDEO_CODEC_LIBX264, VIDEO_CODEC_H264_OMX, AUDIO_CODEC_COPY]
VALID_AUDIO_CODECS = [AUDIO_CODEC_OPUS, VIDEO_CODEC_COPY]
BASIC_INFO_SCHEMA = vol.Schema(
{
@ -84,12 +95,18 @@ CAMERA_SCHEMA = BASIC_INFO_SCHEMA.extend(
{
vol.Optional(CONF_STREAM_ADDRESS): vol.All(ipaddress.ip_address, cv.string),
vol.Optional(CONF_STREAM_SOURCE): cv.string,
vol.Optional(CONF_AUDIO_CODEC, default=DEFAULT_AUDIO_CODEC): vol.In(
VALID_AUDIO_CODECS
),
vol.Optional(CONF_SUPPORT_AUDIO, default=False): cv.boolean,
vol.Optional(CONF_MAX_WIDTH, default=DEFAULT_MAX_WIDTH): cv.positive_int,
vol.Optional(CONF_MAX_HEIGHT, default=DEFAULT_MAX_HEIGHT): cv.positive_int,
vol.Optional(CONF_MAX_FPS, default=DEFAULT_MAX_FPS): cv.positive_int,
vol.Optional(CONF_AUDIO_MAP, default=DEFAULT_AUDIO_MAP): cv.string,
vol.Optional(CONF_VIDEO_MAP, default=DEFAULT_VIDEO_MAP): cv.string,
vol.Optional(CONF_VIDEO_CODEC, default=DEFAULT_VIDEO_CODEC): vol.In(
VALID_VIDEO_CODECS
),
vol.Optional(
CONF_AUDIO_PACKET_SIZE, default=DEFAULT_AUDIO_PACKET_SIZE
): cv.positive_int,

View file

@ -220,6 +220,120 @@ async def test_options_flow_basic(hass):
}
async def test_options_flow_with_cameras(hass):
"""Test config flow options."""
config_entry = _mock_config_entry_with_options_populated()
config_entry.add_to_hass(hass)
hass.states.async_set("climate.old", "off")
hass.states.async_set("camera.native_h264", "off")
hass.states.async_set("camera.transcode_h264", "off")
hass.states.async_set("camera.excluded", "off")
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(
config_entry.entry_id, context={"show_advanced_options": False}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"include_domains": ["fan", "vacuum", "climate", "camera"]},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "exclude"
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"exclude_entities": ["climate.old", "camera.excluded"]},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == "cameras"
result3 = await hass.config_entries.options.async_configure(
result2["flow_id"], user_input={"camera_copy": ["camera.native_h264"]},
)
assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result3["step_id"] == "advanced"
with patch("homeassistant.components.homekit.async_setup_entry", return_value=True):
result4 = await hass.config_entries.options.async_configure(
result3["flow_id"],
user_input={"safe_mode": True, "zeroconf_default_interface": False},
)
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert config_entry.options == {
"auto_start": False,
"filter": {
"exclude_domains": [],
"exclude_entities": ["climate.old", "camera.excluded"],
"include_domains": ["fan", "vacuum", "climate", "camera"],
"include_entities": [],
},
"entity_config": {"camera.native_h264": {"video_codec": "copy"}},
"safe_mode": True,
"zeroconf_default_interface": False,
}
# Now run though again and verify we can turn off copy
result = await hass.config_entries.options.async_init(
config_entry.entry_id, context={"show_advanced_options": False}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"include_domains": ["fan", "vacuum", "climate", "camera"]},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "exclude"
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"exclude_entities": ["climate.old", "camera.excluded"]},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == "cameras"
result3 = await hass.config_entries.options.async_configure(
result2["flow_id"], user_input={"camera_copy": []},
)
assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result3["step_id"] == "advanced"
with patch("homeassistant.components.homekit.async_setup_entry", return_value=True):
result4 = await hass.config_entries.options.async_configure(
result3["flow_id"],
user_input={"safe_mode": True, "zeroconf_default_interface": False},
)
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert config_entry.options == {
"auto_start": False,
"filter": {
"exclude_domains": [],
"exclude_entities": ["climate.old", "camera.excluded"],
"include_domains": ["fan", "vacuum", "climate", "camera"],
"include_entities": [],
},
"entity_config": {"camera.native_h264": {}},
"safe_mode": True,
"zeroconf_default_interface": False,
}
async def test_options_flow_blocked_when_from_yaml(hass):
"""Test config flow options."""

View file

@ -8,8 +8,12 @@ import pytest
from homeassistant.components import camera, ffmpeg
from homeassistant.components.homekit.accessories import HomeBridge
from homeassistant.components.homekit.const import (
AUDIO_CODEC_COPY,
CONF_AUDIO_CODEC,
CONF_STREAM_SOURCE,
CONF_SUPPORT_AUDIO,
CONF_VIDEO_CODEC,
VIDEO_CODEC_COPY,
)
from homeassistant.components.homekit.type_cameras import Camera
from homeassistant.components.homekit.type_switches import Switch
@ -18,6 +22,10 @@ from homeassistant.setup import async_setup_component
from tests.async_mock import AsyncMock, MagicMock, patch
MOCK_START_STREAM_TLV = "ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA"
MOCK_END_POINTS_TLV = "ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA=="
MOCK_START_STREAM_SESSION_UUID = UUID("3303d503-17cc-469a-b672-92436a71a2f6")
@pytest.fixture()
def run_driver(hass):
@ -81,49 +89,47 @@ async def test_camera_stream_source_configured(hass, run_driver, events):
assert acc.aid == 2
assert acc.category == 17 # Camera
stream_service = acc.get_service("CameraRTPStreamManagement")
endpoints_config_char = stream_service.get_characteristic("SetupEndpoints")
assert endpoints_config_char.setter_callback
stream_config_char = stream_service.get_characteristic(
"SelectedRTPStreamConfiguration"
)
assert stream_config_char.setter_callback
acc.set_endpoints(
"ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA=="
)
acc.set_endpoints(MOCK_END_POINTS_TLV)
session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID]
working_ffmpeg = _get_working_mock_ffmpeg()
with patch(
"homeassistant.components.demo.camera.DemoCamera.stream_source",
return_value=None,
), patch(
"homeassistant.components.homekit.type_cameras.HAFFmpeg",
return_value=_get_working_mock_ffmpeg(),
return_value=working_ffmpeg,
):
acc.set_selected_stream_configuration(
"ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA"
)
acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV)
await hass.async_block_till_done()
session_info = {
"id": "mock",
"v_srtp_key": "key",
"a_srtp_key": "key",
"v_port": "0",
"a_port": "0",
"address": "0.0.0.0",
}
acc.sessions[UUID("3303d503-17cc-469a-b672-92436a71a2f6")] = session_info
expected_output = (
"-map 0:v:0 -an -c:v libx264 -profile:v high -tune zerolatency -pix_fmt "
"yuv420p -r 30 -b:v 299k -bufsize 1196k -maxrate 299k -payload_type 99 -ssrc {v_ssrc} -f "
"rtp -srtp_out_suite AES_CM_128_HMAC_SHA1_80 -srtp_out_params "
"zdPmNLWeI86DtLJHvVLI6YPvqhVeeiLsNtrAgbgL "
"srtp://192.168.208.5:51246?rtcpport=51246&localrtcpport=51246&pkt_size=1316 -map 0:a:0 "
"-vn -c:a libopus -application lowdelay -ac 1 -ar 24k -b:a 24k -bufsize 96k -payload_type "
"110 -ssrc {a_ssrc} -f rtp -srtp_out_suite AES_CM_128_HMAC_SHA1_80 -srtp_out_params "
"shnETgfD+7xUQ8zRdsaytY11wu6CO73IJ+RZVJpU "
"srtp://192.168.208.5:51108?rtcpport=51108&localrtcpport=51108&pkt_size=188"
)
working_ffmpeg.open.assert_called_with(
cmd=[],
input_source="-i /dev/null",
output=expected_output.format(**session_info),
stdout_pipe=False,
)
with patch(
"homeassistant.components.demo.camera.DemoCamera.stream_source",
return_value="rtsp://example.local",
), patch(
"homeassistant.components.homekit.type_cameras.HAFFmpeg",
return_value=_get_working_mock_ffmpeg(),
return_value=working_ffmpeg,
):
acc.set_selected_stream_configuration(
"ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA"
)
acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV)
await acc.stop_stream(session_info)
# Calling a second time should not throw
await acc.stop_stream(session_info)
@ -171,26 +177,8 @@ async def test_camera_stream_source_configured_with_failing_ffmpeg(
assert acc.aid == 2
assert acc.category == 17 # Camera
stream_service = acc.get_service("CameraRTPStreamManagement")
endpoints_config_char = stream_service.get_characteristic("SetupEndpoints")
assert endpoints_config_char.setter_callback
stream_config_char = stream_service.get_characteristic(
"SelectedRTPStreamConfiguration"
)
assert stream_config_char.setter_callback
acc.set_endpoints(
"ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA=="
)
session_info = {
"id": "mock",
"v_srtp_key": "key",
"a_srtp_key": "key",
"v_port": "0",
"a_port": "0",
"address": "0.0.0.0",
}
acc.sessions[UUID("3303d503-17cc-469a-b672-92436a71a2f6")] = session_info
acc.set_endpoints(MOCK_END_POINTS_TLV)
session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID]
with patch(
"homeassistant.components.demo.camera.DemoCamera.stream_source",
@ -199,9 +187,7 @@ async def test_camera_stream_source_configured_with_failing_ffmpeg(
"homeassistant.components.homekit.type_cameras.HAFFmpeg",
return_value=_get_failing_mock_ffmpeg(),
):
acc.set_selected_stream_configuration(
"ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA"
)
acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV)
await acc.stop_stream(session_info)
# Calling a second time should not throw
await acc.stop_stream(session_info)
@ -225,26 +211,8 @@ async def test_camera_stream_source_found(hass, run_driver, events):
assert acc.aid == 2
assert acc.category == 17 # Camera
stream_service = acc.get_service("CameraRTPStreamManagement")
endpoints_config_char = stream_service.get_characteristic("SetupEndpoints")
assert endpoints_config_char.setter_callback
stream_config_char = stream_service.get_characteristic(
"SelectedRTPStreamConfiguration"
)
assert stream_config_char.setter_callback
acc.set_endpoints(
"ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA=="
)
session_info = {
"id": "mock",
"v_srtp_key": "key",
"a_srtp_key": "key",
"v_port": "0",
"a_port": "0",
"address": "0.0.0.0",
}
acc.sessions[UUID("3303d503-17cc-469a-b672-92436a71a2f6")] = session_info
acc.set_endpoints(MOCK_END_POINTS_TLV)
session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID]
with patch(
"homeassistant.components.demo.camera.DemoCamera.stream_source",
@ -253,9 +221,7 @@ async def test_camera_stream_source_found(hass, run_driver, events):
"homeassistant.components.homekit.type_cameras.HAFFmpeg",
return_value=_get_working_mock_ffmpeg(),
):
acc.set_selected_stream_configuration(
"ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA"
)
acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV)
await acc.stop_stream(session_info)
await hass.async_block_till_done()
@ -266,9 +232,7 @@ async def test_camera_stream_source_found(hass, run_driver, events):
"homeassistant.components.homekit.type_cameras.HAFFmpeg",
return_value=_get_working_mock_ffmpeg(),
):
acc.set_selected_stream_configuration(
"ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA"
)
acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV)
await acc.stop_stream(session_info)
await hass.async_block_till_done()
@ -290,26 +254,8 @@ async def test_camera_stream_source_fails(hass, run_driver, events):
assert acc.aid == 2
assert acc.category == 17 # Camera
stream_service = acc.get_service("CameraRTPStreamManagement")
endpoints_config_char = stream_service.get_characteristic("SetupEndpoints")
assert endpoints_config_char.setter_callback
stream_config_char = stream_service.get_characteristic(
"SelectedRTPStreamConfiguration"
)
assert stream_config_char.setter_callback
acc.set_endpoints(
"ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA=="
)
session_info = {
"id": "mock",
"v_srtp_key": "key",
"a_srtp_key": "key",
"v_port": "0",
"a_port": "0",
"address": "0.0.0.0",
}
acc.sessions[UUID("3303d503-17cc-469a-b672-92436a71a2f6")] = session_info
acc.set_endpoints(MOCK_END_POINTS_TLV)
session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID]
with patch(
"homeassistant.components.demo.camera.DemoCamera.stream_source",
@ -318,9 +264,7 @@ async def test_camera_stream_source_fails(hass, run_driver, events):
"homeassistant.components.homekit.type_cameras.HAFFmpeg",
return_value=_get_working_mock_ffmpeg(),
):
acc.set_selected_stream_configuration(
"ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA"
)
acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV)
await acc.stop_stream(session_info)
await hass.async_block_till_done()
@ -340,13 +284,76 @@ async def test_camera_with_no_stream(hass, run_driver, events):
assert acc.aid == 2
assert acc.category == 17 # Camera
acc.set_endpoints(
"ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA=="
)
acc.set_selected_stream_configuration(
"ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA"
)
acc.set_endpoints(MOCK_END_POINTS_TLV)
acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV)
await hass.async_block_till_done()
with pytest.raises(HomeAssistantError):
await hass.async_add_executor_job(acc.get_snapshot, 1024)
async def test_camera_stream_source_configured_and_copy_codec(hass, run_driver, events):
"""Test a camera that can stream with a configured source."""
await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}})
await async_setup_component(
hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}}
)
entity_id = "camera.demo_camera"
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
acc = Camera(
hass,
run_driver,
"Camera",
entity_id,
2,
{
CONF_STREAM_SOURCE: "/dev/null",
CONF_SUPPORT_AUDIO: True,
CONF_VIDEO_CODEC: VIDEO_CODEC_COPY,
CONF_AUDIO_CODEC: AUDIO_CODEC_COPY,
},
)
bridge = HomeBridge("hass", run_driver, "Test Bridge")
bridge.add_accessory(acc)
await acc.run_handler()
assert acc.aid == 2
assert acc.category == 17 # Camera
acc.set_endpoints(MOCK_END_POINTS_TLV)
session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID]
working_ffmpeg = _get_working_mock_ffmpeg()
with patch(
"homeassistant.components.demo.camera.DemoCamera.stream_source",
return_value=None,
), patch(
"homeassistant.components.homekit.type_cameras.HAFFmpeg",
return_value=working_ffmpeg,
):
acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV)
await acc.stop_stream(session_info)
await hass.async_block_till_done()
expected_output = (
"-map 0:v:0 -an -c:v copy -tune zerolatency -pix_fmt yuv420p -r 30 -b:v 299k "
"-bufsize 1196k -maxrate 299k -payload_type 99 -ssrc {v_ssrc} -f rtp -srtp_out_suite "
"AES_CM_128_HMAC_SHA1_80 -srtp_out_params zdPmNLWeI86DtLJHvVLI6YPvqhVeeiLsNtrAgbgL "
"srtp://192.168.208.5:51246?rtcpport=51246&localrtcpport=51246&pkt_size=1316 -map 0:a:0 "
"-vn -c:a copy -ac 1 -ar 24k -b:a 24k -bufsize 96k -payload_type 110 -ssrc {a_ssrc} "
"-f rtp -srtp_out_suite AES_CM_128_HMAC_SHA1_80 -srtp_out_params "
"shnETgfD+7xUQ8zRdsaytY11wu6CO73IJ+RZVJpU "
"srtp://192.168.208.5:51108?rtcpport=51108&localrtcpport=51108&pkt_size=188"
)
working_ffmpeg.open.assert_called_with(
cmd=[],
input_source="-i /dev/null",
output=expected_output.format(**session_info),
stdout_pipe=False,
)