support generic tracks with multiple formats (https://github.com/aler9/rtsp-simple-server/issues/1103)

This commit is contained in:
aler9
2022-10-28 19:10:54 +02:00
parent 394c2f0696
commit 6e6be34636
8 changed files with 243 additions and 157 deletions

View File

@@ -246,10 +246,14 @@ func TestClientRead(t *testing.T) {
require.Equal(t, mustParseURL(scheme+"://"+listenIP+":8554/test/stream?param=value"), req.URL) require.Equal(t, mustParseURL(scheme+"://"+listenIP+":8554/test/stream?param=value"), req.URL)
track := &TrackGeneric{ track := &TrackGeneric{
Media: "application", Media: "application",
Formats: []string{"97"}, Payloads: []TrackGenericPayload{{
RTPMap: "97 private/90000", Type: 97,
RTPMap: "97 private/90000",
}},
} }
err = track.Init()
require.NoError(t, err)
tracks := Tracks{track} tracks := Tracks{track}
tracks.setControls() tracks.setControls()

104
track.go
View File

@@ -28,76 +28,110 @@ type Track interface {
url(*url.URL) (*url.URL, error) url(*url.URL) (*url.URL, error)
} }
func newTrackFromMediaDescription(md *psdp.MediaDescription) (Track, error) { func getControlAttribute(attributes []psdp.Attribute) string {
control := func() string { for _, attr := range attributes {
for _, attr := range md.Attributes { if attr.Key == "control" {
if attr.Key == "control" { return attr.Value
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) { func getFmtpAttribute(attributes []psdp.Attribute, payloadType uint8) string {
rtpmap, ok := md.Attribute("rtpmap") for _, attr := range attributes {
if !ok { if attr.Key == "fmtp" {
return "", 0 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, " ") func getCodecAndClock(attributes []psdp.Attribute, payloadType uint8) (string, string) {
if len(rtpmapParts) != 2 { rtpmap := getRtpmapAttribute(attributes, payloadType)
return "", 0 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 { if err != nil {
return "", 0 return nil, err
} }
payloadType := uint8(tmp) payloadType := uint8(tmp)
return rtpmapParts[1], payloadType codec, clock := getCodecAndClock(md.Attributes, payloadType)
}()
if len(md.MediaName.Formats) == 1 {
switch { switch {
case md.MediaName.Media == "video": case md.MediaName.Media == "video":
switch { switch {
case md.MediaName.Formats[0] == "26": case payloadType == 26:
return newTrackJPEGFromMediaDescription(control) return newTrackJPEGFromMediaDescription(control)
case md.MediaName.Formats[0] == "32": case payloadType == 32:
return newTrackMPEG2VideoFromMediaDescription(control) return newTrackMPEG2VideoFromMediaDescription(control)
case rtpmapPart1 == "H264/90000": case codec == "H264" && clock == "90000":
return newTrackH264FromMediaDescription(control, payloadType, md) return newTrackH264FromMediaDescription(control, payloadType, md)
case rtpmapPart1 == "H265/90000": case codec == "H265" && clock == "90000":
return newTrackH265FromMediaDescription(control, payloadType, md) return newTrackH265FromMediaDescription(control, payloadType, md)
case rtpmapPart1 == "VP8/90000": case codec == "VP8" && clock == "90000":
return newTrackVP8FromMediaDescription(control, payloadType, md) return newTrackVP8FromMediaDescription(control, payloadType, md)
case rtpmapPart1 == "VP9/90000": case codec == "VP9" && clock == "90000":
return newTrackVP9FromMediaDescription(control, payloadType, md) return newTrackVP9FromMediaDescription(control, payloadType, md)
} }
case md.MediaName.Media == "audio": case md.MediaName.Media == "audio":
switch { switch {
case md.MediaName.Formats[0] == "0": case payloadType == 0:
return newTrackPCMUFromMediaDescription(control, rtpmapPart1) return newTrackPCMUFromMediaDescription(control, clock)
case md.MediaName.Formats[0] == "8": case payloadType == 8:
return newTrackPCMAFromMediaDescription(control, rtpmapPart1) return newTrackPCMAFromMediaDescription(control, clock)
case md.MediaName.Formats[0] == "14": case payloadType == 14:
return newTrackMPEG2AudioFromMediaDescription(control) return newTrackMPEG2AudioFromMediaDescription(control)
case strings.HasPrefix(strings.ToLower(rtpmapPart1), "mpeg4-generic/"): case strings.ToLower(codec) == "mpeg4-generic":
return newTrackMPEG4AudioFromMediaDescription(control, payloadType, md) return newTrackMPEG4AudioFromMediaDescription(control, payloadType, md)
case strings.HasPrefix(rtpmapPart1, "opus/"): case codec == "opus":
return newTrackOpusFromMediaDescription(control, payloadType, rtpmapPart1, md) return newTrackOpusFromMediaDescription(control, payloadType, clock, md)
} }
} }
} }

View File

@@ -8,152 +8,166 @@ import (
psdp "github.com/pion/sdp/v3" psdp "github.com/pion/sdp/v3"
) )
func trackGenericGetClockRate(formats []string, rtpmap string) (int, error) { func findClockRate(track *TrackGeneric) (int, error) {
if len(formats) < 1 { // RFC 4566
return 0, fmt.Errorf("no formats provided") // 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 // get clock rate from payload type
// https://en.wikipedia.org/wiki/RTP_payload_formats // https://en.wikipedia.org/wiki/RTP_payload_formats
switch formats[0] { switch payload.Type {
case "0", "1", "2", "3", "4", "5", "7", "8", "9", "12", "13", "15", "18": case 0, 1, 2, 3, 4, 5, 7, 8, 9, 12, 13, 15, 18:
return 8000, nil return 8000, nil
case "6": case 6:
return 16000, nil return 16000, nil
case "10", "11": case 10, 11:
return 44100, nil return 44100, nil
case "14", "25", "26", "28", "31", "32", "33", "34": case 14, 25, 26, 28, 31, 32, 33, 34:
return 90000, nil return 90000, nil
case "16": case 16:
return 11025, nil return 11025, nil
case "17": case 17:
return 22050, nil return 22050, nil
} }
// get clock rate from rtpmap // get clock rate from rtpmap
// https://tools.ietf.org/html/rfc4566 // https://tools.ietf.org/html/rfc4566
// a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>] // a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
if rtpmap == "" { if payload.RTPMap == "" {
return 0, fmt.Errorf("attribute 'rtpmap' not found") return 0, fmt.Errorf("attribute 'rtpmap' not found")
} }
tmp := strings.Split(rtpmap, " ") tmp := strings.Split(payload.RTPMap, "/")
if len(tmp) < 2 {
return 0, fmt.Errorf("invalid rtpmap (%v)", rtpmap)
}
tmp = strings.Split(tmp[1], "/")
if len(tmp) != 2 && len(tmp) != 3 { 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) v, err := strconv.ParseInt(tmp[1], 10, 64)
if err != nil { if err != nil {
return 0, err return 0, err
} }
return int(v), nil 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. // TrackGeneric is a generic track.
type TrackGeneric struct { type TrackGeneric struct {
Media string Media string
Formats []string Payloads []TrackGenericPayload
RTPMap string
FMTP string
trackBase trackBase
clockRate int
} }
func newTrackGenericFromMediaDescription( func newTrackGenericFromMediaDescription(
control string, control string,
md *psdp.MediaDescription, md *psdp.MediaDescription,
) (*TrackGeneric, error) { ) (*TrackGeneric, error) {
rtpmap := func() string { t := &TrackGeneric{
for _, attr := range md.Attributes { Media: md.MediaName.Media,
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,
trackBase: trackBase{ trackBase: trackBase{
control: control, 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. // ClockRate returns the track clock rate.
func (t *TrackGeneric) ClockRate() int { func (t *TrackGeneric) ClockRate() int {
clockRate, _ := trackGenericGetClockRate(t.Formats, t.RTPMap) return t.clockRate
return clockRate
} }
func (t *TrackGeneric) clone() Track { func (t *TrackGeneric) clone() Track {
return &TrackGeneric{ return &TrackGeneric{
Media: t.Media, Media: t.Media,
Formats: t.Formats, Payloads: append([]TrackGenericPayload(nil), t.Payloads...),
RTPMap: t.RTPMap,
FMTP: t.FMTP,
trackBase: t.trackBase, trackBase: t.trackBase,
clockRate: t.clockRate,
} }
} }
// MediaDescription returns the track media description in SDP format. // MediaDescription returns the track media description in SDP format.
func (t *TrackGeneric) MediaDescription() *psdp.MediaDescription { 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{ return &psdp.MediaDescription{
MediaName: psdp.MediaName{ MediaName: psdp.MediaName{
Media: t.Media, Media: t.Media,
Protos: []string{"RTP", "AVP"}, Protos: []string{"RTP", "AVP"},
Formats: t.Formats, Formats: formats,
}, },
Attributes: func() []psdp.Attribute { Attributes: attributes,
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
}(),
} }
} }

View File

@@ -9,12 +9,21 @@ import (
func TestTrackGenericClone(t *testing.T) { func TestTrackGenericClone(t *testing.T) {
track := &TrackGeneric{ track := &TrackGeneric{
Media: "video", Media: "video",
Formats: []string{"100", "101"}, Payloads: []TrackGenericPayload{
RTPMap: "98 H265/90000", {
FMTP: "98 profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + Type: 98,
"sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", 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() clone := track.clone()
require.NotSame(t, track, clone) require.NotSame(t, track, clone)
@@ -23,17 +32,27 @@ func TestTrackGenericClone(t *testing.T) {
func TestTrackGenericMediaDescription(t *testing.T) { func TestTrackGenericMediaDescription(t *testing.T) {
track := &TrackGeneric{ track := &TrackGeneric{
Media: "video", Media: "video",
Formats: []string{"100", "101"}, Payloads: []TrackGenericPayload{
RTPMap: "98 H265/90000", {
FMTP: "98 profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + Type: 98,
"sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", 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{ require.Equal(t, &psdp.MediaDescription{
MediaName: psdp.MediaName{ MediaName: psdp.MediaName{
Media: "video", Media: "video",
Protos: []string{"RTP", "AVP"}, Protos: []string{"RTP", "AVP"},
Formats: []string{"100", "101"}, Formats: []string{"98", "100"},
}, },
Attributes: []psdp.Attribute{ Attributes: []psdp.Attribute{
{ {

View File

@@ -20,20 +20,20 @@ type TrackOpus struct {
func newTrackOpusFromMediaDescription( func newTrackOpusFromMediaDescription(
control string, control string,
payloadType uint8, payloadType uint8,
rtpmapPart1 string, clock string,
md *psdp.MediaDescription, md *psdp.MediaDescription,
) (*TrackOpus, error) { ) (*TrackOpus, error) {
tmp := strings.SplitN(rtpmapPart1, "/", 3) tmp := strings.SplitN(clock, "/", 32)
if len(tmp) != 3 { if len(tmp) != 2 {
return nil, fmt.Errorf("invalid rtpmap (%v)", rtpmapPart1) 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 { if err != nil {
return nil, err return nil, err
} }
channelCount, err := strconv.ParseInt(tmp[2], 10, 64) channelCount, err := strconv.ParseInt(tmp[1], 10, 64)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -17,8 +17,8 @@ func newTrackPCMAFromMediaDescription(
rtpmapPart1 string) (*TrackPCMA, error, rtpmapPart1 string) (*TrackPCMA, error,
) { ) {
tmp := strings.Split(rtpmapPart1, "/") tmp := strings.Split(rtpmapPart1, "/")
if len(tmp) >= 3 && tmp[2] != "1" { if len(tmp) == 2 && tmp[1] != "1" {
return nil, fmt.Errorf("PCMA tracks must have only one channel") return nil, fmt.Errorf("PCMU tracks can have only one channel")
} }
return &TrackPCMA{ return &TrackPCMA{

View File

@@ -14,11 +14,11 @@ type TrackPCMU struct {
func newTrackPCMUFromMediaDescription( func newTrackPCMUFromMediaDescription(
control string, control string,
rtpmapPart1 string) (*TrackPCMU, error, clock string) (*TrackPCMU, error,
) { ) {
tmp := strings.Split(rtpmapPart1, "/") tmp := strings.SplitN(clock, "/", 2)
if len(tmp) >= 3 && tmp[2] != "1" { if len(tmp) == 2 && tmp[1] != "1" {
return nil, fmt.Errorf("PCMU tracks must have only one channel") return nil, fmt.Errorf("PCMU tracks can have only one channel")
} }
return &TrackPCMU{ return &TrackPCMU{

View File

@@ -457,26 +457,41 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
MediaName: psdp.MediaName{ MediaName: psdp.MediaName{
Media: "video", Media: "video",
Protos: []string{"RTP", "AVP"}, Protos: []string{"RTP", "AVP"},
Formats: []string{"98", "96"}, Formats: []string{"96", "98"},
}, },
Attributes: []psdp.Attribute{ Attributes: []psdp.Attribute{
{ {
Key: "rtpmap", Key: "rtpmap",
Value: "98 H265/90000", Value: "96 H264/90000",
},
{
Key: "rtpmap",
Value: "98 MetaData",
},
{
Key: "rtcp-mux",
}, },
{ {
Key: "fmtp", Key: "fmtp",
Value: "98 profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + Value: "96 packetization-mode=1;profile-level-id=4d002a;" +
"sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", "sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==",
}, },
}, },
}, },
&TrackGeneric{ &TrackGeneric{
Media: "video", Media: "video",
Formats: []string{"98", "96"}, Payloads: []TrackGenericPayload{
RTPMap: "98 H265/90000", {
FMTP: "98 profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + Type: 96,
"sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", 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{}, Formats: []string{},
}, },
}, },
"unable to get clock rate: no formats provided", "no media formats found",
}, },
{ {
"no rtpmap", "no rtpmap",
@@ -527,11 +542,11 @@ func TestTrackNewFromMediaDescriptionErrors(t *testing.T) {
Attributes: []psdp.Attribute{ Attributes: []psdp.Attribute{
{ {
Key: "rtpmap", 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", "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", "invalid clockrate 3",
@@ -725,7 +740,7 @@ func TestTrackNewFromMediaDescriptionErrors(t *testing.T) {
}, },
}, },
}, },
"invalid rtpmap (opus/48000)", "invalid clock (48000)",
}, },
{ {
"opus invalid 2", "opus invalid 2",