H264 depacketizer emits a corrupt IDR when a consumer attaches mid-FU-A
I noticed intermittent frame.jpeg 500 / exit status 183 errors with go2rtc connected to several Nest cameras.
High Level Description
api/frame.jpeg (and anything else using h264.RTPDepay, e.g. the keyframe consumer)
intermittently fails with exit status 183 / exit status 69 on WebRTC sources such as
nest:. The failure rate scales with how often the source sends keyframes and how big they
are. With a Nest camera forced to a 2 s keyframe interval (~150–240 KB IDRs) roughly half
of all snapshots fail.
ffmpeg's stderr (not surfaced by go2rtc today) says:
[h264] non-existing PPS 12 referenced
[h264] A non-intra slice in an IDR NAL unit.
[h264] no frame!
Error opening input: Invalid data found when processing inputRoot cause
pkg/h264/rtp.go RTPDepay uses pion/rtp codecs.H264Packet to reassemble FU-A
fragments. pion appends every FU-A fragment to fuaBuffer regardless of the start
bit (h264_packet.go, parseBody, case naluType == fuaNALUType), and when the end bit
arrives it synthesizes a NAL header from the FU indicator/header and returns the buffer.
A consumer that attaches while a fragmented NAL is in flight therefore receives a NAL that
has a valid IDR header (0x65) but whose body is the tail of the slice. There's no slice header,
so ffmpeg reads garbage (non-intra slice in an IDR NAL unit). The same happens when the
start fragment is lost. Because IsKeyframe() only looks at the NAL type, the keyframe
consumer happily accepts it and hands it to ffmpeg.
A dump of one failing sample: NAL units 7:24 8:4 5:156010 — SPS and PPS are fine
(prepended from sprop-parameter-sets), the "IDR" is a fragment tail.
Repro Steps
- Any FU-A-fragmenting H264 source with large keyframes (Nest WebRTC is a reliable one; a
ffmpeg:source with-g 60 -x264-params keyint=60and a high bitrate should do too). - Hammer
curl http://go2rtc:1984/api/frame.jpeg?src=camin a loop. - Some fraction of requests return 500
exit status 183.
Unit test: feed RTPDepay the FU-A packets of one NAL starting from the third fragment —
it emits a NAL instead of nothing.
Fix
Track FU-A state in RTPDepay: ignore fragments until one carrying the start bit is seen,
and start a fresh depacketizer when a start bit arrives while a previous fragment set never
finished. PR to follow. The underlying defect is in pion/rtp, filed as https://github.com/pion/rtp/issues/370.
Version
1.9.14+dev (master c245815), linux/amd64, Docker.
Source: AlexxIT/go2rtc