"""Contains the Camera accessory and related. When a HAP client (e.g. iOS) wants to start a video stream it does the following: [0. Read supported RTP configuration] [0. Read supported video configuration] [0. Read supported audio configuration] [0. Read the current streaming status] 1. Sets the SetupEndpoints characteristic to notify the camera about its IP address, selected security parameters, etc. 2. The camera responds to the above by setting the SetupEndpoints with its IP address, etc. 3. The client sets the SelectedRTPStreamConfiguration characteristic to notify the camera of its prefered audio and video configuration and to initiate the start of the streaming. 4. The camera starts the streaming with the above configuration. [5. At some point the client can reconfigure or stop the stream similarly to step 3.] """ import asyncio import functools import ipaddress import logging import os import struct import sys from uuid import UUID from pyhap import RESOURCE_DIR, tlv from pyhap.accessory import Accessory from pyhap.const import CATEGORY_CAMERA from pyhap.util import byte_bool, to_base64_str if sys.version_info >= (3, 11): from asyncio import timeout as async_timeout else: from async_timeout import timeout as async_timeout SETUP_TYPES = { "SESSION_ID": b"\x01", "STATUS": b"\x02", "ADDRESS": b"\x03", "VIDEO_SRTP_PARAM": b"\x04", "AUDIO_SRTP_PARAM": b"\x05", "VIDEO_SSRC": b"\x06", "AUDIO_SSRC": b"\x07", } SETUP_STATUS = { "SUCCESS": b"\x00", "BUSY": b"\x01", "ERROR": b"\x02", } SETUP_IPV = { "IPV4": b"\x00", "IPV6": b"\x01", } SETUP_ADDR_INFO = { "ADDRESS_VER": b"\x01", "ADDRESS": b"\x02", "VIDEO_RTP_PORT": b"\x03", "AUDIO_RTP_PORT": b"\x04", } SETUP_SRTP_PARAM = { "CRYPTO": b"\x01", "MASTER_KEY": b"\x02", "MASTER_SALT": b"\x03", } STREAMING_STATUS = { "AVAILABLE": b"\x00", "STREAMING": b"\x01", "BUSY": b"\x02", } RTP_CONFIG_TYPES = { "CRYPTO": b"\x02", } SRTP_CRYPTO_SUITES = { "AES_CM_128_HMAC_SHA1_80": b"\x00", "AES_CM_256_HMAC_SHA1_80": b"\x01", "NONE": b"\x02", } VIDEO_TYPES = { "CODEC": b"\x01", "CODEC_PARAM": b"\x02", "ATTRIBUTES": b"\x03", "RTP_PARAM": b"\x04", } VIDEO_CODEC_TYPES = { "H264": b"\x00", } VIDEO_CODEC_PARAM_TYPES = { "PROFILE_ID": b"\x01", "LEVEL": b"\x02", "PACKETIZATION_MODE": b"\x03", "CVO_ENABLED": b"\x04", "CVO_ID": b"\x05", } VIDEO_CODEC_PARAM_CVO_TYPES = { "UNSUPPORTED": b"\x01", "SUPPORTED": b"\x02", } VIDEO_CODEC_PARAM_PROFILE_ID_TYPES = { "BASELINE": b"\x00", "MAIN": b"\x01", "HIGH": b"\x02", } VIDEO_CODEC_PARAM_LEVEL_TYPES = { "TYPE3_1": b"\x00", "TYPE3_2": b"\x01", "TYPE4_0": b"\x02", } VIDEO_CODEC_PARAM_PACKETIZATION_MODE_TYPES = { "NON_INTERLEAVED": b"\x00", } VIDEO_ATTRIBUTES_TYPES = { "IMAGE_WIDTH": b"\x01", "IMAGE_HEIGHT": b"\x02", "FRAME_RATE": b"\x03", } SUPPORTED_VIDEO_CONFIG_TAG = b"\x01" SELECTED_STREAM_CONFIGURATION_TYPES = { "SESSION": b"\x01", "VIDEO": b"\x02", "AUDIO": b"\x03", } RTP_PARAM_TYPES = { "PAYLOAD_TYPE": b"\x01", "SYNCHRONIZATION_SOURCE": b"\x02", "MAX_BIT_RATE": b"\x03", "RTCP_SEND_INTERVAL": b"\x04", "MAX_MTU": b"\x05", "COMFORT_NOISE_PAYLOAD_TYPE": b"\x06", } AUDIO_TYPES = { "CODEC": b"\x01", "CODEC_PARAM": b"\x02", "RTP_PARAM": b"\x03", "COMFORT_NOISE": b"\x04", } AUDIO_CODEC_TYPES = { "PCMU": b"\x00", "PCMA": b"\x01", "AACELD": b"\x02", "OPUS": b"\x03", } AUDIO_CODEC_PARAM_TYPES = { "CHANNEL": b"\x01", "BIT_RATE": b"\x02", "SAMPLE_RATE": b"\x03", "PACKET_TIME": b"\x04", } AUDIO_CODEC_PARAM_BIT_RATE_TYPES = { "VARIABLE": b"\x00", "CONSTANT": b"\x01", } AUDIO_CODEC_PARAM_SAMPLE_RATE_TYPES = { "KHZ_8": b"\x00", "KHZ_16": b"\x01", "KHZ_24": b"\x02", } SUPPORTED_AUDIO_CODECS_TAG = b"\x01" SUPPORTED_COMFORT_NOISE_TAG = b"\x02" SUPPORTED_AUDIO_CONFIG_TAG = b"\x02" SET_CONFIG_REQUEST_TAG = b"\x02" SESSION_ID = b"\x01" NO_SRTP = b"\x01\x01\x02\x02\x00\x03\x00" """Configuration value for no SRTP.""" FFMPEG_CMD = ( "ffmpeg -re -f avfoundation -framerate {fps} -i 0:0 -threads 0 " "-vcodec libx264 -an -pix_fmt yuv420p -r {fps} -f rawvideo -tune zerolatency " "-vf scale={width}:{height} -b:v {v_max_bitrate}k -bufsize {v_max_bitrate}k " "-payload_type 99 -ssrc {v_ssrc} -f rtp " "-srtp_out_suite AES_CM_128_HMAC_SHA1_80 -srtp_out_params {v_srtp_key} " "srtp://{address}:{v_port}?rtcpport={v_port}&" "localrtcpport={v_port}&pkt_size=1378" ) """Template for the ffmpeg command.""" logger = logging.getLogger(__name__) class Camera(Accessory): """An Accessory that can negotiated camera stream settings with iOS and start a stream. """ category = CATEGORY_CAMERA @staticmethod def get_supported_rtp_config(support_srtp): """Return a tlv representation of the RTP configuration we support. SRTP support allows only the AES_CM_128_HMAC_SHA1_80 cipher for now. :param support_srtp: True if SRTP is supported, False otherwise. :type support_srtp: bool """ if support_srtp: crypto = SRTP_CRYPTO_SUITES["AES_CM_128_HMAC_SHA1_80"] else: crypto = SRTP_CRYPTO_SUITES["NONE"] return tlv.encode(RTP_CONFIG_TYPES["CRYPTO"], crypto, to_base64=True) @staticmethod def get_supported_video_stream_config(video_params): """Return a tlv representation of the supported video stream configuration. Expected video parameters: - codec - resolutions :param video_params: Supported video configurations :type video_params: dict """ codec_params_tlv = tlv.encode( VIDEO_CODEC_PARAM_TYPES["PACKETIZATION_MODE"], VIDEO_CODEC_PARAM_PACKETIZATION_MODE_TYPES["NON_INTERLEAVED"], ) codec_params = video_params["codec"] for profile in codec_params["profiles"]: codec_params_tlv += tlv.encode( VIDEO_CODEC_PARAM_TYPES["PROFILE_ID"], profile ) for level in codec_params["levels"]: codec_params_tlv += tlv.encode(VIDEO_CODEC_PARAM_TYPES["LEVEL"], level) attr_tlv = b"" for resolution in video_params["resolutions"]: res_tlv = tlv.encode( VIDEO_ATTRIBUTES_TYPES["IMAGE_WIDTH"], struct.pack("