diff --git a/client_read_test.go b/client_read_test.go index 22c72b2e..65148102 100644 --- a/client_read_test.go +++ b/client_read_test.go @@ -246,10 +246,14 @@ func TestClientRead(t *testing.T) { require.Equal(t, mustParseURL(scheme+"://"+listenIP+":8554/test/stream?param=value"), req.URL) track := &TrackGeneric{ - Media: "application", - Formats: []string{"97"}, - RTPMap: "97 private/90000", + Media: "application", + Payloads: []TrackGenericPayload{{ + Type: 97, + RTPMap: "97 private/90000", + }}, } + err = track.Init() + require.NoError(t, err) tracks := Tracks{track} tracks.setControls() diff --git a/track.go b/track.go index 6fdfeda7..1442b173 100644 --- a/track.go +++ b/track.go @@ -28,76 +28,110 @@ type Track interface { url(*url.URL) (*url.URL, error) } -func newTrackFromMediaDescription(md *psdp.MediaDescription) (Track, error) { - control := func() string { - for _, attr := range md.Attributes { - if attr.Key == "control" { - return attr.Value +func getControlAttribute(attributes []psdp.Attribute) string { + for _, attr := range attributes { + if attr.Key == "control" { + return attr.Value + } + } + return "" +} + +func getRtpmapAttribute(attributes []psdp.Attribute, payloadType uint8) string { + for _, attr := range attributes { + if attr.Key == "rtpmap" { + v := strings.TrimSpace(attr.Value) + if parts := strings.SplitN(v, " ", 2); len(parts) == 2 { + if tmp, err := strconv.ParseInt(parts[0], 10, 8); err == nil && uint8(tmp) == payloadType { + return parts[1] + } } } - return "" - }() + } + return "" +} - rtpmapPart1, payloadType := func() (string, uint8) { - rtpmap, ok := md.Attribute("rtpmap") - if !ok { - return "", 0 +func getFmtpAttribute(attributes []psdp.Attribute, payloadType uint8) string { + for _, attr := range attributes { + if attr.Key == "fmtp" { + if parts := strings.SplitN(attr.Value, " ", 2); len(parts) == 2 { + if tmp, err := strconv.ParseInt(parts[0], 10, 8); err == nil && uint8(tmp) == payloadType { + return parts[1] + } + } } - rtpmap = strings.TrimSpace(rtpmap) + } + return "" +} - rtpmapParts := strings.Split(rtpmap, " ") - if len(rtpmapParts) != 2 { - return "", 0 - } +func getCodecAndClock(attributes []psdp.Attribute, payloadType uint8) (string, string) { + rtpmap := getRtpmapAttribute(attributes, payloadType) + if rtpmap == "" { + return "", "" + } - tmp, err := strconv.ParseInt(rtpmapParts[0], 10, 64) + parts2 := strings.SplitN(rtpmap, "/", 2) + if len(parts2) != 2 { + return "", "" + } + + return parts2[0], parts2[1] +} + +func newTrackFromMediaDescription(md *psdp.MediaDescription) (Track, error) { + if len(md.MediaName.Formats) == 0 { + return nil, fmt.Errorf("no media formats found") + } + + control := getControlAttribute(md.Attributes) + + if len(md.MediaName.Formats) == 1 { + tmp, err := strconv.ParseInt(md.MediaName.Formats[0], 10, 8) if err != nil { - return "", 0 + return nil, err } payloadType := uint8(tmp) - return rtpmapParts[1], payloadType - }() + codec, clock := getCodecAndClock(md.Attributes, payloadType) - if len(md.MediaName.Formats) == 1 { switch { case md.MediaName.Media == "video": switch { - case md.MediaName.Formats[0] == "26": + case payloadType == 26: return newTrackJPEGFromMediaDescription(control) - case md.MediaName.Formats[0] == "32": + case payloadType == 32: return newTrackMPEG2VideoFromMediaDescription(control) - case rtpmapPart1 == "H264/90000": + case codec == "H264" && clock == "90000": return newTrackH264FromMediaDescription(control, payloadType, md) - case rtpmapPart1 == "H265/90000": + case codec == "H265" && clock == "90000": return newTrackH265FromMediaDescription(control, payloadType, md) - case rtpmapPart1 == "VP8/90000": + case codec == "VP8" && clock == "90000": return newTrackVP8FromMediaDescription(control, payloadType, md) - case rtpmapPart1 == "VP9/90000": + case codec == "VP9" && clock == "90000": return newTrackVP9FromMediaDescription(control, payloadType, md) } case md.MediaName.Media == "audio": switch { - case md.MediaName.Formats[0] == "0": - return newTrackPCMUFromMediaDescription(control, rtpmapPart1) + case payloadType == 0: + return newTrackPCMUFromMediaDescription(control, clock) - case md.MediaName.Formats[0] == "8": - return newTrackPCMAFromMediaDescription(control, rtpmapPart1) + case payloadType == 8: + return newTrackPCMAFromMediaDescription(control, clock) - case md.MediaName.Formats[0] == "14": + case payloadType == 14: return newTrackMPEG2AudioFromMediaDescription(control) - case strings.HasPrefix(strings.ToLower(rtpmapPart1), "mpeg4-generic/"): + case strings.ToLower(codec) == "mpeg4-generic": return newTrackMPEG4AudioFromMediaDescription(control, payloadType, md) - case strings.HasPrefix(rtpmapPart1, "opus/"): - return newTrackOpusFromMediaDescription(control, payloadType, rtpmapPart1, md) + case codec == "opus": + return newTrackOpusFromMediaDescription(control, payloadType, clock, md) } } } diff --git a/track_generic.go b/track_generic.go index cf9071e0..c2ce47e7 100644 --- a/track_generic.go +++ b/track_generic.go @@ -8,152 +8,166 @@ import ( psdp "github.com/pion/sdp/v3" ) -func trackGenericGetClockRate(formats []string, rtpmap string) (int, error) { - if len(formats) < 1 { - return 0, fmt.Errorf("no formats provided") - } +func findClockRate(track *TrackGeneric) (int, error) { + // RFC 4566 + // When a list of + // payload type numbers is given, this implies that all of these + // payload formats MAY be used in the session, but the first of these + // formats SHOULD be used as the default format for the session + payload := track.Payloads[0] // get clock rate from payload type // https://en.wikipedia.org/wiki/RTP_payload_formats - switch formats[0] { - case "0", "1", "2", "3", "4", "5", "7", "8", "9", "12", "13", "15", "18": + switch payload.Type { + case 0, 1, 2, 3, 4, 5, 7, 8, 9, 12, 13, 15, 18: return 8000, nil - case "6": + case 6: return 16000, nil - case "10", "11": + case 10, 11: return 44100, nil - case "14", "25", "26", "28", "31", "32", "33", "34": + case 14, 25, 26, 28, 31, 32, 33, 34: return 90000, nil - case "16": + case 16: return 11025, nil - case "17": + case 17: return 22050, nil } // get clock rate from rtpmap // https://tools.ietf.org/html/rfc4566 // a=rtpmap: / [/] - if rtpmap == "" { + if payload.RTPMap == "" { return 0, fmt.Errorf("attribute 'rtpmap' not found") } - tmp := strings.Split(rtpmap, " ") - if len(tmp) < 2 { - return 0, fmt.Errorf("invalid rtpmap (%v)", rtpmap) - } - - tmp = strings.Split(tmp[1], "/") + tmp := strings.Split(payload.RTPMap, "/") if len(tmp) != 2 && len(tmp) != 3 { - return 0, fmt.Errorf("invalid rtpmap (%v)", rtpmap) + return 0, fmt.Errorf("invalid rtpmap (%v)", payload.RTPMap) } v, err := strconv.ParseInt(tmp[1], 10, 64) if err != nil { return 0, err } + return int(v), nil } +// TrackGenericPayload is a payload of a TrackGeneric. +type TrackGenericPayload struct { + Type uint8 + RTPMap string + FMTP string +} + // TrackGeneric is a generic track. type TrackGeneric struct { - Media string - Formats []string - RTPMap string - FMTP string + Media string + Payloads []TrackGenericPayload trackBase + + clockRate int } func newTrackGenericFromMediaDescription( control string, md *psdp.MediaDescription, ) (*TrackGeneric, error) { - rtpmap := func() string { - for _, attr := range md.Attributes { - if attr.Key == "rtpmap" { - return attr.Value - } - } - return "" - }() - - _, err := trackGenericGetClockRate(md.MediaName.Formats, rtpmap) - if err != nil { - return nil, fmt.Errorf("unable to get clock rate: %s", err) - } - - fmtp := func() string { - for _, attr := range md.Attributes { - if attr.Key == "fmtp" { - return attr.Value - } - } - return "" - }() - - return &TrackGeneric{ - Media: md.MediaName.Media, - Formats: md.MediaName.Formats, - RTPMap: rtpmap, - FMTP: fmtp, + t := &TrackGeneric{ + Media: md.MediaName.Media, trackBase: trackBase{ control: control, }, - }, nil + } + + for _, format := range md.MediaName.Formats { + tmp, err := strconv.ParseInt(format, 10, 8) + if err != nil { + return nil, err + } + payloadType := uint8(tmp) + + t.Payloads = append(t.Payloads, TrackGenericPayload{ + Type: payloadType, + RTPMap: getRtpmapAttribute(md.Attributes, payloadType), + FMTP: getFmtpAttribute(md.Attributes, payloadType), + }) + } + + err := t.Init() + if err != nil { + return nil, err + } + + return t, nil +} + +// Init initializes a TrackGeneric +func (t *TrackGeneric) Init() error { + var err error + t.clockRate, err = findClockRate(t) + if err != nil { + return fmt.Errorf("unable to get clock rate: %s", err) + } + + return nil } // ClockRate returns the track clock rate. func (t *TrackGeneric) ClockRate() int { - clockRate, _ := trackGenericGetClockRate(t.Formats, t.RTPMap) - return clockRate + return t.clockRate } func (t *TrackGeneric) clone() Track { return &TrackGeneric{ Media: t.Media, - Formats: t.Formats, - RTPMap: t.RTPMap, - FMTP: t.FMTP, + Payloads: append([]TrackGenericPayload(nil), t.Payloads...), trackBase: t.trackBase, + clockRate: t.clockRate, } } // MediaDescription returns the track media description in SDP format. func (t *TrackGeneric) MediaDescription() *psdp.MediaDescription { + formats := make([]string, len(t.Payloads)) + for i, pl := range t.Payloads { + formats[i] = strconv.FormatInt(int64(pl.Type), 10) + } + + var attributes []psdp.Attribute + + for _, pl := range t.Payloads { + if pl.RTPMap != "" { + attributes = append(attributes, psdp.Attribute{ + Key: "rtpmap", + Value: strconv.FormatInt(int64(pl.Type), 10) + " " + pl.RTPMap, + }) + } + if pl.FMTP != "" { + attributes = append(attributes, psdp.Attribute{ + Key: "fmtp", + Value: strconv.FormatInt(int64(pl.Type), 10) + " " + pl.FMTP, + }) + } + } + + attributes = append(attributes, psdp.Attribute{ + Key: "control", + Value: t.control, + }) + return &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: t.Media, Protos: []string{"RTP", "AVP"}, - Formats: t.Formats, + Formats: formats, }, - Attributes: func() []psdp.Attribute { - var ret []psdp.Attribute - - if t.RTPMap != "" { - ret = append(ret, psdp.Attribute{ - Key: "rtpmap", - Value: t.RTPMap, - }) - } - - if t.FMTP != "" { - ret = append(ret, psdp.Attribute{ - Key: "fmtp", - Value: t.FMTP, - }) - } - - ret = append(ret, psdp.Attribute{ - Key: "control", - Value: t.control, - }) - - return ret - }(), + Attributes: attributes, } } diff --git a/track_generic_test.go b/track_generic_test.go index 1d4c6827..dde50ade 100644 --- a/track_generic_test.go +++ b/track_generic_test.go @@ -9,12 +9,21 @@ import ( func TestTrackGenericClone(t *testing.T) { track := &TrackGeneric{ - Media: "video", - Formats: []string{"100", "101"}, - RTPMap: "98 H265/90000", - FMTP: "98 profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + - "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", + Media: "video", + Payloads: []TrackGenericPayload{ + { + Type: 98, + RTPMap: "H265/90000", + FMTP: "profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + + "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", + }, + { + Type: 100, + }, + }, } + err := track.Init() + require.NoError(t, err) clone := track.clone() require.NotSame(t, track, clone) @@ -23,17 +32,27 @@ func TestTrackGenericClone(t *testing.T) { func TestTrackGenericMediaDescription(t *testing.T) { track := &TrackGeneric{ - Media: "video", - Formats: []string{"100", "101"}, - RTPMap: "98 H265/90000", - FMTP: "98 profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + - "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", + Media: "video", + Payloads: []TrackGenericPayload{ + { + Type: 98, + RTPMap: "H265/90000", + FMTP: "profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + + "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", + }, + { + Type: 100, + }, + }, } + err := track.Init() + require.NoError(t, err) + require.Equal(t, &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "video", Protos: []string{"RTP", "AVP"}, - Formats: []string{"100", "101"}, + Formats: []string{"98", "100"}, }, Attributes: []psdp.Attribute{ { diff --git a/track_opus.go b/track_opus.go index 6f574c69..481f27ed 100644 --- a/track_opus.go +++ b/track_opus.go @@ -20,20 +20,20 @@ type TrackOpus struct { func newTrackOpusFromMediaDescription( control string, payloadType uint8, - rtpmapPart1 string, + clock string, md *psdp.MediaDescription, ) (*TrackOpus, error) { - tmp := strings.SplitN(rtpmapPart1, "/", 3) - if len(tmp) != 3 { - return nil, fmt.Errorf("invalid rtpmap (%v)", rtpmapPart1) + tmp := strings.SplitN(clock, "/", 32) + if len(tmp) != 2 { + return nil, fmt.Errorf("invalid clock (%v)", clock) } - sampleRate, err := strconv.ParseInt(tmp[1], 10, 64) + sampleRate, err := strconv.ParseInt(tmp[0], 10, 64) if err != nil { return nil, err } - channelCount, err := strconv.ParseInt(tmp[2], 10, 64) + channelCount, err := strconv.ParseInt(tmp[1], 10, 64) if err != nil { return nil, err } diff --git a/track_pcma.go b/track_pcma.go index 734a27dc..2051e6ed 100644 --- a/track_pcma.go +++ b/track_pcma.go @@ -17,8 +17,8 @@ func newTrackPCMAFromMediaDescription( rtpmapPart1 string) (*TrackPCMA, error, ) { tmp := strings.Split(rtpmapPart1, "/") - if len(tmp) >= 3 && tmp[2] != "1" { - return nil, fmt.Errorf("PCMA tracks must have only one channel") + if len(tmp) == 2 && tmp[1] != "1" { + return nil, fmt.Errorf("PCMU tracks can have only one channel") } return &TrackPCMA{ diff --git a/track_pcmu.go b/track_pcmu.go index b41400b7..d5b5852f 100644 --- a/track_pcmu.go +++ b/track_pcmu.go @@ -14,11 +14,11 @@ type TrackPCMU struct { func newTrackPCMUFromMediaDescription( control string, - rtpmapPart1 string) (*TrackPCMU, error, + clock string) (*TrackPCMU, error, ) { - tmp := strings.Split(rtpmapPart1, "/") - if len(tmp) >= 3 && tmp[2] != "1" { - return nil, fmt.Errorf("PCMU tracks must have only one channel") + tmp := strings.SplitN(clock, "/", 2) + if len(tmp) == 2 && tmp[1] != "1" { + return nil, fmt.Errorf("PCMU tracks can have only one channel") } return &TrackPCMU{ diff --git a/track_test.go b/track_test.go index ceb31407..5b79cd71 100644 --- a/track_test.go +++ b/track_test.go @@ -457,26 +457,41 @@ func TestTrackNewFromMediaDescription(t *testing.T) { MediaName: psdp.MediaName{ Media: "video", Protos: []string{"RTP", "AVP"}, - Formats: []string{"98", "96"}, + Formats: []string{"96", "98"}, }, Attributes: []psdp.Attribute{ { Key: "rtpmap", - Value: "98 H265/90000", + Value: "96 H264/90000", + }, + { + Key: "rtpmap", + Value: "98 MetaData", + }, + { + Key: "rtcp-mux", }, { Key: "fmtp", - Value: "98 profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + - "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", + Value: "96 packetization-mode=1;profile-level-id=4d002a;" + + "sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==", }, }, }, &TrackGeneric{ - Media: "video", - Formats: []string{"98", "96"}, - RTPMap: "98 H265/90000", - FMTP: "98 profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + - "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", + Media: "video", + Payloads: []TrackGenericPayload{ + { + Type: 96, + RTPMap: "H264/90000", + FMTP: "packetization-mode=1;profile-level-id=4d002a;sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==", + }, + { + Type: 98, + RTPMap: "MetaData", + }, + }, + clockRate: 90000, }, }, } { @@ -503,7 +518,7 @@ func TestTrackNewFromMediaDescriptionErrors(t *testing.T) { Formats: []string{}, }, }, - "unable to get clock rate: no formats provided", + "no media formats found", }, { "no rtpmap", @@ -527,11 +542,11 @@ func TestTrackNewFromMediaDescriptionErrors(t *testing.T) { Attributes: []psdp.Attribute{ { Key: "rtpmap", - Value: "96", + Value: "97 mpeg4-generic/48000/2", }, }, }, - "unable to get clock rate: invalid rtpmap (96)", + "unable to get clock rate: attribute 'rtpmap' not found", }, { "invalid clockrate 2", @@ -548,7 +563,7 @@ func TestTrackNewFromMediaDescriptionErrors(t *testing.T) { }, }, }, - "unable to get clock rate: invalid rtpmap (96 mpeg4-generic)", + "unable to get clock rate: invalid rtpmap (mpeg4-generic)", }, { "invalid clockrate 3", @@ -725,7 +740,7 @@ func TestTrackNewFromMediaDescriptionErrors(t *testing.T) { }, }, }, - "invalid rtpmap (opus/48000)", + "invalid clock (48000)", }, { "opus invalid 2",