ci / rust (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / package-preview (push) Canceled after 0s
ci / package-installer (push) Canceled after 0s
ci / linux-agent (push) Canceled after 0s
ci / edge-service (push) Canceled after 0s
ci / coturn-pop (push) Canceled after 0s
ci / package-windows-host (push) Canceled after 0s
303 lines
8.3 KiB
Go
303 lines
8.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type testEncodedRing struct {
|
|
records []*EncodedRingRecord
|
|
}
|
|
|
|
func (r *testEncodedRing) Pop() (*EncodedRingRecord, error) {
|
|
if len(r.records) == 0 {
|
|
return nil, nil
|
|
}
|
|
record := r.records[0]
|
|
r.records = r.records[1:]
|
|
return record, nil
|
|
}
|
|
|
|
func (*testEncodedRing) Close() error { return nil }
|
|
|
|
func TestVideoAccessUnitRoundTripThroughFragments(t *testing.T) {
|
|
payload := bytes.Repeat([]byte{0x5a}, videoDatagramPayloadMax*2+17)
|
|
unit := VideoAccessUnit{
|
|
StreamID: 1,
|
|
Generation: 2,
|
|
Sequence: 3,
|
|
FrameID: 4,
|
|
PTS: 5,
|
|
DTS: 6,
|
|
Flags: videoFlagKeyFrame,
|
|
Payload: payload,
|
|
}
|
|
datagrams, err := fragmentVideoAccessUnit(unit)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(datagrams) != 3 {
|
|
t.Fatalf("got %d datagrams, want 3", len(datagrams))
|
|
}
|
|
|
|
reassembler := newVideoReassembler()
|
|
var recovered *VideoAccessUnit
|
|
now := time.Unix(100, 0)
|
|
for _, datagram := range datagrams {
|
|
recovered, err = reassembler.push(datagram, now)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if recovered == nil {
|
|
t.Fatal("frame was not reassembled")
|
|
}
|
|
if !bytes.Equal(recovered.Payload, payload) || recovered.Flags&videoFlagKeyFrame == 0 {
|
|
t.Fatal("reassembled frame does not match source")
|
|
}
|
|
}
|
|
|
|
func TestVideoReassemblerExpiresIncompleteFrame(t *testing.T) {
|
|
datagrams, err := fragmentVideoAccessUnit(VideoAccessUnit{
|
|
StreamID: 1,
|
|
Generation: 1,
|
|
Sequence: 1,
|
|
FrameID: 1,
|
|
PTS: 1,
|
|
DTS: 1,
|
|
Flags: videoFlagKeyFrame,
|
|
Payload: bytes.Repeat([]byte{1}, videoDatagramPayloadMax+1),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reassembler := newVideoReassembler()
|
|
now := time.Unix(200, 0)
|
|
if _, err := reassembler.push(datagrams[0], now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := reassembler.push(datagrams[0], now.Add(videoReassemblyTimeout+time.Nanosecond)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(reassembler.frames) != 1 {
|
|
t.Fatalf("expected only the new incomplete assembly, got %d", len(reassembler.frames))
|
|
}
|
|
}
|
|
|
|
func TestVideoDatagramRejectsInvalidFragmentFlags(t *testing.T) {
|
|
datagrams, err := fragmentVideoAccessUnit(VideoAccessUnit{
|
|
StreamID: 1,
|
|
Generation: 1,
|
|
Sequence: 1,
|
|
FrameID: 1,
|
|
Payload: []byte{1},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
datagrams[0][56] = 0
|
|
datagrams[0][57] = 0
|
|
if _, err := decodeVideoDatagram(datagrams[0]); err == nil {
|
|
t.Fatal("invalid first/last flags were accepted")
|
|
}
|
|
}
|
|
|
|
func TestVideoDatagramSizeUsesCompleteDatagramLimit(t *testing.T) {
|
|
if err := validateVideoDatagramSize(make([]byte, videoDatagramMaxBytes)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := validateVideoDatagramSize(make([]byte, videoDatagramMaxBytes+1)); err == nil {
|
|
t.Fatal("oversized datagram was accepted")
|
|
}
|
|
}
|
|
|
|
func TestPrepareDatagramKeepsOpaquePacketsUnparsed(t *testing.T) {
|
|
input := []byte("audio-or-control")
|
|
prepared, kind, err := prepareDatagram(input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if kind != datagramOpaque || !bytes.Equal(prepared, input) {
|
|
t.Fatalf("unexpected opaque packet result: kind=%d payload=%q", kind, prepared)
|
|
}
|
|
prepared[0] = 'X'
|
|
if input[0] == 'X' {
|
|
t.Fatal("prepared datagram aliases the UDP input buffer")
|
|
}
|
|
}
|
|
|
|
func TestPrepareDatagramRecognizesVideoPackets(t *testing.T) {
|
|
datagrams, err := fragmentVideoAccessUnit(VideoAccessUnit{
|
|
StreamID: 1, Generation: 1, Sequence: 1, FrameID: 1, Payload: []byte{1},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, kind, err := prepareDatagram(datagrams[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if kind != datagramVideo {
|
|
t.Fatalf("got datagram kind %d, want video", kind)
|
|
}
|
|
}
|
|
|
|
func TestAudioPacketRoundTripIsIndependent(t *testing.T) {
|
|
input, err := encodeAudioDatagram(AudioPacket{
|
|
StreamID: 1, Generation: 2, Sequence: 3, PTS: 4, DurationMS: 20,
|
|
Payload: []byte{0x01, 0x02, 0x03},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
decoded, err := decodeAudioDatagram(input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if decoded.StreamID != 1 || decoded.Generation != 2 || decoded.Sequence != 3 ||
|
|
decoded.PTS != 4 || decoded.DurationMS != 20 || !bytes.Equal(decoded.Payload, []byte{1, 2, 3}) {
|
|
t.Fatalf("decoded audio packet does not match source: %+v", decoded)
|
|
}
|
|
prepared, kind, err := prepareDatagram(input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if kind != datagramOpaque || !bytes.Equal(prepared, input) {
|
|
t.Fatalf("audio packet was not preserved as opaque transport data")
|
|
}
|
|
}
|
|
|
|
func TestAudioDatagramRejectsInvalidDurationAndSize(t *testing.T) {
|
|
if _, err := encodeAudioDatagram(AudioPacket{
|
|
StreamID: 1, Generation: 1, Sequence: 1, PTS: 1, DurationMS: 15, Payload: []byte{1},
|
|
}); err == nil {
|
|
t.Fatal("invalid audio duration was accepted")
|
|
}
|
|
if err := validateAudioDatagramSize(make([]byte, audioDatagramMaxBytes+1)); err == nil {
|
|
t.Fatal("oversized audio datagram was accepted")
|
|
}
|
|
}
|
|
|
|
func TestAudioDatagramRejectsReservedByteAndUnknownFlags(t *testing.T) {
|
|
encoded, err := encodeAudioDatagram(AudioPacket{
|
|
StreamID: 1, Generation: 2, Sequence: 3, PTS: 4,
|
|
DurationMS: 20, Payload: []byte("opus"),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
encoded[5] = 1
|
|
if _, err := decodeAudioDatagram(encoded); err == nil {
|
|
t.Fatal("audio reserved byte was accepted")
|
|
}
|
|
encoded[5] = 0
|
|
binary.LittleEndian.PutUint16(encoded[38:40], 2)
|
|
if _, err := decodeAudioDatagram(encoded); err == nil {
|
|
t.Fatal("unknown audio flags were accepted")
|
|
}
|
|
}
|
|
|
|
func TestPumpEncodedRingFragmentsAndMarksKeyFrame(t *testing.T) {
|
|
payload := bytes.Repeat([]byte{0x42}, videoDatagramPayloadMax+3)
|
|
ring := &testEncodedRing{records: []*EncodedRingRecord{{
|
|
Sequence: 9, PTS: 123, KeyFrame: true, Payload: payload,
|
|
}}}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
var datagrams [][]byte
|
|
send := func(data []byte) error {
|
|
datagrams = append(datagrams, append([]byte(nil), data...))
|
|
if len(datagrams) == 2 {
|
|
cancel()
|
|
}
|
|
return nil
|
|
}
|
|
err := pumpEncodedRing(ctx, ring, 7, 8, send)
|
|
if err != context.Canceled {
|
|
t.Fatalf("pump returned %v, want context cancellation", err)
|
|
}
|
|
if len(datagrams) != 2 {
|
|
t.Fatalf("got %d datagrams, want 2", len(datagrams))
|
|
}
|
|
first, err := decodeVideoDatagram(datagrams[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.Flags&videoFlagKeyFrame == 0 || first.PTS != 123 || first.FrameID != 9 {
|
|
t.Fatalf("ring metadata was not preserved: %+v", first)
|
|
}
|
|
}
|
|
|
|
func TestPumpEncodedRingRejectsInvalidRecord(t *testing.T) {
|
|
ring := &testEncodedRing{records: []*EncodedRingRecord{{Sequence: 0, Payload: []byte{1}}}}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
err := pumpEncodedRing(ctx, ring, 1, 1, func([]byte) error { return nil })
|
|
if err == nil {
|
|
t.Fatal("invalid encoded ring record was accepted")
|
|
}
|
|
}
|
|
|
|
func TestPumpAudioRingEncodesRDA1AndDiscontinuity(t *testing.T) {
|
|
ring := &testEncodedRing{records: []*EncodedRingRecord{{
|
|
Sequence: 7,
|
|
PTS: 400_000,
|
|
Kind: 2,
|
|
Payload: []byte("opus"),
|
|
}}}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
var datagram []byte
|
|
err := pumpAudioRing(ctx, ring, 11, 12, func(data []byte) error {
|
|
datagram = append([]byte(nil), data...)
|
|
cancel()
|
|
return nil
|
|
})
|
|
if err != context.Canceled {
|
|
t.Fatalf("pump returned %v, want context cancellation", err)
|
|
}
|
|
packet, err := decodeAudioDatagram(datagram)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if packet.StreamID != 11 || packet.Generation != 12 || packet.Sequence != 7 ||
|
|
packet.PTS != 400_000 || packet.DurationMS != 20 ||
|
|
packet.Flags&audioFlagDiscontinuity == 0 || !bytes.Equal(packet.Payload, []byte("opus")) {
|
|
t.Fatalf("audio ring metadata was not preserved: %+v", packet)
|
|
}
|
|
}
|
|
|
|
func TestPumpAudioRingRejectsInvalidRecords(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
record *EncodedRingRecord
|
|
}{
|
|
{name: "zero sequence", record: &EncodedRingRecord{Payload: []byte{1}}},
|
|
{name: "empty payload", record: &EncodedRingRecord{Sequence: 1}},
|
|
{
|
|
name: "oversized payload",
|
|
record: &EncodedRingRecord{
|
|
Sequence: 1,
|
|
Payload: make([]byte, audioPacketMaxBytes+1),
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
ring := &testEncodedRing{records: []*EncodedRingRecord{test.record}}
|
|
err := pumpAudioRing(
|
|
context.Background(),
|
|
ring,
|
|
1,
|
|
1,
|
|
func([]byte) error { return nil },
|
|
)
|
|
if err == nil {
|
|
t.Fatal("invalid audio ring record was accepted")
|
|
}
|
|
})
|
|
}
|
|
}
|