1
0
Fork 0
mirror of https://github.com/ossrs/srs.git synced 2025-03-09 15:49:59 +00:00

For regression test, add srs-bench to 3rdparty

This commit is contained in:
winlin 2021-03-04 13:23:01 +08:00
parent de87dd427d
commit 876210f6c9
1158 changed files with 256967 additions and 3 deletions

View file

@ -0,0 +1,24 @@
### JetBrains IDE ###
#####################
.idea/
### Emacs Temporary Files ###
#############################
*~
### Folders ###
###############
bin/
vendor/
node_modules/
### Files ###
#############
*.ivf
*.ogg
tags
cover.out
*.sw[poe]
*.wasm
examples/sfu-ws/cert.pem
examples/sfu-ws/key.pem

View file

@ -0,0 +1,89 @@
linters-settings:
govet:
check-shadowing: true
misspell:
locale: US
exhaustive:
default-signifies-exhaustive: true
gomodguard:
blocked:
modules:
- github.com/pkg/errors:
recommendations:
- errors
linters:
enable:
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers
- bodyclose # checks whether HTTP response body is closed successfully
- deadcode # Finds unused code
- depguard # Go linter that checks if package imports are in a list of acceptable packages
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
- dupl # Tool for code clone detection
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases
- exhaustive # check exhaustiveness of enum switch statements
- exportloopref # checks for pointers to enclosing loop variables
- gci # Gci control golang package import order and make it always deterministic.
- gochecknoglobals # Checks that no globals are present in Go code
- gochecknoinits # Checks that no init functions are present in Go code
- gocognit # Computes and checks the cognitive complexity of functions
- goconst # Finds repeated strings that could be replaced by a constant
- gocritic # The most opinionated Go source code linter
- godox # Tool for detection of FIXME, TODO and other comment keywords
- goerr113 # Golang linter to check the errors handling expressions
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification
- gofumpt # Gofumpt checks whether code was gofumpt-ed.
- goheader # Checks is file header matches to pattern
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports
- golint # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes
- gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations.
- goprintffuncname # Checks that printf-like functions are named with `f` at the end
- gosec # Inspects source code for security problems
- gosimple # Linter for Go source code that specializes in simplifying a code
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
- ineffassign # Detects when assignments to existing variables are not used
- misspell # Finds commonly misspelled English words in comments
- nakedret # Finds naked returns in functions greater than a specified function length
- noctx # noctx finds sending http request without context.Context
- scopelint # Scopelint checks for unpinned variables in go programs
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks
- structcheck # Finds unused struct fields
- stylecheck # Stylecheck is a replacement for golint
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code
- unconvert # Remove unnecessary type conversions
- unparam # Reports unused function parameters
- unused # Checks Go code for unused constants, variables, functions and types
- varcheck # Finds unused global variables and constants
- whitespace # Tool for detection of leading and trailing whitespace
disable:
- funlen # Tool for detection of long functions
- gocyclo # Computes and checks the cyclomatic complexity of functions
- godot # Check if comments end in a period
- gomnd # An analyzer to detect magic numbers.
- lll # Reports long lines
- maligned # Tool to detect Go structs that would take less memory if their fields were sorted
- nestif # Reports deeply nested if statements
- nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity
- nolintlint # Reports ill-formed or insufficient nolint directives
- prealloc # Finds slice declarations that could potentially be preallocated
- rowserrcheck # checks whether Err of rows is checked successfully
- sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed.
- testpackage # linter that makes you use a separate _test package
- wsl # Whitespace Linter - Forces you to use empty lines!
issues:
exclude-use-default: false
exclude-rules:
# Allow complex tests, better to be self contained
- path: _test\.go
linters:
- gocognit
# Allow complex main function in examples
- path: examples
text: "of func `main` is high"
linters:
- gocognit
run:
skip-dirs-use-default: false

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,59 @@
<h1 align="center">
<br>
Pion SDP
<br>
</h1>
<h4 align="center">A Go implementation of the SDP</h4>
<p align="center">
<a href="https://pion.ly"><img src="https://img.shields.io/badge/pion-sdp-gray.svg?longCache=true&colorB=brightgreen" alt="Pion SDP"></a>
<a href="https://sourcegraph.com/github.com/pion/sdp?badge"><img src="https://sourcegraph.com/github.com/pion/sdp/-/badge.svg" alt="Sourcegraph Widget"></a>
<a href="https://pion.ly/slack"><img src="https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen" alt="Slack Widget"></a>
<br>
<a href="https://travis-ci.org/pion/sdp"><img src="https://travis-ci.org/pion/sdp.svg?branch=master" alt="Build Status"></a>
<a href="https://pkg.go.dev/github.com/pion/sdp/v2"><img src="https://godoc.org/github.com/pion/sdp?status.svg" alt="GoDoc"></a>
<a href="https://codecov.io/gh/pion/sdp"><img src="https://codecov.io/gh/pion/sdp/branch/master/graph/badge.svg" alt="Coverage Status"></a>
<a href="https://goreportcard.com/report/github.com/pion/sdp"><img src="https://goreportcard.com/badge/github.com/pion/sdp" alt="Go Report Card"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
</p>
<br>
See [DESIGN.md](DESIGN.md) for an overview of features and future goals.
### Roadmap
The library is used as a part of our WebRTC implementation. Please refer to that [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones.
### Community
Pion has an active community on the [Golang Slack](https://invite.slack.golangbridge.org/). Sign up and join the **#pion** channel for discussions and support. You can also use [Pion mailing list](https://groups.google.com/forum/#!forum/pion).
We are always looking to support **your projects**. Please reach out if you have something to build!
If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly)
### Contributing
Check out the **[contributing wiki](https://github.com/pion/webrtc/wiki/Contributing)** to join the group of amazing people making this project possible:
* [John Bradley](https://github.com/kc5nra) - *Original Author*
* [Sean DuBois](https://github.com/Sean-Der) - *Original Author*
* [Michiel De Backker](https://github.com/backkem) - *Public API, Initialization*
* [Konstantin Itskov](https://github.com/trivigy) - *Fix documentation*
* [chenkaiC4](https://github.com/chenkaiC4) - *Fix GolangCI Linter*
* [Woodrow Douglass](https://github.com/wdouglass) *RTCP, RTP improvements, G.722 support, Bugfixes*
* [Michael MacDonald](https://github.com/mjmac)
* [Max Hawkins](https://github.com/maxhawkins)
* [mchlrhw](https://github.com/mchlrhw)
* [Hugo Arregui](https://github.com/hugoArregui)
* [Guilherme Souza](https://github.com/gqgs)
* [adwpc](https://github.com/adwpc) - *extmap add transport-cc*
* [Atsushi Watanabe](https://github.com/at-wat)
* [Luke S](https://github.com/encounter)
* [Jerko Steiner](https://github.com/jeremija)
* [Roman Romanenko](https://github.com/r-novel)
* [Jason Brady](https://github.com/jbrady42)
* [Kory Miller](https://github.com/jbrady42/korymiller1489)
* [ZHENK](https://github.com/scorpionknifes)
* [Tarrence van As](https://github.com/tarrencev)
* [Maxim Oransky](https://github.com/sdfsdhgjkbmnmxc)
* [Graham King](https://github.com/grahamking/)
### License
MIT License - see [LICENSE](LICENSE) for full text

View file

@ -0,0 +1,231 @@
package sdp
import (
"errors"
"fmt"
"io"
)
var errDocumentStart = errors.New("already on document start")
type syntaxError struct {
s string
i int
}
func (e syntaxError) Error() string {
if e.i < 0 {
e.i = 0
}
head, middle, tail := e.s[:e.i], e.s[e.i:e.i+1], e.s[e.i+1:]
return fmt.Sprintf("%s --> %s <-- %s", head, middle, tail)
}
type baseLexer struct {
value []byte
pos int
}
func (l baseLexer) syntaxError() error {
return syntaxError{s: string(l.value), i: l.pos - 1}
}
func (l *baseLexer) unreadByte() error {
if l.pos <= 0 {
return errDocumentStart
}
l.pos--
return nil
}
func (l *baseLexer) readByte() (byte, error) {
if l.pos >= len(l.value) {
return byte(0), io.EOF
}
ch := l.value[l.pos]
l.pos++
return ch, nil
}
func (l *baseLexer) nextLine() error {
for {
ch, err := l.readByte()
if err == io.EOF {
return nil
} else if err != nil {
return err
}
if !isNewline(ch) {
return l.unreadByte()
}
}
}
func (l *baseLexer) readWhitespace() error {
for {
ch, err := l.readByte()
if err == io.EOF {
return nil
} else if err != nil {
return err
}
if !isWhitespace(ch) {
return l.unreadByte()
}
}
}
func (l *baseLexer) readUint64Field() (i uint64, err error) {
for {
ch, err := l.readByte()
if err == io.EOF && i > 0 {
break
} else if err != nil {
return i, err
}
if isNewline(ch) {
if err := l.unreadByte(); err != nil {
return i, err
}
break
}
if isWhitespace(ch) {
if err := l.readWhitespace(); err != nil {
return i, err
}
break
}
switch ch {
case '0':
i *= 10
case '1':
i = i*10 + 1
case '2':
i = i*10 + 2
case '3':
i = i*10 + 3
case '4':
i = i*10 + 4
case '5':
i = i*10 + 5
case '6':
i = i*10 + 6
case '7':
i = i*10 + 7
case '8':
i = i*10 + 8
case '9':
i = i*10 + 9
default:
return i, l.syntaxError()
}
}
return i, nil
}
// Returns next field on this line or empty string if no more fields on line
func (l *baseLexer) readField() (string, error) {
start := l.pos
stop := start
for {
stop = l.pos
ch, err := l.readByte()
if err == io.EOF && stop > start {
break
} else if err != nil {
return "", err
}
if isNewline(ch) {
if err := l.unreadByte(); err != nil {
return "", err
}
break
}
if isWhitespace(ch) {
if err := l.readWhitespace(); err != nil {
return "", err
}
break
}
}
return string(l.value[start:stop]), nil
}
// Returns symbols until line end
func (l *baseLexer) readLine() (string, error) {
start := l.pos
trim := 1
for {
ch, err := l.readByte()
if err != nil {
return "", err
}
if ch == '\r' {
trim++
}
if ch == '\n' {
return string(l.value[start : l.pos-trim]), nil
}
}
}
func (l *baseLexer) readString(until byte) (string, error) {
start := l.pos
for {
ch, err := l.readByte()
if err != nil {
return "", err
}
if ch == until {
return string(l.value[start:l.pos]), nil
}
}
}
func (l *baseLexer) readType() (string, error) {
for {
b, err := l.readByte()
if err != nil {
return "", err
}
if isNewline(b) {
continue
}
err = l.unreadByte()
if err != nil {
return "", err
}
key, err := l.readString('=')
if err != nil {
return key, err
}
if len(key) == 2 {
return key, nil
}
return key, l.syntaxError()
}
}
func isNewline(ch byte) bool { return ch == '\n' || ch == '\r' }
func isWhitespace(ch byte) bool { return ch == ' ' || ch == '\t' }
func anyOf(element string, data ...string) bool {
for _, v := range data {
if element == v {
return true
}
}
return false
}

View file

@ -0,0 +1,20 @@
#
# DO NOT EDIT THIS FILE
#
# It is automatically copied from https://github.com/pion/.goassets repository.
#
coverage:
status:
project:
default:
# Allow decreasing 2% of total coverage to avoid noise.
threshold: 2%
patch:
default:
target: 70%
only_pulls: true
ignore:
- "examples/*"
- "examples/**/*"

View file

@ -0,0 +1,110 @@
package sdp
import (
"strconv"
"strings"
)
// Information describes the "i=" field which provides textual information
// about the session.
type Information string
func (i Information) String() string {
return string(i)
}
// ConnectionInformation defines the representation for the "c=" field
// containing connection data.
type ConnectionInformation struct {
NetworkType string
AddressType string
Address *Address
}
func (c ConnectionInformation) String() string {
parts := []string{c.NetworkType, c.AddressType}
if c.Address != nil && c.Address.String() != "" {
parts = append(parts, c.Address.String())
}
return strings.Join(parts, " ")
}
// Address desribes a structured address token from within the "c=" field.
type Address struct {
Address string
TTL *int
Range *int
}
func (c *Address) String() string {
var parts []string
parts = append(parts, c.Address)
if c.TTL != nil {
parts = append(parts, strconv.Itoa(*c.TTL))
}
if c.Range != nil {
parts = append(parts, strconv.Itoa(*c.Range))
}
return strings.Join(parts, "/")
}
// Bandwidth describes an optional field which denotes the proposed bandwidth
// to be used by the session or media.
type Bandwidth struct {
Experimental bool
Type string
Bandwidth uint64
}
func (b Bandwidth) String() string {
var output string
if b.Experimental {
output += "X-"
}
output += b.Type + ":" + strconv.FormatUint(b.Bandwidth, 10)
return output
}
// EncryptionKey describes the "k=" which conveys encryption key information.
type EncryptionKey string
func (s EncryptionKey) String() string {
return string(s)
}
// Attribute describes the "a=" field which represents the primary means for
// extending SDP.
type Attribute struct {
Key string
Value string
}
// NewPropertyAttribute constructs a new attribute
func NewPropertyAttribute(key string) Attribute {
return Attribute{
Key: key,
}
}
// NewAttribute constructs a new attribute
func NewAttribute(key, value string) Attribute {
return Attribute{
Key: key,
Value: value,
}
}
func (a Attribute) String() string {
output := a.Key
if len(a.Value) > 0 {
output += ":" + a.Value
}
return output
}
// IsICECandidate returns true if the attribute key equals "candidate".
func (a Attribute) IsICECandidate() bool {
return a.Key == "candidate"
}

View file

@ -0,0 +1,59 @@
package sdp
import "errors"
// Direction is a marker for transmission directon of an endpoint
type Direction int
const (
// DirectionSendRecv is for bidirectional communication
DirectionSendRecv Direction = iota + 1
// DirectionSendOnly is for outgoing communication
DirectionSendOnly
// DirectionRecvOnly is for incoming communication
DirectionRecvOnly
// DirectionInactive is for no communication
DirectionInactive
)
const (
directionSendRecvStr = "sendrecv"
directionSendOnlyStr = "sendonly"
directionRecvOnlyStr = "recvonly"
directionInactiveStr = "inactive"
directionUnknownStr = ""
)
var errDirectionString = errors.New("invalid direction string")
// NewDirection defines a procedure for creating a new direction from a raw
// string.
func NewDirection(raw string) (Direction, error) {
switch raw {
case directionSendRecvStr:
return DirectionSendRecv, nil
case directionSendOnlyStr:
return DirectionSendOnly, nil
case directionRecvOnlyStr:
return DirectionRecvOnly, nil
case directionInactiveStr:
return DirectionInactive, nil
default:
return Direction(unknown), errDirectionString
}
}
func (t Direction) String() string {
switch t {
case DirectionSendRecv:
return directionSendRecvStr
case DirectionSendOnly:
return directionSendOnlyStr
case DirectionRecvOnly:
return directionRecvOnlyStr
case DirectionInactive:
return directionInactiveStr
default:
return directionUnknownStr
}
}

View file

@ -0,0 +1,108 @@
package sdp
import (
"fmt"
"net/url"
"strconv"
"strings"
)
// Default ext values
const (
DefExtMapValueABSSendTime = 1
DefExtMapValueTransportCC = 2
DefExtMapValueSDESMid = 3
DefExtMapValueSDESRTPStreamID = 4
ABSSendTimeURI = "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time"
TransportCCURI = "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"
SDESMidURI = "urn:ietf:params:rtp-hdrext:sdes:mid"
SDESRTPStreamIDURI = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"
AudioLevelURI = "urn:ietf:params:rtp-hdrext:ssrc-audio-level"
)
// ExtMap represents the activation of a single RTP header extension
type ExtMap struct {
Value int
Direction Direction
URI *url.URL
ExtAttr *string
}
// Clone converts this object to an Attribute
func (e *ExtMap) Clone() Attribute {
return Attribute{Key: "extmap", Value: e.string()}
}
// Unmarshal creates an Extmap from a string
func (e *ExtMap) Unmarshal(raw string) error {
parts := strings.SplitN(raw, ":", 2)
if len(parts) != 2 {
return fmt.Errorf("%w: %v", errSyntaxError, raw)
}
fields := strings.Fields(parts[1])
if len(fields) < 2 {
return fmt.Errorf("%w: %v", errSyntaxError, raw)
}
valdir := strings.Split(fields[0], "/")
value, err := strconv.ParseInt(valdir[0], 10, 64)
if (value < 1) || (value > 246) {
return fmt.Errorf("%w: %v -- extmap key must be in the range 1-256", errSyntaxError, valdir[0])
}
if err != nil {
return fmt.Errorf("%w: %v", errSyntaxError, valdir[0])
}
var direction Direction
if len(valdir) == 2 {
direction, err = NewDirection(valdir[1])
if err != nil {
return err
}
}
uri, err := url.Parse(fields[1])
if err != nil {
return err
}
if len(fields) == 3 {
tmp := fields[2]
e.ExtAttr = &tmp
}
e.Value = int(value)
e.Direction = direction
e.URI = uri
return nil
}
// Marshal creates a string from an ExtMap
func (e *ExtMap) Marshal() string {
return e.Name() + ":" + e.string()
}
func (e *ExtMap) string() string {
output := fmt.Sprintf("%d", e.Value)
dirstring := e.Direction.String()
if dirstring != directionUnknownStr {
output += "/" + dirstring
}
if e.URI != nil {
output += " " + e.URI.String()
}
if e.ExtAttr != nil {
output += " " + *e.ExtAttr
}
return output
}
// Name returns the constant name of this object
func (e *ExtMap) Name() string {
return "extmap"
}

View file

@ -0,0 +1,31 @@
// +build gofuzz
package sdp
// Fuzz implements a randomized fuzz test of the sdp
// 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`
//
// And run the fuzzer on the corpus:
// `go-fuzz`
func Fuzz(data []byte) int {
// Check that unmarshalling any byte slice does not panic.
var sd SessionDescription
if err := sd.Unmarshal(data); err != nil {
return 0
}
// Check that we can marshal anything we unmarshalled.
_, err := sd.Marshal()
if err != nil {
panic("failed to marshal") // nolint
}
// It'd be nice to check that if we round trip Marshal then Unmarshal,
// we get the original back. Right now, though, we frequently don't,
// and we'd need to fix that first.
return 1
}

View file

@ -0,0 +1,8 @@
module github.com/pion/sdp/v3
go 1.13
require (
github.com/pion/randutil v0.1.0
github.com/stretchr/testify v1.6.1
)

View file

@ -0,0 +1,13 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -0,0 +1,205 @@
package sdp
import (
"fmt"
"net/url"
"strconv"
"time"
)
// Constants for SDP attributes used in JSEP
const (
AttrKeyCandidate = "candidate"
AttrKeyEndOfCandidates = "end-of-candidates"
AttrKeyIdentity = "identity"
AttrKeyGroup = "group"
AttrKeySSRC = "ssrc"
AttrKeySSRCGroup = "ssrc-group"
AttrKeyMsid = "msid"
AttrKeyMsidSemantic = "msid-semantic"
AttrKeyConnectionSetup = "setup"
AttrKeyMID = "mid"
AttrKeyICELite = "ice-lite"
AttrKeyRTCPMux = "rtcp-mux"
AttrKeyRTCPRsize = "rtcp-rsize"
AttrKeyInactive = "inactive"
AttrKeyRecvOnly = "recvonly"
AttrKeySendOnly = "sendonly"
AttrKeySendRecv = "sendrecv"
AttrKeyExtMap = "extmap"
)
// Constants for semantic tokens used in JSEP
const (
SemanticTokenLipSynchronization = "LS"
SemanticTokenFlowIdentification = "FID"
SemanticTokenForwardErrorCorrection = "FEC"
SemanticTokenWebRTCMediaStreams = "WMS"
)
// Constants for extmap key
const (
ExtMapValueTransportCC = 3
)
func extMapURI() map[int]string {
return map[int]string{
ExtMapValueTransportCC: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01",
}
}
// API to match draft-ietf-rtcweb-jsep
// Move to webrtc or its own package?
// NewJSEPSessionDescription creates a new SessionDescription with
// some settings that are required by the JSEP spec.
//
// Note: Since v2.4.0, session ID has been fixed to use crypto random according to
// JSEP spec, so that NewJSEPSessionDescription now returns error as a second
// return value.
func NewJSEPSessionDescription(identity bool) (*SessionDescription, error) {
sid, err := newSessionID()
if err != nil {
return nil, err
}
d := &SessionDescription{
Version: 0,
Origin: Origin{
Username: "-",
SessionID: sid,
SessionVersion: uint64(time.Now().Unix()),
NetworkType: "IN",
AddressType: "IP4",
UnicastAddress: "0.0.0.0",
},
SessionName: "-",
TimeDescriptions: []TimeDescription{
{
Timing: Timing{
StartTime: 0,
StopTime: 0,
},
RepeatTimes: nil,
},
},
Attributes: []Attribute{
// "Attribute(ice-options:trickle)", // TODO: implement trickle ICE
},
}
if identity {
d.WithPropertyAttribute(AttrKeyIdentity)
}
return d, nil
}
// WithPropertyAttribute adds a property attribute 'a=key' to the session description
func (s *SessionDescription) WithPropertyAttribute(key string) *SessionDescription {
s.Attributes = append(s.Attributes, NewPropertyAttribute(key))
return s
}
// WithValueAttribute adds a value attribute 'a=key:value' to the session description
func (s *SessionDescription) WithValueAttribute(key, value string) *SessionDescription {
s.Attributes = append(s.Attributes, NewAttribute(key, value))
return s
}
// WithFingerprint adds a fingerprint to the session description
func (s *SessionDescription) WithFingerprint(algorithm, value string) *SessionDescription {
return s.WithValueAttribute("fingerprint", algorithm+" "+value)
}
// WithMedia adds a media description to the session description
func (s *SessionDescription) WithMedia(md *MediaDescription) *SessionDescription {
s.MediaDescriptions = append(s.MediaDescriptions, md)
return s
}
// NewJSEPMediaDescription creates a new MediaName with
// some settings that are required by the JSEP spec.
func NewJSEPMediaDescription(codecType string, codecPrefs []string) *MediaDescription {
return &MediaDescription{
MediaName: MediaName{
Media: codecType,
Port: RangedPort{Value: 9},
Protos: []string{"UDP", "TLS", "RTP", "SAVPF"},
},
ConnectionInformation: &ConnectionInformation{
NetworkType: "IN",
AddressType: "IP4",
Address: &Address{
Address: "0.0.0.0",
},
},
}
}
// WithPropertyAttribute adds a property attribute 'a=key' to the media description
func (d *MediaDescription) WithPropertyAttribute(key string) *MediaDescription {
d.Attributes = append(d.Attributes, NewPropertyAttribute(key))
return d
}
// WithValueAttribute adds a value attribute 'a=key:value' to the media description
func (d *MediaDescription) WithValueAttribute(key, value string) *MediaDescription {
d.Attributes = append(d.Attributes, NewAttribute(key, value))
return d
}
// WithFingerprint adds a fingerprint to the media description
func (d *MediaDescription) WithFingerprint(algorithm, value string) *MediaDescription {
return d.WithValueAttribute("fingerprint", algorithm+" "+value)
}
// WithICECredentials adds ICE credentials to the media description
func (d *MediaDescription) WithICECredentials(username, password string) *MediaDescription {
return d.
WithValueAttribute("ice-ufrag", username).
WithValueAttribute("ice-pwd", password)
}
// WithCodec adds codec information to the media description
func (d *MediaDescription) WithCodec(payloadType uint8, name string, clockrate uint32, channels uint16, fmtp string) *MediaDescription {
d.MediaName.Formats = append(d.MediaName.Formats, strconv.Itoa(int(payloadType)))
rtpmap := fmt.Sprintf("%d %s/%d", payloadType, name, clockrate)
if channels > 0 {
rtpmap += fmt.Sprintf("/%d", channels)
}
d.WithValueAttribute("rtpmap", rtpmap)
if fmtp != "" {
d.WithValueAttribute("fmtp", fmt.Sprintf("%d %s", payloadType, fmtp))
}
return d
}
// WithMediaSource adds media source information to the media description
func (d *MediaDescription) WithMediaSource(ssrc uint32, cname, streamLabel, label string) *MediaDescription {
return d.
WithValueAttribute("ssrc", fmt.Sprintf("%d cname:%s", ssrc, cname)). // Deprecated but not phased out?
WithValueAttribute("ssrc", fmt.Sprintf("%d msid:%s %s", ssrc, streamLabel, label)).
WithValueAttribute("ssrc", fmt.Sprintf("%d mslabel:%s", ssrc, streamLabel)). // Deprecated but not phased out?
WithValueAttribute("ssrc", fmt.Sprintf("%d label:%s", ssrc, label)) // Deprecated but not phased out?
}
// WithCandidate adds an ICE candidate to the media description
// Deprecated: use WithICECandidate instead
func (d *MediaDescription) WithCandidate(value string) *MediaDescription {
return d.WithValueAttribute("candidate", value)
}
// WithExtMap adds an extmap to the media description
func (d *MediaDescription) WithExtMap(e ExtMap) *MediaDescription {
return d.WithPropertyAttribute(e.Marshal())
}
// WithTransportCCExtMap adds an extmap to the media description
func (d *MediaDescription) WithTransportCCExtMap() *MediaDescription {
uri, _ := url.Parse(extMapURI()[ExtMapValueTransportCC])
e := ExtMap{
Value: ExtMapValueTransportCC,
URI: uri,
}
return d.WithExtMap(e)
}

View file

@ -0,0 +1,136 @@
package sdp
import (
"strings"
)
// Marshal takes a SDP struct to text
// https://tools.ietf.org/html/rfc4566#section-5
// Session description
// v= (protocol version)
// o= (originator and session identifier)
// s= (session name)
// i=* (session information)
// u=* (URI of description)
// e=* (email address)
// p=* (phone number)
// c=* (connection information -- not required if included in
// all media)
// b=* (zero or more bandwidth information lines)
// One or more time descriptions ("t=" and "r=" lines; see below)
// z=* (time zone adjustments)
// k=* (encryption key)
// a=* (zero or more session attribute lines)
// Zero or more media descriptions
//
// Time description
// t= (time the session is active)
// r=* (zero or more repeat times)
//
// Media description, if present
// m= (media name and transport address)
// i=* (media title)
// c=* (connection information -- optional if included at
// session level)
// b=* (zero or more bandwidth information lines)
// k=* (encryption key)
// a=* (zero or more media attribute lines)
func (s *SessionDescription) Marshal() ([]byte, error) {
m := make(marshaller, 0, 1024)
m.addKeyValue("v=", s.Version.String())
m.addKeyValue("o=", s.Origin.String())
m.addKeyValue("s=", s.SessionName.String())
if s.SessionInformation != nil {
m.addKeyValue("i=", s.SessionInformation.String())
}
if s.URI != nil {
m.addKeyValue("u=", s.URI.String())
}
if s.EmailAddress != nil {
m.addKeyValue("e=", s.EmailAddress.String())
}
if s.PhoneNumber != nil {
m.addKeyValue("p=", s.PhoneNumber.String())
}
if s.ConnectionInformation != nil {
m.addKeyValue("c=", s.ConnectionInformation.String())
}
for _, b := range s.Bandwidth {
m.addKeyValue("b=", b.String())
}
for _, td := range s.TimeDescriptions {
m.addKeyValue("t=", td.Timing.String())
for _, r := range td.RepeatTimes {
m.addKeyValue("r=", r.String())
}
}
if len(s.TimeZones) > 0 {
var b strings.Builder
for i, z := range s.TimeZones {
if i > 0 {
b.WriteString(" ")
}
b.WriteString(z.String())
}
m.addKeyValue("z=", b.String())
}
if s.EncryptionKey != nil {
m.addKeyValue("k=", s.EncryptionKey.String())
}
for _, a := range s.Attributes {
m.addKeyValue("a=", a.String())
}
for _, md := range s.MediaDescriptions {
m.addKeyValue("m=", md.MediaName.String())
if md.MediaTitle != nil {
m.addKeyValue("i=", md.MediaTitle.String())
}
if md.ConnectionInformation != nil {
m.addKeyValue("c=", md.ConnectionInformation.String())
}
for _, b := range md.Bandwidth {
m.addKeyValue("b=", b.String())
}
if md.EncryptionKey != nil {
m.addKeyValue("k=", md.EncryptionKey.String())
}
for _, a := range md.Attributes {
m.addKeyValue("a=", a.String())
}
}
return m.bytes(), nil
}
// marshaller contains state during marshaling.
type marshaller []byte
func (m *marshaller) addKeyValue(key, value string) {
if value == "" {
return
}
*m = append(*m, key...)
*m = append(*m, value...)
*m = append(*m, "\r\n"...)
}
func (m *marshaller) bytes() []byte {
return *m
}

View file

@ -0,0 +1,80 @@
package sdp
import (
"strconv"
"strings"
)
// MediaDescription represents a media type.
// https://tools.ietf.org/html/rfc4566#section-5.14
type MediaDescription struct {
// m=<media> <port>/<number of ports> <proto> <fmt> ...
// https://tools.ietf.org/html/rfc4566#section-5.14
MediaName MediaName
// i=<session description>
// https://tools.ietf.org/html/rfc4566#section-5.4
MediaTitle *Information
// c=<nettype> <addrtype> <connection-address>
// https://tools.ietf.org/html/rfc4566#section-5.7
ConnectionInformation *ConnectionInformation
// b=<bwtype>:<bandwidth>
// https://tools.ietf.org/html/rfc4566#section-5.8
Bandwidth []Bandwidth
// k=<method>
// k=<method>:<encryption key>
// https://tools.ietf.org/html/rfc4566#section-5.12
EncryptionKey *EncryptionKey
// a=<attribute>
// a=<attribute>:<value>
// https://tools.ietf.org/html/rfc4566#section-5.13
Attributes []Attribute
}
// Attribute returns the value of an attribute and if it exists
func (d *MediaDescription) Attribute(key string) (string, bool) {
for _, a := range d.Attributes {
if a.Key == key {
return a.Value, true
}
}
return "", false
}
// RangedPort supports special format for the media field "m=" port value. If
// it may be necessary to specify multiple transport ports, the protocol allows
// to write it as: <port>/<number of ports> where number of ports is a an
// offsetting range.
type RangedPort struct {
Value int
Range *int
}
func (p *RangedPort) String() string {
output := strconv.Itoa(p.Value)
if p.Range != nil {
output += "/" + strconv.Itoa(*p.Range)
}
return output
}
// MediaName describes the "m=" field storage structure.
type MediaName struct {
Media string
Port RangedPort
Protos []string
Formats []string
}
func (m MediaName) String() string {
return strings.Join([]string{
m.Media,
m.Port.String(),
strings.Join(m.Protos, "/"),
strings.Join(m.Formats, " "),
}, " ")
}

View file

@ -0,0 +1,15 @@
{
"extends": [
"config:base"
],
"postUpdateOptions": [
"gomodTidy"
],
"commitBody": "Generated by renovateBot",
"packageRules": [
{
"packagePatterns": ["^golang.org/x/"],
"schedule": ["on the first day of the month"]
}
]
}

View file

@ -0,0 +1,2 @@
// Package sdp implements Session Description Protocol (SDP)
package sdp

View file

@ -0,0 +1,146 @@
package sdp
import (
"fmt"
"net/url"
"strconv"
)
// SessionDescription is a a well-defined format for conveying sufficient
// information to discover and participate in a multimedia session.
type SessionDescription struct {
// v=0
// https://tools.ietf.org/html/rfc4566#section-5.1
Version Version
// o=<username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address>
// https://tools.ietf.org/html/rfc4566#section-5.2
Origin Origin
// s=<session name>
// https://tools.ietf.org/html/rfc4566#section-5.3
SessionName SessionName
// i=<session description>
// https://tools.ietf.org/html/rfc4566#section-5.4
SessionInformation *Information
// u=<uri>
// https://tools.ietf.org/html/rfc4566#section-5.5
URI *url.URL
// e=<email-address>
// https://tools.ietf.org/html/rfc4566#section-5.6
EmailAddress *EmailAddress
// p=<phone-number>
// https://tools.ietf.org/html/rfc4566#section-5.6
PhoneNumber *PhoneNumber
// c=<nettype> <addrtype> <connection-address>
// https://tools.ietf.org/html/rfc4566#section-5.7
ConnectionInformation *ConnectionInformation
// b=<bwtype>:<bandwidth>
// https://tools.ietf.org/html/rfc4566#section-5.8
Bandwidth []Bandwidth
// https://tools.ietf.org/html/rfc4566#section-5.9
// https://tools.ietf.org/html/rfc4566#section-5.10
TimeDescriptions []TimeDescription
// z=<adjustment time> <offset> <adjustment time> <offset> ...
// https://tools.ietf.org/html/rfc4566#section-5.11
TimeZones []TimeZone
// k=<method>
// k=<method>:<encryption key>
// https://tools.ietf.org/html/rfc4566#section-5.12
EncryptionKey *EncryptionKey
// a=<attribute>
// a=<attribute>:<value>
// https://tools.ietf.org/html/rfc4566#section-5.13
Attributes []Attribute
// https://tools.ietf.org/html/rfc4566#section-5.14
MediaDescriptions []*MediaDescription
}
// Attribute returns the value of an attribute and if it exists
func (s *SessionDescription) Attribute(key string) (string, bool) {
for _, a := range s.Attributes {
if a.Key == key {
return a.Value, true
}
}
return "", false
}
// Version describes the value provided by the "v=" field which gives
// the version of the Session Description Protocol.
type Version int
func (v Version) String() string {
return strconv.Itoa(int(v))
}
// Origin defines the structure for the "o=" field which provides the
// originator of the session plus a session identifier and version number.
type Origin struct {
Username string
SessionID uint64
SessionVersion uint64
NetworkType string
AddressType string
UnicastAddress string
}
func (o Origin) String() string {
return fmt.Sprintf(
"%v %d %d %v %v %v",
o.Username,
o.SessionID,
o.SessionVersion,
o.NetworkType,
o.AddressType,
o.UnicastAddress,
)
}
// SessionName describes a structured representations for the "s=" field
// and is the textual session name.
type SessionName string
func (s SessionName) String() string {
return string(s)
}
// EmailAddress describes a structured representations for the "e=" line
// which specifies email contact information for the person responsible for
// the conference.
type EmailAddress string
func (e EmailAddress) String() string {
return string(e)
}
// PhoneNumber describes a structured representations for the "p=" line
// specify phone contact information for the person responsible for the
// conference.
type PhoneNumber string
func (p PhoneNumber) String() string {
return string(p)
}
// TimeZone defines the structured object for "z=" line which describes
// repeated sessions scheduling.
type TimeZone struct {
AdjustmentTime uint64
Offset int64
}
func (z TimeZone) String() string {
return strconv.FormatUint(z.AdjustmentTime, 10) + " " + strconv.FormatInt(z.Offset, 10)
}

View file

@ -0,0 +1,51 @@
package sdp
import (
"strconv"
"strings"
)
// TimeDescription describes "t=", "r=" fields of the session description
// which are used to specify the start and stop times for a session as well as
// repeat intervals and durations for the scheduled session.
type TimeDescription struct {
// t=<start-time> <stop-time>
// https://tools.ietf.org/html/rfc4566#section-5.9
Timing Timing
// r=<repeat interval> <active duration> <offsets from start-time>
// https://tools.ietf.org/html/rfc4566#section-5.10
RepeatTimes []RepeatTime
}
// Timing defines the "t=" field's structured representation for the start and
// stop times.
type Timing struct {
StartTime uint64
StopTime uint64
}
func (t Timing) String() string {
output := strconv.FormatUint(t.StartTime, 10)
output += " " + strconv.FormatUint(t.StopTime, 10)
return output
}
// RepeatTime describes the "r=" fields of the session description which
// represents the intervals and durations for repeated scheduled sessions.
type RepeatTime struct {
Interval int64
Duration int64
Offsets []int64
}
func (r RepeatTime) String() string {
fields := make([]string, 0)
fields = append(fields, strconv.FormatInt(r.Interval, 10))
fields = append(fields, strconv.FormatInt(r.Duration, 10))
for _, value := range r.Offsets {
fields = append(fields, strconv.FormatInt(value, 10))
}
return strings.Join(fields, " ")
}

View file

@ -0,0 +1,907 @@
package sdp
import (
"errors"
"fmt"
"net/url"
"strconv"
"strings"
)
var (
errSDPInvalidSyntax = errors.New("sdp: invalid syntax")
errSDPInvalidNumericValue = errors.New("sdp: invalid numeric value")
errSDPInvalidValue = errors.New("sdp: invalid value")
errSDPInvalidPortValue = errors.New("sdp: invalid port value")
)
// Unmarshal is the primary function that deserializes the session description
// message and stores it inside of a structured SessionDescription object.
//
// The States Transition Table describes the computation flow between functions
// (namely s1, s2, s3, ...) for a parsing procedure that complies with the
// specifications laid out by the rfc4566#section-5 as well as by JavaScript
// Session Establishment Protocol draft. Links:
// https://tools.ietf.org/html/rfc4566#section-5
// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-24
//
// https://tools.ietf.org/html/rfc4566#section-5
// Session description
// v= (protocol version)
// o= (originator and session identifier)
// s= (session name)
// i=* (session information)
// u=* (URI of description)
// e=* (email address)
// p=* (phone number)
// c=* (connection information -- not required if included in
// all media)
// b=* (zero or more bandwidth information lines)
// One or more time descriptions ("t=" and "r=" lines; see below)
// z=* (time zone adjustments)
// k=* (encryption key)
// a=* (zero or more session attribute lines)
// Zero or more media descriptions
//
// Time description
// t= (time the session is active)
// r=* (zero or more repeat times)
//
// Media description, if present
// m= (media name and transport address)
// i=* (media title)
// c=* (connection information -- optional if included at
// session level)
// b=* (zero or more bandwidth information lines)
// k=* (encryption key)
// a=* (zero or more media attribute lines)
//
// In order to generate the following state table and draw subsequent
// deterministic finite-state automota ("DFA") the following regex was used to
// derive the DFA:
// vosi?u?e?p?c?b*(tr*)+z?k?a*(mi?c?b*k?a*)*
// possible place and state to exit:
// ** * * * ** * * * *
// 99 1 1 1 11 1 1 1 1
// 3 1 1 26 5 5 4 4
//
// Please pay close attention to the `k`, and `a` parsing states. In the table
// below in order to distinguish between the states belonging to the media
// description as opposed to the session description, the states are marked
// with an asterisk ("a*", "k*").
// +--------+----+-------+----+-----+----+-----+---+----+----+---+---+-----+---+---+----+---+----+
// | STATES | a* | a*,k* | a | a,k | b | b,c | e | i | m | o | p | r,t | s | t | u | v | z |
// +--------+----+-------+----+-----+----+-----+---+----+----+---+---+-----+---+---+----+---+----+
// | s1 | | | | | | | | | | | | | | | | 2 | |
// | s2 | | | | | | | | | | 3 | | | | | | | |
// | s3 | | | | | | | | | | | | | 4 | | | | |
// | s4 | | | | | | 5 | 6 | 7 | | | 8 | | | 9 | 10 | | |
// | s5 | | | | | 5 | | | | | | | | | 9 | | | |
// | s6 | | | | | | 5 | | | | | 8 | | | 9 | | | |
// | s7 | | | | | | 5 | 6 | | | | 8 | | | 9 | 10 | | |
// | s8 | | | | | | 5 | | | | | | | | 9 | | | |
// | s9 | | | | 11 | | | | | 12 | | | 9 | | | | | 13 |
// | s10 | | | | | | 5 | 6 | | | | 8 | | | 9 | | | |
// | s11 | | | 11 | | | | | | 12 | | | | | | | | |
// | s12 | | 14 | | | | 15 | | 16 | 12 | | | | | | | | |
// | s13 | | | | 11 | | | | | 12 | | | | | | | | |
// | s14 | 14 | | | | | | | | 12 | | | | | | | | |
// | s15 | | 14 | | | 15 | | | | 12 | | | | | | | | |
// | s16 | | 14 | | | | 15 | | | 12 | | | | | | | | |
// +--------+----+-------+----+-----+----+-----+---+----+----+---+---+-----+---+---+----+---+----+
func (s *SessionDescription) Unmarshal(value []byte) error {
l := new(lexer)
l.desc = s
l.value = value
for state := s1; state != nil; {
var err error
state, err = state(l)
if err != nil {
return err
}
}
return nil
}
func s1(l *lexer) (stateFn, error) {
return l.handleType(func(key string) stateFn {
if key == "v=" {
return unmarshalProtocolVersion
}
return nil
})
}
func s2(l *lexer) (stateFn, error) {
return l.handleType(func(key string) stateFn {
if key == "o=" {
return unmarshalOrigin
}
return nil
})
}
func s3(l *lexer) (stateFn, error) {
return l.handleType(func(key string) stateFn {
if key == "s=" {
return unmarshalSessionName
}
return nil
})
}
func s4(l *lexer) (stateFn, error) {
return l.handleType(func(key string) stateFn {
switch key {
case "i=":
return unmarshalSessionInformation
case "u=":
return unmarshalURI
case "e=":
return unmarshalEmail
case "p=":
return unmarshalPhone
case "c=":
return unmarshalSessionConnectionInformation
case "b=":
return unmarshalSessionBandwidth
case "t=":
return unmarshalTiming
}
return nil
})
}
func s5(l *lexer) (stateFn, error) {
return l.handleType(func(key string) stateFn {
switch key {
case "b=":
return unmarshalSessionBandwidth
case "t=":
return unmarshalTiming
}
return nil
})
}
func s6(l *lexer) (stateFn, error) {
return l.handleType(func(key string) stateFn {
switch key {
case "p=":
return unmarshalPhone
case "c=":
return unmarshalSessionConnectionInformation
case "b=":
return unmarshalSessionBandwidth
case "t=":
return unmarshalTiming
}
return nil
})
}
func s7(l *lexer) (stateFn, error) {
return l.handleType(func(key string) stateFn {
switch key {
case "u=":
return unmarshalURI
case "e=":
return unmarshalEmail
case "p=":
return unmarshalPhone
case "c=":
return unmarshalSessionConnectionInformation
case "b=":
return unmarshalSessionBandwidth
case "t=":
return unmarshalTiming
}
return nil
})
}
func s8(l *lexer) (stateFn, error) {
return l.handleType(func(key string) stateFn {
switch key {
case "c=":
return unmarshalSessionConnectionInformation
case "b=":
return unmarshalSessionBandwidth
case "t=":
return unmarshalTiming
}
return nil
})
}
func s9(l *lexer) (stateFn, error) {
return l.handleType(func(key string) stateFn {
switch key {
case "z=":
return unmarshalTimeZones
case "k=":
return unmarshalSessionEncryptionKey
case "a=":
return unmarshalSessionAttribute
case "r=":
return unmarshalRepeatTimes
case "t=":
return unmarshalTiming
case "m=":
return unmarshalMediaDescription
}
return nil
})
}
func s10(l *lexer) (stateFn, error) {
return l.handleType(func(key string) stateFn {
switch key {
case "e=":
return unmarshalEmail
case "p=":
return unmarshalPhone
case "c=":
return unmarshalSessionConnectionInformation
case "b=":
return unmarshalSessionBandwidth
case "t=":
return unmarshalTiming
}
return nil
})
}
func s11(l *lexer) (stateFn, error) {
return l.handleType(func(key string) stateFn {
switch key {
case "a=":
return unmarshalSessionAttribute
case "m=":
return unmarshalMediaDescription
}
return nil
})
}
func s12(l *lexer) (stateFn, error) {
return l.handleType(func(key string) stateFn {
switch key {
case "a=":
return unmarshalMediaAttribute
case "k=":
return unmarshalMediaEncryptionKey
case "b=":
return unmarshalMediaBandwidth
case "c=":
return unmarshalMediaConnectionInformation
case "i=":
return unmarshalMediaTitle
case "m=":
return unmarshalMediaDescription
}
return nil
})
}
func s13(l *lexer) (stateFn, error) {
return l.handleType(func(key string) stateFn {
switch key {
case "a=":
return unmarshalSessionAttribute
case "k=":
return unmarshalSessionEncryptionKey
case "m=":
return unmarshalMediaDescription
}
return nil
})
}
func s14(l *lexer) (stateFn, error) {
return l.handleType(func(key string) stateFn {
switch key {
case "a=":
return unmarshalMediaAttribute
case "k=":
// Non-spec ordering
return unmarshalMediaEncryptionKey
case "b=":
// Non-spec ordering
return unmarshalMediaBandwidth
case "c=":
// Non-spec ordering
return unmarshalMediaConnectionInformation
case "i=":
// Non-spec ordering
return unmarshalMediaTitle
case "m=":
return unmarshalMediaDescription
}
return nil
})
}
func s15(l *lexer) (stateFn, error) {
return l.handleType(func(key string) stateFn {
switch key {
case "a=":
return unmarshalMediaAttribute
case "k=":
return unmarshalMediaEncryptionKey
case "b=":
return unmarshalMediaBandwidth
case "c=":
return unmarshalMediaConnectionInformation
case "i=":
// Non-spec ordering
return unmarshalMediaTitle
case "m=":
return unmarshalMediaDescription
}
return nil
})
}
func s16(l *lexer) (stateFn, error) {
return l.handleType(func(key string) stateFn {
switch key {
case "a=":
return unmarshalMediaAttribute
case "k=":
return unmarshalMediaEncryptionKey
case "c=":
return unmarshalMediaConnectionInformation
case "b=":
return unmarshalMediaBandwidth
case "i=":
// Non-spec ordering
return unmarshalMediaTitle
case "m=":
return unmarshalMediaDescription
}
return nil
})
}
func unmarshalProtocolVersion(l *lexer) (stateFn, error) {
version, err := l.readUint64Field()
if err != nil {
return nil, err
}
// As off the latest draft of the rfc this value is required to be 0.
// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-24#section-5.8.1
if version != 0 {
return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, version)
}
if err := l.nextLine(); err != nil {
return nil, err
}
return s2, nil
}
func unmarshalOrigin(l *lexer) (stateFn, error) {
var err error
l.desc.Origin.Username, err = l.readField()
if err != nil {
return nil, err
}
l.desc.Origin.SessionID, err = l.readUint64Field()
if err != nil {
return nil, err
}
l.desc.Origin.SessionVersion, err = l.readUint64Field()
if err != nil {
return nil, err
}
l.desc.Origin.NetworkType, err = l.readField()
if err != nil {
return nil, err
}
// Set according to currently registered with IANA
// https://tools.ietf.org/html/rfc4566#section-8.2.6
if !anyOf(l.desc.Origin.NetworkType, "IN") {
return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, l.desc.Origin.NetworkType)
}
l.desc.Origin.AddressType, err = l.readField()
if err != nil {
return nil, err
}
// Set according to currently registered with IANA
// https://tools.ietf.org/html/rfc4566#section-8.2.7
if !anyOf(l.desc.Origin.AddressType, "IP4", "IP6") {
return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, l.desc.Origin.AddressType)
}
l.desc.Origin.UnicastAddress, err = l.readField()
if err != nil {
return nil, err
}
if err := l.nextLine(); err != nil {
return nil, err
}
return s3, nil
}
func unmarshalSessionName(l *lexer) (stateFn, error) {
value, err := l.readLine()
if err != nil {
return nil, err
}
l.desc.SessionName = SessionName(value)
return s4, nil
}
func unmarshalSessionInformation(l *lexer) (stateFn, error) {
value, err := l.readLine()
if err != nil {
return nil, err
}
sessionInformation := Information(value)
l.desc.SessionInformation = &sessionInformation
return s7, nil
}
func unmarshalURI(l *lexer) (stateFn, error) {
value, err := l.readLine()
if err != nil {
return nil, err
}
l.desc.URI, err = url.Parse(value)
if err != nil {
return nil, err
}
return s10, nil
}
func unmarshalEmail(l *lexer) (stateFn, error) {
value, err := l.readLine()
if err != nil {
return nil, err
}
emailAddress := EmailAddress(value)
l.desc.EmailAddress = &emailAddress
return s6, nil
}
func unmarshalPhone(l *lexer) (stateFn, error) {
value, err := l.readLine()
if err != nil {
return nil, err
}
phoneNumber := PhoneNumber(value)
l.desc.PhoneNumber = &phoneNumber
return s8, nil
}
func unmarshalSessionConnectionInformation(l *lexer) (stateFn, error) {
var err error
l.desc.ConnectionInformation, err = l.unmarshalConnectionInformation()
if err != nil {
return nil, err
}
return s5, nil
}
func (l *lexer) unmarshalConnectionInformation() (*ConnectionInformation, error) {
var err error
var c ConnectionInformation
c.NetworkType, err = l.readField()
if err != nil {
return nil, err
}
// Set according to currently registered with IANA
// https://tools.ietf.org/html/rfc4566#section-8.2.6
if !anyOf(c.NetworkType, "IN") {
return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, c.NetworkType)
}
c.AddressType, err = l.readField()
if err != nil {
return nil, err
}
// Set according to currently registered with IANA
// https://tools.ietf.org/html/rfc4566#section-8.2.7
if !anyOf(c.AddressType, "IP4", "IP6") {
return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, c.AddressType)
}
address, err := l.readField()
if err != nil {
return nil, err
}
if address != "" {
c.Address = new(Address)
c.Address.Address = address
}
if err := l.nextLine(); err != nil {
return nil, err
}
return &c, nil
}
func unmarshalSessionBandwidth(l *lexer) (stateFn, error) {
value, err := l.readLine()
if err != nil {
return nil, err
}
bandwidth, err := unmarshalBandwidth(value)
if err != nil {
return nil, fmt.Errorf("%w `b=%v`", errSDPInvalidValue, value)
}
l.desc.Bandwidth = append(l.desc.Bandwidth, *bandwidth)
return s5, nil
}
func unmarshalBandwidth(value string) (*Bandwidth, error) {
parts := strings.Split(value, ":")
if len(parts) != 2 {
return nil, fmt.Errorf("%w `b=%v`", errSDPInvalidValue, parts)
}
experimental := strings.HasPrefix(parts[0], "X-")
if experimental {
parts[0] = strings.TrimPrefix(parts[0], "X-")
} else if !anyOf(parts[0], "CT", "AS") {
// Set according to currently registered with IANA
// https://tools.ietf.org/html/rfc4566#section-5.8
return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, parts[0])
}
bandwidth, err := strconv.ParseUint(parts[1], 10, 64)
if err != nil {
return nil, fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, parts[1])
}
return &Bandwidth{
Experimental: experimental,
Type: parts[0],
Bandwidth: bandwidth,
}, nil
}
func unmarshalTiming(l *lexer) (stateFn, error) {
var err error
var td TimeDescription
td.Timing.StartTime, err = l.readUint64Field()
if err != nil {
return nil, err
}
td.Timing.StopTime, err = l.readUint64Field()
if err != nil {
return nil, err
}
if err := l.nextLine(); err != nil {
return nil, err
}
l.desc.TimeDescriptions = append(l.desc.TimeDescriptions, td)
return s9, nil
}
func unmarshalRepeatTimes(l *lexer) (stateFn, error) {
var err error
var newRepeatTime RepeatTime
latestTimeDesc := &l.desc.TimeDescriptions[len(l.desc.TimeDescriptions)-1]
field, err := l.readField()
if err != nil {
return nil, err
}
newRepeatTime.Interval, err = parseTimeUnits(field)
if err != nil {
return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, field)
}
field, err = l.readField()
if err != nil {
return nil, err
}
newRepeatTime.Duration, err = parseTimeUnits(field)
if err != nil {
return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, field)
}
for {
field, err := l.readField()
if err != nil {
return nil, err
}
if field == "" {
break
}
offset, err := parseTimeUnits(field)
if err != nil {
return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, field)
}
newRepeatTime.Offsets = append(newRepeatTime.Offsets, offset)
}
if err := l.nextLine(); err != nil {
return nil, err
}
latestTimeDesc.RepeatTimes = append(latestTimeDesc.RepeatTimes, newRepeatTime)
return s9, nil
}
func unmarshalTimeZones(l *lexer) (stateFn, error) {
// These fields are transimitted in pairs
// z=<adjustment time> <offset> <adjustment time> <offset> ....
// so we are making sure that there are actually multiple of 2 total.
for {
var err error
var timeZone TimeZone
timeZone.AdjustmentTime, err = l.readUint64Field()
if err != nil {
return nil, err
}
offset, err := l.readField()
if err != nil {
return nil, err
}
if offset == "" {
break
}
timeZone.Offset, err = parseTimeUnits(offset)
if err != nil {
return nil, err
}
l.desc.TimeZones = append(l.desc.TimeZones, timeZone)
}
if err := l.nextLine(); err != nil {
return nil, err
}
return s13, nil
}
func unmarshalSessionEncryptionKey(l *lexer) (stateFn, error) {
value, err := l.readLine()
if err != nil {
return nil, err
}
encryptionKey := EncryptionKey(value)
l.desc.EncryptionKey = &encryptionKey
return s11, nil
}
func unmarshalSessionAttribute(l *lexer) (stateFn, error) {
value, err := l.readLine()
if err != nil {
return nil, err
}
i := strings.IndexRune(value, ':')
var a Attribute
if i > 0 {
a = NewAttribute(value[:i], value[i+1:])
} else {
a = NewPropertyAttribute(value)
}
l.desc.Attributes = append(l.desc.Attributes, a)
return s11, nil
}
func unmarshalMediaDescription(l *lexer) (stateFn, error) {
var newMediaDesc MediaDescription
// <media>
field, err := l.readField()
if err != nil {
return nil, err
}
// Set according to currently registered with IANA
// https://tools.ietf.org/html/rfc4566#section-5.14
if !anyOf(field, "audio", "video", "text", "application", "message") {
return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, field)
}
newMediaDesc.MediaName.Media = field
// <port>
field, err = l.readField()
if err != nil {
return nil, err
}
parts := strings.Split(field, "/")
newMediaDesc.MediaName.Port.Value, err = parsePort(parts[0])
if err != nil {
return nil, fmt.Errorf("%w `%v`", errSDPInvalidPortValue, parts[0])
}
if len(parts) > 1 {
var portRange int
portRange, err = strconv.Atoi(parts[1])
if err != nil {
return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, parts)
}
newMediaDesc.MediaName.Port.Range = &portRange
}
// <proto>
field, err = l.readField()
if err != nil {
return nil, err
}
// Set according to currently registered with IANA
// https://tools.ietf.org/html/rfc4566#section-5.14
for _, proto := range strings.Split(field, "/") {
if !anyOf(proto, "UDP", "RTP", "AVP", "SAVP", "SAVPF", "TLS", "DTLS", "SCTP", "AVPF") {
return nil, fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, field)
}
newMediaDesc.MediaName.Protos = append(newMediaDesc.MediaName.Protos, proto)
}
// <fmt>...
for {
field, err = l.readField()
if err != nil {
return nil, err
}
if field == "" {
break
}
newMediaDesc.MediaName.Formats = append(newMediaDesc.MediaName.Formats, field)
}
if err := l.nextLine(); err != nil {
return nil, err
}
l.desc.MediaDescriptions = append(l.desc.MediaDescriptions, &newMediaDesc)
return s12, nil
}
func unmarshalMediaTitle(l *lexer) (stateFn, error) {
value, err := l.readLine()
if err != nil {
return nil, err
}
latestMediaDesc := l.desc.MediaDescriptions[len(l.desc.MediaDescriptions)-1]
mediaTitle := Information(value)
latestMediaDesc.MediaTitle = &mediaTitle
return s16, nil
}
func unmarshalMediaConnectionInformation(l *lexer) (stateFn, error) {
var err error
latestMediaDesc := l.desc.MediaDescriptions[len(l.desc.MediaDescriptions)-1]
latestMediaDesc.ConnectionInformation, err = l.unmarshalConnectionInformation()
if err != nil {
return nil, err
}
return s15, nil
}
func unmarshalMediaBandwidth(l *lexer) (stateFn, error) {
value, err := l.readLine()
if err != nil {
return nil, err
}
latestMediaDesc := l.desc.MediaDescriptions[len(l.desc.MediaDescriptions)-1]
bandwidth, err := unmarshalBandwidth(value)
if err != nil {
return nil, fmt.Errorf("%w `b=%v`", errSDPInvalidSyntax, value)
}
latestMediaDesc.Bandwidth = append(latestMediaDesc.Bandwidth, *bandwidth)
return s15, nil
}
func unmarshalMediaEncryptionKey(l *lexer) (stateFn, error) {
value, err := l.readLine()
if err != nil {
return nil, err
}
latestMediaDesc := l.desc.MediaDescriptions[len(l.desc.MediaDescriptions)-1]
encryptionKey := EncryptionKey(value)
latestMediaDesc.EncryptionKey = &encryptionKey
return s14, nil
}
func unmarshalMediaAttribute(l *lexer) (stateFn, error) {
value, err := l.readLine()
if err != nil {
return nil, err
}
i := strings.IndexRune(value, ':')
var a Attribute
if i > 0 {
a = NewAttribute(value[:i], value[i+1:])
} else {
a = NewPropertyAttribute(value)
}
latestMediaDesc := l.desc.MediaDescriptions[len(l.desc.MediaDescriptions)-1]
latestMediaDesc.Attributes = append(latestMediaDesc.Attributes, a)
return s14, nil
}
func parseTimeUnits(value string) (num int64, err error) {
k := timeShorthand(value[len(value)-1])
if k > 0 {
num, err = strconv.ParseInt(value[:len(value)-1], 10, 64)
} else {
k = 1
num, err = strconv.ParseInt(value, 10, 64)
}
if err != nil {
return 0, fmt.Errorf("%w `%v`", errSDPInvalidValue, value)
}
return num * k, nil
}
func timeShorthand(b byte) int64 {
// Some time offsets in the protocol can be provided with a shorthand
// notation. This code ensures to convert it to NTP timestamp format.
switch b {
case 'd': // days
return 86400
case 'h': // hours
return 3600
case 'm': // minutes
return 60
case 's': // seconds (allowed for completeness)
return 1
default:
return 0
}
}
func parsePort(value string) (int, error) {
port, err := strconv.Atoi(value)
if err != nil {
return 0, fmt.Errorf("%w `%v`", errSDPInvalidPortValue, port)
}
if port < 0 || port > 65536 {
return 0, fmt.Errorf("%w -- out of range `%v`", errSDPInvalidPortValue, port)
}
return port, nil
}

View file

@ -0,0 +1,318 @@
package sdp
import (
"errors"
"fmt"
"io"
"sort"
"strconv"
"strings"
"github.com/pion/randutil"
)
const (
attributeKey = "a="
)
var (
errExtractCodecRtpmap = errors.New("could not extract codec from rtpmap")
errExtractCodecFmtp = errors.New("could not extract codec from fmtp")
errExtractCodecRtcpFb = errors.New("could not extract codec from rtcp-fb")
errPayloadTypeNotFound = errors.New("payload type not found")
errCodecNotFound = errors.New("codec not found")
errSyntaxError = errors.New("SyntaxError")
)
// ConnectionRole indicates which of the end points should initiate the connection establishment
type ConnectionRole int
const (
// ConnectionRoleActive indicates the endpoint will initiate an outgoing connection.
ConnectionRoleActive ConnectionRole = iota + 1
// ConnectionRolePassive indicates the endpoint will accept an incoming connection.
ConnectionRolePassive
// ConnectionRoleActpass indicates the endpoint is willing to accept an incoming connection or to initiate an outgoing connection.
ConnectionRoleActpass
// ConnectionRoleHoldconn indicates the endpoint does not want the connection to be established for the time being.
ConnectionRoleHoldconn
)
func (t ConnectionRole) String() string {
switch t {
case ConnectionRoleActive:
return "active"
case ConnectionRolePassive:
return "passive"
case ConnectionRoleActpass:
return "actpass"
case ConnectionRoleHoldconn:
return "holdconn"
default:
return "Unknown"
}
}
func newSessionID() (uint64, error) {
// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-26#section-5.2.1
// Session ID is recommended to be constructed by generating a 64-bit
// quantity with the highest bit set to zero and the remaining 63-bits
// being cryptographically random.
id, err := randutil.CryptoUint64()
return id & (^(uint64(1) << 63)), err
}
// Codec represents a codec
type Codec struct {
PayloadType uint8
Name string
ClockRate uint32
EncodingParameters string
Fmtp string
RTCPFeedback []string
}
const (
unknown = iota
)
func (c Codec) String() string {
return fmt.Sprintf("%d %s/%d/%s (%s) [%s]", c.PayloadType, c.Name, c.ClockRate, c.EncodingParameters, c.Fmtp, strings.Join(c.RTCPFeedback, ", "))
}
func parseRtpmap(rtpmap string) (Codec, error) {
var codec Codec
parsingFailed := errExtractCodecRtpmap
// a=rtpmap:<payload type> <encoding name>/<clock rate>[/<encoding parameters>]
split := strings.Split(rtpmap, " ")
if len(split) != 2 {
return codec, parsingFailed
}
ptSplit := strings.Split(split[0], ":")
if len(ptSplit) != 2 {
return codec, parsingFailed
}
ptInt, err := strconv.Atoi(ptSplit[1])
if err != nil {
return codec, parsingFailed
}
codec.PayloadType = uint8(ptInt)
split = strings.Split(split[1], "/")
codec.Name = split[0]
parts := len(split)
if parts > 1 {
rate, err := strconv.Atoi(split[1])
if err != nil {
return codec, parsingFailed
}
codec.ClockRate = uint32(rate)
}
if parts > 2 {
codec.EncodingParameters = split[2]
}
return codec, nil
}
func parseFmtp(fmtp string) (Codec, error) {
var codec Codec
parsingFailed := errExtractCodecFmtp
// a=fmtp:<format> <format specific parameters>
split := strings.Split(fmtp, " ")
if len(split) != 2 {
return codec, parsingFailed
}
formatParams := split[1]
split = strings.Split(split[0], ":")
if len(split) != 2 {
return codec, parsingFailed
}
ptInt, err := strconv.Atoi(split[1])
if err != nil {
return codec, parsingFailed
}
codec.PayloadType = uint8(ptInt)
codec.Fmtp = formatParams
return codec, nil
}
func parseRtcpFb(rtcpFb string) (Codec, error) {
var codec Codec
parsingFailed := errExtractCodecRtcpFb
// a=ftcp-fb:<payload type> <RTCP feedback type> [<RTCP feedback parameter>]
split := strings.SplitN(rtcpFb, " ", 2)
if len(split) != 2 {
return codec, parsingFailed
}
ptSplit := strings.Split(split[0], ":")
if len(ptSplit) != 2 {
return codec, parsingFailed
}
ptInt, err := strconv.Atoi(ptSplit[1])
if err != nil {
return codec, parsingFailed
}
codec.PayloadType = uint8(ptInt)
codec.RTCPFeedback = append(codec.RTCPFeedback, split[1])
return codec, nil
}
func mergeCodecs(codec Codec, codecs map[uint8]Codec) {
savedCodec := codecs[codec.PayloadType]
if savedCodec.PayloadType == 0 {
savedCodec.PayloadType = codec.PayloadType
}
if savedCodec.Name == "" {
savedCodec.Name = codec.Name
}
if savedCodec.ClockRate == 0 {
savedCodec.ClockRate = codec.ClockRate
}
if savedCodec.EncodingParameters == "" {
savedCodec.EncodingParameters = codec.EncodingParameters
}
if savedCodec.Fmtp == "" {
savedCodec.Fmtp = codec.Fmtp
}
savedCodec.RTCPFeedback = append(savedCodec.RTCPFeedback, codec.RTCPFeedback...)
codecs[savedCodec.PayloadType] = savedCodec
}
func (s *SessionDescription) buildCodecMap() map[uint8]Codec {
codecs := make(map[uint8]Codec)
for _, m := range s.MediaDescriptions {
for _, a := range m.Attributes {
attr := a.String()
switch {
case strings.HasPrefix(attr, "rtpmap:"):
codec, err := parseRtpmap(attr)
if err == nil {
mergeCodecs(codec, codecs)
}
case strings.HasPrefix(attr, "fmtp:"):
codec, err := parseFmtp(attr)
if err == nil {
mergeCodecs(codec, codecs)
}
case strings.HasPrefix(attr, "rtcp-fb:"):
codec, err := parseRtcpFb(attr)
if err == nil {
mergeCodecs(codec, codecs)
}
}
}
}
return codecs
}
func equivalentFmtp(want, got string) bool {
wantSplit := strings.Split(want, ";")
gotSplit := strings.Split(got, ";")
if len(wantSplit) != len(gotSplit) {
return false
}
sort.Strings(wantSplit)
sort.Strings(gotSplit)
for i, wantPart := range wantSplit {
wantPart = strings.TrimSpace(wantPart)
gotPart := strings.TrimSpace(gotSplit[i])
if gotPart != wantPart {
return false
}
}
return true
}
func codecsMatch(wanted, got Codec) bool {
if wanted.Name != "" && !strings.EqualFold(wanted.Name, got.Name) {
return false
}
if wanted.ClockRate != 0 && wanted.ClockRate != got.ClockRate {
return false
}
if wanted.EncodingParameters != "" && wanted.EncodingParameters != got.EncodingParameters {
return false
}
if wanted.Fmtp != "" && !equivalentFmtp(wanted.Fmtp, got.Fmtp) {
return false
}
return true
}
// GetCodecForPayloadType scans the SessionDescription for the given payload type and returns the codec
func (s *SessionDescription) GetCodecForPayloadType(payloadType uint8) (Codec, error) {
codecs := s.buildCodecMap()
codec, ok := codecs[payloadType]
if ok {
return codec, nil
}
return codec, errPayloadTypeNotFound
}
// GetPayloadTypeForCodec scans the SessionDescription for a codec that matches the provided codec
// as closely as possible and returns its payload type
func (s *SessionDescription) GetPayloadTypeForCodec(wanted Codec) (uint8, error) {
codecs := s.buildCodecMap()
for payloadType, codec := range codecs {
if codecsMatch(wanted, codec) {
return payloadType, nil
}
}
return 0, errCodecNotFound
}
type stateFn func(*lexer) (stateFn, error)
type lexer struct {
desc *SessionDescription
baseLexer
}
type keyToState func(key string) stateFn
func (l *lexer) handleType(fn keyToState) (stateFn, error) {
key, err := l.readType()
if err == io.EOF && key == "" {
return nil, nil
} else if err != nil {
return nil, err
}
if res := fn(key); res != nil {
return res, nil
}
return nil, l.syntaxError()
}