diff --git a/pkg/description/media.go b/pkg/description/media.go index 1f92ee03..88d06efd 100644 --- a/pkg/description/media.go +++ b/pkg/description/media.go @@ -8,6 +8,7 @@ import ( "sort" "strconv" "strings" + "unicode" psdp "github.com/pion/sdp/v3" @@ -17,9 +18,23 @@ import ( var smartRegexp = regexp.MustCompile("^([0-9]+) (.*?)/90000") -func getControlAttribute(attributes []psdp.Attribute) string { +func replaceSmartPayloadType(payloadType string, attributes []psdp.Attribute) string { + if payloadType == "smart/1/90000" { + for _, attr := range attributes { + if attr.Key == "rtpmap" { + sm := smartRegexp.FindStringSubmatch(attr.Value) + if sm != nil { + return sm[1] + } + } + } + } + return payloadType +} + +func getAttribute(attributes []psdp.Attribute, key string) string { for _, attr := range attributes { - if attr.Key == "control" { + if attr.Key == key { return attr.Value } } @@ -92,6 +107,15 @@ func sortedKeys(fmtp map[string]string) []string { return keys } +func isAlphaNumeric(v string) bool { + for _, r := range v { + if !unicode.IsLetter(r) && !unicode.IsNumber(r) { + return false + } + } + return true +} + // MediaDirection is the direction of a media stream. type MediaDirection string @@ -118,7 +142,10 @@ type Media struct { // Media type. Type MediaType - // Direction of the stream. + // Media ID (optional). + ID string + + // Direction of the stream (optional). Direction MediaDirection // Control attribute. @@ -131,22 +158,18 @@ type Media struct { // Unmarshal decodes the media from the SDP format. func (m *Media) Unmarshal(md *psdp.MediaDescription) error { m.Type = MediaType(md.MediaName.Media) + + m.ID = getAttribute(md.Attributes, "mid") + if m.ID != "" && !isAlphaNumeric(m.ID) { + return fmt.Errorf("invalid mid: %v", m.ID) + } + m.Direction = getDirection(md.Attributes) - m.Control = getControlAttribute(md.Attributes) + m.Control = getAttribute(md.Attributes, "control") m.Formats = nil for _, payloadType := range md.MediaName.Formats { - if payloadType == "smart/1/90000" { - for _, attr := range md.Attributes { - if attr.Key == "rtpmap" { - sm := smartRegexp.FindStringSubmatch(attr.Value) - if sm != nil { - payloadType = sm[1] - break - } - } - } - } + payloadType = replaceSmartPayloadType(payloadType, md.Attributes) tmp, err := strconv.ParseUint(payloadType, 10, 8) if err != nil { @@ -179,12 +202,13 @@ func (m Media) Marshal() *psdp.MediaDescription { Media: string(m.Type), Protos: []string{"RTP", "AVP"}, }, - Attributes: []psdp.Attribute{ - { - Key: "control", - Value: m.Control, - }, - }, + } + + if m.ID != "" { + md.Attributes = append(md.Attributes, psdp.Attribute{ + Key: "mid", + Value: m.ID, + }) } if m.Direction != "" { @@ -193,6 +217,11 @@ func (m Media) Marshal() *psdp.MediaDescription { }) } + md.Attributes = append(md.Attributes, psdp.Attribute{ + Key: "control", + Value: m.Control, + }) + for _, forma := range m.Formats { typ := strconv.FormatUint(uint64(forma.PayloadType()), 10) md.MediaName.Formats = append(md.MediaName.Formats, typ) diff --git a/pkg/description/session.go b/pkg/description/session.go index b3525cc1..1875f050 100644 --- a/pkg/description/session.go +++ b/pkg/description/session.go @@ -9,6 +9,33 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/url" ) +func atLeastOneHasMID(medias []*Media) bool { + for _, media := range medias { + if media.ID != "" { + return true + } + } + return false +} + +func atLeastOneDoesntHaveMID(medias []*Media) bool { + for _, media := range medias { + if media.ID == "" { + return true + } + } + return false +} + +func hasMediaWithID(medias []*Media, id string) bool { + for _, media := range medias { + if media.ID == id { + return true + } + } + return false +} + // Session is the description of a RTSP stream. type Session struct { // base URL of the stream (read only). @@ -48,9 +75,18 @@ func (d *Session) Unmarshal(ssd *sdp.SessionDescription) error { if err != nil { return fmt.Errorf("media %d is invalid: %v", i+1, err) } + + if m.ID != "" && hasMediaWithID(d.Medias[:i], m.ID) { + return fmt.Errorf("duplicate media IDs") + } + d.Medias[i] = &m } + if atLeastOneHasMID(d.Medias) && atLeastOneDoesntHaveMID(d.Medias) { + return fmt.Errorf("media IDs sent partially") + } + return nil } diff --git a/pkg/description/session_test.go b/pkg/description/session_test.go index c374e34d..09616095 100644 --- a/pkg/description/session_test.go +++ b/pkg/description/session_test.go @@ -51,8 +51,8 @@ var casesSession = []struct { "a=rtpmap:97 H264/90000\r\n" + "a=fmtp:97 packetization-mode=1; profile-level-id=640028; sprop-parameter-sets=Z2QAKKy0A8ARPyo=,aO4Bniw=\r\n" + "m=audio 0 RTP/AVP 0\r\n" + - "a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=a\r\n" + "a=recvonly\r\n" + + "a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=a\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "m=application 0 RTP/AVP 107\r\n" + "a=control\r\n", @@ -121,8 +121,8 @@ var casesSession = []struct { "a=rtpmap:97 H264/90000\r\n" + "a=fmtp:97 packetization-mode=1; profile-level-id=640028; sprop-parameter-sets=Z2QAKKy0A8ARPyo=,aO4Bniw=\r\n" + "m=audio 0 RTP/AVP 0\r\n" + - "a=control:trackID=2\r\n" + "a=recvonly\r\n" + + "a=control:trackID=2\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "m=application 0 RTP/AVP 107\r\n" + "a=control\r\n", @@ -264,8 +264,9 @@ var casesSession = []struct { "c=IN IP4 0.0.0.0\r\n" + "t=0 0\r\n" + "m=audio 0 RTP/AVP 111 103 104 9 102 0 8 106 105 13 110 112 113 126\r\n" + - "a=control\r\n" + + "a=mid:audio\r\n" + "a=sendonly\r\n" + + "a=control\r\n" + "a=rtpmap:111 opus/48000/2\r\n" + "a=fmtp:111 sprop-stereo=0\r\n" + "a=rtpmap:103 ISAC/16000\r\n" + @@ -282,8 +283,9 @@ var casesSession = []struct { "a=rtpmap:113 telephone-event/16000\r\n" + "a=rtpmap:126 telephone-event/8000\r\n" + "m=video 0 RTP/AVP 96 97 98 99 100 101 127 124 125\r\n" + - "a=control\r\n" + + "a=mid:video\r\n" + "a=sendonly\r\n" + + "a=control\r\n" + "a=rtpmap:96 VP8/90000\r\n" + "a=rtpmap:97 rtx/90000\r\n" + "a=fmtp:97 apt=96\r\n" + @@ -301,6 +303,7 @@ var casesSession = []struct { Title: ``, Medias: []*Media{ { + ID: "audio", Type: MediaTypeAudio, Direction: MediaDirectionSendonly, Formats: []format.Format{ @@ -368,6 +371,7 @@ var casesSession = []struct { }, }, { + ID: "video", Type: MediaTypeVideo, Direction: MediaDirectionSendonly, Formats: []format.Format{ @@ -496,16 +500,16 @@ var casesSession = []struct { "c=IN IP4 0.0.0.0\r\n" + "t=0 0\r\n" + "m=video 0 RTP/AVP 26\r\n" + - "a=control:rtsp://192.168.0.1/video\r\n" + "a=recvonly\r\n" + + "a=control:rtsp://192.168.0.1/video\r\n" + "a=rtpmap:26 JPEG/90000\r\n" + "m=audio 0 RTP/AVP 0\r\n" + - "a=control:rtsp://192.168.0.1/audio\r\n" + "a=recvonly\r\n" + + "a=control:rtsp://192.168.0.1/audio\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "m=audio 0 RTP/AVP 0\r\n" + - "a=control:rtsp://192.168.0.1/audioback\r\n" + "a=sendonly\r\n" + + "a=control:rtsp://192.168.0.1/audioback\r\n" + "a=rtpmap:0 PCMU/8000\r\n", Session{ Title: `RTSP Session with audiobackchannel`, @@ -710,6 +714,27 @@ func FuzzSessionUnmarshalErrors(f *testing.F) { "a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=zzz1210\r\n" + "a=control:streamid=1\r\n") + f.Add("v=0\r\n" + + "o=- 4158123474391860926 2 IN IP4 127.0.0.1\r\n" + + "s=-\r\n" + + "t=0 0\r\n" + + "m=video 42504 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=fmtp:96 packetization-mode=1\r\n" + + "m=audio 0 RTP/AVP/TCP 0\r\n" + + "a=mid:2\r\n") + + f.Add("v=0\r\n" + + "o=- 4158123474391860926 2 IN IP4 127.0.0.1\r\n" + + "s=-\r\n" + + "t=0 0\r\n" + + "m=video 42504 RTP/AVP 96\r\n" + + "a=mid:2\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=fmtp:96 packetization-mode=1\r\n" + + "m=audio 0 RTP/AVP/TCP 0\r\n" + + "a=mid:2\r\n") + f.Fuzz(func(t *testing.T, enc string) { var sd sdp.SessionDescription err := sd.Unmarshal([]byte(enc)) diff --git a/pkg/description/testdata/fuzz/FuzzSessionUnmarshalErrors/b0432aab46b15405 b/pkg/description/testdata/fuzz/FuzzSessionUnmarshalErrors/b0432aab46b15405 new file mode 100644 index 00000000..56c826fb --- /dev/null +++ b/pkg/description/testdata/fuzz/FuzzSessionUnmarshalErrors/b0432aab46b15405 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("v=0\ns=0\nm=video 0 AVP 0\na=mid: ")