Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions pkg/h264/rtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,25 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {

buf := make([]byte, 0, 512*1024) // 512K

// A depayloader attaching to a live stream can land in the middle of a
// fragmented NAL unit. codecs.H264Packet appends FU-A fragments without
// checking the start bit, so those orphan fragments are assembled and
// emitted under a synthesized NAL header - a NAL that looks valid (and
// passes IsKeyframe when the fragmented type is 5) but is missing its
// head, including the slice header. Consumers then fail to decode it.
// RFC 6184 §5.8 requires discarding fragments that follow a lost start,
// and pion's own SampleBuilder gates on IsPartitionHead for this reason.
// Wait for a partition head before feeding the depayloader anything.
synced := false

return func(packet *rtp.Packet) {
if !synced {
if !depack.IsPartitionHead(packet.Payload) {
return
}
synced = true
}

//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)

payload, err := depack.Unmarshal(packet.Payload)
Expand Down
53 changes: 53 additions & 0 deletions pkg/h264/rtp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package h264

import (
"testing"

"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
"github.com/stretchr/testify/require"
)

// fuA builds an FU-A packet carrying one fragment of a fragmented NAL unit.
func fuA(naluType byte, start, end bool, seq uint16, payload []byte) *rtp.Packet {
const fuAType = 28
indicator := byte(0x60) | fuAType // nri=3, type=FU-A
header := naluType
if start {
header |= 0x80
}
if end {
header |= 0x40
}
return &rtp.Packet{
Header: rtp.Header{SequenceNumber: seq, Marker: end},
Payload: append([]byte{indicator, header}, payload...),
}
}

// A depayloader that attaches mid-way through a fragmented IDR must not emit a
// NAL assembled from the leftover fragments. Without a partition-head gate,
// codecs.H264Packet appends the tail fragments and synthesizes a NAL header,
// producing a keyframe-shaped NAL whose slice header is missing - undecodable,
// but indistinguishable from a real keyframe to IsKeyframe.
func TestRTPDepay_SkipsNALUStartedBeforeAttach(t *testing.T) {
codec := &core.Codec{Name: core.CodecH264}

var got [][]byte
depay := RTPDepay(codec, func(packet *core.Packet) {
got = append(got, append([]byte(nil), packet.Payload...))
})

// Attach mid-NAL: the start fragment (and everything before it) was never
// seen. Only the tail of a fragmented IDR arrives.
depay(fuA(NALUTypeIFrame, false, false, 100, []byte{0xaa, 0xbb}))
depay(fuA(NALUTypeIFrame, false, true, 101, []byte{0xcc, 0xdd}))

require.Empty(t, got, "must not emit a NAL whose start fragment was never received")

// A complete NAL that begins after we attached must still be delivered.
depay(fuA(NALUTypeIFrame, true, false, 102, []byte{0x01, 0x02}))
depay(fuA(NALUTypeIFrame, false, true, 103, []byte{0x03, 0x04}))

require.Len(t, got, 1, "a NAL received in full must be emitted")
}