Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/source/api/camera_talkback.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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__
36 changes: 31 additions & 5 deletions pyhap/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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,
)
Expand Down
21 changes: 20 additions & 1 deletion pyhap/camera_talkback.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import socket
import struct
import threading
from typing import cast
from typing import Protocol, cast

import pylibsrtp

Expand Down Expand Up @@ -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.

Expand Down