From 7446ff478fdda9801ce0f7b87b1f78538479a189 Mon Sep 17 00:00:00 2001 From: Assaf Inbal Date: Wed, 12 Apr 2023 23:46:21 +0300 Subject: [PATCH] Add h264_v4l2m2m codec and profiles to HomeKit cameras (#91246) --- homeassistant/components/homekit/const.py | 4 + .../components/homekit/type_cameras.py | 7 +- homeassistant/components/homekit/util.py | 8 +- tests/components/homekit/test_type_cameras.py | 75 +++++++++++++++++++ 4 files changed, 90 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 4517f9c5a5e..81dbf4f7e2e 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -19,6 +19,8 @@ VIDEO_CODEC_COPY = "copy" VIDEO_CODEC_LIBX264 = "libx264" AUDIO_CODEC_OPUS = "libopus" VIDEO_CODEC_H264_OMX = "h264_omx" +VIDEO_CODEC_H264_V4L2M2M = "h264_v4l2m2m" +VIDEO_PROFILE_NAMES = ["baseline", "main", "high"] AUDIO_CODEC_COPY = "copy" # #### Attributes #### @@ -54,6 +56,7 @@ CONF_STREAM_ADDRESS = "stream_address" CONF_STREAM_SOURCE = "stream_source" CONF_SUPPORT_AUDIO = "support_audio" CONF_VIDEO_CODEC = "video_codec" +CONF_VIDEO_PROFILE_NAMES = "video_profile_names" CONF_VIDEO_MAP = "video_map" CONF_VIDEO_PACKET_SIZE = "video_packet_size" CONF_STREAM_COUNT = "stream_count" @@ -71,6 +74,7 @@ DEFAULT_MAX_WIDTH = 1920 DEFAULT_PORT = 21063 DEFAULT_CONFIG_FLOW_PORT = 21064 DEFAULT_VIDEO_CODEC = VIDEO_CODEC_LIBX264 +DEFAULT_VIDEO_PROFILE_NAMES = VIDEO_PROFILE_NAMES DEFAULT_VIDEO_MAP = "0:v:0" DEFAULT_VIDEO_PACKET_SIZE = 1316 DEFAULT_STREAM_COUNT = 3 diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 9d7f2ae4c6b..3bc2b1ed6ae 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -40,6 +40,7 @@ from .const import ( CONF_VIDEO_CODEC, CONF_VIDEO_MAP, CONF_VIDEO_PACKET_SIZE, + CONF_VIDEO_PROFILE_NAMES, DEFAULT_AUDIO_CODEC, DEFAULT_AUDIO_MAP, DEFAULT_AUDIO_PACKET_SIZE, @@ -51,6 +52,7 @@ from .const import ( DEFAULT_VIDEO_CODEC, DEFAULT_VIDEO_MAP, DEFAULT_VIDEO_PACKET_SIZE, + DEFAULT_VIDEO_PROFILE_NAMES, SERV_DOORBELL, SERV_MOTION_SENSOR, SERV_SPEAKER, @@ -111,8 +113,6 @@ RESOLUTIONS = [ (1600, 1200), ] -VIDEO_PROFILE_NAMES = ["baseline", "main", "high"] - FFMPEG_WATCH_INTERVAL = timedelta(seconds=5) FFMPEG_LOGGER = "ffmpeg_logger" FFMPEG_WATCHER = "ffmpeg_watcher" @@ -128,6 +128,7 @@ CONFIG_DEFAULTS = { CONF_AUDIO_MAP: DEFAULT_AUDIO_MAP, CONF_VIDEO_MAP: DEFAULT_VIDEO_MAP, CONF_VIDEO_CODEC: DEFAULT_VIDEO_CODEC, + CONF_VIDEO_PROFILE_NAMES: DEFAULT_VIDEO_PROFILE_NAMES, CONF_AUDIO_PACKET_SIZE: DEFAULT_AUDIO_PACKET_SIZE, CONF_VIDEO_PACKET_SIZE: DEFAULT_VIDEO_PACKET_SIZE, CONF_STREAM_COUNT: DEFAULT_STREAM_COUNT, @@ -346,7 +347,7 @@ class Camera(HomeAccessory, PyhapCamera): if self.config[CONF_VIDEO_CODEC] != "copy": video_profile = ( "-profile:v " - + VIDEO_PROFILE_NAMES[ + + self.config[CONF_VIDEO_PROFILE_NAMES][ int.from_bytes(stream_config["v_profile_id"], byteorder="big") ] + " " diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 5f0838d91a9..0e3bcbfee86 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -95,6 +95,7 @@ from .const import ( TYPE_VALVE, VIDEO_CODEC_COPY, VIDEO_CODEC_H264_OMX, + VIDEO_CODEC_H264_V4L2M2M, VIDEO_CODEC_LIBX264, ) @@ -107,7 +108,12 @@ MAX_VERSION_PART = 2**32 - 1 MAX_PORT = 65535 -VALID_VIDEO_CODECS = [VIDEO_CODEC_LIBX264, VIDEO_CODEC_H264_OMX, AUDIO_CODEC_COPY] +VALID_VIDEO_CODECS = [ + VIDEO_CODEC_LIBX264, + VIDEO_CODEC_H264_OMX, + VIDEO_CODEC_H264_V4L2M2M, + AUDIO_CODEC_COPY, +] VALID_AUDIO_CODECS = [AUDIO_CODEC_OPUS, VIDEO_CODEC_COPY] BASIC_INFO_SCHEMA = vol.Schema( diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 645452363e0..1ce876ee584 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -19,11 +19,13 @@ from homeassistant.components.homekit.const import ( CONF_STREAM_SOURCE, CONF_SUPPORT_AUDIO, CONF_VIDEO_CODEC, + CONF_VIDEO_PROFILE_NAMES, SERV_DOORBELL, SERV_MOTION_SENSOR, SERV_STATELESS_PROGRAMMABLE_SWITCH, VIDEO_CODEC_COPY, VIDEO_CODEC_H264_OMX, + VIDEO_CODEC_H264_V4L2M2M, ) from homeassistant.components.homekit.type_cameras import Camera from homeassistant.components.homekit.type_switches import Switch @@ -516,6 +518,79 @@ async def test_camera_stream_source_configured_and_copy_codec( ) +async def test_camera_stream_source_configured_and_override_profile_names( + hass: HomeAssistant, run_driver, events +) -> None: + """Test a camera that can stream with a configured source over overridden profile names.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + + 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_H264_V4L2M2M, + CONF_VIDEO_PROFILE_NAMES: ["0", "2", "4"], + CONF_AUDIO_CODEC: AUDIO_CODEC_COPY, + }, + ) + bridge = HomeBridge("hass", run_driver, "Test Bridge") + bridge.add_accessory(acc) + + await acc.run() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + await _async_setup_endpoints(hass, acc) + 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, + ): + await _async_start_streaming(hass, acc) + await _async_reconfigure_stream(hass, acc, session_info, {}) + await _async_stop_all_streams(hass, acc) + + expected_output = ( + "-map 0:v:0 -an -c:v h264_v4l2m2m -profile:v 4 -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, + extra_cmd="-hide_banner -nostats", + stderr_pipe=True, + ) + + async def test_camera_streaming_fails_after_starting_ffmpeg( hass: HomeAssistant, run_driver, events ) -> None: