Skip to content

Camera live stream has no audio: on_add_audio_stream is never handled and no audio subscribe is sent #295

Description

@jakeovski

Summary

Camera live stream delivers video only — the browser receives zero audio tracks — even though the device publishes audio and Agora announces it on every session. I traced this end to end on a camera-equipped feeder, HA 2026.7.4, integration 1.27.0, and found the audio path is switched off in several independent places.

I have a working fork with the integration-side fixes and would happily open a PR if the approach looks right — but I've hit a genuine wall at the go2rtc boundary and would value your read on it.

The device does publish audio

Captured with custom_components.petkit at DEBUG, every session:

WS ← [on_add_audio_stream] {'_message': {'audio': True, 'cname': '<redacted>',
      'pt': 69, 'ssrcId': 20000, 'uid': <device-uid>}, '_type': 'on_add_audio_stream'}

while the only subscribe ever sent is:

Agora subscribe: stream_id=<device-uid> ssrc_id=40000 codec=h264

Audio is audible in the official PetKit app, so the hardware and cloud side are fine.

What I found (1.27.0)

  1. on_add_audio_stream has no handler. The dispatch table in agora_websocket.py registers on_add_video_stream only, so the announcement is received and discarded.
  2. No audio subscribe is ever sent. _send_subscribe() already accepts stream_type, but is only ever called with the default "video".
  3. whep_proxy.py builds the handler with disable_audio_answer=True, which forces the answer's audio m-line to a=inactive.
  4. The answer declares a=ssrc / msid lines for video only_build_video_ssrc_lines() has no audio counterpart.
  5. Ordering. on_add_video_stream arrives ~20 ms before on_add_audio_stream, and the answer is finalized from the video handler, so the audio SSRC is not yet known when the m-line is built.

One trap worth flagging: awaiting the audio announcement from inside a handler deadlocks, because handlers run within async for raw_message in websocket — the wait starves the loop that would deliver the message. It has to be a deferral that returns None and lets the audio handler finalize.

After fixing 1–5

The answer is correct:

ANSWER built locally; audio_streams={<device-uid>: {'ssrcId': 20000, 'cname': '<redacted>', 'pt': 69}}
audio_line=m=audio 9 UDP/TLS/RTP/SAVPF 69 0 8

and the browser negotiates a live audio transceiver:

transceiver mid=0 kind=audio direction=recvonly currentDirection=recvonly

Where I'm stuck

The browser still gets no audio:

receivers:   audio → readyState "live", muted: true
inbound-rtp: video only
codecs:      ["video/H264"]

The browser's remote description is not the answer we build — it is go2rtc's:

ours    : m=audio 9     UDP/TLS/RTP/SAVPF 69 0 8
browser : m=audio 53460 UDP/TLS/RTP/SAVPF 111 0 8

go2rtc makes its own offer to Agora and its own answer to the browser. Agora forwards the publisher's payload type (69) unchanged, and PT 69 was never in go2rtc's offer, so it appears to be discarded. I tried both leaving the standard payload types in our answer and injecting PT 69 with an opus rtpmap — neither produced audio. whep_direct is not an escape hatch, since it is also backed by the shared go2rtc stream and returns 503 without it.

Questions:

  • Is Agora's SFU expected to rewrite the payload type to the subscriber's negotiated value, or does it always forward the publisher's PT?
  • Does go2rtc receive the audio track at all here? I could not reach the HA-managed go2rtc API to check.
  • Would forcing the go2rtc source to normalise audio (e.g. #audio=opus) be the right lever, or does this need PT rewriting in the WHEP upstream?

Environment

  • Home Assistant 2026.7.4 (HAOS), HA-managed go2rtc
  • integration 1.27.0
  • Device: feeder with camera, audio ssrcId 20000 / pt 69, video ssrcId 40000 / h264
  • Chrome (desktop)

Fork with the fixes: https://github.com/jakeovski/homeassistant_petkit (releases 1.27.1–1.27.12; the meaningful commits are the audio handler, the audio subscribe, disable_audio_answer=False, the audio SSRC lines, and the answer deferral).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions