// 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= .... // 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) < 3 { 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++ { 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 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] // 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 { 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 }