Skip to content

feat: audio switch so the physical handset keeps its speaker during live views - #7

Open
MIGI10 wants to merge 1 commit into
cmos486:mainfrom
MIGI10:feat/video-only-sessions
Open

MIGI10 wants to merge 1 commit into
cmos486:mainfrom
MIGI10:feat/video-only-sessions

Conversation

@MIGI10

@MIGI10 MIGI10 commented Sep 16, 2026

Copy link
Copy Markdown

Audio switch: keep the physical handset's speaker during live views

Problem

Every live view makes Ring route the intercom's incoming audio to the WebRTC session: the physical handset keeps showing video but its speaker goes silent (you can talk, you can't hear) until the stream closes. That's the right behaviour for the camera entity/companion card, which plays that audio, but wrong when the picture is consumed by something that never plays audio or does not need it — a go2rtc/Frigate restream for recording, or a TV/kiosk display that shows who is at the door while the user still picks up the physical handset to talk. In that second case the current behaviour is actively harmful: the handset rings, the TV shows the visitor, the user picks up and can't hear them.

Cause

Two things in the signaling python-ring-doorbell sends for every session:

  • live_viewstream_options: {audio_enabled: true}
  • on camera_connectedcamera_options: {stealth_mode: false} — this is what ring-client-api calls activateCameraSpeaker().

The snapshot path in this component already uses audio_enabled: false and doesn't send camera_options, and snapshots don't take the handset audio.

Change

switch.<device>_audio, default on (= today's behaviour), state restored across restarts.

switch live view gets physical handset
on video + audio video, speaker muted while a session is open
off video only video + audio
  • Every session now runs on _GatedRingWebRtcStream, a RingWebRtcStream subclass whose websocket property wraps the signaling socket in an AudioGate. The gate rewrites exactly two outgoing messages — live_viewstream_options.audio_enabled, camera_optionsstealth_mode = not audio — and delegates everything else (recv, close, async for). No library code is copied.
  • Toggling the switch while sessions are open is applied in place: stream_options + camera_options are sent as session messages (the same way ring-client-api sends them after activate_session). No renegotiation, no gap in the picture. The browser's audio transceiver is recvonly from the start and simply plays whatever RTP arrives.
  • Independently of the switch, an offer is treated as video-only when it carries a session-level a=x-video-only attribute, or when every audio m-line is a=inactive / rejected (port 0). That is how a go2rtc/WHEP bridge, which cannot alter its own offer, asks for a session that never takes the audio.

New files: audio.py (policy, gate, hot toggle), switch.py. __init__.py loses the conditional class and gains the gate.

Why the out-of-band attribute

Rewriting the audio m-line of the offer itself (a=inactive or port 0) makes Ring stop sending video, so a consumer that can't change its own offer (go2rtc's WHEP client always offers video+audio recvonly) needs a way to flag the session that doesn't alter the media description. Unknown session attributes are ignored by Ring.

Tested

Ring Intercom Handset Video on a BTicino analog handset, HA 2026.x, go2rtc (Frigate-embedded) consuming through a small WHEP→camera/webrtc/offer bridge that inserts a=x-video-only:

  • video streams to go2rtc/RTSP as before
  • the BTicino handset keeps hearing the visitor during the stream (video on the TV, conversation on the handset)
  • companion card unchanged (still gets audio, still mutes the handset — expected)
  • debug log shows WebRTC <id>: audio=False, live_view sent with stream_options={'audio_enabled': False, ...}, camera_options sent as {'stealth_mode': True, ...}

Unit-checked against the real RingWebRtcStream class: offer classifier, gate rewrite in both states, passthrough of recv/close/async for, and the hot toggle (stream_options + camera_options carry the session's session_id/dialog_id, later library camera_options follow the new state).

Hot toggle on hardware: on mid-session works every time (handset mutes, live view gets audio). Off mid-session reliably mutes the live view; the handset usually gets its audio back at once, but occasionally only when the session ends. apply_audio_live sends stealth_mode: true first and repeats it after 1 s to improve the odds; documented as a known limitation in the README.

Docs

README gets an "Audio switch" section under the Lovelace options.

🤖 Generated with Claude Code

…ive views

Every live view made Ring route the intercom's incoming audio to the
WebRTC session (live_view audio_enabled=true + camera_options
stealth_mode=false, which ring-client-api calls activateCameraSpeaker),
so the physical handset kept video but lost its speaker while a stream
was open. Fine for the companion card, wrong for consumers that never
play audio or don't need it: go2rtc/Frigate restreams, or a TV display
that shows the visitor while the user talks on the physical handset.

Add switch.<device>_audio (default on = current behaviour, state
restored). Off makes live views video-only: live_view goes out with
audio_enabled=false and camera_options with stealth_mode=true. Toggling
while sessions run is applied in place via stream_options /
camera_options session messages, no renegotiation.

All sessions now go through an AudioGate websocket proxy on a
RingWebRtcStream subclass; no library code is copied. Offers flagged
video-only (session attribute "a=x-video-only", or all audio m-lines
inactive / rejected) never take the audio regardless of the switch,
which is how a go2rtc/WHEP bridge that cannot alter its own offer asks
for it. Rewriting the audio m-line itself made Ring stop sending video,
hence the out-of-band attribute.

Tested on a Ring Intercom Handset Video (BTicino handset) with go2rtc
(WHEP) as consumer: video streams, the handset keeps hearing the
visitor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@MIGI10

MIGI10 commented Sep 16, 2026

Copy link
Copy Markdown
Author

Mention that the initial feature in mind was to allow asking for a video-only session (for go2rtc to consume and display on a TV monitor without blocking the physical handset audio). The switch was added later to add value to the HA camera entity, taking advantage of the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant