test(webrtc): add STAP-A IDR detection tests (issue #18)
Some checks failed
ci / vet + build (push) Failing after 5m2s
ci / race tests (push) Has been skipped
ci / WebRTC smoke (5-viewer fanout) (push) Has been skipped
ci / WebRTC latency p95 gate (push) Has been skipped

Three new test functions covering the STAP-A (NAL type 24) packetisation
mode added to isH264IDRStart:
- LeadingIDR: STAP-A where first NAL is type 5 → true
- LeadingNonIDR: STAP-A where first NAL is SPS (type 7) → false
- Truncated: STAP-A with < 4 bytes → false, no panic
This commit is contained in:
Zac Gaetano 2026-05-10 13:20:59 -04:00
parent 8266ca72e6
commit 38d75b10b0

View file

@ -82,6 +82,55 @@ func TestIsH264IDRStart_FUA_TruncatedPayload(t *testing.T) {
}
}
func TestIsH264IDRStart_STAPA_LeadingIDR(t *testing.T) {
// STAP-A (type 24): byte 0 = NAL header (0x78 = type 24),
// bytes 1-2 = first NAL size (big-endian), byte 3 = first NAL header.
// First NAL type = 5 (IDR) → should be detected.
p := makePacket([]byte{
0x78, // STAP-A header: NRI=3, type=24
0x00, 0x03, // first NAL size = 3 bytes
0x65, 0x88, 0x84, // first NAL: type 5 (IDR slice)
0x00, 0x02, // second NAL size = 2 bytes (SPS, doesn't matter)
0x67, 0x42, // second NAL: SPS
})
if !isH264IDRStart(p) {
t.Error("STAP-A with leading IDR NAL (type 5) should be detected as IDR start")
}
}
func TestIsH264IDRStart_STAPA_LeadingNonIDR(t *testing.T) {
// STAP-A where the first NAL is SPS (type 7), not IDR.
// Common pattern: encoders bundle SPS+PPS+IDR in separate STAP-A,
// then IDR in a single NAL or FU-A. This STAP-A should not trigger reset.
p := makePacket([]byte{
0x78, // STAP-A header
0x00, 0x03, // first NAL size = 3
0x67, 0x42, 0x00, // first NAL: SPS (type 7)
})
if isH264IDRStart(p) {
t.Error("STAP-A with leading SPS (type 7) should not be IDR start")
}
}
func TestIsH264IDRStart_STAPA_Truncated(t *testing.T) {
// STAP-A with fewer than 4 bytes — cannot safely read first NAL header.
tests := []struct {
name string
payload []byte
}{
{"1 byte (header only)", []byte{0x78}},
{"2 bytes (header + 1 size byte)", []byte{0x78, 0x00}},
{"3 bytes (header + size, no NAL)", []byte{0x78, 0x00, 0x01}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if isH264IDRStart(makePacket(tt.payload)) {
t.Errorf("truncated STAP-A (%d bytes) should not panic or return true", len(tt.payload))
}
})
}
}
func TestIsH264IDRStart_Opus(t *testing.T) {
// Opus RTP payload starts with a TOC byte — definitely not H.264
p := makePacket([]byte{0xf8, 0xff, 0xfe})
@ -157,6 +206,32 @@ func TestKeyFrameCache_SecondIDRResetsAgain(t *testing.T) {
}
}
func TestKeyFrameCache_STAPA_IDR_ResetsCache(t *testing.T) {
// Verify that a STAP-A with a leading IDR NAL correctly resets the burst,
// just like a single-NAL IDR packet does.
c := newKeyFrameCache()
// Pre-load some P-frames.
for i := 0; i < 5; i++ {
c.push(makePacket([]byte{0x41, byte(i)}))
}
stapA := makePacket([]byte{
0x78, // STAP-A header
0x00, 0x03, // first NAL size = 3
0x65, 0x88, 0x84, // first NAL: IDR (type 5)
})
c.push(stapA)
snap := c.snapshot()
if len(snap) != 1 {
t.Errorf("STAP-A IDR should reset burst to 1 packet, got %d", len(snap))
}
if snap[0] != stapA {
t.Error("snapshot should contain the STAP-A IDR packet")
}
}
func TestKeyFrameCache_MaxPacketsCap(t *testing.T) {
c := newKeyFrameCache()
c.maxPackets = 5