mirror of
https://github.com/pion/webrtc.git
synced 2025-10-05 15:16:52 +08:00
@@ -9,7 +9,6 @@ linters:
|
|||||||
disable:
|
disable:
|
||||||
- maligned
|
- maligned
|
||||||
- lll
|
- lll
|
||||||
- dupl
|
|
||||||
- gocritic
|
- gocritic
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- gochecknoglobals
|
- gochecknoglobals
|
||||||
|
@@ -4,11 +4,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pions/rtcp"
|
||||||
"github.com/pions/webrtc"
|
"github.com/pions/webrtc"
|
||||||
"github.com/pions/webrtc/examples/util"
|
"github.com/pions/webrtc/examples/util"
|
||||||
gst "github.com/pions/webrtc/examples/util/gstreamer-sink"
|
gst "github.com/pions/webrtc/examples/util/gstreamer-sink"
|
||||||
"github.com/pions/webrtc/pkg/ice"
|
"github.com/pions/webrtc/pkg/ice"
|
||||||
"github.com/pions/webrtc/pkg/rtcp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@@ -4,11 +4,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pions/rtcp"
|
||||||
"github.com/pions/webrtc"
|
"github.com/pions/webrtc"
|
||||||
"github.com/pions/webrtc/examples/util"
|
"github.com/pions/webrtc/examples/util"
|
||||||
"github.com/pions/webrtc/pkg/ice"
|
"github.com/pions/webrtc/pkg/ice"
|
||||||
"github.com/pions/webrtc/pkg/media/ivfwriter"
|
"github.com/pions/webrtc/pkg/media/ivfwriter"
|
||||||
"github.com/pions/webrtc/pkg/rtcp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@@ -7,10 +7,10 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pions/rtcp"
|
||||||
|
"github.com/pions/rtp"
|
||||||
"github.com/pions/webrtc"
|
"github.com/pions/webrtc"
|
||||||
"github.com/pions/webrtc/examples/util"
|
"github.com/pions/webrtc/examples/util"
|
||||||
"github.com/pions/webrtc/pkg/rtcp"
|
|
||||||
"github.com/pions/webrtc/pkg/rtp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var peerConnectionConfig = webrtc.RTCConfiguration{
|
var peerConnectionConfig = webrtc.RTCConfiguration{
|
||||||
|
6
go.mod
6
go.mod
@@ -3,9 +3,12 @@ module github.com/pions/webrtc
|
|||||||
require (
|
require (
|
||||||
github.com/pions/datachannel v1.2.0
|
github.com/pions/datachannel v1.2.0
|
||||||
github.com/pions/dtls v1.1.0
|
github.com/pions/dtls v1.1.0
|
||||||
|
github.com/pions/rtcp v1.0.0
|
||||||
|
github.com/pions/rtp v1.0.0
|
||||||
github.com/pions/sctp v1.3.0
|
github.com/pions/sctp v1.3.0
|
||||||
github.com/pions/sdp v1.3.0
|
github.com/pions/sdp v1.3.0
|
||||||
github.com/pions/stun v0.1.0
|
github.com/pions/srtp v1.0.1
|
||||||
|
github.com/pions/stun v0.2.0
|
||||||
github.com/pions/transport v0.1.0
|
github.com/pions/transport v0.1.0
|
||||||
github.com/pkg/errors v0.8.1
|
github.com/pkg/errors v0.8.1
|
||||||
github.com/stretchr/testify v1.3.0
|
github.com/stretchr/testify v1.3.0
|
||||||
@@ -19,5 +22,4 @@ require (
|
|||||||
github.com/marten-seemann/qtls v0.0.0-20190110111248-7a090b30c3ab // indirect
|
github.com/marten-seemann/qtls v0.0.0-20190110111248-7a090b30c3ab // indirect
|
||||||
github.com/onsi/ginkgo v1.7.0 // indirect
|
github.com/onsi/ginkgo v1.7.0 // indirect
|
||||||
github.com/onsi/gomega v1.4.3 // indirect
|
github.com/onsi/gomega v1.4.3 // indirect
|
||||||
github.com/pions/srtp v1.0.0
|
|
||||||
)
|
)
|
||||||
|
12
go.sum
12
go.sum
@@ -25,14 +25,18 @@ github.com/pions/datachannel v1.2.0 h1:N12qhHSRVlgBcaal2Hi4skdz7VI4yz6bNC5IJDMzC
|
|||||||
github.com/pions/datachannel v1.2.0/go.mod h1:MKPEKJRwX/a9/tyQvcVTUI9szyf8ZuUyZxSA9AVMSro=
|
github.com/pions/datachannel v1.2.0/go.mod h1:MKPEKJRwX/a9/tyQvcVTUI9szyf8ZuUyZxSA9AVMSro=
|
||||||
github.com/pions/dtls v1.1.0 h1:NR/F/+gifVWvFOX/v2rHkIUpJ0UOISAyiS53d19nimU=
|
github.com/pions/dtls v1.1.0 h1:NR/F/+gifVWvFOX/v2rHkIUpJ0UOISAyiS53d19nimU=
|
||||||
github.com/pions/dtls v1.1.0/go.mod h1:OgJcO0SqrDdQzqkCTdAp4xCQlbCmwZtGyhbthbq9zIA=
|
github.com/pions/dtls v1.1.0/go.mod h1:OgJcO0SqrDdQzqkCTdAp4xCQlbCmwZtGyhbthbq9zIA=
|
||||||
|
github.com/pions/rtcp v1.0.0 h1:kYGe6RegZ63yVDkqXaru1+kHZAqHEufP3zfRAGKPycI=
|
||||||
|
github.com/pions/rtcp v1.0.0/go.mod h1:Q5twXlqiz775Yn37X0cl4lAsfSk8EiHgeNkte59jBY4=
|
||||||
|
github.com/pions/rtp v1.0.0 h1:H/TUg7bhgBT/mQsUx0adW3cmgwqPmygoYbbRTc3Y7Ek=
|
||||||
|
github.com/pions/rtp v1.0.0/go.mod h1:GDIt4UYlSz7za4vfaLqihGJJ+yLvgPshnqrF/lm3vcM=
|
||||||
github.com/pions/sctp v1.3.0 h1:FSoWK1yFhNHj7ZLfGw0+h0/YOXqkMR9C58lz59uiTMY=
|
github.com/pions/sctp v1.3.0 h1:FSoWK1yFhNHj7ZLfGw0+h0/YOXqkMR9C58lz59uiTMY=
|
||||||
github.com/pions/sctp v1.3.0/go.mod h1:GZTG/xApE7wdUFEQq2Rmzgxl/+YaB/L1k8xUl1D5bmo=
|
github.com/pions/sctp v1.3.0/go.mod h1:GZTG/xApE7wdUFEQq2Rmzgxl/+YaB/L1k8xUl1D5bmo=
|
||||||
github.com/pions/sdp v1.3.0 h1:HIv6ZvdzRPq+H4T8EV8K9pZ4EGnYOr14HFZ9Rksh6/0=
|
github.com/pions/sdp v1.3.0 h1:HIv6ZvdzRPq+H4T8EV8K9pZ4EGnYOr14HFZ9Rksh6/0=
|
||||||
github.com/pions/sdp v1.3.0/go.mod h1:moNMmnVSlx8rBBb39U9t0Rdr7xvMlqiJjHlMESRad5k=
|
github.com/pions/sdp v1.3.0/go.mod h1:moNMmnVSlx8rBBb39U9t0Rdr7xvMlqiJjHlMESRad5k=
|
||||||
github.com/pions/srtp v1.0.0 h1:o6z6IN0fqctOSGWTLvl4PRwlAUxVFeRsOhDFspgXRq4=
|
github.com/pions/srtp v1.0.1 h1:4yjEChdlRYDP46Mc7NqvzQX+DHlJaH9cu627jNbCCVw=
|
||||||
github.com/pions/srtp v1.0.0/go.mod h1:9Bd1FxrpHN0kvbV6qjL3HHnixqnSZ4ZNpEXEXP9/wM8=
|
github.com/pions/srtp v1.0.1/go.mod h1:egXe0STDyQDXLm7hjOMzuk7rkAhJ1SHOx+tTgtw/cQs=
|
||||||
github.com/pions/stun v0.1.0 h1:q2Sh13EV5tGGAAm9BkCemO9SAAIjWAFf6qijwpMOfA4=
|
github.com/pions/stun v0.2.0 h1:spIzpfkEg6HV+2iIo6qeOsAjtadZKzbXbrd2e9ZCCcs=
|
||||||
github.com/pions/stun v0.1.0/go.mod h1:NW0iSMbGXxYmO5/g6m73x8UlUoZbKN5pyraXOMonyS0=
|
github.com/pions/stun v0.2.0/go.mod h1:rMdCIsqqnTLC4MOHJE3LNiFQRfIjUDzI1kzx//7oPOM=
|
||||||
github.com/pions/transport v0.0.0-20190110151433-e7cbf7d5f464/go.mod h1:HLhzI7I0k8TyiQ99hfRZNRf84lG76eaFnZHnVy/wFnM=
|
github.com/pions/transport v0.0.0-20190110151433-e7cbf7d5f464/go.mod h1:HLhzI7I0k8TyiQ99hfRZNRf84lG76eaFnZHnVy/wFnM=
|
||||||
github.com/pions/transport v0.1.0 h1:9IEn3i8pmK8rMyQIqhT2RozgXJNH4k+IuNDzV5y+ddw=
|
github.com/pions/transport v0.1.0 h1:9IEn3i8pmK8rMyQIqhT2RozgXJNH4k+IuNDzV5y+ddw=
|
||||||
github.com/pions/transport v0.1.0/go.mod h1:HLhzI7I0k8TyiQ99hfRZNRf84lG76eaFnZHnVy/wFnM=
|
github.com/pions/transport v0.1.0/go.mod h1:HLhzI7I0k8TyiQ99hfRZNRf84lG76eaFnZHnVy/wFnM=
|
||||||
|
@@ -15,11 +15,3 @@ func RandSeq(n int) string {
|
|||||||
}
|
}
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPadding Returns the padding required to make the length a multiple of 4
|
|
||||||
func GetPadding(len int) int {
|
|
||||||
if len%4 == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return 4 - (len % 4)
|
|
||||||
}
|
|
||||||
|
@@ -3,8 +3,6 @@ package util
|
|||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRandSeq(t *testing.T) {
|
func TestRandSeq(t *testing.T) {
|
||||||
@@ -17,24 +15,3 @@ func TestRandSeq(t *testing.T) {
|
|||||||
t.Errorf("RandSeq should be AlphaNumeric only")
|
t.Errorf("RandSeq should be AlphaNumeric only")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetPadding(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
type testCase struct {
|
|
||||||
input int
|
|
||||||
result int
|
|
||||||
}
|
|
||||||
|
|
||||||
cases := []testCase{
|
|
||||||
{input: 0, result: 0},
|
|
||||||
{input: 1, result: 3},
|
|
||||||
{input: 2, result: 2},
|
|
||||||
{input: 3, result: 1},
|
|
||||||
{input: 4, result: 0},
|
|
||||||
{input: 100, result: 0},
|
|
||||||
{input: 500, result: 0},
|
|
||||||
}
|
|
||||||
for _, testCase := range cases {
|
|
||||||
assert.Equalf(GetPadding(testCase.input), testCase.result, "Test case returned wrong value for input %d", testCase.input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -3,9 +3,9 @@ package webrtc
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/pions/rtp"
|
||||||
|
"github.com/pions/rtp/codecs"
|
||||||
"github.com/pions/sdp"
|
"github.com/pions/sdp"
|
||||||
"github.com/pions/webrtc/pkg/rtp"
|
|
||||||
"github.com/pions/webrtc/pkg/rtp/codecs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PayloadTypes for the default codecs
|
// PayloadTypes for the default codecs
|
||||||
|
@@ -5,8 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/pions/webrtc/pkg/rtp"
|
"github.com/pions/rtp"
|
||||||
"github.com/pions/webrtc/pkg/rtp/codecs"
|
"github.com/pions/rtp/codecs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IVFWriter is used to take RTP packets and write them to an IVF on disk
|
// IVFWriter is used to take RTP packets and write them to an IVF on disk
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
package samplebuilder
|
package samplebuilder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/pions/rtp"
|
||||||
"github.com/pions/webrtc/pkg/media"
|
"github.com/pions/webrtc/pkg/media"
|
||||||
"github.com/pions/webrtc/pkg/rtp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SampleBuilder contains all packets
|
// SampleBuilder contains all packets
|
||||||
|
@@ -3,8 +3,8 @@ package samplebuilder
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pions/rtp"
|
||||||
"github.com/pions/webrtc/pkg/media"
|
"github.com/pions/webrtc/pkg/media"
|
||||||
"github.com/pions/webrtc/pkg/rtp"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -1,17 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var (
|
|
||||||
errInvalidTotalLost = errors.New("rtcp: invalid total lost count")
|
|
||||||
errInvalidHeader = errors.New("rtcp: invalid header")
|
|
||||||
errTooManyReports = errors.New("rtcp: too many reports")
|
|
||||||
errTooManyChunks = errors.New("rtcp: too many chunks")
|
|
||||||
errTooManySources = errors.New("rtcp: too many sources")
|
|
||||||
errPacketTooShort = errors.New("rtcp: packet too short")
|
|
||||||
errWrongType = errors.New("rtcp: wrong packet type")
|
|
||||||
errSDESTextTooLong = errors.New("rtcp: sdes must be < 255 octets long")
|
|
||||||
errSDESMissingType = errors.New("rtcp: sdes item missing type")
|
|
||||||
errReasonTooLong = errors.New("rtcp: reason must be < 255 octets long")
|
|
||||||
errBadVersion = errors.New("rtcp: invalid packet version")
|
|
||||||
)
|
|
@@ -1,51 +0,0 @@
|
|||||||
// +build gofuzz
|
|
||||||
|
|
||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Fuzz implements a randomized fuzz test of the rtcp
|
|
||||||
// parser using go-fuzz.
|
|
||||||
//
|
|
||||||
// To run the fuzzer, first download go-fuzz:
|
|
||||||
// `go get github.com/dvyukov/go-fuzz/...`
|
|
||||||
//
|
|
||||||
// Then build the testing package:
|
|
||||||
// `go-fuzz-build github.com/pions/webrtc`
|
|
||||||
//
|
|
||||||
// And run the fuzzer on the corpus:
|
|
||||||
// ```
|
|
||||||
// mkdir workdir
|
|
||||||
//
|
|
||||||
// # optionally add a starter corpus of valid rtcp packets.
|
|
||||||
// # the corpus should be as compact and diverse as possible.
|
|
||||||
// cp -r ~/my-rtcp-packets workdir/corpus
|
|
||||||
//
|
|
||||||
// go-fuzz -bin=ase-fuzz.zip -workdir=workdir
|
|
||||||
// ````
|
|
||||||
func Fuzz(data []byte) int {
|
|
||||||
r := NewReader(bytes.NewReader(data))
|
|
||||||
for {
|
|
||||||
_, data, err := r.ReadPacket()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
packet, _, err := Unmarshal(data)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := packet.Marshal(); err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
@@ -1,147 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
|
|
||||||
"github.com/pions/webrtc/internal/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The Goodbye packet indicates that one or more sources are no longer active.
|
|
||||||
type Goodbye struct {
|
|
||||||
// The SSRC/CSRC identifiers that are no longer active
|
|
||||||
Sources []uint32
|
|
||||||
// Optional text indicating the reason for leaving, e.g., "camera malfunction" or "RTP loop detected"
|
|
||||||
Reason string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal encodes the Goodbye packet in binary
|
|
||||||
func (g Goodbye) Marshal() ([]byte, error) {
|
|
||||||
/*
|
|
||||||
* 0 1 2 3
|
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* |V=2|P| SC | PT=BYE=203 | length |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | SSRC/CSRC |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* : ... :
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* (opt) | length | reason for leaving ...
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
*/
|
|
||||||
|
|
||||||
rawPacket := make([]byte, g.len())
|
|
||||||
packetBody := rawPacket[headerLength:]
|
|
||||||
|
|
||||||
if len(g.Sources) > countMax {
|
|
||||||
return nil, errTooManySources
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, s := range g.Sources {
|
|
||||||
binary.BigEndian.PutUint32(packetBody[i*ssrcLength:], s)
|
|
||||||
}
|
|
||||||
|
|
||||||
if g.Reason != "" {
|
|
||||||
reason := []byte(g.Reason)
|
|
||||||
|
|
||||||
if len(reason) > sdesMaxOctetCount {
|
|
||||||
return nil, errReasonTooLong
|
|
||||||
}
|
|
||||||
|
|
||||||
reasonOffset := len(g.Sources) * ssrcLength
|
|
||||||
packetBody[reasonOffset] = uint8(len(reason))
|
|
||||||
copy(packetBody[reasonOffset+1:], reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
hData, err := g.Header().Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
copy(rawPacket, hData)
|
|
||||||
|
|
||||||
return rawPacket, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal decodes the Goodbye packet from binary
|
|
||||||
func (g *Goodbye) Unmarshal(rawPacket []byte) error {
|
|
||||||
/*
|
|
||||||
* 0 1 2 3
|
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* |V=2|P| SC | PT=BYE=203 | length |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | SSRC/CSRC |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* : ... :
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* (opt) | length | reason for leaving ...
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
*/
|
|
||||||
|
|
||||||
var header Header
|
|
||||||
if err := header.Unmarshal(rawPacket); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if header.Type != TypeGoodbye {
|
|
||||||
return errWrongType
|
|
||||||
}
|
|
||||||
|
|
||||||
if util.GetPadding(len(rawPacket)) != 0 {
|
|
||||||
return errPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
g.Sources = make([]uint32, header.Count)
|
|
||||||
|
|
||||||
reasonOffset := int(headerLength + header.Count*ssrcLength)
|
|
||||||
if reasonOffset > len(rawPacket) {
|
|
||||||
return errPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < int(header.Count); i++ {
|
|
||||||
offset := headerLength + i*ssrcLength
|
|
||||||
|
|
||||||
g.Sources[i] = binary.BigEndian.Uint32(rawPacket[offset:])
|
|
||||||
}
|
|
||||||
|
|
||||||
if reasonOffset < len(rawPacket) {
|
|
||||||
reasonLen := int(rawPacket[reasonOffset])
|
|
||||||
reasonEnd := reasonOffset + 1 + reasonLen
|
|
||||||
|
|
||||||
if reasonEnd > len(rawPacket) {
|
|
||||||
return errPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
g.Reason = string(rawPacket[reasonOffset+1 : reasonEnd])
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header returns the Header associated with this packet.
|
|
||||||
func (g *Goodbye) Header() Header {
|
|
||||||
return Header{
|
|
||||||
Padding: false,
|
|
||||||
Count: uint8(len(g.Sources)),
|
|
||||||
Type: TypeGoodbye,
|
|
||||||
Length: uint16((g.len() / 4) - 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Goodbye) len() int {
|
|
||||||
srcsLength := len(g.Sources) * ssrcLength
|
|
||||||
reasonLength := len(g.Reason) + 1
|
|
||||||
|
|
||||||
l := headerLength + srcsLength + reasonLength
|
|
||||||
|
|
||||||
// align to 32-bit boundary
|
|
||||||
return l + util.GetPadding(l)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DestinationSSRC returns an array of SSRC values that this packet refers to.
|
|
||||||
func (g *Goodbye) DestinationSSRC() []uint32 {
|
|
||||||
out := make([]uint32, len(g.Sources))
|
|
||||||
copy(out, g.Sources)
|
|
||||||
return out
|
|
||||||
}
|
|
@@ -1,207 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGoodbyeUnmarshal(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Data []byte
|
|
||||||
Want Goodbye
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, BYE, len=12
|
|
||||||
0x81, 0xcb, 0x00, 0x0c,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// len=3, text=FOO
|
|
||||||
0x03, 0x46, 0x4f, 0x4f,
|
|
||||||
},
|
|
||||||
Want: Goodbye{
|
|
||||||
Sources: []uint32{0x902f9e2e},
|
|
||||||
Reason: "FOO",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "invalid octet count",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, BYE, len=12
|
|
||||||
0x81, 0xcb, 0x00, 0x0c,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// len=4, text=FOO
|
|
||||||
0x04, 0x46, 0x4f, 0x4f,
|
|
||||||
},
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "wrong type",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SDES, len=12
|
|
||||||
0x81, 0xca, 0x00, 0x0c,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// len=3, text=FOO
|
|
||||||
0x03, 0x46, 0x4f, 0x4f,
|
|
||||||
},
|
|
||||||
WantError: errWrongType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "short reason",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, BYE, len=12
|
|
||||||
0x81, 0xcb, 0x00, 0x0c,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// len=3, text=F + padding
|
|
||||||
0x01, 0x46, 0x00, 0x00,
|
|
||||||
},
|
|
||||||
Want: Goodbye{
|
|
||||||
Sources: []uint32{0x902f9e2e},
|
|
||||||
Reason: "F",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "not byte aligned",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, BYE, len=10
|
|
||||||
0x81, 0xcb, 0x00, 0x0a,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// len=1, text=F
|
|
||||||
0x01, 0x46,
|
|
||||||
},
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "bad count in header",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=2, BYE, len=8
|
|
||||||
0x82, 0xcb, 0x00, 0x0c,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
},
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "empty packet",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=0, BYE, len=4
|
|
||||||
0x80, 0xcb, 0x00, 0x04,
|
|
||||||
},
|
|
||||||
Want: Goodbye{
|
|
||||||
Sources: []uint32{},
|
|
||||||
Reason: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "nil",
|
|
||||||
Data: nil,
|
|
||||||
WantError: errInvalidHeader,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
var bye Goodbye
|
|
||||||
err := bye.Unmarshal(test.Data)
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Fatalf("Unmarshal %q bye: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := bye, test.Want; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("Unmarshal %q bye: got %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestGoodbyeRoundTrip(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Bye Goodbye
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "empty",
|
|
||||||
Bye: Goodbye{
|
|
||||||
Sources: []uint32{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Bye: Goodbye{
|
|
||||||
Sources: []uint32{
|
|
||||||
0x01020304,
|
|
||||||
0x05060708,
|
|
||||||
},
|
|
||||||
Reason: "because",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "empty reason",
|
|
||||||
Bye: Goodbye{
|
|
||||||
Sources: []uint32{0x01020304},
|
|
||||||
Reason: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "reason no source",
|
|
||||||
Bye: Goodbye{
|
|
||||||
Sources: []uint32{},
|
|
||||||
Reason: "foo",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "short reason",
|
|
||||||
Bye: Goodbye{
|
|
||||||
Sources: []uint32{},
|
|
||||||
Reason: "f",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "count overflow",
|
|
||||||
Bye: Goodbye{
|
|
||||||
Sources: tooManySources,
|
|
||||||
},
|
|
||||||
WantError: errTooManySources,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "reason too long",
|
|
||||||
Bye: Goodbye{
|
|
||||||
Sources: []uint32{},
|
|
||||||
Reason: tooLongText,
|
|
||||||
},
|
|
||||||
WantError: errReasonTooLong,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
data, err := test.Bye.Marshal()
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Fatalf("Marshal %q: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var bye Goodbye
|
|
||||||
if err := bye.Unmarshal(data); err != nil {
|
|
||||||
t.Fatalf("Unmarshal %q: %v", test.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := bye, test.Bye; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("%q sdes round trip: got %#v, want %#v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// a slice with enough sources to overflow an 5-bit int
|
|
||||||
var tooManySources []uint32
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
for i := 0; i < (1 << 5); i++ {
|
|
||||||
tooManySources = append(tooManySources, 0x00)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,135 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PacketType specifies the type of an RTCP packet
|
|
||||||
type PacketType uint8
|
|
||||||
|
|
||||||
// RTCP packet types registered with IANA. See: https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml#rtp-parameters-4
|
|
||||||
const (
|
|
||||||
TypeSenderReport PacketType = 200 // RFC 3550, 6.4.1
|
|
||||||
TypeReceiverReport PacketType = 201 // RFC 3550, 6.4.2
|
|
||||||
TypeSourceDescription PacketType = 202 // RFC 3550, 6.5
|
|
||||||
TypeGoodbye PacketType = 203 // RFC 3550, 6.6
|
|
||||||
TypeApplicationDefined PacketType = 204 // RFC 3550, 6.7 (unimplemented)
|
|
||||||
TypeTransportSpecificFeedback PacketType = 205 // RFC 4585, 6051
|
|
||||||
TypePayloadSpecificFeedback PacketType = 206 // RFC 4585, 6.3
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
// Transport and Payload specific feedback messages overload the count field to act as a message type. those are listed here
|
|
||||||
const (
|
|
||||||
FormatSLI uint8 = 2
|
|
||||||
FormatPLI uint8 = 1
|
|
||||||
FormatTLN uint8 = 1
|
|
||||||
FormatRRR uint8 = 5
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p PacketType) String() string {
|
|
||||||
switch p {
|
|
||||||
case TypeSenderReport:
|
|
||||||
return "SR"
|
|
||||||
case TypeReceiverReport:
|
|
||||||
return "RR"
|
|
||||||
case TypeSourceDescription:
|
|
||||||
return "SDES"
|
|
||||||
case TypeGoodbye:
|
|
||||||
return "BYE"
|
|
||||||
case TypeApplicationDefined:
|
|
||||||
return "APP"
|
|
||||||
case TypeTransportSpecificFeedback:
|
|
||||||
return "TSFB"
|
|
||||||
case TypePayloadSpecificFeedback:
|
|
||||||
return "PSFB"
|
|
||||||
default:
|
|
||||||
return string(p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rtpVersion = 2
|
|
||||||
|
|
||||||
// A Header is the common header shared by all RTCP packets
|
|
||||||
type Header struct {
|
|
||||||
// If the padding bit is set, this individual RTCP packet contains
|
|
||||||
// some additional padding octets at the end which are not part of
|
|
||||||
// the control information but are included in the length field.
|
|
||||||
Padding bool
|
|
||||||
// The number of reception reports, sources contained or FMT in this packet (depending on the Type)
|
|
||||||
Count uint8
|
|
||||||
// The RTCP packet type for this packet
|
|
||||||
Type PacketType
|
|
||||||
// The length of this RTCP packet in 32-bit words minus one,
|
|
||||||
// including the header and any padding.
|
|
||||||
Length uint16
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
headerLength = 4
|
|
||||||
versionShift = 6
|
|
||||||
versionMask = 0x3
|
|
||||||
paddingShift = 5
|
|
||||||
paddingMask = 0x1
|
|
||||||
countShift = 0
|
|
||||||
countMask = 0x1f
|
|
||||||
countMax = (1 << 5) - 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// Marshal encodes the Header in binary
|
|
||||||
func (h Header) Marshal() ([]byte, error) {
|
|
||||||
/*
|
|
||||||
* 0 1 2 3
|
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* |V=2|P| RC | PT=SR=200 | length |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
*/
|
|
||||||
rawPacket := make([]byte, headerLength)
|
|
||||||
|
|
||||||
rawPacket[0] |= rtpVersion << versionShift
|
|
||||||
|
|
||||||
if h.Padding {
|
|
||||||
rawPacket[0] |= 1 << paddingShift
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.Count > 31 {
|
|
||||||
return nil, errInvalidHeader
|
|
||||||
}
|
|
||||||
rawPacket[0] |= h.Count << countShift
|
|
||||||
|
|
||||||
rawPacket[1] = uint8(h.Type)
|
|
||||||
|
|
||||||
binary.BigEndian.PutUint16(rawPacket[2:], h.Length)
|
|
||||||
|
|
||||||
return rawPacket, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal decodes the Header from binary
|
|
||||||
func (h *Header) Unmarshal(rawPacket []byte) error {
|
|
||||||
if len(rawPacket) < headerLength {
|
|
||||||
return errInvalidHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 0 1 2 3
|
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* |V=2|P| RC | PT | length |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
*/
|
|
||||||
|
|
||||||
version := rawPacket[0] >> versionShift & versionMask
|
|
||||||
if version != rtpVersion {
|
|
||||||
return errBadVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Padding = (rawPacket[0] >> paddingShift & paddingMask) > 0
|
|
||||||
h.Count = rawPacket[0] >> countShift & countMask
|
|
||||||
|
|
||||||
h.Type = PacketType(rawPacket[1])
|
|
||||||
|
|
||||||
h.Length = binary.BigEndian.Uint16(rawPacket[2:])
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@@ -1,113 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHeaderUnmarshal(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Data []byte
|
|
||||||
Want Header
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, RR, len=7
|
|
||||||
0x81, 0xc9, 0x00, 0x07,
|
|
||||||
},
|
|
||||||
Want: Header{
|
|
||||||
Padding: false,
|
|
||||||
Count: 1,
|
|
||||||
Type: TypeReceiverReport,
|
|
||||||
Length: 7,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "also valid",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=1, count=1, BYE, len=7
|
|
||||||
0xa1, 0xcc, 0x00, 0x07,
|
|
||||||
},
|
|
||||||
Want: Header{
|
|
||||||
Padding: true,
|
|
||||||
Count: 1,
|
|
||||||
Type: TypeApplicationDefined,
|
|
||||||
Length: 7,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "bad version",
|
|
||||||
Data: []byte{
|
|
||||||
// v=0, p=0, count=0, RR, len=4
|
|
||||||
0x00, 0xc9, 0x00, 0x04,
|
|
||||||
},
|
|
||||||
WantError: errBadVersion,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
var h Header
|
|
||||||
err := h.Unmarshal(test.Data)
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Fatalf("Unmarshal %q header: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := h, test.Want; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("Unmarshal %q header: got %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestHeaderRoundTrip(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Header Header
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Header: Header{
|
|
||||||
Padding: true,
|
|
||||||
Count: 31,
|
|
||||||
Type: TypeSenderReport,
|
|
||||||
Length: 4,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "also valid",
|
|
||||||
Header: Header{
|
|
||||||
Padding: false,
|
|
||||||
Count: 28,
|
|
||||||
Type: TypeReceiverReport,
|
|
||||||
Length: 65535,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "invalid count",
|
|
||||||
Header: Header{
|
|
||||||
Count: 40,
|
|
||||||
},
|
|
||||||
WantError: errInvalidHeader,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
data, err := test.Header.Marshal()
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Errorf("Marshal %q: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var decoded Header
|
|
||||||
if err := decoded.Unmarshal(data); err != nil {
|
|
||||||
t.Errorf("Unmarshal %q: %v", test.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := decoded, test.Header; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("%q header round trip: got %#v, want %#v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,62 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
// Packet represents an RTCP packet, a protocol used for out-of-band statistics and control information for an RTP session
|
|
||||||
type Packet interface {
|
|
||||||
Header() Header
|
|
||||||
// DestinationSSRC returns an array of SSRC values that this packet refers to.
|
|
||||||
DestinationSSRC() []uint32
|
|
||||||
|
|
||||||
Marshal() ([]byte, error)
|
|
||||||
Unmarshal(rawPacket []byte) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal is a factory a polymorphic RTCP packet, and its header,
|
|
||||||
func Unmarshal(rawPacket []byte) (Packet, Header, error) {
|
|
||||||
var h Header
|
|
||||||
var p Packet
|
|
||||||
|
|
||||||
err := h.Unmarshal(rawPacket)
|
|
||||||
if err != nil {
|
|
||||||
return nil, h, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch h.Type {
|
|
||||||
case TypeSenderReport:
|
|
||||||
p = new(SenderReport)
|
|
||||||
|
|
||||||
case TypeReceiverReport:
|
|
||||||
p = new(ReceiverReport)
|
|
||||||
|
|
||||||
case TypeSourceDescription:
|
|
||||||
p = new(SourceDescription)
|
|
||||||
|
|
||||||
case TypeGoodbye:
|
|
||||||
p = new(Goodbye)
|
|
||||||
|
|
||||||
case TypeTransportSpecificFeedback:
|
|
||||||
switch h.Count {
|
|
||||||
case FormatTLN:
|
|
||||||
p = new(TransportLayerNack)
|
|
||||||
case FormatRRR:
|
|
||||||
p = new(RapidResynchronizationRequest)
|
|
||||||
default:
|
|
||||||
p = new(RawPacket)
|
|
||||||
}
|
|
||||||
|
|
||||||
case TypePayloadSpecificFeedback:
|
|
||||||
switch h.Count {
|
|
||||||
case FormatPLI:
|
|
||||||
p = new(PictureLossIndication)
|
|
||||||
case FormatSLI:
|
|
||||||
p = new(SliceLossIndication)
|
|
||||||
default:
|
|
||||||
p = new(RawPacket)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
p = new(RawPacket)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = p.Unmarshal(rawPacket)
|
|
||||||
return p, h, err
|
|
||||||
}
|
|
@@ -1,87 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The PictureLossIndication packet informs the encoder about the loss of an undefined amount of coded video data belonging to one or more pictures
|
|
||||||
type PictureLossIndication struct {
|
|
||||||
// SSRC of sender
|
|
||||||
SenderSSRC uint32
|
|
||||||
|
|
||||||
// SSRC where the loss was experienced
|
|
||||||
MediaSSRC uint32
|
|
||||||
|
|
||||||
header Header
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
pliLength = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
// Marshal encodes the PictureLossIndication in binary
|
|
||||||
func (p PictureLossIndication) Marshal() ([]byte, error) {
|
|
||||||
/*
|
|
||||||
* PLI does not require parameters. Therefore, the length field MUST be
|
|
||||||
* 2, and there MUST NOT be any Feedback Control Information.
|
|
||||||
*
|
|
||||||
* The semantics of this FB message is independent of the payload type.
|
|
||||||
*/
|
|
||||||
rawPacket := make([]byte, p.len())
|
|
||||||
packetBody := rawPacket[headerLength:]
|
|
||||||
|
|
||||||
binary.BigEndian.PutUint32(packetBody, p.SenderSSRC)
|
|
||||||
binary.BigEndian.PutUint32(packetBody[4:], p.MediaSSRC)
|
|
||||||
|
|
||||||
h := Header{
|
|
||||||
Count: FormatPLI,
|
|
||||||
Type: TypePayloadSpecificFeedback,
|
|
||||||
Length: pliLength,
|
|
||||||
}
|
|
||||||
hData, err := h.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
copy(rawPacket, hData)
|
|
||||||
|
|
||||||
return rawPacket, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal decodes the PictureLossIndication from binary
|
|
||||||
func (p *PictureLossIndication) Unmarshal(rawPacket []byte) error {
|
|
||||||
if len(rawPacket) < (headerLength + (ssrcLength * 2)) {
|
|
||||||
return errPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
var h Header
|
|
||||||
if err := h.Unmarshal(rawPacket); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.Type != TypePayloadSpecificFeedback || h.Count != FormatPLI {
|
|
||||||
return errWrongType
|
|
||||||
}
|
|
||||||
|
|
||||||
p.SenderSSRC = binary.BigEndian.Uint32(rawPacket[headerLength:])
|
|
||||||
p.MediaSSRC = binary.BigEndian.Uint32(rawPacket[headerLength+ssrcLength:])
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header returns the Header associated with this packet.
|
|
||||||
func (p *PictureLossIndication) Header() Header {
|
|
||||||
return p.header
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PictureLossIndication) len() int {
|
|
||||||
return headerLength + ssrcLength*2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PictureLossIndication) String() string {
|
|
||||||
return fmt.Sprintf("PictureLossIndication %x %x", p.SenderSSRC, p.MediaSSRC)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DestinationSSRC returns an array of SSRC values that this packet refers to.
|
|
||||||
func (p *PictureLossIndication) DestinationSSRC() []uint32 {
|
|
||||||
return []uint32{p.MediaSSRC}
|
|
||||||
}
|
|
@@ -1,124 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPictureLossIndicationUnmarshal(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Data []byte
|
|
||||||
Want PictureLossIndication
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, FMT=1, PSFB, len=1
|
|
||||||
0x81, 0xce, 0x00, 0x02,
|
|
||||||
// ssrc=0x0
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
// ssrc=0x4bc4fcb4
|
|
||||||
0x4b, 0xc4, 0xfc, 0xb4,
|
|
||||||
},
|
|
||||||
Want: PictureLossIndication{
|
|
||||||
SenderSSRC: 0x0,
|
|
||||||
MediaSSRC: 0x4bc4fcb4,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "packet too short",
|
|
||||||
Data: []byte{
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
},
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "invalid header",
|
|
||||||
Data: []byte{
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
},
|
|
||||||
WantError: errBadVersion,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "wrong type",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, FMT=1, RR, len=1
|
|
||||||
0x81, 0xc9, 0x00, 0x02,
|
|
||||||
// ssrc=0x0
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
// ssrc=0x4bc4fcb4
|
|
||||||
0x4b, 0xc4, 0xfc, 0xb4,
|
|
||||||
},
|
|
||||||
WantError: errWrongType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "wrong fmt",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, FMT=2, RR, len=1
|
|
||||||
0x82, 0xc9, 0x00, 0x02,
|
|
||||||
// ssrc=0x0
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
// ssrc=0x4bc4fcb4
|
|
||||||
0x4b, 0xc4, 0xfc, 0xb4,
|
|
||||||
},
|
|
||||||
WantError: errWrongType,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
var pli PictureLossIndication
|
|
||||||
err := pli.Unmarshal(test.Data)
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Fatalf("Unmarshal %q rr: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := pli, test.Want; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("Unmarshal %q rr: got %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPictureLossIndicationRoundTrip(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Packet PictureLossIndication
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Packet: PictureLossIndication{
|
|
||||||
SenderSSRC: 1,
|
|
||||||
MediaSSRC: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "also valid",
|
|
||||||
Packet: PictureLossIndication{
|
|
||||||
SenderSSRC: 5000,
|
|
||||||
MediaSSRC: 6000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
data, err := test.Packet.Marshal()
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Fatalf("Marshal %q: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var decoded PictureLossIndication
|
|
||||||
if err := decoded.Unmarshal(data); err != nil {
|
|
||||||
t.Fatalf("Unmarshal %q: %v", test.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := decoded, test.Packet; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("%q rr round trip: got %#v, want %#v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,87 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The RapidResynchronizationRequest packet informs the encoder about the loss of an undefined amount of coded video data belonging to one or more pictures
|
|
||||||
type RapidResynchronizationRequest struct {
|
|
||||||
// SSRC of sender
|
|
||||||
SenderSSRC uint32
|
|
||||||
|
|
||||||
// SSRC of the media source
|
|
||||||
MediaSSRC uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
rrrLength = 2
|
|
||||||
rrrHeaderLength = ssrcLength * 2
|
|
||||||
rrrMediaOffset = 4
|
|
||||||
)
|
|
||||||
|
|
||||||
// Marshal encodes the RapidResynchronizationRequest in binary
|
|
||||||
func (p RapidResynchronizationRequest) Marshal() ([]byte, error) {
|
|
||||||
/*
|
|
||||||
* RRR does not require parameters. Therefore, the length field MUST be
|
|
||||||
* 2, and there MUST NOT be any Feedback Control Information.
|
|
||||||
*
|
|
||||||
* The semantics of this FB message is independent of the payload type.
|
|
||||||
*/
|
|
||||||
rawPacket := make([]byte, p.len())
|
|
||||||
packetBody := rawPacket[headerLength:]
|
|
||||||
|
|
||||||
binary.BigEndian.PutUint32(packetBody, p.SenderSSRC)
|
|
||||||
binary.BigEndian.PutUint32(packetBody[rrrMediaOffset:], p.MediaSSRC)
|
|
||||||
|
|
||||||
hData, err := p.Header().Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
copy(rawPacket, hData)
|
|
||||||
|
|
||||||
return rawPacket, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal decodes the RapidResynchronizationRequest from binary
|
|
||||||
func (p *RapidResynchronizationRequest) Unmarshal(rawPacket []byte) error {
|
|
||||||
|
|
||||||
if len(rawPacket) < (headerLength + (ssrcLength * 2)) {
|
|
||||||
return errPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
var h Header
|
|
||||||
if err := h.Unmarshal(rawPacket); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.Type != TypeTransportSpecificFeedback || h.Count != FormatRRR {
|
|
||||||
return errWrongType
|
|
||||||
}
|
|
||||||
|
|
||||||
p.SenderSSRC = binary.BigEndian.Uint32(rawPacket[headerLength:])
|
|
||||||
p.MediaSSRC = binary.BigEndian.Uint32(rawPacket[headerLength+ssrcLength:])
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *RapidResynchronizationRequest) len() int {
|
|
||||||
return headerLength + rrrHeaderLength
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header returns the Header associated with this packet.
|
|
||||||
func (p *RapidResynchronizationRequest) Header() Header {
|
|
||||||
return Header{
|
|
||||||
Count: FormatRRR,
|
|
||||||
Type: TypeTransportSpecificFeedback,
|
|
||||||
Length: rrrLength,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DestinationSSRC returns an array of SSRC values that this packet refers to.
|
|
||||||
func (p *RapidResynchronizationRequest) DestinationSSRC() []uint32 {
|
|
||||||
return []uint32{p.MediaSSRC}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *RapidResynchronizationRequest) String() string {
|
|
||||||
return fmt.Sprintf("RapidResynchronizationRequest %x %x", p.SenderSSRC, p.MediaSSRC)
|
|
||||||
}
|
|
@@ -1,114 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRapidResynchronizationRequestUnmarshal(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Data []byte
|
|
||||||
Want RapidResynchronizationRequest
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Data: []byte{
|
|
||||||
// RapidResynchronizationRequest
|
|
||||||
0x85, 0xcd, 0x0, 0x2,
|
|
||||||
// sender=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// media=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
},
|
|
||||||
Want: RapidResynchronizationRequest{
|
|
||||||
SenderSSRC: 0x902f9e2e,
|
|
||||||
MediaSSRC: 0x902f9e2e,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "short report",
|
|
||||||
Data: []byte{
|
|
||||||
0x85, 0xcd, 0x0, 0x2,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// report ends early
|
|
||||||
},
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "wrong type",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SR, len=7
|
|
||||||
0x81, 0xc8, 0x0, 0x7,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// ssrc=0xbc5e9a40
|
|
||||||
0xbc, 0x5e, 0x9a, 0x40,
|
|
||||||
// fracLost=0, totalLost=0
|
|
||||||
0x0, 0x0, 0x0, 0x0,
|
|
||||||
// lastSeq=0x46e1
|
|
||||||
0x0, 0x0, 0x46, 0xe1,
|
|
||||||
// jitter=273
|
|
||||||
0x0, 0x0, 0x1, 0x11,
|
|
||||||
// lsr=0x9f36432
|
|
||||||
0x9, 0xf3, 0x64, 0x32,
|
|
||||||
// delay=150137
|
|
||||||
0x0, 0x2, 0x4a, 0x79,
|
|
||||||
},
|
|
||||||
WantError: errWrongType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "nil",
|
|
||||||
Data: nil,
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
var rrr RapidResynchronizationRequest
|
|
||||||
err := rrr.Unmarshal(test.Data)
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Fatalf("Unmarshal %q rr: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := rrr, test.Want; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("Unmarshal %q rr: got %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRapidResynchronizationRequestRoundTrip(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Report RapidResynchronizationRequest
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Report: RapidResynchronizationRequest{
|
|
||||||
SenderSSRC: 0x902f9e2e,
|
|
||||||
MediaSSRC: 0x902f9e2e,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
data, err := test.Report.Marshal()
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Fatalf("Marshal %q: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var decoded RapidResynchronizationRequest
|
|
||||||
if err := decoded.Unmarshal(data); err != nil {
|
|
||||||
t.Fatalf("Unmarshal %q: %v", test.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := decoded, test.Report; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("%q rrr round trip: got %#v, want %#v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,35 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
// RawPacket represents an unparsed RTCP packet. It's returned by Unmarshal when
|
|
||||||
// a packet with an unknown type is encountered.
|
|
||||||
type RawPacket []byte
|
|
||||||
|
|
||||||
// Marshal encodes the packet in binary.
|
|
||||||
func (r RawPacket) Marshal() ([]byte, error) {
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal decodes the packet from binary.
|
|
||||||
func (r *RawPacket) Unmarshal(b []byte) error {
|
|
||||||
if len(b) < (headerLength) {
|
|
||||||
return errPacketTooShort
|
|
||||||
}
|
|
||||||
*r = b
|
|
||||||
|
|
||||||
var h Header
|
|
||||||
return h.Unmarshal(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header returns the Header associated with this packet.
|
|
||||||
func (r RawPacket) Header() Header {
|
|
||||||
var h Header
|
|
||||||
if err := h.Unmarshal(r); err != nil {
|
|
||||||
return Header{}
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// DestinationSSRC returns an array of SSRC values that this packet refers to.
|
|
||||||
func (r *RawPacket) DestinationSSRC() []uint32 {
|
|
||||||
return []uint32{}
|
|
||||||
}
|
|
@@ -1,61 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRawPacketRoundTrip(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Packet RawPacket
|
|
||||||
WantMarshalError error
|
|
||||||
WantUnmarshalError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Packet: RawPacket([]byte{
|
|
||||||
// v=2, p=0, count=1, BYE, len=12
|
|
||||||
0x81, 0xcb, 0x00, 0x0c,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// len=3, text=FOO
|
|
||||||
0x03, 0x46, 0x4f, 0x4f,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "short header",
|
|
||||||
Packet: RawPacket([]byte{0x00}),
|
|
||||||
WantUnmarshalError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "invalid header",
|
|
||||||
Packet: RawPacket([]byte{
|
|
||||||
// v=0, p=0, count=0, RR, len=4
|
|
||||||
0x00, 0xc9, 0x00, 0x04,
|
|
||||||
}),
|
|
||||||
WantUnmarshalError: errBadVersion,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
data, err := test.Packet.Marshal()
|
|
||||||
if got, want := err, test.WantMarshalError; got != want {
|
|
||||||
t.Fatalf("Marshal %q: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var decoded RawPacket
|
|
||||||
err = decoded.Unmarshal(data)
|
|
||||||
if got, want := err, test.WantUnmarshalError; got != want {
|
|
||||||
t.Fatalf("Unmarshal %q: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := decoded, test.Packet; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("%q raw round trip: got %#v, want %#v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,41 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import "io"
|
|
||||||
|
|
||||||
// A Reader reads packets from an RTCP combined packet.
|
|
||||||
type Reader struct {
|
|
||||||
r io.Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewReader creates a new Reader reading from r.
|
|
||||||
func NewReader(r io.Reader) *Reader {
|
|
||||||
return &Reader{r}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadPacket reads one packet from r.
|
|
||||||
//
|
|
||||||
// It returns the parsed packet Header and a byte slice containing the encoded
|
|
||||||
// packet data (including the header). How the packet data is parsed depends on
|
|
||||||
// the Type field contained in the Header.
|
|
||||||
func (r *Reader) ReadPacket() (header Header, data []byte, err error) {
|
|
||||||
// First grab the header
|
|
||||||
headerBuf := make([]byte, headerLength)
|
|
||||||
if _, err := io.ReadFull(r.r, headerBuf); err != nil {
|
|
||||||
return header, data, err
|
|
||||||
}
|
|
||||||
if err := header.Unmarshal(headerBuf); err != nil {
|
|
||||||
return header, data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
packetLen := (header.Length + 1) * 4
|
|
||||||
|
|
||||||
// Then grab the rest
|
|
||||||
bodyBuf := make([]byte, packetLen-headerLength)
|
|
||||||
if _, err := io.ReadFull(r.r, bodyBuf); err != nil {
|
|
||||||
return header, data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
data = append(headerBuf, bodyBuf...)
|
|
||||||
|
|
||||||
return header, data, nil
|
|
||||||
}
|
|
@@ -1,204 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// An RTCP packet from a packet dump
|
|
||||||
var realPacket = []byte{
|
|
||||||
// Receiver Report (offset=0)
|
|
||||||
// v=2, p=0, count=1, RR, len=7
|
|
||||||
0x81, 0xc9, 0x0, 0x7,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// ssrc=0xbc5e9a40
|
|
||||||
0xbc, 0x5e, 0x9a, 0x40,
|
|
||||||
// fracLost=0, totalLost=0
|
|
||||||
0x0, 0x0, 0x0, 0x0,
|
|
||||||
// lastSeq=0x46e1
|
|
||||||
0x0, 0x0, 0x46, 0xe1,
|
|
||||||
// jitter=273
|
|
||||||
0x0, 0x0, 0x1, 0x11,
|
|
||||||
// lsr=0x9f36432
|
|
||||||
0x9, 0xf3, 0x64, 0x32,
|
|
||||||
// delay=150137
|
|
||||||
0x0, 0x2, 0x4a, 0x79,
|
|
||||||
|
|
||||||
// Source Description (offset=32)
|
|
||||||
// v=2, p=0, count=1, SDES, len=12
|
|
||||||
0x81, 0xca, 0x0, 0xc,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// CNAME, len=38
|
|
||||||
0x1, 0x26,
|
|
||||||
// text="{9c00eb92-1afb-9d49-a47d-91f64eee69f5}"
|
|
||||||
0x7b, 0x39, 0x63, 0x30,
|
|
||||||
0x30, 0x65, 0x62, 0x39,
|
|
||||||
0x32, 0x2d, 0x31, 0x61,
|
|
||||||
0x66, 0x62, 0x2d, 0x39,
|
|
||||||
0x64, 0x34, 0x39, 0x2d,
|
|
||||||
0x61, 0x34, 0x37, 0x64,
|
|
||||||
0x2d, 0x39, 0x31, 0x66,
|
|
||||||
0x36, 0x34, 0x65, 0x65,
|
|
||||||
0x65, 0x36, 0x39, 0x66,
|
|
||||||
0x35, 0x7d,
|
|
||||||
// END + padding
|
|
||||||
0x0, 0x0, 0x0, 0x0,
|
|
||||||
|
|
||||||
// Goodbye (offset=84)
|
|
||||||
// v=2, p=0, count=1, BYE, len=1
|
|
||||||
0x81, 0xcb, 0x0, 0x1,
|
|
||||||
// source=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
|
|
||||||
// Picture Loss Indication (offset=92)
|
|
||||||
0x81, 0xce, 0x0, 0x2,
|
|
||||||
// sender=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// media=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
|
|
||||||
// RapidResynchronizationRequest (offset=104)
|
|
||||||
0x85, 0xcd, 0x0, 0x2,
|
|
||||||
// sender=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// media=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnmarshal(t *testing.T) {
|
|
||||||
r := NewReader(bytes.NewReader(realPacket))
|
|
||||||
|
|
||||||
// ReceiverReport
|
|
||||||
_, packet, err := r.ReadPacket()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Read rr: %v", err)
|
|
||||||
}
|
|
||||||
var parsed Packet
|
|
||||||
if parsed, _, err = Unmarshal(packet); err != nil {
|
|
||||||
t.Errorf("Unmarshal parsed: %v", err)
|
|
||||||
}
|
|
||||||
assert.IsType(t, parsed, (*ReceiverReport)(nil), "Unmarshalled to incorrect type")
|
|
||||||
|
|
||||||
wantRR := &ReceiverReport{
|
|
||||||
SSRC: 0x902f9e2e,
|
|
||||||
Reports: []ReceptionReport{{
|
|
||||||
SSRC: 0xbc5e9a40,
|
|
||||||
FractionLost: 0,
|
|
||||||
TotalLost: 0,
|
|
||||||
LastSequenceNumber: 0x46e1,
|
|
||||||
Jitter: 273,
|
|
||||||
LastSenderReport: 0x9f36432,
|
|
||||||
Delay: 150137,
|
|
||||||
}},
|
|
||||||
ProfileExtensions: []byte{},
|
|
||||||
}
|
|
||||||
if got, want := wantRR, parsed; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("Unmarshal rr: got %#v, want %#v", got, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SourceDescription
|
|
||||||
_, packet, err = r.ReadPacket()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Read sdes: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsed, _, err = Unmarshal(packet); err != nil {
|
|
||||||
t.Errorf("Unmarshal parsed: %v", err)
|
|
||||||
}
|
|
||||||
assert.IsType(t, parsed, (*SourceDescription)(nil), "Unmarshalled to incorrect type")
|
|
||||||
|
|
||||||
wantSdes := &SourceDescription{
|
|
||||||
Chunks: []SourceDescriptionChunk{
|
|
||||||
{
|
|
||||||
Source: 0x902f9e2e,
|
|
||||||
Items: []SourceDescriptionItem{
|
|
||||||
{
|
|
||||||
Type: SDESCNAME,
|
|
||||||
Text: "{9c00eb92-1afb-9d49-a47d-91f64eee69f5}",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := parsed, wantSdes; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("Unmarshal sdes: got %#v, want %#v", got, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Goodbye
|
|
||||||
_, packet, err = r.ReadPacket()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Read bye: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsed, _, err = Unmarshal(packet); err != nil {
|
|
||||||
t.Errorf("Unmarshal parsed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.IsType(t, parsed, (*Goodbye)(nil), "Unmarshalled to incorrect type")
|
|
||||||
|
|
||||||
wantBye := &Goodbye{
|
|
||||||
Sources: []uint32{0x902f9e2e},
|
|
||||||
}
|
|
||||||
if got, want := parsed, wantBye; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("Unmarshal bye: got %#v, want %#v", got, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PictureLossIndication
|
|
||||||
_, packet, err = r.ReadPacket()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Read pli: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsed, _, err = Unmarshal(packet); err != nil {
|
|
||||||
t.Errorf("Unmarshal parsed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.IsType(t, parsed, (*PictureLossIndication)(nil), "Unmarshalled to incorrect type")
|
|
||||||
|
|
||||||
wantPli := &PictureLossIndication{
|
|
||||||
SenderSSRC: 0x902f9e2e,
|
|
||||||
MediaSSRC: 0x902f9e2e,
|
|
||||||
}
|
|
||||||
if got, want := parsed, wantPli; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("Unmarshal pli: got %#v, want %#v", got, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RapidResynchronizationRequest
|
|
||||||
_, packet, err = r.ReadPacket()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Read rrr: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsed, _, err = Unmarshal(packet); err != nil {
|
|
||||||
t.Errorf("Unmarshal parsed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.IsType(t, parsed, (*RapidResynchronizationRequest)(nil), "Unmarshalled to incorrect type")
|
|
||||||
|
|
||||||
wantRrr := &RapidResynchronizationRequest{
|
|
||||||
SenderSSRC: 0x902f9e2e,
|
|
||||||
MediaSSRC: 0x902f9e2e,
|
|
||||||
}
|
|
||||||
if got, want := parsed, wantRrr; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("Unmarshal rrr: got %#v, want %#v", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReadEOF(t *testing.T) {
|
|
||||||
shortHeader := []byte{
|
|
||||||
0x81, 0xc9, // missing type & len
|
|
||||||
}
|
|
||||||
|
|
||||||
r := NewReader(bytes.NewReader(shortHeader))
|
|
||||||
_, _, err := r.ReadPacket()
|
|
||||||
if got, want := err, io.ErrUnexpectedEOF; got != want {
|
|
||||||
t.Fatalf("read short header: got err = %v, want %v", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,193 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/pions/webrtc/internal/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A ReceiverReport (RR) packet provides reception quality feedback for an RTP stream
|
|
||||||
type ReceiverReport struct {
|
|
||||||
// The synchronization source identifier for the originator of this RR packet.
|
|
||||||
SSRC uint32
|
|
||||||
// Zero or more reception report blocks depending on the number of other
|
|
||||||
// sources heard by this sender since the last report. Each reception report
|
|
||||||
// block conveys statistics on the reception of RTP packets from a
|
|
||||||
// single synchronization source.
|
|
||||||
Reports []ReceptionReport
|
|
||||||
// extra data from the end of the packet; the application can parse this if needed.
|
|
||||||
ProfileExtensions []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
ssrcLength = 4
|
|
||||||
rrSSRCOffset = headerLength
|
|
||||||
rrReportOffset = rrSSRCOffset + ssrcLength
|
|
||||||
)
|
|
||||||
|
|
||||||
// Marshal encodes the ReceiverReport in binary
|
|
||||||
func (r ReceiverReport) Marshal() ([]byte, error) {
|
|
||||||
/*
|
|
||||||
* 0 1 2 3
|
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* header |V=2|P| RC | PT=RR=201 | length |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | SSRC of packet sender |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* report | SSRC_1 (SSRC of first source) |
|
|
||||||
* block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* 1 | fraction lost | cumulative number of packets lost |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | extended highest sequence number received |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | interarrival jitter |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | last SR (LSR) |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | delay since last SR (DLSR) |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* report | SSRC_2 (SSRC of second source) |
|
|
||||||
* block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* 2 : ... :
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* | profile-specific extensions |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
*/
|
|
||||||
|
|
||||||
rawPacket := make([]byte, r.len())
|
|
||||||
packetBody := rawPacket[headerLength:]
|
|
||||||
|
|
||||||
binary.BigEndian.PutUint32(packetBody, r.SSRC)
|
|
||||||
|
|
||||||
for i, rp := range r.Reports {
|
|
||||||
data, err := rp.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
offset := ssrcLength + receptionReportLength*i
|
|
||||||
copy(packetBody[offset:], data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r.Reports) > countMax {
|
|
||||||
return nil, errTooManyReports
|
|
||||||
}
|
|
||||||
|
|
||||||
pe := make([]byte, len(r.ProfileExtensions))
|
|
||||||
copy(pe, r.ProfileExtensions)
|
|
||||||
|
|
||||||
//if the length of the profile extensions isn't devisible
|
|
||||||
//by 4, we need to pad the end.
|
|
||||||
for (len(pe) & 0x3) != 0 {
|
|
||||||
pe = append(pe, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
rawPacket = append(rawPacket, pe...)
|
|
||||||
|
|
||||||
hData, err := r.Header().Marshal()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
copy(rawPacket, hData)
|
|
||||||
|
|
||||||
return rawPacket, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal decodes the ReceiverReport from binary
|
|
||||||
func (r *ReceiverReport) Unmarshal(rawPacket []byte) error {
|
|
||||||
/*
|
|
||||||
* 0 1 2 3
|
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* header |V=2|P| RC | PT=RR=201 | length |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | SSRC of packet sender |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* report | SSRC_1 (SSRC of first source) |
|
|
||||||
* block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* 1 | fraction lost | cumulative number of packets lost |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | extended highest sequence number received |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | interarrival jitter |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | last SR (LSR) |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | delay since last SR (DLSR) |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* report | SSRC_2 (SSRC of second source) |
|
|
||||||
* block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* 2 : ... :
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* | profile-specific extensions |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
*/
|
|
||||||
|
|
||||||
if len(rawPacket) < (headerLength + ssrcLength) {
|
|
||||||
return errPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
var h Header
|
|
||||||
if err := h.Unmarshal(rawPacket); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.Type != TypeReceiverReport {
|
|
||||||
return errWrongType
|
|
||||||
}
|
|
||||||
|
|
||||||
r.SSRC = binary.BigEndian.Uint32(rawPacket[rrSSRCOffset:])
|
|
||||||
|
|
||||||
for i := rrReportOffset; i < len(rawPacket) && len(r.Reports) < int(h.Count); i += receptionReportLength {
|
|
||||||
var rr ReceptionReport
|
|
||||||
if err := rr.Unmarshal(rawPacket[i:]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
r.Reports = append(r.Reports, rr)
|
|
||||||
}
|
|
||||||
r.ProfileExtensions = rawPacket[rrReportOffset+(len(r.Reports)*receptionReportLength):]
|
|
||||||
|
|
||||||
if uint8(len(r.Reports)) != h.Count {
|
|
||||||
return errInvalidHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ReceiverReport) len() int {
|
|
||||||
repsLength := 0
|
|
||||||
for _, rep := range r.Reports {
|
|
||||||
repsLength += rep.len()
|
|
||||||
}
|
|
||||||
return headerLength + ssrcLength + repsLength
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header returns the Header associated with this packet.
|
|
||||||
func (r *ReceiverReport) Header() Header {
|
|
||||||
return Header{
|
|
||||||
Count: uint8(len(r.Reports)),
|
|
||||||
Type: TypeReceiverReport,
|
|
||||||
Length: uint16((r.len()/4)-1) + uint16(util.GetPadding(len(r.ProfileExtensions))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DestinationSSRC returns an array of SSRC values that this packet refers to.
|
|
||||||
func (r *ReceiverReport) DestinationSSRC() []uint32 {
|
|
||||||
out := make([]uint32, len(r.Reports))
|
|
||||||
for i, v := range r.Reports {
|
|
||||||
out[i] = v.SSRC
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReceiverReport) String() string {
|
|
||||||
out := fmt.Sprintf("ReceiverReport from %x\n", r.SSRC)
|
|
||||||
out += fmt.Sprintf("\tSSRC \tLost\tLastSequence\n")
|
|
||||||
for _, i := range r.Reports {
|
|
||||||
out += fmt.Sprintf("\t%x\t%d/%d\t%d\n", i.SSRC, i.FractionLost, i.TotalLost, i.LastSequenceNumber)
|
|
||||||
}
|
|
||||||
out += fmt.Sprintf("\tProfile Extension Data: %v\n", r.ProfileExtensions)
|
|
||||||
return out
|
|
||||||
}
|
|
@@ -1,264 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestReceiverReportUnmarshal(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Data []byte
|
|
||||||
Want ReceiverReport
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, RR, len=7
|
|
||||||
0x81, 0xc9, 0x0, 0x7,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// ssrc=0xbc5e9a40
|
|
||||||
0xbc, 0x5e, 0x9a, 0x40,
|
|
||||||
// fracLost=0, totalLost=0
|
|
||||||
0x0, 0x0, 0x0, 0x0,
|
|
||||||
// lastSeq=0x46e1
|
|
||||||
0x0, 0x0, 0x46, 0xe1,
|
|
||||||
// jitter=273
|
|
||||||
0x0, 0x0, 0x1, 0x11,
|
|
||||||
// lsr=0x9f36432
|
|
||||||
0x9, 0xf3, 0x64, 0x32,
|
|
||||||
// delay=150137
|
|
||||||
0x0, 0x2, 0x4a, 0x79,
|
|
||||||
},
|
|
||||||
Want: ReceiverReport{
|
|
||||||
SSRC: 0x902f9e2e,
|
|
||||||
Reports: []ReceptionReport{{
|
|
||||||
SSRC: 0xbc5e9a40,
|
|
||||||
FractionLost: 0,
|
|
||||||
TotalLost: 0,
|
|
||||||
LastSequenceNumber: 0x46e1,
|
|
||||||
Jitter: 273,
|
|
||||||
LastSenderReport: 0x9f36432,
|
|
||||||
Delay: 150137,
|
|
||||||
}},
|
|
||||||
ProfileExtensions: []byte{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "valid with extension data",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, RR, len=9
|
|
||||||
0x81, 0xc9, 0x0, 0x9,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// ssrc=0xbc5e9a40
|
|
||||||
0xbc, 0x5e, 0x9a, 0x40,
|
|
||||||
// fracLost=0, totalLost=0
|
|
||||||
0x0, 0x0, 0x0, 0x0,
|
|
||||||
// lastSeq=0x46e1
|
|
||||||
0x0, 0x0, 0x46, 0xe1,
|
|
||||||
// jitter=273
|
|
||||||
0x0, 0x0, 0x1, 0x11,
|
|
||||||
// lsr=0x9f36432
|
|
||||||
0x9, 0xf3, 0x64, 0x32,
|
|
||||||
// delay=150137
|
|
||||||
0x0, 0x2, 0x4a, 0x79,
|
|
||||||
// profile-specific extension data
|
|
||||||
0x54, 0x45, 0x53, 0x54,
|
|
||||||
0x44, 0x41, 0x54, 0x41,
|
|
||||||
},
|
|
||||||
Want: ReceiverReport{
|
|
||||||
SSRC: 0x902f9e2e,
|
|
||||||
Reports: []ReceptionReport{{
|
|
||||||
SSRC: 0xbc5e9a40,
|
|
||||||
FractionLost: 0,
|
|
||||||
TotalLost: 0,
|
|
||||||
LastSequenceNumber: 0x46e1,
|
|
||||||
Jitter: 273,
|
|
||||||
LastSenderReport: 0x9f36432,
|
|
||||||
Delay: 150137,
|
|
||||||
}},
|
|
||||||
ProfileExtensions: []byte{
|
|
||||||
0x54, 0x45, 0x53, 0x54,
|
|
||||||
0x44, 0x41, 0x54, 0x41},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "short report",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, RR, len=7
|
|
||||||
0x81, 0xc9, 0x00, 0x0c,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// fracLost=0, totalLost=0
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
// report ends early
|
|
||||||
},
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "wrong type",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SR, len=7
|
|
||||||
0x81, 0xc8, 0x0, 0x7,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// ssrc=0xbc5e9a40
|
|
||||||
0xbc, 0x5e, 0x9a, 0x40,
|
|
||||||
// fracLost=0, totalLost=0
|
|
||||||
0x0, 0x0, 0x0, 0x0,
|
|
||||||
// lastSeq=0x46e1
|
|
||||||
0x0, 0x0, 0x46, 0xe1,
|
|
||||||
// jitter=273
|
|
||||||
0x0, 0x0, 0x1, 0x11,
|
|
||||||
// lsr=0x9f36432
|
|
||||||
0x9, 0xf3, 0x64, 0x32,
|
|
||||||
// delay=150137
|
|
||||||
0x0, 0x2, 0x4a, 0x79,
|
|
||||||
},
|
|
||||||
WantError: errWrongType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "bad count in header",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=2, RR, len=7
|
|
||||||
0x82, 0xc9, 0x0, 0x7,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// ssrc=0xbc5e9a40
|
|
||||||
0xbc, 0x5e, 0x9a, 0x40,
|
|
||||||
// fracLost=0, totalLost=0
|
|
||||||
0x0, 0x0, 0x0, 0x0,
|
|
||||||
// lastSeq=0x46e1
|
|
||||||
0x0, 0x0, 0x46, 0xe1,
|
|
||||||
// jitter=273
|
|
||||||
0x0, 0x0, 0x1, 0x11,
|
|
||||||
// lsr=0x9f36432
|
|
||||||
0x9, 0xf3, 0x64, 0x32,
|
|
||||||
// delay=150137
|
|
||||||
0x0, 0x2, 0x4a, 0x79,
|
|
||||||
},
|
|
||||||
WantError: errInvalidHeader,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "nil",
|
|
||||||
Data: nil,
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
var rr ReceiverReport
|
|
||||||
err := rr.Unmarshal(test.Data)
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Fatalf("Unmarshal %q rr: err = %#v, want %#v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := rr, test.Want; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("Unmarshal %q rr: got %#v, want %#v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReceiverReportRoundTrip(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Report ReceiverReport
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Report: ReceiverReport{
|
|
||||||
SSRC: 1,
|
|
||||||
Reports: []ReceptionReport{
|
|
||||||
{
|
|
||||||
SSRC: 2,
|
|
||||||
FractionLost: 2,
|
|
||||||
TotalLost: 3,
|
|
||||||
LastSequenceNumber: 4,
|
|
||||||
Jitter: 5,
|
|
||||||
LastSenderReport: 6,
|
|
||||||
Delay: 7,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
SSRC: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ProfileExtensions: []byte{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "also valid",
|
|
||||||
Report: ReceiverReport{
|
|
||||||
SSRC: 2,
|
|
||||||
Reports: []ReceptionReport{
|
|
||||||
{
|
|
||||||
SSRC: 999,
|
|
||||||
FractionLost: 30,
|
|
||||||
TotalLost: 12345,
|
|
||||||
LastSequenceNumber: 99,
|
|
||||||
Jitter: 22,
|
|
||||||
LastSenderReport: 92,
|
|
||||||
Delay: 46,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ProfileExtensions: []byte{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "totallost overflow",
|
|
||||||
Report: ReceiverReport{
|
|
||||||
SSRC: 1,
|
|
||||||
Reports: []ReceptionReport{{
|
|
||||||
TotalLost: 1 << 25,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
WantError: errInvalidTotalLost,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "count overflow",
|
|
||||||
Report: ReceiverReport{
|
|
||||||
SSRC: 1,
|
|
||||||
Reports: tooManyReports,
|
|
||||||
},
|
|
||||||
WantError: errTooManyReports,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
data, err := test.Report.Marshal()
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Fatalf("Marshal %q: err = %#v, want %#v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var decoded ReceiverReport
|
|
||||||
if err := decoded.Unmarshal(data); err != nil {
|
|
||||||
t.Fatalf("Unmarshal %q: %#v", test.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := decoded, test.Report; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("%q rr round trip: got %#v, want %#v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// a slice with enough ReceptionReports to overflow an 5-bit int
|
|
||||||
var tooManyReports []ReceptionReport
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
for i := 0; i < (1 << 5); i++ {
|
|
||||||
tooManyReports = append(tooManyReports, ReceptionReport{
|
|
||||||
SSRC: 2,
|
|
||||||
FractionLost: 2,
|
|
||||||
TotalLost: 3,
|
|
||||||
LastSequenceNumber: 4,
|
|
||||||
Jitter: 5,
|
|
||||||
LastSenderReport: 6,
|
|
||||||
Delay: 7,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,130 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import "encoding/binary"
|
|
||||||
|
|
||||||
// A ReceptionReport block conveys statistics on the reception of RTP packets
|
|
||||||
// from a single synchronization source.
|
|
||||||
type ReceptionReport struct {
|
|
||||||
// The SSRC identifier of the source to which the information in this
|
|
||||||
// reception report block pertains.
|
|
||||||
SSRC uint32
|
|
||||||
// The fraction of RTP data packets from source SSRC lost since the
|
|
||||||
// previous SR or RR packet was sent, expressed as a fixed point
|
|
||||||
// number with the binary point at the left edge of the field.
|
|
||||||
FractionLost uint8
|
|
||||||
// The total number of RTP data packets from source SSRC that have
|
|
||||||
// been lost since the beginning of reception.
|
|
||||||
TotalLost uint32
|
|
||||||
// The low 16 bits contain the highest sequence number received in an
|
|
||||||
// RTP data packet from source SSRC, and the most significant 16
|
|
||||||
// bits extend that sequence number with the corresponding count of
|
|
||||||
// sequence number cycles.
|
|
||||||
LastSequenceNumber uint32
|
|
||||||
// An estimate of the statistical variance of the RTP data packet
|
|
||||||
// interarrival time, measured in timestamp units and expressed as an
|
|
||||||
// unsigned integer.
|
|
||||||
Jitter uint32
|
|
||||||
// The middle 32 bits out of 64 in the NTP timestamp received as part of
|
|
||||||
// the most recent RTCP sender report (SR) packet from source SSRC. If no
|
|
||||||
// SR has been received yet, the field is set to zero.
|
|
||||||
LastSenderReport uint32
|
|
||||||
// The delay, expressed in units of 1/65536 seconds, between receiving the
|
|
||||||
// last SR packet from source SSRC and sending this reception report block.
|
|
||||||
// If no SR packet has been received yet from SSRC, the field is set to zero.
|
|
||||||
Delay uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
receptionReportLength = 24
|
|
||||||
fractionLostOffset = 4
|
|
||||||
totalLostOffset = 5
|
|
||||||
lastSeqOffset = 8
|
|
||||||
jitterOffset = 12
|
|
||||||
lastSROffset = 16
|
|
||||||
delayOffset = 20
|
|
||||||
)
|
|
||||||
|
|
||||||
// Marshal encodes the ReceptionReport in binary
|
|
||||||
func (r ReceptionReport) Marshal() ([]byte, error) {
|
|
||||||
/*
|
|
||||||
* 0 1 2 3
|
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* | SSRC |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | fraction lost | cumulative number of packets lost |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | extended highest sequence number received |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | interarrival jitter |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | last SR (LSR) |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | delay since last SR (DLSR) |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
*/
|
|
||||||
|
|
||||||
rawPacket := make([]byte, receptionReportLength)
|
|
||||||
|
|
||||||
binary.BigEndian.PutUint32(rawPacket, r.SSRC)
|
|
||||||
|
|
||||||
rawPacket[fractionLostOffset] = r.FractionLost
|
|
||||||
|
|
||||||
// pack TotalLost into 24 bits
|
|
||||||
if r.TotalLost >= (1 << 25) {
|
|
||||||
return nil, errInvalidTotalLost
|
|
||||||
}
|
|
||||||
tlBytes := rawPacket[totalLostOffset:]
|
|
||||||
tlBytes[0] = byte(r.TotalLost >> 16)
|
|
||||||
tlBytes[1] = byte(r.TotalLost >> 8)
|
|
||||||
tlBytes[2] = byte(r.TotalLost)
|
|
||||||
|
|
||||||
binary.BigEndian.PutUint32(rawPacket[lastSeqOffset:], r.LastSequenceNumber)
|
|
||||||
binary.BigEndian.PutUint32(rawPacket[jitterOffset:], r.Jitter)
|
|
||||||
binary.BigEndian.PutUint32(rawPacket[lastSROffset:], r.LastSenderReport)
|
|
||||||
binary.BigEndian.PutUint32(rawPacket[delayOffset:], r.Delay)
|
|
||||||
|
|
||||||
return rawPacket, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal decodes the ReceptionReport from binary
|
|
||||||
func (r *ReceptionReport) Unmarshal(rawPacket []byte) error {
|
|
||||||
if len(rawPacket) < receptionReportLength {
|
|
||||||
return errPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 0 1 2 3
|
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* | SSRC |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | fraction lost | cumulative number of packets lost |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | extended highest sequence number received |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | interarrival jitter |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | last SR (LSR) |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | delay since last SR (DLSR) |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
*/
|
|
||||||
|
|
||||||
r.SSRC = binary.BigEndian.Uint32(rawPacket)
|
|
||||||
r.FractionLost = rawPacket[fractionLostOffset]
|
|
||||||
|
|
||||||
tlBytes := rawPacket[totalLostOffset:]
|
|
||||||
r.TotalLost = uint32(tlBytes[2]) | uint32(tlBytes[1])<<8 | uint32(tlBytes[0])<<16
|
|
||||||
|
|
||||||
r.LastSequenceNumber = binary.BigEndian.Uint32(rawPacket[lastSeqOffset:])
|
|
||||||
r.Jitter = binary.BigEndian.Uint32(rawPacket[jitterOffset:])
|
|
||||||
r.LastSenderReport = binary.BigEndian.Uint32(rawPacket[lastSROffset:])
|
|
||||||
r.Delay = binary.BigEndian.Uint32(rawPacket[delayOffset:])
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ReceptionReport) len() int {
|
|
||||||
return receptionReportLength
|
|
||||||
}
|
|
@@ -1,221 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import "encoding/binary"
|
|
||||||
|
|
||||||
// A SenderReport (SR) packet provides reception quality feedback for an RTP stream
|
|
||||||
type SenderReport struct {
|
|
||||||
// The synchronization source identifier for the originator of this SR packet.
|
|
||||||
SSRC uint32
|
|
||||||
// The wallclock time when this report was sent so that it may be used in
|
|
||||||
// combination with timestamps returned in reception reports from other
|
|
||||||
// receivers to measure round-trip propagation to those receivers.
|
|
||||||
NTPTime uint64
|
|
||||||
// Corresponds to the same time as the NTP timestamp (above), but in
|
|
||||||
// the same units and with the same random offset as the RTP
|
|
||||||
// timestamps in data packets. This correspondence may be used for
|
|
||||||
// intra- and inter-media synchronization for sources whose NTP
|
|
||||||
// timestamps are synchronized, and may be used by media-independent
|
|
||||||
// receivers to estimate the nominal RTP clock frequency.
|
|
||||||
RTPTime uint32
|
|
||||||
// The total number of RTP data packets transmitted by the sender
|
|
||||||
// since starting transmission up until the time this SR packet was
|
|
||||||
// generated.
|
|
||||||
PacketCount uint32
|
|
||||||
// The total number of payload octets (i.e., not including header or
|
|
||||||
// padding) transmitted in RTP data packets by the sender since
|
|
||||||
// starting transmission up until the time this SR packet was
|
|
||||||
// generated.
|
|
||||||
OctetCount uint32
|
|
||||||
// Zero or more reception report blocks depending on the number of other
|
|
||||||
// sources heard by this sender since the last report. Each reception report
|
|
||||||
// block conveys statistics on the reception of RTP packets from a
|
|
||||||
// single synchronization source.
|
|
||||||
Reports []ReceptionReport
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
srHeaderLength = 24
|
|
||||||
srSSRCOffset = 0
|
|
||||||
srNTPOffset = srSSRCOffset + ssrcLength
|
|
||||||
ntpTimeLength = 8
|
|
||||||
srRTPOffset = srNTPOffset + ntpTimeLength
|
|
||||||
rtpTimeLength = 4
|
|
||||||
srPacketCountOffset = srRTPOffset + rtpTimeLength
|
|
||||||
srPacketCountLength = 4
|
|
||||||
srOctetCountOffset = srPacketCountOffset + srPacketCountLength
|
|
||||||
srOctetCountLength = 4
|
|
||||||
srReportOffset = srOctetCountOffset + srOctetCountLength
|
|
||||||
)
|
|
||||||
|
|
||||||
// Marshal encodes the SenderReport in binary
|
|
||||||
func (r SenderReport) Marshal() ([]byte, error) {
|
|
||||||
/*
|
|
||||||
* 0 1 2 3
|
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* header |V=2|P| RC | PT=SR=200 | length |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | SSRC of sender |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* sender | NTP timestamp, most significant word |
|
|
||||||
* info +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | NTP timestamp, least significant word |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | RTP timestamp |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | sender's packet count |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | sender's octet count |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* report | SSRC_1 (SSRC of first source) |
|
|
||||||
* block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* 1 | fraction lost | cumulative number of packets lost |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | extended highest sequence number received |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | interarrival jitter |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | last SR (LSR) |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | delay since last SR (DLSR) |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* report | SSRC_2 (SSRC of second source) |
|
|
||||||
* block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* 2 : ... :
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* | profile-specific extensions |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
*/
|
|
||||||
|
|
||||||
rawPacket := make([]byte, r.len())
|
|
||||||
packetBody := rawPacket[headerLength:]
|
|
||||||
|
|
||||||
binary.BigEndian.PutUint32(packetBody[srSSRCOffset:], r.SSRC)
|
|
||||||
binary.BigEndian.PutUint64(packetBody[srNTPOffset:], r.NTPTime)
|
|
||||||
binary.BigEndian.PutUint32(packetBody[srRTPOffset:], r.RTPTime)
|
|
||||||
binary.BigEndian.PutUint32(packetBody[srPacketCountOffset:], r.PacketCount)
|
|
||||||
binary.BigEndian.PutUint32(packetBody[srOctetCountOffset:], r.OctetCount)
|
|
||||||
|
|
||||||
for i, rp := range r.Reports {
|
|
||||||
data, err := rp.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
offset := srHeaderLength + receptionReportLength*i
|
|
||||||
copy(packetBody[offset:], data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r.Reports) > countMax {
|
|
||||||
return nil, errTooManyReports
|
|
||||||
}
|
|
||||||
|
|
||||||
hData, err := r.Header().Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
copy(rawPacket, hData)
|
|
||||||
|
|
||||||
return rawPacket, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal decodes the SenderReport from binary
|
|
||||||
func (r *SenderReport) Unmarshal(rawPacket []byte) error {
|
|
||||||
/*
|
|
||||||
* 0 1 2 3
|
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* header |V=2|P| RC | PT=SR=200 | length |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | SSRC of sender |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* sender | NTP timestamp, most significant word |
|
|
||||||
* info +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | NTP timestamp, least significant word |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | RTP timestamp |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | sender's packet count |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | sender's octet count |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* report | SSRC_1 (SSRC of first source) |
|
|
||||||
* block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* 1 | fraction lost | cumulative number of packets lost |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | extended highest sequence number received |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | interarrival jitter |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | last SR (LSR) |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | delay since last SR (DLSR) |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* report | SSRC_2 (SSRC of second source) |
|
|
||||||
* block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* 2 : ... :
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* | profile-specific extensions |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
*/
|
|
||||||
|
|
||||||
if len(rawPacket) < (headerLength + srHeaderLength) {
|
|
||||||
return errPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
var h Header
|
|
||||||
if err := h.Unmarshal(rawPacket); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.Type != TypeSenderReport {
|
|
||||||
return errWrongType
|
|
||||||
}
|
|
||||||
|
|
||||||
packetBody := rawPacket[headerLength:]
|
|
||||||
|
|
||||||
r.SSRC = binary.BigEndian.Uint32(packetBody[srSSRCOffset:])
|
|
||||||
r.NTPTime = binary.BigEndian.Uint64(packetBody[srNTPOffset:])
|
|
||||||
r.RTPTime = binary.BigEndian.Uint32(packetBody[srRTPOffset:])
|
|
||||||
r.PacketCount = binary.BigEndian.Uint32(packetBody[srPacketCountOffset:])
|
|
||||||
r.OctetCount = binary.BigEndian.Uint32(packetBody[srOctetCountOffset:])
|
|
||||||
|
|
||||||
for i := srReportOffset; i < len(packetBody); i += receptionReportLength {
|
|
||||||
var rr ReceptionReport
|
|
||||||
if err := rr.Unmarshal(packetBody[i:]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
r.Reports = append(r.Reports, rr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if uint8(len(r.Reports)) != h.Count {
|
|
||||||
return errInvalidHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DestinationSSRC returns an array of SSRC values that this packet refers to.
|
|
||||||
func (r *SenderReport) DestinationSSRC() []uint32 {
|
|
||||||
out := make([]uint32, len(r.Reports))
|
|
||||||
for i, v := range r.Reports {
|
|
||||||
out[i] = v.SSRC
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *SenderReport) len() int {
|
|
||||||
repsLength := 0
|
|
||||||
for _, rep := range r.Reports {
|
|
||||||
repsLength += rep.len()
|
|
||||||
}
|
|
||||||
return headerLength + srHeaderLength + repsLength
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header returns the Header associated with this packet.
|
|
||||||
func (r *SenderReport) Header() Header {
|
|
||||||
return Header{
|
|
||||||
Count: uint8(len(r.Reports)),
|
|
||||||
Type: TypeSenderReport,
|
|
||||||
Length: uint16((r.len() / 4) - 1),
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,217 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSenderReportUnmarshal(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Data []byte
|
|
||||||
Want SenderReport
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "nil",
|
|
||||||
Data: nil,
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SR, len=7
|
|
||||||
0x81, 0xc8, 0x0, 0x7,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// ntp=0xda8bd1fcdddda05a
|
|
||||||
0xda, 0x8b, 0xd1, 0xfc,
|
|
||||||
0xdd, 0xdd, 0xa0, 0x5a,
|
|
||||||
// rtp=0xaaf4edd5
|
|
||||||
0xaa, 0xf4, 0xed, 0xd5,
|
|
||||||
// packetCount=1
|
|
||||||
0x00, 0x00, 0x00, 0x01,
|
|
||||||
// octetCount=2
|
|
||||||
0x00, 0x00, 0x00, 0x02,
|
|
||||||
// ssrc=0xbc5e9a40
|
|
||||||
0xbc, 0x5e, 0x9a, 0x40,
|
|
||||||
// fracLost=0, totalLost=0
|
|
||||||
0x0, 0x0, 0x0, 0x0,
|
|
||||||
// lastSeq=0x46e1
|
|
||||||
0x0, 0x0, 0x46, 0xe1,
|
|
||||||
// jitter=273
|
|
||||||
0x0, 0x0, 0x1, 0x11,
|
|
||||||
// lsr=0x9f36432
|
|
||||||
0x9, 0xf3, 0x64, 0x32,
|
|
||||||
// delay=150137
|
|
||||||
0x0, 0x2, 0x4a, 0x79,
|
|
||||||
},
|
|
||||||
Want: SenderReport{
|
|
||||||
SSRC: 0x902f9e2e,
|
|
||||||
NTPTime: 0xda8bd1fcdddda05a,
|
|
||||||
RTPTime: 0xaaf4edd5,
|
|
||||||
PacketCount: 1,
|
|
||||||
OctetCount: 2,
|
|
||||||
Reports: []ReceptionReport{{
|
|
||||||
SSRC: 0xbc5e9a40,
|
|
||||||
FractionLost: 0,
|
|
||||||
TotalLost: 0,
|
|
||||||
LastSequenceNumber: 0x46e1,
|
|
||||||
Jitter: 273,
|
|
||||||
LastSenderReport: 0x9f36432,
|
|
||||||
Delay: 150137,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "wrong type",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, RR, len=7
|
|
||||||
0x81, 0xc9, 0x0, 0x7,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// ntp=0xda8bd1fcdddda05a
|
|
||||||
0xda, 0x8b, 0xd1, 0xfc,
|
|
||||||
0xdd, 0xdd, 0xa0, 0x5a,
|
|
||||||
// rtp=0xaaf4edd5
|
|
||||||
0xaa, 0xf4, 0xed, 0xd5,
|
|
||||||
// packetCount=1
|
|
||||||
0x00, 0x00, 0x00, 0x01,
|
|
||||||
// octetCount=2
|
|
||||||
0x00, 0x00, 0x00, 0x02,
|
|
||||||
// ssrc=0xbc5e9a40
|
|
||||||
0xbc, 0x5e, 0x9a, 0x40,
|
|
||||||
// fracLost=0, totalLost=0
|
|
||||||
0x0, 0x0, 0x0, 0x0,
|
|
||||||
// lastSeq=0x46e1
|
|
||||||
0x0, 0x0, 0x46, 0xe1,
|
|
||||||
// jitter=273
|
|
||||||
0x0, 0x0, 0x1, 0x11,
|
|
||||||
// lsr=0x9f36432
|
|
||||||
0x9, 0xf3, 0x64, 0x32,
|
|
||||||
// delay=150137
|
|
||||||
0x0, 0x2, 0x4a, 0x79,
|
|
||||||
},
|
|
||||||
WantError: errWrongType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "bad count in header",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SR, len=7
|
|
||||||
0x82, 0xc8, 0x0, 0x7,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// ntp=0xda8bd1fcdddda05a
|
|
||||||
0xda, 0x8b, 0xd1, 0xfc,
|
|
||||||
0xdd, 0xdd, 0xa0, 0x5a,
|
|
||||||
// rtp=0xaaf4edd5
|
|
||||||
0xaa, 0xf4, 0xed, 0xd5,
|
|
||||||
// packetCount=1
|
|
||||||
0x00, 0x00, 0x00, 0x01,
|
|
||||||
// octetCount=2
|
|
||||||
0x00, 0x00, 0x00, 0x02,
|
|
||||||
// ssrc=0xbc5e9a40
|
|
||||||
0xbc, 0x5e, 0x9a, 0x40,
|
|
||||||
// fracLost=0, totalLost=0
|
|
||||||
0x0, 0x0, 0x0, 0x0,
|
|
||||||
// lastSeq=0x46e1
|
|
||||||
0x0, 0x0, 0x46, 0xe1,
|
|
||||||
// jitter=273
|
|
||||||
0x0, 0x0, 0x1, 0x11,
|
|
||||||
// lsr=0x9f36432
|
|
||||||
0x9, 0xf3, 0x64, 0x32,
|
|
||||||
// delay=150137
|
|
||||||
0x0, 0x2, 0x4a, 0x79,
|
|
||||||
},
|
|
||||||
WantError: errInvalidHeader,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
var sr SenderReport
|
|
||||||
err := sr.Unmarshal(test.Data)
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Fatalf("Unmarshal %q sr: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := sr, test.Want; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("Unmarshal %q sr: got %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSenderReportRoundTrip(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Report SenderReport
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Report: SenderReport{
|
|
||||||
SSRC: 1,
|
|
||||||
NTPTime: 999,
|
|
||||||
RTPTime: 555,
|
|
||||||
PacketCount: 32,
|
|
||||||
OctetCount: 11,
|
|
||||||
Reports: []ReceptionReport{
|
|
||||||
{
|
|
||||||
SSRC: 2,
|
|
||||||
FractionLost: 2,
|
|
||||||
TotalLost: 3,
|
|
||||||
LastSequenceNumber: 4,
|
|
||||||
Jitter: 5,
|
|
||||||
LastSenderReport: 6,
|
|
||||||
Delay: 7,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
SSRC: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "also valid",
|
|
||||||
Report: SenderReport{
|
|
||||||
SSRC: 2,
|
|
||||||
Reports: []ReceptionReport{
|
|
||||||
{
|
|
||||||
SSRC: 999,
|
|
||||||
FractionLost: 30,
|
|
||||||
TotalLost: 12345,
|
|
||||||
LastSequenceNumber: 99,
|
|
||||||
Jitter: 22,
|
|
||||||
LastSenderReport: 92,
|
|
||||||
Delay: 46,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "count overflow",
|
|
||||||
Report: SenderReport{
|
|
||||||
SSRC: 1,
|
|
||||||
Reports: tooManyReports,
|
|
||||||
},
|
|
||||||
WantError: errTooManyReports,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
data, err := test.Report.Marshal()
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Fatalf("Marshal %q: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var decoded SenderReport
|
|
||||||
if err := decoded.Unmarshal(data); err != nil {
|
|
||||||
t.Fatalf("Unmarshal %q: %v", test.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := decoded, test.Report; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("%q sr round trip: got %#v, want %#v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,113 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SLIEntry represents a single entry to the SLI packet's
|
|
||||||
// list of lost slices.
|
|
||||||
type SLIEntry struct {
|
|
||||||
// ID of first lost slice
|
|
||||||
First uint16
|
|
||||||
|
|
||||||
// Number of lost slices
|
|
||||||
Number uint16
|
|
||||||
|
|
||||||
// ID of related picture
|
|
||||||
Picture uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
// The SliceLossIndication packet informs the encoder about the loss of a picture slice
|
|
||||||
type SliceLossIndication struct {
|
|
||||||
// SSRC of sender
|
|
||||||
SenderSSRC uint32
|
|
||||||
|
|
||||||
// SSRC of the media source
|
|
||||||
MediaSSRC uint32
|
|
||||||
|
|
||||||
SLI []SLIEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
sliLength = 2
|
|
||||||
sliOffset = 8
|
|
||||||
)
|
|
||||||
|
|
||||||
// Marshal encodes the SliceLossIndication in binary
|
|
||||||
func (p SliceLossIndication) Marshal() ([]byte, error) {
|
|
||||||
|
|
||||||
if len(p.SLI)+sliLength > math.MaxUint8 {
|
|
||||||
return nil, errTooManyReports
|
|
||||||
}
|
|
||||||
|
|
||||||
rawPacket := make([]byte, sliOffset+(len(p.SLI)*4))
|
|
||||||
binary.BigEndian.PutUint32(rawPacket, p.SenderSSRC)
|
|
||||||
binary.BigEndian.PutUint32(rawPacket[4:], p.MediaSSRC)
|
|
||||||
for i, s := range p.SLI {
|
|
||||||
sli := ((uint32(s.First) & 0x1FFF) << 19) |
|
|
||||||
((uint32(s.Number) & 0x1FFF) << 6) |
|
|
||||||
(uint32(s.Picture) & 0x3F)
|
|
||||||
binary.BigEndian.PutUint32(rawPacket[sliOffset+(4*i):], sli)
|
|
||||||
}
|
|
||||||
hData, err := p.Header().Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return append(hData, rawPacket...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal decodes the SliceLossIndication from binary
|
|
||||||
func (p *SliceLossIndication) Unmarshal(rawPacket []byte) error {
|
|
||||||
if len(rawPacket) < (headerLength + ssrcLength) {
|
|
||||||
return errPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
var h Header
|
|
||||||
if err := h.Unmarshal(rawPacket); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(rawPacket) < (headerLength + int(4*h.Length)) {
|
|
||||||
return errPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.Type != TypeTransportSpecificFeedback || h.Count != FormatSLI {
|
|
||||||
return errWrongType
|
|
||||||
}
|
|
||||||
|
|
||||||
p.SenderSSRC = binary.BigEndian.Uint32(rawPacket[headerLength:])
|
|
||||||
p.MediaSSRC = binary.BigEndian.Uint32(rawPacket[headerLength+ssrcLength:])
|
|
||||||
for i := headerLength + sliOffset; i < (headerLength + int(h.Length*4)); i += 4 {
|
|
||||||
sli := binary.BigEndian.Uint32(rawPacket[i:])
|
|
||||||
p.SLI = append(p.SLI, SLIEntry{
|
|
||||||
uint16((sli >> 19) & 0x1FFF),
|
|
||||||
uint16((sli >> 6) & 0x1FFF),
|
|
||||||
uint8(sli & 0x3F)})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *SliceLossIndication) len() int {
|
|
||||||
return headerLength + sliOffset + (len(p.SLI) * 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header returns the Header associated with this packet.
|
|
||||||
func (p *SliceLossIndication) Header() Header {
|
|
||||||
return Header{
|
|
||||||
Count: FormatSLI,
|
|
||||||
Type: TypeTransportSpecificFeedback,
|
|
||||||
Length: uint16((p.len() / 4) - 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *SliceLossIndication) String() string {
|
|
||||||
return fmt.Sprintf("SliceLossIndication %x %x %+v", p.SenderSSRC, p.MediaSSRC, p.SLI)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DestinationSSRC returns an array of SSRC values that this packet refers to.
|
|
||||||
func (p *SliceLossIndication) DestinationSSRC() []uint32 {
|
|
||||||
return []uint32{p.MediaSSRC}
|
|
||||||
}
|
|
@@ -1,118 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSliceLossIndicationUnmarshal(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Data []byte
|
|
||||||
Want SliceLossIndication
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Data: []byte{
|
|
||||||
// SliceLossIndication
|
|
||||||
0x82, 0xcd, 0x0, 0x3,
|
|
||||||
// sender=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// media=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// nack 0xAAAA, 0x5555
|
|
||||||
0x55, 0x50, 0x00, 0x2C,
|
|
||||||
},
|
|
||||||
Want: SliceLossIndication{
|
|
||||||
SenderSSRC: 0x902f9e2e,
|
|
||||||
MediaSSRC: 0x902f9e2e,
|
|
||||||
SLI: []SLIEntry{{0xaaa, 0, 0x2C}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "short report",
|
|
||||||
Data: []byte{
|
|
||||||
0x81, 0xcd, 0x0, 0x2,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// report ends early
|
|
||||||
},
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "wrong type",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SR, len=7
|
|
||||||
0x81, 0xc8, 0x0, 0x7,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// ssrc=0xbc5e9a40
|
|
||||||
0xbc, 0x5e, 0x9a, 0x40,
|
|
||||||
// fracLost=0, totalLost=0
|
|
||||||
0x0, 0x0, 0x0, 0x0,
|
|
||||||
// lastSeq=0x46e1
|
|
||||||
0x0, 0x0, 0x46, 0xe1,
|
|
||||||
// jitter=273
|
|
||||||
0x0, 0x0, 0x1, 0x11,
|
|
||||||
// lsr=0x9f36432
|
|
||||||
0x9, 0xf3, 0x64, 0x32,
|
|
||||||
// delay=150137
|
|
||||||
0x0, 0x2, 0x4a, 0x79,
|
|
||||||
},
|
|
||||||
WantError: errWrongType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "nil",
|
|
||||||
Data: nil,
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
var sli SliceLossIndication
|
|
||||||
err := sli.Unmarshal(test.Data)
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Fatalf("Unmarshal %q rr: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := sli, test.Want; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("Unmarshal %q rr: got %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSliceLossIndicationRoundTrip(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Report SliceLossIndication
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Report: SliceLossIndication{
|
|
||||||
SenderSSRC: 0x902f9e2e,
|
|
||||||
MediaSSRC: 0x902f9e2e,
|
|
||||||
SLI: []SLIEntry{{1, 0xAA, 0x1F}, {1034, 0x05, 0x6}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
data, err := test.Report.Marshal()
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Fatalf("Marshal %q: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var decoded SliceLossIndication
|
|
||||||
if err := decoded.Unmarshal(data); err != nil {
|
|
||||||
t.Fatalf("Unmarshal %q: %v", test.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := decoded, test.Report; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("%q sli round trip: got %#v, want %#v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,343 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
|
|
||||||
"github.com/pions/webrtc/internal/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SDESType is the item type used in the RTCP SDES control packet.
|
|
||||||
type SDESType uint8
|
|
||||||
|
|
||||||
// RTP SDES item types registered with IANA. See: https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml#rtp-parameters-5
|
|
||||||
const (
|
|
||||||
SDESEnd SDESType = iota // end of SDES list RFC 3550, 6.5
|
|
||||||
SDESCNAME // canonical name RFC 3550, 6.5.1
|
|
||||||
SDESName // user name RFC 3550, 6.5.2
|
|
||||||
SDESEmail // user's electronic mail address RFC 3550, 6.5.3
|
|
||||||
SDESPhone // user's phone number RFC 3550, 6.5.4
|
|
||||||
SDESLocation // geographic user location RFC 3550, 6.5.5
|
|
||||||
SDESTool // name of application or tool RFC 3550, 6.5.6
|
|
||||||
SDESNote // notice about the source RFC 3550, 6.5.7
|
|
||||||
SDESPrivate // private extensions RFC 3550, 6.5.8 (not implemented)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s SDESType) String() string {
|
|
||||||
switch s {
|
|
||||||
case SDESEnd:
|
|
||||||
return "END"
|
|
||||||
case SDESCNAME:
|
|
||||||
return "CNAME"
|
|
||||||
case SDESName:
|
|
||||||
return "NAME"
|
|
||||||
case SDESEmail:
|
|
||||||
return "EMAIL"
|
|
||||||
case SDESPhone:
|
|
||||||
return "PHONE"
|
|
||||||
case SDESLocation:
|
|
||||||
return "LOC"
|
|
||||||
case SDESTool:
|
|
||||||
return "TOOL"
|
|
||||||
case SDESNote:
|
|
||||||
return "NOTE"
|
|
||||||
case SDESPrivate:
|
|
||||||
return "PRIV"
|
|
||||||
default:
|
|
||||||
return string(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
sdesSourceLen = 4
|
|
||||||
sdesTypeLen = 1
|
|
||||||
sdesTypeOffset = 0
|
|
||||||
sdesOctetCountLen = 1
|
|
||||||
sdesOctetCountOffset = 1
|
|
||||||
sdesMaxOctetCount = (1 << 8) - 1
|
|
||||||
sdesTextOffset = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
// A SourceDescription (SDES) packet describes the sources in an RTP stream.
|
|
||||||
type SourceDescription struct {
|
|
||||||
Chunks []SourceDescriptionChunk
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal encodes the SourceDescription in binary
|
|
||||||
func (s SourceDescription) Marshal() ([]byte, error) {
|
|
||||||
/*
|
|
||||||
* 0 1 2 3
|
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* header |V=2|P| SC | PT=SDES=202 | length |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* chunk | SSRC/CSRC_1 |
|
|
||||||
* 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | SDES items |
|
|
||||||
* | ... |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* chunk | SSRC/CSRC_2 |
|
|
||||||
* 2 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | SDES items |
|
|
||||||
* | ... |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
*/
|
|
||||||
|
|
||||||
rawPacket := make([]byte, s.len())
|
|
||||||
packetBody := rawPacket[headerLength:]
|
|
||||||
|
|
||||||
chunkOffset := 0
|
|
||||||
for _, c := range s.Chunks {
|
|
||||||
data, err := c.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
copy(packetBody[chunkOffset:], data)
|
|
||||||
chunkOffset += len(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(s.Chunks) > countMax {
|
|
||||||
return nil, errTooManyChunks
|
|
||||||
}
|
|
||||||
|
|
||||||
hData, err := s.Header().Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
copy(rawPacket, hData)
|
|
||||||
|
|
||||||
return rawPacket, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal decodes the SourceDescription from binary
|
|
||||||
func (s *SourceDescription) Unmarshal(rawPacket []byte) error {
|
|
||||||
/*
|
|
||||||
* 0 1 2 3
|
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* header |V=2|P| SC | PT=SDES=202 | length |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* chunk | SSRC/CSRC_1 |
|
|
||||||
* 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | SDES items |
|
|
||||||
* | ... |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* chunk | SSRC/CSRC_2 |
|
|
||||||
* 2 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | SDES items |
|
|
||||||
* | ... |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
*/
|
|
||||||
|
|
||||||
var h Header
|
|
||||||
if err := h.Unmarshal(rawPacket); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.Type != TypeSourceDescription {
|
|
||||||
return errWrongType
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := headerLength; i < len(rawPacket); {
|
|
||||||
var chunk SourceDescriptionChunk
|
|
||||||
if err := chunk.Unmarshal(rawPacket[i:]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.Chunks = append(s.Chunks, chunk)
|
|
||||||
|
|
||||||
i += chunk.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(s.Chunks) != int(h.Count) {
|
|
||||||
return errInvalidHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SourceDescription) len() int {
|
|
||||||
chunksLength := 0
|
|
||||||
for _, c := range s.Chunks {
|
|
||||||
chunksLength += c.len()
|
|
||||||
}
|
|
||||||
return headerLength + chunksLength
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header returns the Header associated with this packet.
|
|
||||||
func (s *SourceDescription) Header() Header {
|
|
||||||
return Header{
|
|
||||||
Count: uint8(len(s.Chunks)),
|
|
||||||
Type: TypeSourceDescription,
|
|
||||||
Length: uint16((s.len() / 4) - 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A SourceDescriptionChunk contains items describing a single RTP source
|
|
||||||
type SourceDescriptionChunk struct {
|
|
||||||
// The source (ssrc) or contributing source (csrc) identifier this packet describes
|
|
||||||
Source uint32
|
|
||||||
Items []SourceDescriptionItem
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal encodes the SourceDescriptionChunk in binary
|
|
||||||
func (s SourceDescriptionChunk) Marshal() ([]byte, error) {
|
|
||||||
/*
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* | SSRC/CSRC_1 |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | SDES items |
|
|
||||||
* | ... |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
*/
|
|
||||||
|
|
||||||
rawPacket := make([]byte, sdesSourceLen)
|
|
||||||
binary.BigEndian.PutUint32(rawPacket, s.Source)
|
|
||||||
|
|
||||||
for _, it := range s.Items {
|
|
||||||
data, err := it.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rawPacket = append(rawPacket, data...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The list of items in each chunk MUST be terminated by one or more null octets
|
|
||||||
rawPacket = append(rawPacket, uint8(SDESEnd))
|
|
||||||
|
|
||||||
// additional null octets MUST be included if needed to pad until the next 32-bit boundary
|
|
||||||
rawPacket = append(rawPacket, make([]byte, util.GetPadding(len(rawPacket)))...)
|
|
||||||
|
|
||||||
return rawPacket, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal decodes the SourceDescriptionChunk from binary
|
|
||||||
func (s *SourceDescriptionChunk) Unmarshal(rawPacket []byte) error {
|
|
||||||
/*
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* | SSRC/CSRC_1 |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | SDES items |
|
|
||||||
* | ... |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
*/
|
|
||||||
|
|
||||||
if len(rawPacket) < (sdesSourceLen + sdesTypeLen) {
|
|
||||||
return errPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Source = binary.BigEndian.Uint32(rawPacket)
|
|
||||||
|
|
||||||
for i := 4; i < len(rawPacket); {
|
|
||||||
if pktType := SDESType(rawPacket[i]); pktType == SDESEnd {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var it SourceDescriptionItem
|
|
||||||
if err := it.Unmarshal(rawPacket[i:]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.Items = append(s.Items, it)
|
|
||||||
i += it.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
return errPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s SourceDescriptionChunk) len() int {
|
|
||||||
len := sdesSourceLen
|
|
||||||
for _, it := range s.Items {
|
|
||||||
len += it.len()
|
|
||||||
}
|
|
||||||
len += sdesTypeLen // for terminating null octet
|
|
||||||
|
|
||||||
// align to 32-bit boundary
|
|
||||||
len += util.GetPadding(len)
|
|
||||||
|
|
||||||
return len
|
|
||||||
}
|
|
||||||
|
|
||||||
// A SourceDescriptionItem is a part of a SourceDescription that describes a stream.
|
|
||||||
type SourceDescriptionItem struct {
|
|
||||||
// The type identifier for this item. eg, SDESCNAME for canonical name description.
|
|
||||||
//
|
|
||||||
// Type zero or SDESEnd is interpreted as the end of an item list and cannot be used.
|
|
||||||
Type SDESType
|
|
||||||
// Text is a unicode text blob associated with the item. Its meaning varies based on the item's Type.
|
|
||||||
Text string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s SourceDescriptionItem) len() int {
|
|
||||||
/*
|
|
||||||
* 0 1 2 3
|
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | CNAME=1 | length | user and domain name ...
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
*/
|
|
||||||
return sdesTypeLen + sdesOctetCountLen + len([]byte(s.Text))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal encodes the SourceDescriptionItem in binary
|
|
||||||
func (s SourceDescriptionItem) Marshal() ([]byte, error) {
|
|
||||||
/*
|
|
||||||
* 0 1 2 3
|
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | CNAME=1 | length | user and domain name ...
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
*/
|
|
||||||
|
|
||||||
if s.Type == SDESEnd {
|
|
||||||
return nil, errSDESMissingType
|
|
||||||
}
|
|
||||||
|
|
||||||
rawPacket := make([]byte, sdesTypeLen+sdesOctetCountLen)
|
|
||||||
|
|
||||||
rawPacket[sdesTypeOffset] = uint8(s.Type)
|
|
||||||
|
|
||||||
txtBytes := []byte(s.Text)
|
|
||||||
octetCount := len(txtBytes)
|
|
||||||
if octetCount > sdesMaxOctetCount {
|
|
||||||
return nil, errSDESTextTooLong
|
|
||||||
}
|
|
||||||
rawPacket[sdesOctetCountOffset] = uint8(octetCount)
|
|
||||||
|
|
||||||
rawPacket = append(rawPacket, txtBytes...)
|
|
||||||
|
|
||||||
return rawPacket, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal decodes the SourceDescriptionItem from binary
|
|
||||||
func (s *SourceDescriptionItem) Unmarshal(rawPacket []byte) error {
|
|
||||||
/*
|
|
||||||
* 0 1 2 3
|
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | CNAME=1 | length | user and domain name ...
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
*/
|
|
||||||
|
|
||||||
if len(rawPacket) < (sdesTypeLen + sdesOctetCountLen) {
|
|
||||||
return errPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Type = SDESType(rawPacket[sdesTypeOffset])
|
|
||||||
|
|
||||||
octetCount := int(rawPacket[sdesOctetCountOffset])
|
|
||||||
if sdesTextOffset+octetCount > len(rawPacket) {
|
|
||||||
return errPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
txtBytes := rawPacket[sdesTextOffset : sdesTextOffset+octetCount]
|
|
||||||
s.Text = string(txtBytes)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DestinationSSRC returns an array of SSRC values that this packet refers to.
|
|
||||||
func (s *SourceDescription) DestinationSSRC() []uint32 {
|
|
||||||
out := make([]uint32, len(s.Chunks))
|
|
||||||
for i, v := range s.Chunks {
|
|
||||||
out[i] = v.Source
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
@@ -1,370 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSourceDescriptionUnmarshal(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Data []byte
|
|
||||||
Want SourceDescription
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "nil",
|
|
||||||
Data: nil,
|
|
||||||
WantError: errInvalidHeader,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "no chunks",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SDES, len=8
|
|
||||||
0x80, 0xca, 0x00, 0x04,
|
|
||||||
},
|
|
||||||
Want: SourceDescription{
|
|
||||||
Chunks: nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "missing type",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SDES, len=8
|
|
||||||
0x81, 0xca, 0x00, 0x08,
|
|
||||||
// ssrc=0x00000000
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
},
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "bad cname length",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SDES, len=10
|
|
||||||
0x81, 0xca, 0x00, 0x0a,
|
|
||||||
// ssrc=0x00000000
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
// CNAME, len = 1
|
|
||||||
0x01, 0x01,
|
|
||||||
},
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "short cname",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SDES, len=9
|
|
||||||
0x81, 0xca, 0x00, 0x09,
|
|
||||||
// ssrc=0x00000000
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
// CNAME, Missing length
|
|
||||||
0x01,
|
|
||||||
},
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "no end",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SDES, len=11
|
|
||||||
0x81, 0xca, 0x00, 0x0b,
|
|
||||||
// ssrc=0x00000000
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
// CNAME, len=1, content=A
|
|
||||||
0x01, 0x02, 0x41,
|
|
||||||
// Missing END
|
|
||||||
},
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "bad octet count",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SDES, len=10
|
|
||||||
0x81, 0xca, 0x00, 0x0a,
|
|
||||||
// ssrc=0x00000000
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
// CNAME, len=1
|
|
||||||
0x01, 0x01,
|
|
||||||
},
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "zero item chunk",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SDES, len=12
|
|
||||||
0x81, 0xca, 0x00, 0x0c,
|
|
||||||
// ssrc=0x01020304
|
|
||||||
0x01, 0x02, 0x03, 0x04,
|
|
||||||
// END + padding
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
},
|
|
||||||
Want: SourceDescription{
|
|
||||||
Chunks: []SourceDescriptionChunk{{
|
|
||||||
Source: 0x01020304,
|
|
||||||
Items: nil,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "wrong type",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SR, len=12
|
|
||||||
0x81, 0xc8, 0x00, 0x0c,
|
|
||||||
// ssrc=0x01020304
|
|
||||||
0x01, 0x02, 0x03, 0x04,
|
|
||||||
// END + padding
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
},
|
|
||||||
WantError: errWrongType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "bad count in header",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SDES, len=12
|
|
||||||
0x81, 0xca, 0x00, 0x0c,
|
|
||||||
},
|
|
||||||
WantError: errInvalidHeader,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "empty string",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SDES, len=12
|
|
||||||
0x81, 0xca, 0x00, 0x0c,
|
|
||||||
// ssrc=0x01020304
|
|
||||||
0x01, 0x02, 0x03, 0x04,
|
|
||||||
// CNAME, len=0
|
|
||||||
0x01, 0x00,
|
|
||||||
// END + padding
|
|
||||||
0x00, 0x00,
|
|
||||||
},
|
|
||||||
Want: SourceDescription{
|
|
||||||
Chunks: []SourceDescriptionChunk{{
|
|
||||||
Source: 0x01020304,
|
|
||||||
Items: []SourceDescriptionItem{
|
|
||||||
{
|
|
||||||
Type: SDESCNAME,
|
|
||||||
Text: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "two items",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SDES, len=16
|
|
||||||
0x81, 0xca, 0x00, 0x10,
|
|
||||||
// ssrc=0x10000000
|
|
||||||
0x10, 0x00, 0x00, 0x00,
|
|
||||||
// CNAME, len=1, content=A
|
|
||||||
0x01, 0x01, 0x41,
|
|
||||||
// PHONE, len=1, content=B
|
|
||||||
0x04, 0x01, 0x42,
|
|
||||||
// END + padding
|
|
||||||
0x00, 0x00,
|
|
||||||
},
|
|
||||||
Want: SourceDescription{
|
|
||||||
Chunks: []SourceDescriptionChunk{
|
|
||||||
{
|
|
||||||
Source: 0x10000000,
|
|
||||||
Items: []SourceDescriptionItem{
|
|
||||||
{
|
|
||||||
Type: SDESCNAME,
|
|
||||||
Text: "A",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: SDESPhone,
|
|
||||||
Text: "B",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "two chunks",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=2, SDES, len=24
|
|
||||||
0x82, 0xca, 0x00, 0x18,
|
|
||||||
// ssrc=0x01020304
|
|
||||||
0x01, 0x02, 0x03, 0x04,
|
|
||||||
// Chunk 1
|
|
||||||
// CNAME, len=1, content=A
|
|
||||||
0x01, 0x01, 0x41,
|
|
||||||
// END
|
|
||||||
0x00,
|
|
||||||
// Chunk 2
|
|
||||||
// SSRC 0x05060708
|
|
||||||
0x05, 0x06, 0x07, 0x08,
|
|
||||||
// CNAME, len=3, content=BCD
|
|
||||||
0x01, 0x03, 0x42, 0x43, 0x44,
|
|
||||||
// END
|
|
||||||
0x00, 0x00, 0x00,
|
|
||||||
},
|
|
||||||
Want: SourceDescription{
|
|
||||||
Chunks: []SourceDescriptionChunk{
|
|
||||||
{
|
|
||||||
Source: 0x01020304,
|
|
||||||
Items: []SourceDescriptionItem{
|
|
||||||
{
|
|
||||||
Type: SDESCNAME,
|
|
||||||
Text: "A",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Source: 0x05060708,
|
|
||||||
Items: []SourceDescriptionItem{
|
|
||||||
{
|
|
||||||
Type: SDESCNAME,
|
|
||||||
Text: "BCD",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
var sdes SourceDescription
|
|
||||||
err := sdes.Unmarshal(test.Data)
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Fatalf("Unmarshal %q: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := sdes, test.Want; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("Unmarshal %q: got %#v, want %#v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSourceDescriptionRoundTrip(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Desc SourceDescription
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Desc: SourceDescription{
|
|
||||||
Chunks: []SourceDescriptionChunk{
|
|
||||||
{
|
|
||||||
Source: 1,
|
|
||||||
Items: []SourceDescriptionItem{
|
|
||||||
{
|
|
||||||
Type: SDESCNAME,
|
|
||||||
Text: "test@example.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Source: 2,
|
|
||||||
Items: []SourceDescriptionItem{
|
|
||||||
{
|
|
||||||
Type: SDESNote,
|
|
||||||
Text: "some note",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: SDESNote,
|
|
||||||
Text: "another note",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "item without type",
|
|
||||||
Desc: SourceDescription{
|
|
||||||
Chunks: []SourceDescriptionChunk{{
|
|
||||||
Source: 1,
|
|
||||||
Items: []SourceDescriptionItem{{
|
|
||||||
Text: "test@example.com",
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
WantError: errSDESMissingType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "zero items",
|
|
||||||
Desc: SourceDescription{
|
|
||||||
Chunks: []SourceDescriptionChunk{{
|
|
||||||
Source: 1,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "email item",
|
|
||||||
Desc: SourceDescription{
|
|
||||||
Chunks: []SourceDescriptionChunk{{
|
|
||||||
Source: 1,
|
|
||||||
Items: []SourceDescriptionItem{{
|
|
||||||
Type: SDESEmail,
|
|
||||||
Text: "test@example.com",
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "empty text",
|
|
||||||
Desc: SourceDescription{
|
|
||||||
Chunks: []SourceDescriptionChunk{{
|
|
||||||
Source: 1,
|
|
||||||
Items: []SourceDescriptionItem{{
|
|
||||||
Type: SDESCNAME,
|
|
||||||
Text: "",
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "text too long",
|
|
||||||
Desc: SourceDescription{
|
|
||||||
Chunks: []SourceDescriptionChunk{{
|
|
||||||
Items: []SourceDescriptionItem{{
|
|
||||||
Type: SDESCNAME,
|
|
||||||
Text: tooLongText,
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
WantError: errSDESTextTooLong,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "count overflow",
|
|
||||||
Desc: SourceDescription{
|
|
||||||
Chunks: tooManyChunks,
|
|
||||||
},
|
|
||||||
WantError: errTooManyChunks,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
data, err := test.Desc.Marshal()
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Fatalf("Marshal %q: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var decoded SourceDescription
|
|
||||||
if err := decoded.Unmarshal(data); err != nil {
|
|
||||||
t.Fatalf("Unmarshal %q: %v", test.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := decoded, test.Desc; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("%q sdes round trip: got %#v, want %#v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// a slice with enough SourceDescriptionChunks to overflow an 5-bit int
|
|
||||||
var tooManyChunks []SourceDescriptionChunk
|
|
||||||
var tooLongText string
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
for i := 0; i < (1 << 5); i++ {
|
|
||||||
tooManyChunks = append(tooManyChunks, SourceDescriptionChunk{})
|
|
||||||
}
|
|
||||||
for i := 0; i < (1 << 8); i++ {
|
|
||||||
tooLongText += "x"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,134 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PacketBitmap shouldn't be used like a normal integral,
|
|
||||||
// so it's type is masked here. Access it with PacketList().
|
|
||||||
type PacketBitmap uint16
|
|
||||||
|
|
||||||
// NackPair is a wire-representation of a collection of
|
|
||||||
// Lost RTP packets
|
|
||||||
type NackPair struct {
|
|
||||||
// ID of lost packets
|
|
||||||
PacketID uint16
|
|
||||||
|
|
||||||
// Bitmask of following lost packets
|
|
||||||
LostPackets PacketBitmap
|
|
||||||
}
|
|
||||||
|
|
||||||
// The TransportLayerNack packet informs the encoder about the loss of a transport packet
|
|
||||||
// IETF RFC 4585, Section 6.2.1
|
|
||||||
// https://tools.ietf.org/html/rfc4585#section-6.2.1
|
|
||||||
type TransportLayerNack struct {
|
|
||||||
// SSRC of sender
|
|
||||||
SenderSSRC uint32
|
|
||||||
|
|
||||||
// SSRC of the media source
|
|
||||||
MediaSSRC uint32
|
|
||||||
|
|
||||||
Nacks []NackPair
|
|
||||||
}
|
|
||||||
|
|
||||||
// PacketList returns a list of Nack'd packets that's referenced by a NackPair
|
|
||||||
func (n *NackPair) PacketList() []uint16 {
|
|
||||||
out := make([]uint16, 1, 17)
|
|
||||||
out[0] = n.PacketID
|
|
||||||
b := n.LostPackets
|
|
||||||
for i := uint16(0); b != 0; i++ {
|
|
||||||
if (b & (1 << i)) != 0 {
|
|
||||||
b &^= (1 << i)
|
|
||||||
out = append(out, n.PacketID+i+1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
tlnLength = 2
|
|
||||||
nackOffset = 8
|
|
||||||
)
|
|
||||||
|
|
||||||
// Marshal encodes the TransportLayerNack in binary
|
|
||||||
func (p TransportLayerNack) Marshal() ([]byte, error) {
|
|
||||||
|
|
||||||
if len(p.Nacks)+tlnLength > math.MaxUint8 {
|
|
||||||
return nil, errTooManyReports
|
|
||||||
}
|
|
||||||
|
|
||||||
rawPacket := make([]byte, nackOffset+(len(p.Nacks)*4))
|
|
||||||
binary.BigEndian.PutUint32(rawPacket, p.SenderSSRC)
|
|
||||||
binary.BigEndian.PutUint32(rawPacket[4:], p.MediaSSRC)
|
|
||||||
for i := 0; i < len(p.Nacks); i++ {
|
|
||||||
binary.BigEndian.PutUint16(rawPacket[nackOffset+(4*i):], p.Nacks[i].PacketID)
|
|
||||||
binary.BigEndian.PutUint16(rawPacket[nackOffset+(4*i)+2:], uint16(p.Nacks[i].LostPackets))
|
|
||||||
}
|
|
||||||
h := p.Header()
|
|
||||||
hData, err := h.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return append(hData, rawPacket...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal decodes the TransportLayerNack from binary
|
|
||||||
func (p *TransportLayerNack) Unmarshal(rawPacket []byte) error {
|
|
||||||
if len(rawPacket) < (headerLength + ssrcLength) {
|
|
||||||
return errPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
var h Header
|
|
||||||
if err := h.Unmarshal(rawPacket); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(rawPacket) < (headerLength + int(4*h.Length)) {
|
|
||||||
return errPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.Type != TypeTransportSpecificFeedback || h.Count != FormatTLN {
|
|
||||||
return errWrongType
|
|
||||||
}
|
|
||||||
|
|
||||||
p.SenderSSRC = binary.BigEndian.Uint32(rawPacket[headerLength:])
|
|
||||||
p.MediaSSRC = binary.BigEndian.Uint32(rawPacket[headerLength+ssrcLength:])
|
|
||||||
for i := headerLength + nackOffset; i < (headerLength + int(h.Length*4)); i += 4 {
|
|
||||||
p.Nacks = append(p.Nacks, NackPair{
|
|
||||||
binary.BigEndian.Uint16(rawPacket[i:]),
|
|
||||||
PacketBitmap(binary.BigEndian.Uint16(rawPacket[i+2:])),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *TransportLayerNack) len() int {
|
|
||||||
return headerLength + nackOffset + (len(p.Nacks) * 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header returns the Header associated with this packet.
|
|
||||||
func (p *TransportLayerNack) Header() Header {
|
|
||||||
return Header{
|
|
||||||
Count: FormatTLN,
|
|
||||||
Type: TypeTransportSpecificFeedback,
|
|
||||||
Length: uint16((p.len() / 4) - 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *TransportLayerNack) String() string {
|
|
||||||
o := "Packets Lost:\n"
|
|
||||||
for _, n := range p.Nacks {
|
|
||||||
for _, m := range n.PacketList() {
|
|
||||||
o += fmt.Sprintf("\t%d\n", m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
// DestinationSSRC returns an array of SSRC values that this packet refers to.
|
|
||||||
func (p *TransportLayerNack) DestinationSSRC() []uint32 {
|
|
||||||
return []uint32{p.MediaSSRC}
|
|
||||||
}
|
|
@@ -1,118 +0,0 @@
|
|||||||
package rtcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTransportLayerNackUnmarshal(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Data []byte
|
|
||||||
Want TransportLayerNack
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Data: []byte{
|
|
||||||
// TransportLayerNack
|
|
||||||
0x81, 0xcd, 0x0, 0x3,
|
|
||||||
// sender=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// media=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// nack 0xAAAA, 0x5555
|
|
||||||
0xaa, 0xaa, 0x55, 0x55,
|
|
||||||
},
|
|
||||||
Want: TransportLayerNack{
|
|
||||||
SenderSSRC: 0x902f9e2e,
|
|
||||||
MediaSSRC: 0x902f9e2e,
|
|
||||||
Nacks: []NackPair{{0xaaaa, 0x5555}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "short report",
|
|
||||||
Data: []byte{
|
|
||||||
0x81, 0xcd, 0x0, 0x2,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// report ends early
|
|
||||||
},
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "wrong type",
|
|
||||||
Data: []byte{
|
|
||||||
// v=2, p=0, count=1, SR, len=7
|
|
||||||
0x81, 0xc8, 0x0, 0x7,
|
|
||||||
// ssrc=0x902f9e2e
|
|
||||||
0x90, 0x2f, 0x9e, 0x2e,
|
|
||||||
// ssrc=0xbc5e9a40
|
|
||||||
0xbc, 0x5e, 0x9a, 0x40,
|
|
||||||
// fracLost=0, totalLost=0
|
|
||||||
0x0, 0x0, 0x0, 0x0,
|
|
||||||
// lastSeq=0x46e1
|
|
||||||
0x0, 0x0, 0x46, 0xe1,
|
|
||||||
// jitter=273
|
|
||||||
0x0, 0x0, 0x1, 0x11,
|
|
||||||
// lsr=0x9f36432
|
|
||||||
0x9, 0xf3, 0x64, 0x32,
|
|
||||||
// delay=150137
|
|
||||||
0x0, 0x2, 0x4a, 0x79,
|
|
||||||
},
|
|
||||||
WantError: errWrongType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "nil",
|
|
||||||
Data: nil,
|
|
||||||
WantError: errPacketTooShort,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
var tln TransportLayerNack
|
|
||||||
err := tln.Unmarshal(test.Data)
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Fatalf("Unmarshal %q rr: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := tln, test.Want; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("Unmarshal %q rr: got %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTransportLayerNackRoundTrip(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
Name string
|
|
||||||
Report TransportLayerNack
|
|
||||||
WantError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "valid",
|
|
||||||
Report: TransportLayerNack{
|
|
||||||
SenderSSRC: 0x902f9e2e,
|
|
||||||
MediaSSRC: 0x902f9e2e,
|
|
||||||
Nacks: []NackPair{{1, 0xAA}, {1034, 0x05}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
data, err := test.Report.Marshal()
|
|
||||||
if got, want := err, test.WantError; got != want {
|
|
||||||
t.Fatalf("Marshal %q: err = %v, want %v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var decoded TransportLayerNack
|
|
||||||
if err := decoded.Unmarshal(data); err != nil {
|
|
||||||
t.Fatalf("Unmarshal %q: %v", test.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := decoded, test.Report; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("%q tln round trip: got %#v, want %#v", test.Name, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
package codecs
|
|
||||||
|
|
||||||
func min(a, b int) int {
|
|
||||||
if a < b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
@@ -1,18 +0,0 @@
|
|||||||
package codecs
|
|
||||||
|
|
||||||
// G722Payloader payloads G722 packets
|
|
||||||
type G722Payloader struct{}
|
|
||||||
|
|
||||||
// Payload fragments an G722 packet across one or more byte arrays
|
|
||||||
func (p *G722Payloader) Payload(mtu int, payload []byte) [][]byte {
|
|
||||||
var out [][]byte
|
|
||||||
for len(payload) > mtu {
|
|
||||||
o := make([]byte, mtu)
|
|
||||||
copy(o, payload[:mtu])
|
|
||||||
payload = payload[mtu:]
|
|
||||||
out = append(out, o)
|
|
||||||
}
|
|
||||||
o := make([]byte, len(payload))
|
|
||||||
copy(o, payload)
|
|
||||||
return append(out, o)
|
|
||||||
}
|
|
@@ -1,47 +0,0 @@
|
|||||||
package codecs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"math"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestG722Payloader(t *testing.T) {
|
|
||||||
p := G722Payloader{}
|
|
||||||
|
|
||||||
const (
|
|
||||||
testlen = 10000
|
|
||||||
testmtu = 1500
|
|
||||||
)
|
|
||||||
|
|
||||||
//generate random 8-bit g722 samples
|
|
||||||
samples := make([]byte, testlen)
|
|
||||||
_, err := rand.Read(samples)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("RNG Error: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
//make a copy, for payloader input
|
|
||||||
samplesIn := make([]byte, testlen)
|
|
||||||
copy(samplesIn, samples)
|
|
||||||
|
|
||||||
//split our samples into payloads
|
|
||||||
payloads := p.Payload(testmtu, samplesIn)
|
|
||||||
|
|
||||||
outcnt := int(math.Ceil(float64(testlen) / testmtu))
|
|
||||||
if len(payloads) != outcnt {
|
|
||||||
t.Fatalf("Generated %d payloads instead of %d", len(payloads), outcnt)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(samplesIn, samples) {
|
|
||||||
t.Fatal("Modified input samples")
|
|
||||||
}
|
|
||||||
|
|
||||||
samplesOut := bytes.Join(payloads, []byte{})
|
|
||||||
|
|
||||||
if !bytes.Equal(samplesIn, samplesOut) {
|
|
||||||
t.Fatal("Output samples don't match")
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,123 +0,0 @@
|
|||||||
package codecs
|
|
||||||
|
|
||||||
// H264Payloader payloads H264 packets
|
|
||||||
type H264Payloader struct{}
|
|
||||||
|
|
||||||
const (
|
|
||||||
fuaHeaderSize = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
func emitNalus(nals []byte, emit func([]byte)) {
|
|
||||||
nextInd := func(nalu []byte, start int) (indStart int, indLen int) {
|
|
||||||
zeroCount := 0
|
|
||||||
|
|
||||||
for i, b := range nalu[start:] {
|
|
||||||
if b == 0 {
|
|
||||||
zeroCount++
|
|
||||||
continue
|
|
||||||
} else if b == 1 {
|
|
||||||
if zeroCount >= 2 {
|
|
||||||
return start + i - zeroCount, zeroCount + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
zeroCount = 0
|
|
||||||
}
|
|
||||||
return -1, -1
|
|
||||||
}
|
|
||||||
|
|
||||||
nextIndStart, nextIndLen := nextInd(nals, 0)
|
|
||||||
if nextIndStart == -1 {
|
|
||||||
emit(nals)
|
|
||||||
} else {
|
|
||||||
for nextIndStart != -1 {
|
|
||||||
prevStart := nextIndStart + nextIndLen
|
|
||||||
nextIndStart, nextIndLen = nextInd(nals, prevStart)
|
|
||||||
if nextIndStart != -1 {
|
|
||||||
emit(nals[prevStart:nextIndStart])
|
|
||||||
} else {
|
|
||||||
// Emit until end of stream, no end indicator found
|
|
||||||
emit(nals[prevStart:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Payload fragments a H264 packet across one or more byte arrays
|
|
||||||
func (p *H264Payloader) Payload(mtu int, payload []byte) [][]byte {
|
|
||||||
|
|
||||||
var payloads [][]byte
|
|
||||||
|
|
||||||
emitNalus(payload, func(nalu []byte) {
|
|
||||||
naluType := nalu[0] & 0x1F
|
|
||||||
naluRefIdc := nalu[0] & 0x60
|
|
||||||
|
|
||||||
if naluType == 9 || naluType == 12 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single NALU
|
|
||||||
if len(nalu) <= mtu {
|
|
||||||
out := make([]byte, len(nalu))
|
|
||||||
copy(out, nalu)
|
|
||||||
payloads = append(payloads, out)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// FU-A
|
|
||||||
maxFragmentSize := mtu - fuaHeaderSize
|
|
||||||
|
|
||||||
// The FU payload consists of fragments of the payload of the fragmented
|
|
||||||
// NAL unit so that if the fragmentation unit payloads of consecutive
|
|
||||||
// FUs are sequentially concatenated, the payload of the fragmented NAL
|
|
||||||
// unit can be reconstructed. The NAL unit type octet of the fragmented
|
|
||||||
// NAL unit is not included as such in the fragmentation unit payload,
|
|
||||||
// but rather the information of the NAL unit type octet of the
|
|
||||||
// fragmented NAL unit is conveyed in the F and NRI fields of the FU
|
|
||||||
// indicator octet of the fragmentation unit and in the type field of
|
|
||||||
// the FU header. An FU payload MAY have any number of octets and MAY
|
|
||||||
// be empty.
|
|
||||||
|
|
||||||
naluData := nalu
|
|
||||||
// According to the RFC, the first octet is skipped due to redundant information
|
|
||||||
naluDataIndex := 1
|
|
||||||
naluDataLength := len(nalu) - naluDataIndex
|
|
||||||
naluDataRemaining := naluDataLength
|
|
||||||
|
|
||||||
for naluDataRemaining > 0 {
|
|
||||||
currentFragmentSize := min(maxFragmentSize, naluDataRemaining)
|
|
||||||
out := make([]byte, fuaHeaderSize+currentFragmentSize)
|
|
||||||
|
|
||||||
// +---------------+
|
|
||||||
// |0|1|2|3|4|5|6|7|
|
|
||||||
// +-+-+-+-+-+-+-+-+
|
|
||||||
// |F|NRI| Type |
|
|
||||||
// +---------------+
|
|
||||||
out[0] = 28
|
|
||||||
out[0] |= naluRefIdc
|
|
||||||
|
|
||||||
// +---------------+
|
|
||||||
//|0|1|2|3|4|5|6|7|
|
|
||||||
//+-+-+-+-+-+-+-+-+
|
|
||||||
//|S|E|R| Type |
|
|
||||||
//+---------------+
|
|
||||||
|
|
||||||
out[1] = naluType
|
|
||||||
if naluDataRemaining == naluDataLength {
|
|
||||||
// Set start bit
|
|
||||||
out[1] |= 1 << 7
|
|
||||||
} else if naluDataRemaining-currentFragmentSize == 0 {
|
|
||||||
// Set end bit
|
|
||||||
out[1] |= 1 << 6
|
|
||||||
}
|
|
||||||
|
|
||||||
copy(out[fuaHeaderSize:], naluData[naluDataIndex:naluDataIndex+currentFragmentSize])
|
|
||||||
payloads = append(payloads, out)
|
|
||||||
|
|
||||||
naluDataRemaining -= currentFragmentSize
|
|
||||||
naluDataIndex += currentFragmentSize
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
return payloads
|
|
||||||
}
|
|
@@ -1,24 +0,0 @@
|
|||||||
package codecs
|
|
||||||
|
|
||||||
import "github.com/pions/webrtc/pkg/rtp"
|
|
||||||
|
|
||||||
// OpusPayloader payloads Opus packets
|
|
||||||
type OpusPayloader struct{}
|
|
||||||
|
|
||||||
// Payload fragments an Opus packet across one or more byte arrays
|
|
||||||
func (p *OpusPayloader) Payload(mtu int, payload []byte) [][]byte {
|
|
||||||
out := make([]byte, len(payload))
|
|
||||||
copy(out, payload)
|
|
||||||
return [][]byte{out}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpusPacket represents the VP8 header that is stored in the payload of an RTP Packet
|
|
||||||
type OpusPacket struct {
|
|
||||||
Payload []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal parses the passed byte slice and stores the result in the OpusPacket this method is called upon
|
|
||||||
func (p *OpusPacket) Unmarshal(packet *rtp.Packet) ([]byte, error) {
|
|
||||||
p.Payload = packet.Payload
|
|
||||||
return p.Payload, nil
|
|
||||||
}
|
|
@@ -1,117 +0,0 @@
|
|||||||
package codecs
|
|
||||||
|
|
||||||
import "github.com/pions/webrtc/pkg/rtp"
|
|
||||||
|
|
||||||
// VP8Payloader payloads VP8 packets
|
|
||||||
type VP8Payloader struct{}
|
|
||||||
|
|
||||||
const (
|
|
||||||
vp8HeaderSize = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// Payload fragments a VP8 packet across one or more byte arrays
|
|
||||||
func (p *VP8Payloader) Payload(mtu int, payload []byte) [][]byte {
|
|
||||||
|
|
||||||
/*
|
|
||||||
* https://tools.ietf.org/html/rfc7741#section-4.2
|
|
||||||
*
|
|
||||||
* 0 1 2 3 4 5 6 7
|
|
||||||
* +-+-+-+-+-+-+-+-+
|
|
||||||
* |X|R|N|S|R| PID | (REQUIRED)
|
|
||||||
* +-+-+-+-+-+-+-+-+
|
|
||||||
* X: |I|L|T|K| RSV | (OPTIONAL)
|
|
||||||
* +-+-+-+-+-+-+-+-+
|
|
||||||
* I: |M| PictureID | (OPTIONAL)
|
|
||||||
* +-+-+-+-+-+-+-+-+
|
|
||||||
* L: | TL0PICIDX | (OPTIONAL)
|
|
||||||
* +-+-+-+-+-+-+-+-+
|
|
||||||
* T/K: |TID|Y| KEYIDX | (OPTIONAL)
|
|
||||||
* +-+-+-+-+-+-+-+-+
|
|
||||||
* S: Start of VP8 partition. SHOULD be set to 1 when the first payload
|
|
||||||
* octet of the RTP packet is the beginning of a new VP8 partition,
|
|
||||||
* and MUST NOT be 1 otherwise. The S bit MUST be set to 1 for the
|
|
||||||
* first packet of each encoded frame.
|
|
||||||
*/
|
|
||||||
|
|
||||||
maxFragmentSize := mtu - vp8HeaderSize
|
|
||||||
|
|
||||||
payloadData := payload
|
|
||||||
payloadDataRemaining := len(payload)
|
|
||||||
payloadDataIndex := 0
|
|
||||||
var payloads [][]byte
|
|
||||||
for payloadDataRemaining > 0 {
|
|
||||||
currentFragmentSize := min(maxFragmentSize, payloadDataRemaining)
|
|
||||||
out := make([]byte, vp8HeaderSize+currentFragmentSize)
|
|
||||||
if payloadDataRemaining == len(payload) {
|
|
||||||
out[0] = 0x10
|
|
||||||
}
|
|
||||||
|
|
||||||
copy(out[vp8HeaderSize:], payloadData[payloadDataIndex:payloadDataIndex+currentFragmentSize])
|
|
||||||
payloads = append(payloads, out)
|
|
||||||
|
|
||||||
payloadDataRemaining -= currentFragmentSize
|
|
||||||
payloadDataIndex += currentFragmentSize
|
|
||||||
}
|
|
||||||
|
|
||||||
return payloads
|
|
||||||
}
|
|
||||||
|
|
||||||
// VP8Packet represents the VP8 header that is stored in the payload of an RTP Packet
|
|
||||||
type VP8Packet struct {
|
|
||||||
// Required Header
|
|
||||||
X uint8 /* extended controlbits present */
|
|
||||||
N uint8 /* (non-reference frame) when set to 1 this frame can be discarded */
|
|
||||||
S uint8 /* start of VP8 partition */
|
|
||||||
PID uint8 /* partition index */
|
|
||||||
|
|
||||||
// Optional Header
|
|
||||||
I uint8 /* 1 if PictureID is present */
|
|
||||||
L uint8 /* 1 if TL0PICIDX is present */
|
|
||||||
T uint8 /* 1 if TID is present */
|
|
||||||
K uint8 /* 1 if KEYIDX is present */
|
|
||||||
PictureID uint16 /* 8 or 16 bits, picture ID */
|
|
||||||
TL0PICIDX uint8 /* 8 bits temporal level zero index */
|
|
||||||
|
|
||||||
Payload []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal parses the passed byte slice and stores the result in the VP8Packet this method is called upon
|
|
||||||
func (p *VP8Packet) Unmarshal(packet *rtp.Packet) ([]byte, error) {
|
|
||||||
payload := packet.Payload
|
|
||||||
|
|
||||||
payloadIndex := 0
|
|
||||||
|
|
||||||
p.X = (payload[payloadIndex] & 0x80) >> 7
|
|
||||||
p.N = (payload[payloadIndex] & 0x20) >> 5
|
|
||||||
p.S = (payload[payloadIndex] & 0x10) >> 4
|
|
||||||
p.PID = payload[payloadIndex] & 0x07
|
|
||||||
|
|
||||||
payloadIndex++
|
|
||||||
|
|
||||||
if p.X == 1 {
|
|
||||||
p.I = (payload[payloadIndex] & 0x80) >> 7
|
|
||||||
p.L = (payload[payloadIndex] & 0x40) >> 6
|
|
||||||
p.T = (payload[payloadIndex] & 0x20) >> 5
|
|
||||||
p.K = (payload[payloadIndex] & 0x10) >> 4
|
|
||||||
payloadIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.I == 1 { // PID present?
|
|
||||||
if payload[payloadIndex]&0x80 > 0 { // M == 1, PID is 16bit
|
|
||||||
payloadIndex += 2
|
|
||||||
} else {
|
|
||||||
payloadIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.L == 1 {
|
|
||||||
payloadIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.T == 1 || p.K == 1 {
|
|
||||||
payloadIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Payload = payload[payloadIndex:]
|
|
||||||
return p.Payload, nil
|
|
||||||
}
|
|
@@ -1,6 +0,0 @@
|
|||||||
package rtp
|
|
||||||
|
|
||||||
// Depacketizer depacketizes a RTP payload, removing any RTP specific data from the payload
|
|
||||||
type Depacketizer interface {
|
|
||||||
Unmarshal(packet *Packet) ([]byte, error)
|
|
||||||
}
|
|
@@ -1,219 +0,0 @@
|
|||||||
package rtp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Header represents an RTP packet header
|
|
||||||
type Header struct {
|
|
||||||
Version uint8
|
|
||||||
Padding bool
|
|
||||||
Extension bool
|
|
||||||
Marker bool
|
|
||||||
PayloadOffset int
|
|
||||||
PayloadType uint8
|
|
||||||
SequenceNumber uint16
|
|
||||||
Timestamp uint32
|
|
||||||
SSRC uint32
|
|
||||||
CSRC []uint32
|
|
||||||
ExtensionProfile uint16
|
|
||||||
ExtensionPayload []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// Packet represents an RTP Packet
|
|
||||||
type Packet struct {
|
|
||||||
Header
|
|
||||||
Raw []byte
|
|
||||||
Payload []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
headerLength = 4
|
|
||||||
versionShift = 6
|
|
||||||
versionMask = 0x3
|
|
||||||
paddingShift = 5
|
|
||||||
paddingMask = 0x1
|
|
||||||
extensionShift = 4
|
|
||||||
extensionMask = 0x1
|
|
||||||
ccMask = 0xF
|
|
||||||
markerShift = 7
|
|
||||||
markerMask = 0x1
|
|
||||||
ptMask = 0x7F
|
|
||||||
seqNumOffset = 2
|
|
||||||
seqNumLength = 2
|
|
||||||
timestampOffset = 4
|
|
||||||
timestampLength = 4
|
|
||||||
ssrcOffset = 8
|
|
||||||
ssrcLength = 4
|
|
||||||
csrcOffset = 12
|
|
||||||
csrcLength = 4
|
|
||||||
)
|
|
||||||
|
|
||||||
// String helps with debugging by printing packet information in a readable way
|
|
||||||
func (p Packet) String() string {
|
|
||||||
out := "RTP PACKET:\n"
|
|
||||||
|
|
||||||
out += fmt.Sprintf("\tVersion: %v\n", p.Version)
|
|
||||||
out += fmt.Sprintf("\tMarker: %v\n", p.Marker)
|
|
||||||
out += fmt.Sprintf("\tPayload Type: %d\n", p.PayloadType)
|
|
||||||
out += fmt.Sprintf("\tSequence Number: %d\n", p.SequenceNumber)
|
|
||||||
out += fmt.Sprintf("\tTimestamp: %d\n", p.Timestamp)
|
|
||||||
out += fmt.Sprintf("\tSSRC: %d (%x)\n", p.SSRC, p.SSRC)
|
|
||||||
out += fmt.Sprintf("\tPayload Length: %d\n", len(p.Payload))
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal parses the passed byte slice and stores the result in the Header this method is called upon
|
|
||||||
func (h *Header) Unmarshal(rawPacket []byte) error {
|
|
||||||
if len(rawPacket) < headerLength {
|
|
||||||
return errors.Errorf("RTP header size insufficient; %d < %d", len(rawPacket), headerLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 0 1 2 3
|
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* |V=2|P|X| CC |M| PT | sequence number |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | timestamp |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | synchronization source (SSRC) identifier |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* | contributing source (CSRC) identifiers |
|
|
||||||
* | .... |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
*/
|
|
||||||
|
|
||||||
h.Version = rawPacket[0] >> versionShift & versionMask
|
|
||||||
h.Padding = (rawPacket[0] >> paddingShift & paddingMask) > 0
|
|
||||||
h.Extension = (rawPacket[0] >> extensionShift & extensionMask) > 0
|
|
||||||
h.CSRC = make([]uint32, rawPacket[0]&ccMask)
|
|
||||||
|
|
||||||
h.Marker = (rawPacket[1] >> markerShift & markerMask) > 0
|
|
||||||
h.PayloadType = rawPacket[1] & ptMask
|
|
||||||
|
|
||||||
h.SequenceNumber = binary.BigEndian.Uint16(rawPacket[seqNumOffset : seqNumOffset+seqNumLength])
|
|
||||||
h.Timestamp = binary.BigEndian.Uint32(rawPacket[timestampOffset : timestampOffset+timestampLength])
|
|
||||||
h.SSRC = binary.BigEndian.Uint32(rawPacket[ssrcOffset : ssrcOffset+ssrcLength])
|
|
||||||
|
|
||||||
currOffset := csrcOffset + (len(h.CSRC) * csrcLength)
|
|
||||||
if len(rawPacket) < currOffset {
|
|
||||||
return errors.Errorf("RTP header size insufficient; %d < %d", len(rawPacket), currOffset)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range h.CSRC {
|
|
||||||
offset := csrcOffset + (i * csrcLength)
|
|
||||||
h.CSRC[i] = binary.BigEndian.Uint32(rawPacket[offset:])
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.Extension {
|
|
||||||
if len(rawPacket) < currOffset+4 {
|
|
||||||
return errors.Errorf("RTP header size insufficient for extension; %d < %d", len(rawPacket), currOffset)
|
|
||||||
}
|
|
||||||
|
|
||||||
h.ExtensionProfile = binary.BigEndian.Uint16(rawPacket[currOffset:])
|
|
||||||
currOffset += 2
|
|
||||||
extensionLength := int(binary.BigEndian.Uint16(rawPacket[currOffset:])) * 4
|
|
||||||
currOffset += 2
|
|
||||||
|
|
||||||
if len(rawPacket) < currOffset+extensionLength {
|
|
||||||
return errors.Errorf("RTP header size insufficient for extension length; %d < %d", len(rawPacket), currOffset+extensionLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
h.ExtensionPayload = rawPacket[currOffset : currOffset+extensionLength]
|
|
||||||
currOffset += len(h.ExtensionPayload)
|
|
||||||
}
|
|
||||||
h.PayloadOffset = currOffset
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal parses the passed byte slice and stores the result in the Packet this method is called upon
|
|
||||||
func (p *Packet) Unmarshal(rawPacket []byte) error {
|
|
||||||
if err := p.Header.Unmarshal(rawPacket); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Payload = rawPacket[p.PayloadOffset:]
|
|
||||||
p.Raw = rawPacket
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal returns a raw RTP header for the instance it is called upon
|
|
||||||
func (h *Header) Marshal() ([]byte, error) {
|
|
||||||
/*
|
|
||||||
* 0 1 2 3
|
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* |V=2|P|X| CC |M| PT | sequence number |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | timestamp |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
* | synchronization source (SSRC) identifier |
|
|
||||||
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
|
||||||
* | contributing source (CSRC) identifiers |
|
|
||||||
* | .... |
|
|
||||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
*/
|
|
||||||
|
|
||||||
rawHeaderLength := 12 + (len(h.CSRC) * csrcLength)
|
|
||||||
if h.Extension {
|
|
||||||
rawHeaderLength += 4 + len(h.ExtensionPayload)
|
|
||||||
}
|
|
||||||
rawHeader := make([]byte, rawHeaderLength)
|
|
||||||
|
|
||||||
rawHeader[0] |= h.Version << versionShift
|
|
||||||
if h.Padding {
|
|
||||||
rawHeader[0] |= 1 << paddingShift
|
|
||||||
}
|
|
||||||
if h.Extension {
|
|
||||||
rawHeader[0] |= 1 << extensionShift
|
|
||||||
}
|
|
||||||
rawHeader[0] |= uint8(len(h.CSRC))
|
|
||||||
|
|
||||||
if h.Marker {
|
|
||||||
rawHeader[1] |= 1 << markerShift
|
|
||||||
}
|
|
||||||
rawHeader[1] |= h.PayloadType
|
|
||||||
|
|
||||||
binary.BigEndian.PutUint16(rawHeader[seqNumOffset:], h.SequenceNumber)
|
|
||||||
binary.BigEndian.PutUint32(rawHeader[timestampOffset:], h.Timestamp)
|
|
||||||
binary.BigEndian.PutUint32(rawHeader[ssrcOffset:], h.SSRC)
|
|
||||||
|
|
||||||
for i, csrc := range h.CSRC {
|
|
||||||
binary.BigEndian.PutUint32(rawHeader[csrcOffset+(i*csrcLength):], csrc)
|
|
||||||
}
|
|
||||||
|
|
||||||
currOffset := csrcOffset + (len(h.CSRC) * csrcLength)
|
|
||||||
|
|
||||||
for i := range h.CSRC {
|
|
||||||
offset := csrcOffset + (i * csrcLength)
|
|
||||||
h.CSRC[i] = binary.BigEndian.Uint32(rawHeader[offset:])
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.Extension {
|
|
||||||
binary.BigEndian.PutUint16(rawHeader[currOffset:], h.ExtensionProfile)
|
|
||||||
currOffset += 2
|
|
||||||
binary.BigEndian.PutUint16(rawHeader[currOffset:], uint16(len(h.ExtensionPayload))/4)
|
|
||||||
currOffset += 2
|
|
||||||
copy(rawHeader[currOffset:], h.ExtensionPayload)
|
|
||||||
}
|
|
||||||
|
|
||||||
h.PayloadOffset = csrcOffset + (len(h.CSRC) * csrcLength)
|
|
||||||
return rawHeader, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal returns a raw RTP packet for the instance it is called upon
|
|
||||||
func (p *Packet) Marshal() ([]byte, error) {
|
|
||||||
rawPacket, err := p.Header.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rawPacket = append(rawPacket, p.Payload...)
|
|
||||||
p.Raw = rawPacket
|
|
||||||
return rawPacket, nil
|
|
||||||
}
|
|
@@ -1,70 +0,0 @@
|
|||||||
package rtp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBasic(t *testing.T) {
|
|
||||||
p := &Packet{}
|
|
||||||
|
|
||||||
if err := p.Unmarshal([]byte{}); err == nil {
|
|
||||||
t.Fatal("Unmarshal did not error on zero length packet")
|
|
||||||
}
|
|
||||||
|
|
||||||
rawPkt := []byte{
|
|
||||||
0x90, 0x60, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64,
|
|
||||||
0x27, 0x82, 0x00, 0x01, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, 0x36, 0xbe, 0x88, 0x9e,
|
|
||||||
}
|
|
||||||
parsedPacket := &Packet{
|
|
||||||
Header: Header{
|
|
||||||
Extension: true,
|
|
||||||
ExtensionProfile: 1,
|
|
||||||
ExtensionPayload: []byte{0xFF, 0xFF, 0xFF, 0xFF},
|
|
||||||
Version: 2,
|
|
||||||
PayloadOffset: 20,
|
|
||||||
PayloadType: 96,
|
|
||||||
SequenceNumber: 27023,
|
|
||||||
Timestamp: 3653407706,
|
|
||||||
SSRC: 476325762,
|
|
||||||
CSRC: []uint32{},
|
|
||||||
},
|
|
||||||
|
|
||||||
Payload: rawPkt[20:],
|
|
||||||
Raw: rawPkt,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.Unmarshal(rawPkt); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
} else if !reflect.DeepEqual(p, parsedPacket) {
|
|
||||||
t.Errorf("TestBasic unmarshal: got %#v, want %#v", p, parsedPacket)
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := p.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
} else if !reflect.DeepEqual(raw, rawPkt) {
|
|
||||||
t.Errorf("TestBasic marshal: got %#v, want %#v", raw, rawPkt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtension(t *testing.T) {
|
|
||||||
p := &Packet{}
|
|
||||||
|
|
||||||
missingExtensionPkt := []byte{
|
|
||||||
0x90, 0x60, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64,
|
|
||||||
0x27, 0x82,
|
|
||||||
}
|
|
||||||
if err := p.Unmarshal(missingExtensionPkt); err == nil {
|
|
||||||
t.Fatal("Unmarshal did not error on packet with missing extension data")
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidExtensionLengthPkt := []byte{
|
|
||||||
0x90, 0x60, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64,
|
|
||||||
0x27, 0x82, 0x99, 0x99, 0x99, 0x99,
|
|
||||||
}
|
|
||||||
if err := p.Unmarshal(invalidExtensionLengthPkt); err == nil {
|
|
||||||
t.Fatal("Unmarshal did not error on packet with invalid extension length")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,72 +0,0 @@
|
|||||||
package rtp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Payloader payloads a byte array for use as rtp.Packet payloads
|
|
||||||
type Payloader interface {
|
|
||||||
Payload(mtu int, payload []byte) [][]byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// Packetizer packetizes a payload
|
|
||||||
type Packetizer interface {
|
|
||||||
Packetize(payload []byte, samples uint32) []*Packet
|
|
||||||
}
|
|
||||||
|
|
||||||
type packetizer struct {
|
|
||||||
MTU int
|
|
||||||
PayloadType uint8
|
|
||||||
SSRC uint32
|
|
||||||
Payloader Payloader
|
|
||||||
Sequencer Sequencer
|
|
||||||
Timestamp uint32
|
|
||||||
ClockRate uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPacketizer returns a new instance of a Packetizer for a specific payloader
|
|
||||||
func NewPacketizer(mtu int, pt uint8, ssrc uint32, payloader Payloader, sequencer Sequencer, clockRate uint32) Packetizer {
|
|
||||||
rs := rand.NewSource(time.Now().UnixNano())
|
|
||||||
r := rand.New(rs)
|
|
||||||
|
|
||||||
return &packetizer{
|
|
||||||
MTU: mtu,
|
|
||||||
PayloadType: pt,
|
|
||||||
SSRC: ssrc,
|
|
||||||
Payloader: payloader,
|
|
||||||
Sequencer: sequencer,
|
|
||||||
Timestamp: r.Uint32(),
|
|
||||||
ClockRate: clockRate,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Packetize packetizes the payload of an RTP packet and returns one or more RTP packets
|
|
||||||
func (p *packetizer) Packetize(payload []byte, samples uint32) []*Packet {
|
|
||||||
// Guard against an empty payload
|
|
||||||
if len(payload) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
payloads := p.Payloader.Payload(p.MTU-12, payload)
|
|
||||||
packets := make([]*Packet, len(payloads))
|
|
||||||
|
|
||||||
for i, pp := range payloads {
|
|
||||||
packets[i] = &Packet{
|
|
||||||
Header: Header{
|
|
||||||
Version: 2,
|
|
||||||
Padding: false,
|
|
||||||
Extension: false,
|
|
||||||
Marker: i == len(payloads)-1,
|
|
||||||
PayloadType: p.PayloadType,
|
|
||||||
SequenceNumber: p.Sequencer.NextSequenceNumber(),
|
|
||||||
Timestamp: p.Timestamp, // Figure out how to do timestamps
|
|
||||||
SSRC: p.SSRC,
|
|
||||||
},
|
|
||||||
Payload: pp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.Timestamp += samples
|
|
||||||
|
|
||||||
return packets
|
|
||||||
}
|
|
@@ -1,62 +0,0 @@
|
|||||||
package rtp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
"math/rand"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Sequencer generates sequential sequence numbers for building RTP packets
|
|
||||||
type Sequencer interface {
|
|
||||||
NextSequenceNumber() uint16
|
|
||||||
RollOverCount() uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRandomSequencer returns a new sequencer starting from a random sequence
|
|
||||||
// number
|
|
||||||
func NewRandomSequencer() Sequencer {
|
|
||||||
rs := rand.NewSource(time.Now().UnixNano())
|
|
||||||
r := rand.New(rs)
|
|
||||||
|
|
||||||
return &sequencer{
|
|
||||||
sequenceNumber: uint16(r.Uint32() % math.MaxUint16),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFixedSequencer returns a new sequencer starting from a specific
|
|
||||||
// sequence number
|
|
||||||
func NewFixedSequencer(s uint16) Sequencer {
|
|
||||||
return &sequencer{
|
|
||||||
sequenceNumber: s - 1, // -1 because the first sequence number prepends 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type sequencer struct {
|
|
||||||
sequenceNumber uint16
|
|
||||||
rollOverCount uint64
|
|
||||||
mutex sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NextSequenceNumber increment and returns a new sequence number for
|
|
||||||
// building RTP packets
|
|
||||||
func (s *sequencer) NextSequenceNumber() uint16 {
|
|
||||||
s.mutex.Lock()
|
|
||||||
defer s.mutex.Unlock()
|
|
||||||
|
|
||||||
s.sequenceNumber++
|
|
||||||
if s.sequenceNumber == 0 {
|
|
||||||
s.rollOverCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.sequenceNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
// RollOverCount returns the amount of times the 16bit sequence number
|
|
||||||
// has wrapped
|
|
||||||
func (s *sequencer) RollOverCount() uint64 {
|
|
||||||
s.mutex.Lock()
|
|
||||||
defer s.mutex.Unlock()
|
|
||||||
|
|
||||||
return s.rollOverCount
|
|
||||||
}
|
|
@@ -12,12 +12,12 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pions/rtcp"
|
||||||
|
"github.com/pions/rtp"
|
||||||
"github.com/pions/sdp"
|
"github.com/pions/sdp"
|
||||||
"github.com/pions/webrtc/pkg/ice"
|
"github.com/pions/webrtc/pkg/ice"
|
||||||
"github.com/pions/webrtc/pkg/logging"
|
"github.com/pions/webrtc/pkg/logging"
|
||||||
"github.com/pions/webrtc/pkg/rtcerr"
|
"github.com/pions/webrtc/pkg/rtcerr"
|
||||||
"github.com/pions/webrtc/pkg/rtcp"
|
|
||||||
"github.com/pions/webrtc/pkg/rtp"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -5,9 +5,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pions/rtcp"
|
||||||
"github.com/pions/transport/test"
|
"github.com/pions/transport/test"
|
||||||
"github.com/pions/webrtc/pkg/media"
|
"github.com/pions/webrtc/pkg/media"
|
||||||
"github.com/pions/webrtc/pkg/rtcp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRTCPeerConnection_Media_Sample(t *testing.T) {
|
func TestRTCPeerConnection_Media_Sample(t *testing.T) {
|
||||||
|
@@ -9,9 +9,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pions/rtp"
|
||||||
"github.com/pions/webrtc/pkg/ice"
|
"github.com/pions/webrtc/pkg/ice"
|
||||||
"github.com/pions/webrtc/pkg/media"
|
"github.com/pions/webrtc/pkg/media"
|
||||||
"github.com/pions/webrtc/pkg/rtp"
|
|
||||||
|
|
||||||
"github.com/pions/webrtc/pkg/rtcerr"
|
"github.com/pions/webrtc/pkg/rtcerr"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
package webrtc
|
package webrtc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pions/webrtc/pkg/rtcp"
|
"github.com/pions/rtcp"
|
||||||
"github.com/pions/webrtc/pkg/rtp"
|
"github.com/pions/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RTCRtpReceiver allows an application to inspect the receipt of a RTCTrack
|
// RTCRtpReceiver allows an application to inspect the receipt of a RTCTrack
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
package webrtc
|
package webrtc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/pions/rtcp"
|
||||||
|
"github.com/pions/rtp"
|
||||||
"github.com/pions/webrtc/pkg/media"
|
"github.com/pions/webrtc/pkg/media"
|
||||||
"github.com/pions/webrtc/pkg/rtcp"
|
|
||||||
"github.com/pions/webrtc/pkg/rtp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const rtpOutboundMTU = 1400
|
const rtpOutboundMTU = 1400
|
||||||
|
@@ -4,9 +4,9 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
|
||||||
|
"github.com/pions/rtcp"
|
||||||
|
"github.com/pions/rtp"
|
||||||
"github.com/pions/webrtc/pkg/media"
|
"github.com/pions/webrtc/pkg/media"
|
||||||
"github.com/pions/webrtc/pkg/rtcp"
|
|
||||||
"github.com/pions/webrtc/pkg/rtp"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user