diff --git a/sdp/sdp.go b/sdp/sdp.go index 90c63d77..0e49704d 100644 --- a/sdp/sdp.go +++ b/sdp/sdp.go @@ -2,6 +2,12 @@ package sdp import ( + "errors" + "fmt" + "net/url" + "strconv" + "strings" + psdp "github.com/pion/sdp/v3" ) @@ -12,3 +18,620 @@ type SessionDescription psdp.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 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) unmarshalVersion(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 (s *SessionDescription) unmarshalOrigin(value string) error { + // special case for live reporter app + if value[:3] == "-0 " { + value = "- 0 " + value[3:] + } + + // special case for sone onvif2 cameras + if value[len(value)-1] == ' ' { + value += "127.0.0.1" + } + + fields := strings.Fields(value) + if len(fields) != 6 { + return fmt.Errorf("%w `o=%v`", errSDPInvalidSyntax, fields) + } + + sessionID, err := strconv.ParseUint(fields[1], 10, 64) + if err != nil { + return fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, fields[1]) + } + + sessionVersion, err := strconv.ParseUint(fields[2], 10, 64) + if err != nil { + return fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, fields[2]) + } + + // Set according to currently registered with IANA + // https://tools.ietf.org/html/rfc4566#section-8.2.6 + if i := indexOf(fields[3], []string{"IN"}); i == -1 { + return fmt.Errorf("%w `%v`", errSDPInvalidValue, fields[3]) + } + + // Set according to currently registered with IANA + // https://tools.ietf.org/html/rfc4566#section-8.2.7 + if i := indexOf(fields[4], []string{"IP4", "IP6"}); i == -1 { + return fmt.Errorf("%w `%v`", errSDPInvalidValue, fields[3]) + } + + s.Origin = psdp.Origin{ + Username: fields[0], + SessionID: sessionID, + SessionVersion: sessionVersion, + NetworkType: fields[3], + AddressType: fields[4], + UnicastAddress: fields[5], + } + + 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) { + 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(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: 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 i := indexOf(parts[0], []string{"CT", "AS", "RR"}); i == -1 { + // 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 &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= .... + // 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 { + 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) < 3 { + return fmt.Errorf("%w `r=%v`", errSDPInvalidSyntax, fields) + } + + 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++ { + 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{} + + // + // Set according to currently registered with IANA + // https://tools.ietf.org/html/rfc4566#section-5.14 + if i := indexOf(fields[0], []string{"audio", "video", "text", "application", "message"}); i == -1 { + return fmt.Errorf("%w `%v`", errSDPInvalidValue, fields[0]) + } + newMediaDesc.MediaName.Media = fields[0] + + // + 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 { + portRange, err := strconv.Atoi(parts[1]) + if err != nil { + return fmt.Errorf("%w `%v`", errSDPInvalidValue, parts) + } + newMediaDesc.MediaName.Port.Range = &portRange + } + + // + // 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) + } + + // ... + 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 { + 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 +} + +// 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) + + type stateVal int + + const ( + stateInitial stateVal = iota + stateSession + stateMedia + ) + + 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.unmarshalVersion(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + state = stateSession + + default: + return fmt.Errorf("invalid key: %c (%s)", key, line) + } + + case stateSession: + switch key { + case 'o': + err := s.unmarshalOrigin(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 's': + err := s.unmarshalSessionName(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 'i': + err := s.unmarshalSessionInformation(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 'u': + err := s.unmarshalURI(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 'e': + err := s.unmarshalEmail(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 'p': + err := s.unmarshalPhone(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 'c': + err := s.unmarshalSessionConnectionInformation(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 'b': + err := s.unmarshalSessionBandwidth(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 'z': + err := s.unmarshalTimeZones(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 'k': + err := s.unmarshalSessionEncryptionKey(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 'a': + err := s.unmarshalSessionAttribute(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 't': + err := s.unmarshalTiming(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 'r': + err := s.unmarshalRepeatTimes(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 'm': + err := s.unmarshalMediaDescription(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + state = stateMedia + + default: + return fmt.Errorf("invalid key: %c (%s)", key, line) + } + + case stateMedia: + switch key { + case 'm': + err := s.unmarshalMediaDescription(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 'i': + err := s.unmarshalMediaTitle(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 'c': + err := s.unmarshalMediaConnectionInformation(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 'b': + err := s.unmarshalMediaBandwidth(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 'k': + err := s.unmarshalMediaEncryptionKey(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + case 'a': + err := s.unmarshalMediaAttribute(val) + if err != nil { + return fmt.Errorf("%s (%s)", err, line) + } + + default: + return fmt.Errorf("invalid key: %c (%s)", key, line) + } + } + } + + return nil +} diff --git a/sdp/unmarshal.go b/sdp/unmarshal.go deleted file mode 100644 index bc1e578c..00000000 --- a/sdp/unmarshal.go +++ /dev/null @@ -1,628 +0,0 @@ -package sdp - -import ( - "errors" - "fmt" - "net/url" - "strconv" - "strings" - - psdp "github.com/pion/sdp/v3" -) - -type stateVal int - -const ( - stateInitial stateVal = iota - stateSession - stateMedia -) - -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 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) unmarshalVersion(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 (s *SessionDescription) unmarshalOrigin(value string) error { - // special case for live reporter app - if value[:3] == "-0 " { - value = "- 0 " + value[3:] - } - - // special case for sone onvif2 cameras - if value[len(value)-1] == ' ' { - value += "127.0.0.1" - } - - fields := strings.Fields(value) - if len(fields) != 6 { - return fmt.Errorf("%w `o=%v`", errSDPInvalidSyntax, fields) - } - - sessionID, err := strconv.ParseUint(fields[1], 10, 64) - if err != nil { - return fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, fields[1]) - } - - sessionVersion, err := strconv.ParseUint(fields[2], 10, 64) - if err != nil { - return fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, fields[2]) - } - - // Set according to currently registered with IANA - // https://tools.ietf.org/html/rfc4566#section-8.2.6 - if i := indexOf(fields[3], []string{"IN"}); i == -1 { - return fmt.Errorf("%w `%v`", errSDPInvalidValue, fields[3]) - } - - // Set according to currently registered with IANA - // https://tools.ietf.org/html/rfc4566#section-8.2.7 - if i := indexOf(fields[4], []string{"IP4", "IP6"}); i == -1 { - return fmt.Errorf("%w `%v`", errSDPInvalidValue, fields[3]) - } - - s.Origin = psdp.Origin{ - Username: fields[0], - SessionID: sessionID, - SessionVersion: sessionVersion, - NetworkType: fields[3], - AddressType: fields[4], - UnicastAddress: fields[5], - } - - 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) { - 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(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: 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 i := indexOf(parts[0], []string{"CT", "AS", "RR"}); i == -1 { - // 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 &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= .... - // 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 { - 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) < 3 { - return fmt.Errorf("%w `r=%v`", errSDPInvalidSyntax, fields) - } - - 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++ { - 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{} - - // - // Set according to currently registered with IANA - // https://tools.ietf.org/html/rfc4566#section-5.14 - if i := indexOf(fields[0], []string{"audio", "video", "text", "application", "message"}); i == -1 { - return fmt.Errorf("%w `%v`", errSDPInvalidValue, fields[0]) - } - newMediaDesc.MediaName.Media = fields[0] - - // - 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 { - portRange, err := strconv.Atoi(parts[1]) - if err != nil { - return fmt.Errorf("%w `%v`", errSDPInvalidValue, parts) - } - newMediaDesc.MediaName.Port.Range = &portRange - } - - // - // 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) - } - - // ... - 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 { - 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 -} - -// 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.unmarshalVersion(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - state = stateSession - - default: - return fmt.Errorf("invalid key: %c (%s)", key, line) - } - - case stateSession: - switch key { - case 'o': - err := s.unmarshalOrigin(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 's': - err := s.unmarshalSessionName(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 'i': - err := s.unmarshalSessionInformation(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 'u': - err := s.unmarshalURI(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 'e': - err := s.unmarshalEmail(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 'p': - err := s.unmarshalPhone(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 'c': - err := s.unmarshalSessionConnectionInformation(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 'b': - err := s.unmarshalSessionBandwidth(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 'z': - err := s.unmarshalTimeZones(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 'k': - err := s.unmarshalSessionEncryptionKey(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 'a': - err := s.unmarshalSessionAttribute(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 't': - err := s.unmarshalTiming(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 'r': - err := s.unmarshalRepeatTimes(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 'm': - err := s.unmarshalMediaDescription(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - state = stateMedia - - default: - return fmt.Errorf("invalid key: %c (%s)", key, line) - } - - case stateMedia: - switch key { - case 'm': - err := s.unmarshalMediaDescription(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 'i': - err := s.unmarshalMediaTitle(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 'c': - err := s.unmarshalMediaConnectionInformation(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 'b': - err := s.unmarshalMediaBandwidth(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 'k': - err := s.unmarshalMediaEncryptionKey(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - case 'a': - err := s.unmarshalMediaAttribute(val) - if err != nil { - return fmt.Errorf("%s (%s)", err, line) - } - - default: - return fmt.Errorf("invalid key: %c (%s)", key, line) - } - } - } - - return nil -}