mirror of
https://github.com/aler9/gortsplib
synced 2025-09-27 03:25:52 +08:00
754 lines
17 KiB
Go
754 lines
17 KiB
Go
// Package sdp contains a SDP encoder/decoder compatible with most RTSP implementations.
|
|
package sdp
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
psdp "github.com/pion/sdp/v3"
|
|
)
|
|
|
|
// SessionDescription is a SDP session description.
|
|
type SessionDescription psdp.SessionDescription
|
|
|
|
// Attribute returns the value of an attribute and if it exists
|
|
func (s *SessionDescription) Attribute(key string) (string, bool) {
|
|
return (*psdp.SessionDescription)(s).Attribute(key)
|
|
}
|
|
|
|
// Marshal encodes a SessionDescription.
|
|
func (s *SessionDescription) Marshal() ([]byte, error) {
|
|
return (*psdp.SessionDescription)(s).Marshal()
|
|
}
|
|
|
|
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")
|
|
)
|
|
|
|
func indexOf(element string, data []string) int {
|
|
for k, v := range data {
|
|
if element == v {
|
|
return k
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func anyOf(element string, data ...string) bool {
|
|
for _, v := range data {
|
|
if element == v {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalProtocolVersion(value string) error {
|
|
if value != "0" {
|
|
return fmt.Errorf("invalid version")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalSessionName(value string) error {
|
|
s.SessionName = psdp.SessionName(value)
|
|
return nil
|
|
}
|
|
|
|
func stringsReverseIndexByte(s string, b byte) int {
|
|
for i := len(s) - 2; i >= 0; i-- {
|
|
if s[i] == b {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// This is rewritten from scratch to make it compatible with most RTSP
|
|
// implementations.
|
|
func (s *SessionDescription) unmarshalOrigin(value string) error {
|
|
value = strings.Replace(value, " IN IPV4 ", " IN IP4 ", 1)
|
|
|
|
if strings.HasSuffix(value, " IN") {
|
|
value += " IP4"
|
|
}
|
|
|
|
if strings.HasSuffix(value, "IN IP4") {
|
|
value += " "
|
|
}
|
|
|
|
i := strings.Index(value, " IN IP4 ")
|
|
if i < 0 {
|
|
i = strings.Index(value, " IN IP6 ")
|
|
if i < 0 {
|
|
return fmt.Errorf("%w `o=%v`", errSDPInvalidSyntax, value)
|
|
}
|
|
}
|
|
|
|
s.Origin.NetworkType = value[i+1 : i+3]
|
|
s.Origin.AddressType = value[i+4 : i+7]
|
|
s.Origin.UnicastAddress = strings.TrimSpace(value[i+8:])
|
|
value = value[:i]
|
|
|
|
i = stringsReverseIndexByte(value, ' ')
|
|
if i < 0 {
|
|
return fmt.Errorf("%w `o=%v`", errSDPInvalidSyntax, value)
|
|
}
|
|
|
|
var tmp string
|
|
tmp, value = value[i+1:], value[:i]
|
|
|
|
if i = strings.Index(tmp, "."); i >= 0 {
|
|
tmp = tmp[:i]
|
|
}
|
|
tmp = strings.TrimPrefix(tmp, "-")
|
|
|
|
var err error
|
|
s.Origin.SessionVersion, err = strconv.ParseUint(tmp, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, tmp)
|
|
}
|
|
|
|
if value == "-0" { // live reporter app
|
|
value = "- 0"
|
|
}
|
|
|
|
i = stringsReverseIndexByte(value, ' ')
|
|
if i < 0 {
|
|
return nil
|
|
}
|
|
|
|
tmp, value = value[i+1:], value[:i]
|
|
|
|
switch {
|
|
case strings.HasPrefix(tmp, "0x"), strings.HasPrefix(tmp, "0X"):
|
|
s.Origin.SessionID, err = strconv.ParseUint(tmp[2:], 16, 64)
|
|
case strings.ContainsAny(tmp, "abcdefABCDEF"):
|
|
s.Origin.SessionID, err = strconv.ParseUint(tmp, 16, 64)
|
|
default:
|
|
if i = strings.Index(tmp, "."); i >= 0 {
|
|
tmp = tmp[:i]
|
|
}
|
|
tmp = strings.TrimPrefix(tmp, "-")
|
|
|
|
s.Origin.SessionID, err = strconv.ParseUint(tmp, 10, 64)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, tmp)
|
|
}
|
|
|
|
s.Origin.Username = value
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalSessionInformation(value string) error {
|
|
sessionInformation := psdp.Information(value)
|
|
s.SessionInformation = &sessionInformation
|
|
return nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalURI(value string) error {
|
|
var err error
|
|
s.URI, err = url.Parse(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalEmail(value string) error {
|
|
emailAddress := psdp.EmailAddress(value)
|
|
s.EmailAddress = &emailAddress
|
|
return nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalPhone(value string) error {
|
|
phoneNumber := psdp.PhoneNumber(value)
|
|
s.PhoneNumber = &phoneNumber
|
|
return nil
|
|
}
|
|
|
|
func unmarshalConnectionInformation(value string) (*psdp.ConnectionInformation, error) {
|
|
if value == "IN" {
|
|
return nil, nil
|
|
}
|
|
|
|
value = strings.Replace(value, "IN IPV4 ", "IN IP4 ", 1)
|
|
|
|
if strings.HasPrefix(value, "IN c=IN") {
|
|
value = value[len("IN c="):]
|
|
}
|
|
|
|
fields := strings.Fields(value)
|
|
if len(fields) < 2 {
|
|
return nil, fmt.Errorf("%w `c=%v`", errSDPInvalidSyntax, fields)
|
|
}
|
|
|
|
// Set according to currently registered with IANA
|
|
// https://tools.ietf.org/html/rfc4566#section-8.2.6
|
|
if i := indexOf(strings.ToUpper(fields[0]), []string{"IN"}); i == -1 {
|
|
return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, fields[0])
|
|
}
|
|
|
|
// Set according to currently registered with IANA
|
|
// https://tools.ietf.org/html/rfc4566#section-8.2.7
|
|
if i := indexOf(fields[1], []string{"IP4", "IP6"}); i == -1 {
|
|
return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, fields[1])
|
|
}
|
|
|
|
connAddr := new(psdp.Address)
|
|
if len(fields) > 2 {
|
|
connAddr.Address = fields[2]
|
|
}
|
|
|
|
return &psdp.ConnectionInformation{
|
|
NetworkType: strings.ToUpper(fields[0]),
|
|
AddressType: fields[1],
|
|
Address: connAddr,
|
|
}, nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalSessionConnectionInformation(value string) error {
|
|
var err error
|
|
s.ConnectionInformation, err = unmarshalConnectionInformation(value)
|
|
if err != nil {
|
|
return fmt.Errorf("%w `c=%v`", errSDPInvalidSyntax, value)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func unmarshalBandwidth(value string) (*psdp.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", "TIAS", "RS", "RR") {
|
|
// Set according to currently registered with IANA
|
|
// https://tools.ietf.org/html/rfc4566#section-5.8
|
|
// https://tools.ietf.org/html/rfc3890#section-6.2
|
|
// https://tools.ietf.org/html/rfc3556#section-2
|
|
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 &psdp.Bandwidth{
|
|
Experimental: experimental,
|
|
Type: parts[0],
|
|
Bandwidth: bandwidth,
|
|
}, nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalSessionBandwidth(value string) error {
|
|
bandwidth, err := unmarshalBandwidth(value)
|
|
if err != nil {
|
|
return fmt.Errorf("%w `b=%v`", errSDPInvalidValue, value)
|
|
}
|
|
s.Bandwidth = append(s.Bandwidth, *bandwidth)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalTimeZones(value string) 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.
|
|
fields := strings.Fields(value)
|
|
if len(fields)%2 != 0 {
|
|
return fmt.Errorf("%w `t=%v`", errSDPInvalidSyntax, fields)
|
|
}
|
|
|
|
for i := 0; i < len(fields); i += 2 {
|
|
var timeZone psdp.TimeZone
|
|
|
|
var err error
|
|
timeZone.AdjustmentTime, err = strconv.ParseUint(fields[i], 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("%w `%v`", errSDPInvalidValue, fields)
|
|
}
|
|
|
|
timeZone.Offset, err = parseTimeUnits(fields[i+1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.TimeZones = append(s.TimeZones, timeZone)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalSessionEncryptionKey(value string) error {
|
|
encryptionKey := psdp.EncryptionKey(value)
|
|
s.EncryptionKey = &encryptionKey
|
|
return nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalSessionAttribute(value string) error {
|
|
i := strings.IndexRune(value, ':')
|
|
var a psdp.Attribute
|
|
if i > 0 {
|
|
a = psdp.NewAttribute(value[:i], value[i+1:])
|
|
} else {
|
|
a = psdp.NewPropertyAttribute(value)
|
|
}
|
|
|
|
s.Attributes = append(s.Attributes, a)
|
|
return nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalTiming(value string) error {
|
|
if value == "now-" {
|
|
// special case for some FLIR cameras with invalid timing element
|
|
value = "0 0"
|
|
}
|
|
fields := strings.Fields(value)
|
|
if len(fields) < 2 {
|
|
return fmt.Errorf("%w `t=%v`", errSDPInvalidSyntax, fields)
|
|
}
|
|
|
|
td := psdp.TimeDescription{}
|
|
|
|
var err error
|
|
td.Timing.StartTime, err = strconv.ParseUint(fields[0], 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, fields[1])
|
|
}
|
|
|
|
td.Timing.StopTime, err = strconv.ParseUint(fields[1], 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, fields[1])
|
|
}
|
|
|
|
s.TimeDescriptions = append(s.TimeDescriptions, td)
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseTimeUnits(value string) (int64, error) {
|
|
// Some time offsets in the protocol can be provided with a shorthand
|
|
// notation. This code ensures to convert it to NTP timestamp format.
|
|
// d - days (86400 seconds)
|
|
// h - hours (3600 seconds)
|
|
// m - minutes (60 seconds)
|
|
// s - seconds (allowed for completeness)
|
|
switch value[len(value)-1:] {
|
|
case "d":
|
|
num, err := strconv.ParseInt(value[:len(value)-1], 10, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%w `%v`", errSDPInvalidValue, value)
|
|
}
|
|
return num * 86400, nil
|
|
case "h":
|
|
num, err := strconv.ParseInt(value[:len(value)-1], 10, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%w `%v`", errSDPInvalidValue, value)
|
|
}
|
|
return num * 3600, nil
|
|
case "m":
|
|
num, err := strconv.ParseInt(value[:len(value)-1], 10, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%w `%v`", errSDPInvalidValue, value)
|
|
}
|
|
return num * 60, nil
|
|
}
|
|
|
|
num, err := strconv.ParseInt(value, 10, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%w `%v`", errSDPInvalidValue, value)
|
|
}
|
|
|
|
return num, nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalRepeatTimes(value string) error {
|
|
fields := strings.Fields(value)
|
|
if len(fields) < 2 {
|
|
return fmt.Errorf("%w `r=%v`", errSDPInvalidSyntax, value)
|
|
}
|
|
|
|
latestTimeDesc := &s.TimeDescriptions[len(s.TimeDescriptions)-1]
|
|
|
|
newRepeatTime := psdp.RepeatTime{}
|
|
var err error
|
|
newRepeatTime.Interval, err = parseTimeUnits(fields[0])
|
|
if err != nil {
|
|
return fmt.Errorf("%w `%v`", errSDPInvalidValue, fields)
|
|
}
|
|
|
|
newRepeatTime.Duration, err = parseTimeUnits(fields[1])
|
|
if err != nil {
|
|
return fmt.Errorf("%w `%v`", errSDPInvalidValue, fields)
|
|
}
|
|
|
|
for i := 2; i < len(fields); i++ {
|
|
var offset int64
|
|
offset, err = parseTimeUnits(fields[i])
|
|
if err != nil {
|
|
return fmt.Errorf("%w `%v`", errSDPInvalidValue, fields)
|
|
}
|
|
newRepeatTime.Offsets = append(newRepeatTime.Offsets, offset)
|
|
}
|
|
latestTimeDesc.RepeatTimes = append(latestTimeDesc.RepeatTimes, newRepeatTime)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalMediaDescription(value string) error {
|
|
fields := strings.Fields(value)
|
|
if len(fields) < 4 {
|
|
return fmt.Errorf("%w `m=%v`", errSDPInvalidSyntax, fields)
|
|
}
|
|
|
|
newMediaDesc := &psdp.MediaDescription{}
|
|
|
|
// <media>
|
|
// Set according to currently registered with IANA
|
|
// https://tools.ietf.org/html/rfc4566#section-5.14
|
|
if fields[0] != "video" &&
|
|
fields[0] != "audio" &&
|
|
fields[0] != "application" &&
|
|
!strings.HasPrefix(fields[0], "application/") &&
|
|
fields[0] != "metadata" &&
|
|
fields[0] != "text" {
|
|
return fmt.Errorf("%w `%v`", errSDPInvalidValue, fields[0])
|
|
}
|
|
newMediaDesc.MediaName.Media = fields[0]
|
|
|
|
// <port>
|
|
parts := strings.Split(fields[1], "/")
|
|
var err error
|
|
newMediaDesc.MediaName.Port.Value, err = parsePort(parts[0])
|
|
if err != nil {
|
|
return fmt.Errorf("%w `%v`", errSDPInvalidPortValue, parts[0])
|
|
}
|
|
|
|
if len(parts) > 1 {
|
|
var portRange int
|
|
portRange, err = strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return fmt.Errorf("%w `%v`", errSDPInvalidValue, parts)
|
|
}
|
|
newMediaDesc.MediaName.Port.Range = &portRange
|
|
}
|
|
|
|
// <proto>
|
|
// Set according to currently registered with IANA
|
|
// https://tools.ietf.org/html/rfc4566#section-5.14
|
|
for _, proto := range strings.Split(fields[2], "/") {
|
|
if i := indexOf(proto, []string{
|
|
"UDP", "RTP", "AVP", "SAVP", "SAVPF",
|
|
"MP2T", "TLS", "DTLS", "SCTP", "AVPF", "TCP",
|
|
}); i == -1 {
|
|
return fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, fields[2])
|
|
}
|
|
newMediaDesc.MediaName.Protos = append(newMediaDesc.MediaName.Protos, proto)
|
|
}
|
|
|
|
// <fmt>...
|
|
for i := 3; i < len(fields); i++ {
|
|
newMediaDesc.MediaName.Formats = append(newMediaDesc.MediaName.Formats, fields[i])
|
|
}
|
|
|
|
s.MediaDescriptions = append(s.MediaDescriptions, newMediaDesc)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalMediaTitle(value string) error {
|
|
latestMediaDesc := s.MediaDescriptions[len(s.MediaDescriptions)-1]
|
|
mediaTitle := psdp.Information(value)
|
|
latestMediaDesc.MediaTitle = &mediaTitle
|
|
return nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalMediaConnectionInformation(value string) error {
|
|
if strings.HasPrefix(value, "SM ") {
|
|
return nil
|
|
}
|
|
|
|
latestMediaDesc := s.MediaDescriptions[len(s.MediaDescriptions)-1]
|
|
var err error
|
|
latestMediaDesc.ConnectionInformation, err = unmarshalConnectionInformation(value)
|
|
if err != nil {
|
|
return fmt.Errorf("%w `c=%v`", errSDPInvalidSyntax, value)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalMediaBandwidth(value string) error {
|
|
latestMediaDesc := s.MediaDescriptions[len(s.MediaDescriptions)-1]
|
|
bandwidth, err := unmarshalBandwidth(value)
|
|
if err != nil {
|
|
return fmt.Errorf("%w `b=%v`", errSDPInvalidSyntax, value)
|
|
}
|
|
latestMediaDesc.Bandwidth = append(latestMediaDesc.Bandwidth, *bandwidth)
|
|
return nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalMediaEncryptionKey(value string) error {
|
|
latestMediaDesc := s.MediaDescriptions[len(s.MediaDescriptions)-1]
|
|
encryptionKey := psdp.EncryptionKey(value)
|
|
latestMediaDesc.EncryptionKey = &encryptionKey
|
|
return nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalMediaAttribute(value string) error {
|
|
i := strings.IndexRune(value, ':')
|
|
var a psdp.Attribute
|
|
if i > 0 {
|
|
a = psdp.NewAttribute(value[:i], value[i+1:])
|
|
} else {
|
|
a = psdp.NewPropertyAttribute(value)
|
|
}
|
|
|
|
latestMediaDesc := s.MediaDescriptions[len(s.MediaDescriptions)-1]
|
|
latestMediaDesc.Attributes = append(latestMediaDesc.Attributes, a)
|
|
return nil
|
|
}
|
|
|
|
type unmarshalState int
|
|
|
|
const (
|
|
stateInitial unmarshalState = iota
|
|
stateSession
|
|
stateMedia
|
|
stateTimeDescription
|
|
)
|
|
|
|
func (s *SessionDescription) unmarshalSession(state *unmarshalState, key byte, val string) error {
|
|
switch key {
|
|
case 'o':
|
|
err := s.unmarshalOrigin(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case 's':
|
|
err := s.unmarshalSessionName(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case 'i':
|
|
err := s.unmarshalSessionInformation(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case 'u':
|
|
err := s.unmarshalURI(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case 'e':
|
|
err := s.unmarshalEmail(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case 'p':
|
|
err := s.unmarshalPhone(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case 'c':
|
|
err := s.unmarshalSessionConnectionInformation(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case 'b':
|
|
err := s.unmarshalSessionBandwidth(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case 'z':
|
|
err := s.unmarshalTimeZones(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case 'k':
|
|
err := s.unmarshalSessionEncryptionKey(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case 'a':
|
|
err := s.unmarshalSessionAttribute(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case 't':
|
|
err := s.unmarshalTiming(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*state = stateTimeDescription
|
|
|
|
case 'm':
|
|
err := s.unmarshalMediaDescription(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*state = stateMedia
|
|
|
|
default:
|
|
return fmt.Errorf("invalid key: %c", key)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SessionDescription) unmarshalMedia(key byte, val string) error {
|
|
switch key {
|
|
case 'm':
|
|
err := s.unmarshalMediaDescription(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case 'i':
|
|
err := s.unmarshalMediaTitle(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case 'c':
|
|
err := s.unmarshalMediaConnectionInformation(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case 'b':
|
|
err := s.unmarshalMediaBandwidth(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case 'k':
|
|
err := s.unmarshalMediaEncryptionKey(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case 'a':
|
|
err := s.unmarshalMediaAttribute(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("invalid key: %c", key)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Unmarshal decodes a SessionDescription.
|
|
// This is rewritten from scratch to guarantee compatibility with most RTSP
|
|
// implementations.
|
|
func (s *SessionDescription) Unmarshal(byts []byte) error {
|
|
str := string(byts)
|
|
|
|
state := stateInitial
|
|
|
|
for _, line := range strings.Split(strings.ReplaceAll(str, "\r", ""), "\n") {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
if len(line) < 2 || line[1] != '=' {
|
|
return fmt.Errorf("invalid line: (%s)", line)
|
|
}
|
|
|
|
key := line[0]
|
|
val := line[2:]
|
|
|
|
switch state {
|
|
case stateInitial:
|
|
switch key {
|
|
case 'v':
|
|
err := s.unmarshalProtocolVersion(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
state = stateSession
|
|
|
|
default:
|
|
state = stateSession
|
|
err := s.unmarshalSession(&state, key, val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
case stateSession:
|
|
err := s.unmarshalSession(&state, key, val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case stateMedia:
|
|
err := s.unmarshalMedia(key, val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case stateTimeDescription:
|
|
switch key {
|
|
case 'r':
|
|
err := s.unmarshalRepeatTimes(val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
default:
|
|
state = stateSession
|
|
err := s.unmarshalSession(&state, key, val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|