diff --git a/pkg/description/session.go b/pkg/description/session.go index 1875f050..778865a3 100644 --- a/pkg/description/session.go +++ b/pkg/description/session.go @@ -2,6 +2,7 @@ package description import ( "fmt" + "strings" psdp "github.com/pion/sdp/v3" @@ -36,15 +37,21 @@ func hasMediaWithID(medias []*Media, id string) bool { return false } +// SessionFECGroup is a FEC group. +type SessionFECGroup []string + // Session is the description of a RTSP stream. type Session struct { - // base URL of the stream (read only). + // Base URL of the stream (read only). BaseURL *url.URL - // title of the stream (optional). + // Title of the stream (optional). Title string - // available media streams. + // FEC groups (RFC5109). + FECGroups []SessionFECGroup + + // Media streams. Medias []*Media } @@ -87,6 +94,20 @@ func (d *Session) Unmarshal(ssd *sdp.SessionDescription) error { return fmt.Errorf("media IDs sent partially") } + for _, attr := range ssd.Attributes { + if attr.Key == "group" && strings.HasPrefix(attr.Value, "FEC ") { + group := SessionFECGroup(strings.Split(attr.Value[len("FEC "):], " ")) + + for _, id := range group { + if !hasMediaWithID(d.Medias, id) { + return fmt.Errorf("FEC group points to an invalid media ID: %v", id) + } + } + + d.FECGroups = append(d.FECGroups, group) + } + } + return nil } @@ -132,5 +153,12 @@ func (d Session) Marshal(multicast bool) ([]byte, error) { sout.MediaDescriptions[i] = media.Marshal() } + for _, group := range d.FECGroups { + sout.Attributes = append(sout.Attributes, psdp.Attribute{ + Key: "group", + Value: "FEC " + strings.Join(group, " "), + }) + } + return sout.Marshal() } diff --git a/pkg/description/session_test.go b/pkg/description/session_test.go index 09616095..3068d330 100644 --- a/pkg/description/session_test.go +++ b/pkg/description/session_test.go @@ -629,6 +629,89 @@ var casesSession = []struct { }, }, }, + { + "ulpfec rfc5109", + "v=0\r\n" + + "o=adam 289083124 289083124 IN IP4 host.example.com\r\n" + + "s=ULP FEC Seminar\r\n" + + "t=0 0\r\n" + + "c=IN IP4 224.2.17.12/127\r\n" + + "a=group:FEC 1 2\r\n" + + "a=group:FEC 3 4\r\n" + + "m=audio 30000 RTP/AVP 0\r\n" + + "a=mid:1\r\n" + + "m=application 30002 RTP/AVP 100\r\n" + + "a=rtpmap:100 ulpfec/8000\r\n" + + "a=mid:2\r\n" + + "m=video 30004 RTP/AVP 31\r\n" + + "a=mid:3\r\n" + + "m=application 30004 RTP/AVP 101\r\n" + + "c=IN IP4 224.2.17.13/127\r\n" + + "a=rtpmap:101 ulpfec/8000\r\n" + + "a=mid:4\r\n", + "v=0\r\n" + + "o=- 0 0 IN IP4 127.0.0.1\r\n" + + "s=ULP FEC Seminar\r\n" + + "c=IN IP4 0.0.0.0\r\n" + + "t=0 0\r\n" + + "a=group:FEC 1 2\r\n" + + "a=group:FEC 3 4\r\n" + + "m=audio 0 RTP/AVP 0\r\n" + + "a=mid:1\r\n" + + "a=control\r\n" + + "a=rtpmap:0 PCMU/8000\r\n" + + "m=application 0 RTP/AVP 100\r\n" + + "a=mid:2\r\n" + + "a=control\r\n" + + "a=rtpmap:100 ulpfec/8000\r\n" + + "m=video 0 RTP/AVP 31\r\n" + + "a=mid:3\r\n" + + "a=control\r\n" + + "m=application 0 RTP/AVP 101\r\n" + + "a=mid:4\r\n" + + "a=control\r\n" + + "a=rtpmap:101 ulpfec/8000\r\n", + Session{ + Title: "ULP FEC Seminar", + FECGroups: []SessionFECGroup{ + {"1", "2"}, + {"3", "4"}, + }, + Medias: []*Media{ + { + ID: "1", + Type: MediaTypeAudio, + Formats: []format.Format{&format.G711{MULaw: true}}, + }, + { + ID: "2", + Type: MediaTypeApplication, + Formats: []format.Format{&format.Generic{ + PayloadTyp: 100, + RTPMa: "ulpfec/8000", + ClockRat: 8000, + }}, + }, + { + ID: "3", + Type: MediaTypeVideo, + Formats: []format.Format{&format.Generic{ + PayloadTyp: 31, + ClockRat: 90000, + }}, + }, + { + ID: "4", + Type: MediaTypeApplication, + Formats: []format.Format{&format.Generic{ + PayloadTyp: 101, + RTPMa: "ulpfec/8000", + ClockRat: 8000, + }}, + }, + }, + }, + }, } func TestSessionUnmarshal(t *testing.T) { @@ -735,6 +818,25 @@ func FuzzSessionUnmarshalErrors(f *testing.F) { "m=audio 0 RTP/AVP/TCP 0\r\n" + "a=mid:2\r\n") + f.Add("v=0\r\n" + + "o=adam 289083124 289083124 IN IP4 host.example.com\r\n" + + "s=ULP FEC Seminar\r\n" + + "t=0 0\r\n" + + "c=IN IP4 224.2.17.12/127\r\n" + + "a=group:FEC 1 2\r\n" + + "a=group:FEC 3 4\r\n" + + "m=audio 30000 RTP/AVP 0\r\n" + + "a=mid:1\r\n" + + "m=application 30002 RTP/AVP 100\r\n" + + "a=rtpmap:100 ulpfec/8000\r\n" + + "a=mid:2\r\n" + + "m=video 30004 RTP/AVP 31\r\n" + + "a=mid:3\r\n" + + "m=application 30004 RTP/AVP 101\r\n" + + "c=IN IP4 224.2.17.13/127\r\n" + + "a=rtpmap:101 ulpfec/8000\r\n" + + "a=mid:4\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/fb7e5db5b68fa760 b/pkg/description/testdata/fuzz/FuzzSessionUnmarshalErrors/fb7e5db5b68fa760 new file mode 100644 index 00000000..2ef926cf --- /dev/null +++ b/pkg/description/testdata/fuzz/FuzzSessionUnmarshalErrors/fb7e5db5b68fa760 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("v=0\ns=0\na=group:FEC 0") diff --git a/server_conn.go b/server_conn.go index 64425d29..883bf25b 100644 --- a/server_conn.go +++ b/server_conn.go @@ -26,18 +26,20 @@ func getSessionID(header base.Header) string { func serverSideDescription(d *description.Session, contentBase *url.URL) *description.Session { out := &description.Session{ - Title: d.Title, - Medias: make([]*description.Media, len(d.Medias)), + Title: d.Title, + FECGroups: d.FECGroups, + Medias: make([]*description.Media, len(d.Medias)), } for i, medi := range d.Medias { mc := &description.Media{ Type: medi.Type, + ID: medi.ID, // Direction: skipped for the moment - Formats: medi.Formats, // we have to use trackID=number in order to support clients // like the Grandstream GXV3500. Control: "trackID=" + strconv.FormatInt(int64(i), 10), + Formats: medi.Formats, } // always use the absolute URL of the track as control attribute, in order