diff --git a/docs/source/api/camera_talkback.rst b/docs/source/api/camera_talkback.rst index 11c387f8..26cee765 100644 --- a/docs/source/api/camera_talkback.rst +++ b/docs/source/api/camera_talkback.rst @@ -6,6 +6,9 @@ Camera talkback The receiving side of the camera two-way audio channel. +.. autoclass:: pyhap.camera_talkback.TalkbackReceiverProtocol + :members: + .. autoclass:: pyhap.camera_talkback.TalkbackReceiver :members: :special-members: __init__ \ No newline at end of file diff --git a/pyhap/camera.py b/pyhap/camera.py index d3882a08..28eb218d 100644 --- a/pyhap/camera.py +++ b/pyhap/camera.py @@ -40,7 +40,7 @@ ) from pyhap.accessory import Accessory from pyhap.camera_multi_tier import MultiTierConfig, MultiTierController -from pyhap.camera_talkback import TalkbackReceiver +from pyhap.camera_talkback import TalkbackReceiver, TalkbackReceiverProtocol from pyhap.camera_webrtc import WebRTCConfig, WebRTCController from pyhap.characteristic import Characteristic from pyhap.const import CATEGORY_CAMERA @@ -1920,6 +1920,34 @@ def set_streaming_available(self, stream_idx: int) -> None: self._streaming_status[stream_idx] = STREAMING_STATUS["AVAILABLE"] self._management[stream_idx].get_characteristic("StreamingStatus").notify() + def create_talkback_receiver( + self, + session_id: UUID, + srtp_key_and_salt: bytes, + is_ipv6: bool, + controller_address: str, + ) -> TalkbackReceiverProtocol: + """Create the talkback audio receiver. + + Override this method to provide a custom talkback receiver. + + :param session_id: The session ID this audio belongs to. + :param srtp_key_and_salt: The audio SRTP master key concatenated with the + master salt, exactly as already negotiated in + :meth:`set_endpoints` (``audio_master_key + audio_master_salt``). + Reused here, not renegotiated. + :param is_ipv6: Whether to bind an IPv6 socket instead of IPv4. + :param controller_address: The controller address negotiated during + SetupEndpoints. + :return: A talkback receiver. + """ + return TalkbackReceiver( + srtp_key_and_salt=srtp_key_and_salt, + on_payload=functools.partial(self._talkback_payload_received, session_id), + is_ipv6=is_ipv6, + controller_address=controller_address, + ) + def set_endpoints(self, value: str, stream_idx: int | None = None) -> str: """Configure streaming endpoints. @@ -2007,11 +2035,9 @@ def set_endpoints(self, value: str, stream_idx: int | None = None) -> str: audio_backchannel = None audio_rtp_port = target_audio_port if self.support_talkback: - audio_backchannel = TalkbackReceiver( + audio_backchannel = self.create_talkback_receiver( + session_id=session_id, srtp_key_and_salt=audio_master_key + audio_master_salt, - on_payload=functools.partial( - self._talkback_payload_received, session_id - ), is_ipv6=bool(is_ipv6), controller_address=address, ) diff --git a/pyhap/camera_talkback.py b/pyhap/camera_talkback.py index 9ba3e4f7..a2f869d6 100644 --- a/pyhap/camera_talkback.py +++ b/pyhap/camera_talkback.py @@ -29,7 +29,7 @@ import socket import struct import threading -from typing import cast +from typing import Protocol, cast import pylibsrtp @@ -126,6 +126,25 @@ def is_allowed_sender(addr: tuple[str, int], controller_address: str | None) -> return source == controller_address +class TalkbackReceiverProtocol(Protocol): + """Listens for the HomeKit camera talkback (two-way) audio of one session.""" + + @property + def local_port(self) -> int: + """The local port this receiver is bound to. + + This is the value that must be reported in the ``AUDIO_RTP_PORT`` field + of the ``Accessory Address`` TLV in the ``SetupEndpoints`` response, so + that the controller knows where to send the talkback audio. + """ + + def start(self) -> None: + """Start listening for incoming audio.""" + + def stop(self) -> None: + """Stop listening and release the socket.""" + + class TalkbackReceiver: """Listens for the HomeKit camera talkback (two-way) audio of one session.