From 909871331d6fe1453abf1f507694cd713dd502fb Mon Sep 17 00:00:00 2001 From: AJ Plotkin <61085222+ajplotkin@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:31:16 +0200 Subject: [PATCH 1/2] nest: request periodic keyframes (fixes slow RTSP/consumer stream opens) pkg/webrtc/conn.go sends an RTCP PictureLossIndication every 2s to keep the source emitting keyframes, but only for ModePassiveProducer (WHIP/browser push). Nest is an active-pull WebRTC source (ModeActiveProducer), so it was excluded -- its keyframe interval drifts long when idle, and any consumer that joins mid-GOP (RTSP live view, snapshot grab) waits up to a full keyframe interval to start. Enable the keyframe request for the Nest source, gated on FormatName "nest/webrtc" rather than the mode so other ModeActiveProducer WebRTC sources (ring/tuya battery cams etc.) aren't forced into 2s IDRs, which would be battery- and bandwidth-hostile. PLI is media-plane RTCP -- no SDM API quota impact. Also add defer ticker.Stop() (the upstream pattern relies on GC of an unreferenced ticker, fine on go>=1.23 but explicit is safer). Measured on a Nest doorbell: keyframe interval ~3.3s -> ~1.8s; on-demand frame.jpeg grabs drop to ~1s. Co-Authored-By: Claude Opus 4.8 --- pkg/webrtc/conn.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pkg/webrtc/conn.go b/pkg/webrtc/conn.go index 924fd5504..1499d2f21 100644 --- a/pkg/webrtc/conn.go +++ b/pkg/webrtc/conn.go @@ -87,10 +87,19 @@ func NewConn(pc *webrtc.PeerConnection) *Conn { } } - if c.Mode == core.ModePassiveProducer && remote.Kind() == webrtc.RTPCodecTypeVideo { + // Also request periodic keyframes for the Nest source (ModeActiveProducer, + // FormatName "nest/webrtc"). Upstream only does this for PassiveProducer (WHIP/browser + // push, which have no other keyframe path). Nest is an active-pull WebRTC source, so + // without this its keyframe interval drifts long when idle and RTSP/consumer opens are + // slow. Gating on FormatName (not the mode) avoids forcing a 2s IDR on other + // ModeActiveProducer WebRTC sources (ring/tuya battery cams etc.) where it'd be + // battery- and bandwidth-hostile. PLI is media-plane RTCP: zero SDM API quota impact. + if (c.Mode == core.ModePassiveProducer || c.FormatName == "nest/webrtc") && remote.Kind() == webrtc.RTPCodecTypeVideo { go func() { pkts := []rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(remote.SSRC())}} - for range time.NewTicker(time.Second * 2).C { + t := time.NewTicker(time.Second * 2) + defer t.Stop() + for range t.C { if err := pc.WriteRTCP(pkts); err != nil { return } From 133c89bfcc515618c709aa0b1380ae89cfb89e9b Mon Sep 17 00:00:00 2001 From: AJ Plotkin <61085222+ajplotkin@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:51:27 +0200 Subject: [PATCH 2/2] nest: add sprop-parameter-sets to RTSP SDP (dimensions known at DESCRIBE) The Nest WebRTC source's H264 codec FmtpLine comes from Google's SDP answer, which carries profile-level-id but no sprop-parameter-sets (WebRTC sends SPS/PPS in-band). go2rtc's RTSP server copies FmtpLine verbatim into the DESCRIBE SDP, so an RTSP consumer (e.g. ffmpeg for HomeKit live view) gets no SPS/PPS in the SDP and can't learn video dimensions until an in-band keyframe arrives -- forcing a large -probesize and slow starts (or failing outright at low probesize). Capture SPS (NAL 7) / PPS (NAL 8) from the incoming H264 RTP in the OnTrack receive loop (handling STAP-A bundling, which is how libwebrtc packs SPS+PPS+IDR) and append sprop-parameter-sets to the codec FmtpLine once. Every consumer that clones the codec (RTSP) or reads GetParameterSet (MSE/MP4) then benefits. Gated on FormatName "nest/webrtc" like the keyframe-request patch. Upstream does the equivalent for H265 in pkg/dvrip. With preload + the 2s keyframe request, SPS/PPS are captured well before any DESCRIBE. This lets RTSP consumers use a small -probesize safely (dimensions come from the SDP). Note it does not by itself make stream-copy opens instant: ffmpeg still aligns copy output to the next keyframe, so the ~2s keyframe interval (bounded by the PLI patch) remains the floor. Co-Authored-By: Claude Opus 4.8 --- pkg/webrtc/conn.go | 59 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/pkg/webrtc/conn.go b/pkg/webrtc/conn.go index 1499d2f21..508cc60a7 100644 --- a/pkg/webrtc/conn.go +++ b/pkg/webrtc/conn.go @@ -1,12 +1,15 @@ package webrtc import ( + "encoding/base64" + "encoding/binary" "encoding/json" "fmt" "strings" "time" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/pion/webrtc/v4" @@ -87,13 +90,11 @@ func NewConn(pc *webrtc.PeerConnection) *Conn { } } - // Also request periodic keyframes for the Nest source (ModeActiveProducer, + // Patched: also request periodic keyframes for the Nest source (ModeActiveProducer, // FormatName "nest/webrtc"). Upstream only does this for PassiveProducer (WHIP/browser - // push, which have no other keyframe path). Nest is an active-pull WebRTC source, so - // without this its keyframe interval drifts long when idle and RTSP/consumer opens are - // slow. Gating on FormatName (not the mode) avoids forcing a 2s IDR on other - // ModeActiveProducer WebRTC sources (ring/tuya battery cams etc.) where it'd be - // battery- and bandwidth-hostile. PLI is media-plane RTCP: zero SDM API quota impact. + // push). Gating on FormatName (not the mode) avoids forcing 2s IDRs on other + // ActiveProducer WebRTC sources (ring/tuya battery cams etc.) where it'd be harmful. + // Keeps keyframes ~2s fresh so RTSP consumers (Homebridge live view) start fast. if (c.Mode == core.ModePassiveProducer || c.FormatName == "nest/webrtc") && remote.Kind() == webrtc.RTPCodecTypeVideo { go func() { pkts := []rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(remote.SSRC())}} @@ -107,6 +108,16 @@ func NewConn(pc *webrtc.PeerConnection) *Conn { }() } + // Patched: capture SPS/PPS from the Nest H264 stream and append sprop-parameter-sets to + // the codec FmtpLine, so RTSP consumers (Homebridge live view via ffmpeg) learn video + // dimensions from the DESCRIBE SDP and start fast instead of waiting for an in-band + // keyframe + probe (~3.7s -> near-instant). Google's WebRTC SDP has profile-level-id but + // no sprop; SPS/PPS arrive in-band (usually bundled in a STAP-A). Upstream does the + // equivalent for H265 (pkg/dvrip). Gated on FormatName like the PLI patch above. + captureSprop := c.FormatName == "nest/webrtc" && codec.Name == core.CodecH264 && + !strings.Contains(codec.FmtpLine, "sprop-parameter-sets=") + var spropSPS, spropPPS []byte + for { b := make([]byte, ReceiveMTU) n, _, err := remote.Read(b) @@ -125,6 +136,42 @@ func NewConn(pc *webrtc.PeerConnection) *Conn { continue } + if captureSprop { + save := func(nal []byte) { + if len(nal) == 0 { + return + } + switch nal[0] & 0x1F { + case h264.NALUTypeSPS: + spropSPS = append([]byte(nil), nal...) + case h264.NALUTypePPS: + spropPPS = append([]byte(nil), nal...) + } + } + if pl := packet.Payload; pl[0]&0x1F == 24 { // STAP-A: bundled NALs + for bb := pl[1:]; len(bb) >= 2; { + sz := int(binary.BigEndian.Uint16(bb)) + bb = bb[2:] + if sz < 1 || sz > len(bb) { + break + } + save(bb[:sz]) + bb = bb[sz:] + } + } else { + save(pl) + } + if spropSPS != nil && spropPPS != nil { + if codec.FmtpLine != "" { + codec.FmtpLine += ";" + } + codec.FmtpLine += "sprop-parameter-sets=" + + base64.StdEncoding.EncodeToString(spropSPS) + "," + + base64.StdEncoding.EncodeToString(spropPPS) + captureSprop = false + } + } + track.WriteRTP(packet) } })