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)
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.
- No audio subscribe is ever sent.
_send_subscribe() already accepts stream_type, but is only ever called with the default "video".
whep_proxy.py builds the handler with disable_audio_answer=True, which forces the answer's audio m-line to a=inactive.
- The answer declares
a=ssrc / msid lines for video only — _build_video_ssrc_lines() has no audio counterpart.
- 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).
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.petkitat DEBUG, every session:while the only subscribe ever sent is:
Audio is audible in the official PetKit app, so the hardware and cloud side are fine.
What I found (1.27.0)
on_add_audio_streamhas no handler. The dispatch table inagora_websocket.pyregisterson_add_video_streamonly, so the announcement is received and discarded._send_subscribe()already acceptsstream_type, but is only ever called with the default"video".whep_proxy.pybuilds the handler withdisable_audio_answer=True, which forces the answer's audio m-line toa=inactive.a=ssrc/msidlines for video only —_build_video_ssrc_lines()has no audio counterpart.on_add_video_streamarrives ~20 ms beforeon_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 returnsNoneand lets the audio handler finalize.After fixing 1–5
The answer is correct:
and the browser negotiates a live audio transceiver:
Where I'm stuck
The browser still gets no audio:
The browser's remote description is not the answer we build — it is go2rtc's:
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_directis not an escape hatch, since it is also backed by the shared go2rtc stream and returns 503 without it.Questions:
#audio=opus) be the right lever, or does this need PT rewriting in the WHEP upstream?Environment
ssrcId 20000/pt 69, videossrcId 40000/ h264Fork 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).