simplify FMTP decoding and encoding (#205)

This commit is contained in:
Alessandro Ros
2023-03-13 22:44:31 +01:00
committed by GitHub
parent 13fab2962e
commit bc248c8e1d
32 changed files with 346 additions and 399 deletions

View File

@@ -32,6 +32,31 @@ func getCodecAndClock(rtpMap string) (string, string) {
return parts2[0], parts2[1] return parts2[0], parts2[1]
} }
func decodeFMTP(enc string) map[string]string {
if enc == "" {
return nil
}
ret := make(map[string]string)
for _, kv := range strings.Split(enc, ";") {
kv = strings.Trim(kv, " ")
if len(kv) == 0 {
continue
}
tmp := strings.SplitN(kv, "=", 2)
if len(tmp) != 2 {
continue
}
ret[strings.ToLower(tmp[0])] = tmp[1]
}
return ret
}
// Format is a format of a media. // Format is a format of a media.
// It defines a codec and a payload type used to ship the media. // It defines a codec and a payload type used to ship the media.
type Format interface { type Format interface {
@@ -44,10 +69,10 @@ type Format interface {
// PayloadType returns the payload type. // PayloadType returns the payload type.
PayloadType() uint8 PayloadType() uint8
unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp map[string]string) error
// Marshal encodes the format in SDP format. // Marshal encodes the format in SDP format.
Marshal() (string, string) Marshal() (string, map[string]string)
// PTSEqualsDTS checks whether PTS is equal to DTS in RTP packets. // PTSEqualsDTS checks whether PTS is equal to DTS in RTP packets.
PTSEqualsDTS(*rtp.Packet) bool PTSEqualsDTS(*rtp.Packet) bool
@@ -74,7 +99,7 @@ func Unmarshal(md *psdp.MediaDescription, payloadTypeStr string) (Format, error)
rtpMap := getFormatAttribute(md.Attributes, payloadType, "rtpmap") rtpMap := getFormatAttribute(md.Attributes, payloadType, "rtpmap")
codec, clock := getCodecAndClock(rtpMap) codec, clock := getCodecAndClock(rtpMap)
codec = strings.ToLower(codec) codec = strings.ToLower(codec)
fmtp := getFormatAttribute(md.Attributes, payloadType, "fmtp") fmtp := decodeFMTP(getFormatAttribute(md.Attributes, payloadType, "fmtp"))
format := func() Format { format := func() Format {
switch { switch {

View File

@@ -721,27 +721,6 @@ func TestNewFromMediaDescriptionErrors(t *testing.T) {
}, },
"strconv.ParseInt: parsing \"\": invalid syntax", "strconv.ParseInt: parsing \"\": invalid syntax",
}, },
{
"audio aac fmtp without key",
&psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "audio",
Protos: []string{"RTP", "AVP"},
Formats: []string{"96"},
},
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 mpeg4-generic/48000/2",
},
{
Key: "fmtp",
Value: "96 profile-level-id",
},
},
},
"invalid fmtp (profile-level-id)",
},
{ {
"audio aac missing config", "audio aac missing config",
&psdp.MediaDescription{ &psdp.MediaDescription{
@@ -761,7 +740,7 @@ func TestNewFromMediaDescriptionErrors(t *testing.T) {
}, },
}, },
}, },
"config is missing (profile-level-id=1)", "config is missing",
}, },
{ {
"audio aac invalid config 1", "audio aac invalid config 1",
@@ -824,7 +803,7 @@ func TestNewFromMediaDescriptionErrors(t *testing.T) {
}, },
}, },
}, },
"sizelength is missing (profile-level-id=1; config=1190)", "sizelength is missing",
}, },
{ {
"audio aac invalid sizelength", "audio aac invalid sizelength",
@@ -908,7 +887,7 @@ func TestNewFromMediaDescriptionErrors(t *testing.T) {
}, },
}, },
}, },
"config is missing (aa=bb)", "config is missing",
}, },
{ {
"audio opus invalid 1", "audio opus invalid 1",
@@ -961,27 +940,6 @@ func TestNewFromMediaDescriptionErrors(t *testing.T) {
}, },
"strconv.ParseInt: parsing \"aa\": invalid syntax", "strconv.ParseInt: parsing \"aa\": invalid syntax",
}, },
{
"video h264 invalid fmtp",
&psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "video",
Protos: []string{"RTP", "AVP"},
Formats: []string{"96"},
},
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 H264/90000",
},
{
Key: "fmtp",
Value: "96 aaa",
},
},
},
"invalid fmtp attribute (aaa)",
},
{ {
"video h264 invalid sps", "video h264 invalid sps",
&psdp.MediaDescription{ &psdp.MediaDescription{

View File

@@ -30,17 +30,17 @@ func (t *G711) PayloadType() uint8 {
return 8 return 8
} }
func (t *G711) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { func (t *G711) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp map[string]string) error {
t.MULaw = (payloadType == 0) t.MULaw = (payloadType == 0)
return nil return nil
} }
// Marshal implements Format. // Marshal implements Format.
func (t *G711) Marshal() (string, string) { func (t *G711) Marshal() (string, map[string]string) {
if t.MULaw { if t.MULaw {
return "PCMU/8000", "" return "PCMU/8000", nil
} }
return "PCMA/8000", "" return "PCMA/8000", nil
} }
// PTSEqualsDTS implements Format. // PTSEqualsDTS implements Format.

View File

@@ -28,7 +28,7 @@ func TestG711MediaDescription(t *testing.T) {
rtpmap, fmtp := format.Marshal() rtpmap, fmtp := format.Marshal()
require.Equal(t, "PCMA/8000", rtpmap) require.Equal(t, "PCMA/8000", rtpmap)
require.Equal(t, "", fmtp) require.Equal(t, map[string]string(nil), fmtp)
}) })
t.Run("pcmu", func(t *testing.T) { t.Run("pcmu", func(t *testing.T) {
@@ -38,7 +38,7 @@ func TestG711MediaDescription(t *testing.T) {
rtpmap, fmtp := format.Marshal() rtpmap, fmtp := format.Marshal()
require.Equal(t, "PCMU/8000", rtpmap) require.Equal(t, "PCMU/8000", rtpmap)
require.Equal(t, "", fmtp) require.Equal(t, map[string]string(nil), fmtp)
}) })
} }

View File

@@ -24,13 +24,13 @@ func (t *G722) PayloadType() uint8 {
return 9 return 9
} }
func (t *G722) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { func (t *G722) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp map[string]string) error {
return nil return nil
} }
// Marshal implements Format. // Marshal implements Format.
func (t *G722) Marshal() (string, string) { func (t *G722) Marshal() (string, map[string]string) {
return "G722/8000", "" return "G722/8000", nil
} }
// PTSEqualsDTS implements Format. // PTSEqualsDTS implements Format.

View File

@@ -20,7 +20,7 @@ func TestG722MediaDescription(t *testing.T) {
rtpmap, fmtp := format.Marshal() rtpmap, fmtp := format.Marshal()
require.Equal(t, "G722/8000", rtpmap) require.Equal(t, "G722/8000", rtpmap)
require.Equal(t, "", fmtp) require.Equal(t, map[string]string(nil), fmtp)
} }
func TestG722DecEncoder(t *testing.T) { func TestG722DecEncoder(t *testing.T) {

View File

@@ -55,7 +55,7 @@ func findClockRate(payloadType uint8, rtpMap string) (int, error) {
type Generic struct { type Generic struct {
PayloadTyp uint8 PayloadTyp uint8
RTPMap string RTPMap string
FMTP string FMTP map[string]string
// clock rate of the format. Filled automatically. // clock rate of the format. Filled automatically.
ClockRat int ClockRat int
@@ -82,7 +82,10 @@ func (t *Generic) PayloadType() uint8 {
return t.PayloadTyp return t.PayloadTyp
} }
func (t *Generic) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { func (t *Generic) unmarshal(
payloadType uint8, clock string, codec string,
rtpmap string, fmtp map[string]string,
) error {
t.PayloadTyp = payloadType t.PayloadTyp = payloadType
t.RTPMap = rtpmap t.RTPMap = rtpmap
t.FMTP = fmtp t.FMTP = fmtp
@@ -91,7 +94,7 @@ func (t *Generic) unmarshal(payloadType uint8, clock string, codec string, rtpma
} }
// Marshal implements Format. // Marshal implements Format.
func (t *Generic) Marshal() (string, string) { func (t *Generic) Marshal() (string, map[string]string) {
return t.RTPMap, t.FMTP return t.RTPMap, t.FMTP
} }

View File

@@ -11,8 +11,12 @@ func TestGenericAttributes(t *testing.T) {
format := &Generic{ format := &Generic{
PayloadTyp: 98, PayloadTyp: 98,
RTPMap: "H265/90000", RTPMap: "H265/90000",
FMTP: "profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + FMTP: map[string]string{
"sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", "profile-id": "1",
"sprop-vps": "QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ",
"sprop-sps": "QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=",
"sprop-pps": "RAHgdrAwxmQ=",
},
} }
err := format.Init() err := format.Init()
require.NoError(t, err) require.NoError(t, err)
@@ -27,14 +31,22 @@ func TestGenericMediaDescription(t *testing.T) {
format := &Generic{ format := &Generic{
PayloadTyp: 98, PayloadTyp: 98,
RTPMap: "H265/90000", RTPMap: "H265/90000",
FMTP: "profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + FMTP: map[string]string{
"sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", "profile-id": "1",
"sprop-vps": "QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ",
"sprop-sps": "QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=",
"sprop-pps": "RAHgdrAwxmQ=",
},
} }
err := format.Init() err := format.Init()
require.NoError(t, err) require.NoError(t, err)
rtpmap, fmtp := format.Marshal() rtpmap, fmtp := format.Marshal()
require.Equal(t, "H265/90000", rtpmap) require.Equal(t, "H265/90000", rtpmap)
require.Equal(t, "profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; "+ require.Equal(t, map[string]string{
"sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", fmtp) "profile-id": "1",
"sprop-vps": "QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ",
"sprop-sps": "QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=",
"sprop-pps": "RAHgdrAwxmQ=",
}, fmtp)
} }

View File

@@ -95,34 +95,22 @@ func (t *H264) PayloadType() uint8 {
return t.PayloadTyp return t.PayloadTyp
} }
func (t *H264) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { func (t *H264) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp map[string]string) error {
t.PayloadTyp = payloadType t.PayloadTyp = payloadType
if fmtp != "" { for key, val := range fmtp {
for _, kv := range strings.Split(fmtp, ";") { switch key {
kv = strings.Trim(kv, " ")
if len(kv) == 0 {
continue
}
tmp := strings.SplitN(kv, "=", 2)
if len(tmp) != 2 {
return fmt.Errorf("invalid fmtp attribute (%v)", fmtp)
}
switch tmp[0] {
case "sprop-parameter-sets": case "sprop-parameter-sets":
tmp2 := strings.Split(tmp[1], ",") tmp2 := strings.Split(val, ",")
if len(tmp2) >= 2 { if len(tmp2) >= 2 {
sps, err := base64.StdEncoding.DecodeString(tmp2[0]) sps, err := base64.StdEncoding.DecodeString(tmp2[0])
if err != nil { if err != nil {
return fmt.Errorf("invalid sprop-parameter-sets (%v)", tmp[1]) return fmt.Errorf("invalid sprop-parameter-sets (%v)", val)
} }
pps, err := base64.StdEncoding.DecodeString(tmp2[1]) pps, err := base64.StdEncoding.DecodeString(tmp2[1])
if err != nil { if err != nil {
return fmt.Errorf("invalid sprop-parameter-sets (%v)", tmp[1]) return fmt.Errorf("invalid sprop-parameter-sets (%v)", val)
} }
t.SPS = sps t.SPS = sps
@@ -130,28 +118,29 @@ func (t *H264) unmarshal(payloadType uint8, clock string, codec string, rtpmap s
} }
case "packetization-mode": case "packetization-mode":
tmp2, err := strconv.ParseInt(tmp[1], 10, 64) tmp2, err := strconv.ParseInt(val, 10, 64)
if err != nil { if err != nil {
return fmt.Errorf("invalid packetization-mode (%v)", tmp[1]) return fmt.Errorf("invalid packetization-mode (%v)", val)
} }
t.PacketizationMode = int(tmp2) t.PacketizationMode = int(tmp2)
} }
} }
}
return nil return nil
} }
// Marshal implements Format. // Marshal implements Format.
func (t *H264) Marshal() (string, string) { func (t *H264) Marshal() (string, map[string]string) {
t.mutex.RLock() t.mutex.RLock()
defer t.mutex.RUnlock() defer t.mutex.RUnlock()
var tmp []string fmtp := make(map[string]string)
if t.PacketizationMode != 0 { if t.PacketizationMode != 0 {
tmp = append(tmp, "packetization-mode="+strconv.FormatInt(int64(t.PacketizationMode), 10)) fmtp["packetization-mode"] = strconv.FormatInt(int64(t.PacketizationMode), 10)
} }
var tmp2 []string var tmp2 []string
if t.SPS != nil { if t.SPS != nil {
tmp2 = append(tmp2, base64.StdEncoding.EncodeToString(t.SPS)) tmp2 = append(tmp2, base64.StdEncoding.EncodeToString(t.SPS))
@@ -160,14 +149,10 @@ func (t *H264) Marshal() (string, string) {
tmp2 = append(tmp2, base64.StdEncoding.EncodeToString(t.PPS)) tmp2 = append(tmp2, base64.StdEncoding.EncodeToString(t.PPS))
} }
if tmp2 != nil { if tmp2 != nil {
tmp = append(tmp, "sprop-parameter-sets="+strings.Join(tmp2, ",")) fmtp["sprop-parameter-sets"] = strings.Join(tmp2, ",")
} }
if len(t.SPS) >= 4 { if len(t.SPS) >= 4 {
tmp = append(tmp, "profile-level-id="+strings.ToUpper(hex.EncodeToString(t.SPS[1:4]))) fmtp["profile-level-id"] = strings.ToUpper(hex.EncodeToString(t.SPS[1:4]))
}
var fmtp string
if tmp != nil {
fmtp = strings.Join(tmp, "; ")
} }
return "H264/90000", fmtp return "H264/90000", fmtp

View File

@@ -59,8 +59,11 @@ func TestH264MediaDescription(t *testing.T) {
rtpmap, fmtp := format.Marshal() rtpmap, fmtp := format.Marshal()
require.Equal(t, "H264/90000", rtpmap) require.Equal(t, "H264/90000", rtpmap)
require.Equal(t, "packetization-mode=1; "+ require.Equal(t, map[string]string{
"sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C", fmtp) "packetization-mode": "1",
"sprop-parameter-sets": "Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==",
"profile-level-id": "64000C",
}, fmtp)
}) })
t.Run("no sps/pps", func(t *testing.T) { t.Run("no sps/pps", func(t *testing.T) {
@@ -71,7 +74,9 @@ func TestH264MediaDescription(t *testing.T) {
rtpmap, fmtp := format.Marshal() rtpmap, fmtp := format.Marshal()
require.Equal(t, "H264/90000", rtpmap) require.Equal(t, "H264/90000", rtpmap)
require.Equal(t, "packetization-mode=1", fmtp) require.Equal(t, map[string]string{
"packetization-mode": "1",
}, fmtp)
}) })
} }

View File

@@ -4,7 +4,6 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"strconv" "strconv"
"strings"
"sync" "sync"
"github.com/pion/rtp" "github.com/pion/rtp"
@@ -38,78 +37,61 @@ func (t *H265) PayloadType() uint8 {
return t.PayloadTyp return t.PayloadTyp
} }
func (t *H265) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { func (t *H265) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp map[string]string) error {
t.PayloadTyp = payloadType t.PayloadTyp = payloadType
if fmtp != "" { for key, val := range fmtp {
for _, kv := range strings.Split(fmtp, ";") { switch key {
kv = strings.Trim(kv, " ")
if len(kv) == 0 {
continue
}
tmp := strings.SplitN(kv, "=", 2)
if len(tmp) != 2 {
return fmt.Errorf("invalid fmtp attribute (%v)", fmtp)
}
switch tmp[0] {
case "sprop-vps": case "sprop-vps":
var err error var err error
t.VPS, err = base64.StdEncoding.DecodeString(tmp[1]) t.VPS, err = base64.StdEncoding.DecodeString(val)
if err != nil { if err != nil {
return fmt.Errorf("invalid sprop-vps (%v)", fmtp) return fmt.Errorf("invalid sprop-vps (%v)", fmtp)
} }
case "sprop-sps": case "sprop-sps":
var err error var err error
t.SPS, err = base64.StdEncoding.DecodeString(tmp[1]) t.SPS, err = base64.StdEncoding.DecodeString(val)
if err != nil { if err != nil {
return fmt.Errorf("invalid sprop-sps (%v)", fmtp) return fmt.Errorf("invalid sprop-sps (%v)", fmtp)
} }
case "sprop-pps": case "sprop-pps":
var err error var err error
t.PPS, err = base64.StdEncoding.DecodeString(tmp[1]) t.PPS, err = base64.StdEncoding.DecodeString(val)
if err != nil { if err != nil {
return fmt.Errorf("invalid sprop-pps (%v)", fmtp) return fmt.Errorf("invalid sprop-pps (%v)", fmtp)
} }
case "sprop-max-don-diff": case "sprop-max-don-diff":
tmp, err := strconv.ParseInt(tmp[1], 10, 64) tmp, err := strconv.ParseInt(val, 10, 64)
if err != nil { if err != nil {
return fmt.Errorf("invalid sprop-max-don-diff (%v)", fmtp) return fmt.Errorf("invalid sprop-max-don-diff (%v)", fmtp)
} }
t.MaxDONDiff = int(tmp) t.MaxDONDiff = int(tmp)
} }
} }
}
return nil return nil
} }
// Marshal implements Format. // Marshal implements Format.
func (t *H265) Marshal() (string, string) { func (t *H265) Marshal() (string, map[string]string) {
t.mutex.RLock() t.mutex.RLock()
defer t.mutex.RUnlock() defer t.mutex.RUnlock()
var tmp []string fmtp := make(map[string]string)
if t.VPS != nil { if t.VPS != nil {
tmp = append(tmp, "sprop-vps="+base64.StdEncoding.EncodeToString(t.VPS)) fmtp["sprop-vps"] = base64.StdEncoding.EncodeToString(t.VPS)
} }
if t.SPS != nil { if t.SPS != nil {
tmp = append(tmp, "sprop-sps="+base64.StdEncoding.EncodeToString(t.SPS)) fmtp["sprop-sps"] = base64.StdEncoding.EncodeToString(t.SPS)
} }
if t.PPS != nil { if t.PPS != nil {
tmp = append(tmp, "sprop-pps="+base64.StdEncoding.EncodeToString(t.PPS)) fmtp["sprop-pps"] = base64.StdEncoding.EncodeToString(t.PPS)
} }
if t.MaxDONDiff != 0 { if t.MaxDONDiff != 0 {
tmp = append(tmp, "sprop-max-don-diff="+strconv.FormatInt(int64(t.MaxDONDiff), 10)) fmtp["sprop-max-don-diff"] = strconv.FormatInt(int64(t.MaxDONDiff), 10)
}
var fmtp string
if tmp != nil {
fmtp = strings.Join(tmp, "; ")
} }
return "H265/90000", fmtp return "H265/90000", fmtp

View File

@@ -40,7 +40,11 @@ func TestH265MediaDescription(t *testing.T) {
rtpmap, fmtp := format.Marshal() rtpmap, fmtp := format.Marshal()
require.Equal(t, "H265/90000", rtpmap) require.Equal(t, "H265/90000", rtpmap)
require.Equal(t, "sprop-vps=AQI=; sprop-sps=AwQ=; sprop-pps=BQY=", fmtp) require.Equal(t, map[string]string{
"sprop-vps": "AQI=",
"sprop-sps": "AwQ=",
"sprop-pps": "BQY=",
}, fmtp)
} }
func TestH265DecEncoder(t *testing.T) { func TestH265DecEncoder(t *testing.T) {

View File

@@ -32,7 +32,7 @@ func (t *LPCM) PayloadType() uint8 {
return t.PayloadTyp return t.PayloadTyp
} }
func (t *LPCM) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { func (t *LPCM) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp map[string]string) error {
t.PayloadTyp = payloadType t.PayloadTyp = payloadType
switch codec { switch codec {
@@ -68,7 +68,7 @@ func (t *LPCM) unmarshal(payloadType uint8, clock string, codec string, rtpmap s
} }
// Marshal implements Format. // Marshal implements Format.
func (t *LPCM) Marshal() (string, string) { func (t *LPCM) Marshal() (string, map[string]string) {
var codec string var codec string
switch t.BitDepth { switch t.BitDepth {
case 8: case 8:
@@ -82,7 +82,7 @@ func (t *LPCM) Marshal() (string, string) {
} }
return codec + "/" + strconv.FormatInt(int64(t.SampleRate), 10) + return codec + "/" + strconv.FormatInt(int64(t.SampleRate), 10) +
"/" + strconv.FormatInt(int64(t.ChannelCount), 10), "" "/" + strconv.FormatInt(int64(t.ChannelCount), 10), nil
} }
// PTSEqualsDTS implements Format. // PTSEqualsDTS implements Format.

View File

@@ -34,7 +34,7 @@ func TestLPCMMediaDescription(t *testing.T) {
rtpmap, fmtp := format.Marshal() rtpmap, fmtp := format.Marshal()
require.Equal(t, fmt.Sprintf("L%d/96000/2", ca), rtpmap) require.Equal(t, fmt.Sprintf("L%d/96000/2", ca), rtpmap)
require.Equal(t, "", fmtp) require.Equal(t, map[string]string(nil), fmtp)
}) })
} }
} }

View File

@@ -24,13 +24,13 @@ func (t *MJPEG) PayloadType() uint8 {
return 26 return 26
} }
func (t *MJPEG) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { func (t *MJPEG) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp map[string]string) error {
return nil return nil
} }
// Marshal implements Format. // Marshal implements Format.
func (t *MJPEG) Marshal() (string, string) { func (t *MJPEG) Marshal() (string, map[string]string) {
return "JPEG/90000", "" return "JPEG/90000", nil
} }
// PTSEqualsDTS implements Format. // PTSEqualsDTS implements Format.

View File

@@ -20,7 +20,7 @@ func TestMJPEGMediaDescription(t *testing.T) {
rtpmap, fmtp := format.Marshal() rtpmap, fmtp := format.Marshal()
require.Equal(t, "JPEG/90000", rtpmap) require.Equal(t, "JPEG/90000", rtpmap)
require.Equal(t, "", fmtp) require.Equal(t, map[string]string(nil), fmtp)
} }
func TestMJPEGDecEncoder(t *testing.T) { func TestMJPEGDecEncoder(t *testing.T) {

View File

@@ -22,13 +22,16 @@ func (t *MPEG2Audio) PayloadType() uint8 {
return 14 return 14
} }
func (t *MPEG2Audio) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { func (t *MPEG2Audio) unmarshal(
payloadType uint8, clock string, codec string,
rtpmap string, fmtp map[string]string,
) error {
return nil return nil
} }
// Marshal implements Format. // Marshal implements Format.
func (t *MPEG2Audio) Marshal() (string, string) { func (t *MPEG2Audio) Marshal() (string, map[string]string) {
return "", "" return "", nil
} }
// PTSEqualsDTS implements Format. // PTSEqualsDTS implements Format.

View File

@@ -20,5 +20,5 @@ func TestMPEG2AudioMediaDescription(t *testing.T) {
rtpmap, fmtp := format.Marshal() rtpmap, fmtp := format.Marshal()
require.Equal(t, "", rtpmap) require.Equal(t, "", rtpmap)
require.Equal(t, "", fmtp) require.Equal(t, map[string]string(nil), fmtp)
} }

View File

@@ -22,13 +22,16 @@ func (t *MPEG2Video) PayloadType() uint8 {
return 32 return 32
} }
func (t *MPEG2Video) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { func (t *MPEG2Video) unmarshal(
payloadType uint8, clock string, codec string,
rtpmap string, fmtp map[string]string,
) error {
return nil return nil
} }
// Marshal implements Format. // Marshal implements Format.
func (t *MPEG2Video) Marshal() (string, string) { func (t *MPEG2Video) Marshal() (string, map[string]string) {
return "", "" return "", nil
} }
// PTSEqualsDTS implements Format. // PTSEqualsDTS implements Format.

View File

@@ -20,5 +20,5 @@ func TestMPEG2VideoMediaDescription(t *testing.T) {
rtpmap, fmtp := format.Marshal() rtpmap, fmtp := format.Marshal()
require.Equal(t, "", rtpmap) require.Equal(t, "", rtpmap)
require.Equal(t, "", fmtp) require.Equal(t, map[string]string(nil), fmtp)
} }

View File

@@ -4,7 +4,6 @@ import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"strconv" "strconv"
"strings"
"github.com/pion/rtp" "github.com/pion/rtp"
@@ -36,75 +35,65 @@ func (t *MPEG4Audio) PayloadType() uint8 {
return t.PayloadTyp return t.PayloadTyp
} }
func (t *MPEG4Audio) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { func (t *MPEG4Audio) unmarshal(
payloadType uint8, clock string, codec string,
rtpmap string, fmtp map[string]string,
) error {
t.PayloadTyp = payloadType t.PayloadTyp = payloadType
if fmtp != "" { for key, val := range fmtp {
for _, kv := range strings.Split(fmtp, ";") { switch key {
kv = strings.Trim(kv, " ")
if len(kv) == 0 {
continue
}
tmp := strings.SplitN(kv, "=", 2)
if len(tmp) != 2 {
return fmt.Errorf("invalid fmtp (%v)", fmtp)
}
switch strings.ToLower(tmp[0]) {
case "config": case "config":
enc, err := hex.DecodeString(tmp[1]) enc, err := hex.DecodeString(val)
if err != nil { if err != nil {
return fmt.Errorf("invalid AAC config (%v)", tmp[1]) return fmt.Errorf("invalid AAC config (%v)", val)
} }
t.Config = &mpeg4audio.Config{} t.Config = &mpeg4audio.Config{}
err = t.Config.Unmarshal(enc) err = t.Config.Unmarshal(enc)
if err != nil { if err != nil {
return fmt.Errorf("invalid AAC config (%v)", tmp[1]) return fmt.Errorf("invalid AAC config (%v)", val)
} }
case "sizelength": case "sizelength":
val, err := strconv.ParseUint(tmp[1], 10, 64) n, err := strconv.ParseUint(val, 10, 64)
if err != nil { if err != nil {
return fmt.Errorf("invalid AAC SizeLength (%v)", tmp[1]) return fmt.Errorf("invalid AAC SizeLength (%v)", val)
} }
t.SizeLength = int(val) t.SizeLength = int(n)
case "indexlength": case "indexlength":
val, err := strconv.ParseUint(tmp[1], 10, 64) n, err := strconv.ParseUint(val, 10, 64)
if err != nil { if err != nil {
return fmt.Errorf("invalid AAC IndexLength (%v)", tmp[1]) return fmt.Errorf("invalid AAC IndexLength (%v)", val)
} }
t.IndexLength = int(val) t.IndexLength = int(n)
case "indexdeltalength": case "indexdeltalength":
val, err := strconv.ParseUint(tmp[1], 10, 64) n, err := strconv.ParseUint(val, 10, 64)
if err != nil { if err != nil {
return fmt.Errorf("invalid AAC IndexDeltaLength (%v)", tmp[1]) return fmt.Errorf("invalid AAC IndexDeltaLength (%v)", val)
}
t.IndexDeltaLength = int(val)
} }
t.IndexDeltaLength = int(n)
} }
} }
if t.Config == nil { if t.Config == nil {
return fmt.Errorf("config is missing (%v)", fmtp) return fmt.Errorf("config is missing")
} }
if t.SizeLength == 0 { if t.SizeLength == 0 {
return fmt.Errorf("sizelength is missing (%v)", fmtp) return fmt.Errorf("sizelength is missing")
} }
return nil return nil
} }
// Marshal implements Format. // Marshal implements Format.
func (t *MPEG4Audio) Marshal() (string, string) { func (t *MPEG4Audio) Marshal() (string, map[string]string) {
enc, err := t.Config.Marshal() enc, err := t.Config.Marshal()
if err != nil { if err != nil {
return "", "" return "", nil
} }
sampleRate := t.Config.SampleRate sampleRate := t.Config.SampleRate
@@ -112,27 +101,20 @@ func (t *MPEG4Audio) Marshal() (string, string) {
sampleRate = t.Config.ExtensionSampleRate sampleRate = t.Config.ExtensionSampleRate
} }
fmtp := "profile-level-id=1; " + fmtp := make(map[string]string)
"mode=AAC-hbr; " +
func() string { fmtp["profile-level-id"] = "1"
fmtp["mode"] = "AAC-hbr"
if t.SizeLength > 0 { if t.SizeLength > 0 {
return fmt.Sprintf("sizelength=%d; ", t.SizeLength) fmtp["sizelength"] = strconv.FormatInt(int64(t.SizeLength), 10)
} }
return ""
}() +
func() string {
if t.IndexLength > 0 { if t.IndexLength > 0 {
return fmt.Sprintf("indexlength=%d; ", t.IndexLength) fmtp["indexlength"] = strconv.FormatInt(int64(t.IndexLength), 10)
} }
return ""
}() +
func() string {
if t.IndexDeltaLength > 0 { if t.IndexDeltaLength > 0 {
return fmt.Sprintf("indexdeltalength=%d; ", t.IndexDeltaLength) fmtp["indexdeltalength"] = strconv.FormatInt(int64(t.IndexDeltaLength), 10)
} }
return "" fmtp["config"] = hex.EncodeToString(enc)
}() +
"config=" + hex.EncodeToString(enc)
return "mpeg4-generic/" + strconv.FormatInt(int64(sampleRate), 10) + return "mpeg4-generic/" + strconv.FormatInt(int64(sampleRate), 10) +
"/" + strconv.FormatInt(int64(t.Config.ChannelCount), 10), fmtp "/" + strconv.FormatInt(int64(t.Config.ChannelCount), 10), fmtp

View File

@@ -42,8 +42,14 @@ func TestMPEG4AudioMediaDescription(t *testing.T) {
rtpmap, fmtp := format.Marshal() rtpmap, fmtp := format.Marshal()
require.Equal(t, "mpeg4-generic/48000/2", rtpmap) require.Equal(t, "mpeg4-generic/48000/2", rtpmap)
require.Equal(t, "profile-level-id=1; mode=AAC-hbr; sizelength=13;"+ require.Equal(t, map[string]string{
" indexlength=3; indexdeltalength=3; config=1190", fmtp) "profile-level-id": "1",
"mode": "AAC-hbr",
"sizelength": "13",
"indexlength": "3",
"indexdeltalength": "3",
"config": "1190",
}, fmtp)
} }
func TestMPEG4AudioDecEncoder(t *testing.T) { func TestMPEG4AudioDecEncoder(t *testing.T) {

View File

@@ -33,7 +33,7 @@ func (t *Opus) PayloadType() uint8 {
return t.PayloadTyp return t.PayloadTyp
} }
func (t *Opus) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { func (t *Opus) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp map[string]string) error {
t.PayloadTyp = payloadType t.PayloadTyp = payloadType
tmp := strings.SplitN(clock, "/", 2) tmp := strings.SplitN(clock, "/", 2)
@@ -57,22 +57,9 @@ func (t *Opus) unmarshal(payloadType uint8, clock string, codec string, rtpmap s
return fmt.Errorf("invalid channel count: %d", channelCount) return fmt.Errorf("invalid channel count: %d", channelCount)
} }
if fmtp != "" { for key, val := range fmtp {
for _, kv := range strings.Split(fmtp, ";") { if key == "sprop-stereo" {
kv = strings.Trim(kv, " ") t.IsStereo = (val == "1")
if len(kv) == 0 {
continue
}
tmp := strings.SplitN(kv, "=", 2)
if len(tmp) != 2 {
return fmt.Errorf("invalid fmtp (%v)", fmtp)
}
if strings.ToLower(tmp[0]) == "sprop-stereo" {
t.IsStereo = (tmp[1] == "1")
}
} }
} }
@@ -80,13 +67,15 @@ func (t *Opus) unmarshal(payloadType uint8, clock string, codec string, rtpmap s
} }
// Marshal implements Format. // Marshal implements Format.
func (t *Opus) Marshal() (string, string) { func (t *Opus) Marshal() (string, map[string]string) {
fmtp := "sprop-stereo=" + func() string { fmtp := map[string]string{
"sprop-stereo": func() string {
if t.IsStereo { if t.IsStereo {
return "1" return "1"
} }
return "0" return "0"
}() }(),
}
// RFC7587: The RTP clock rate in "a=rtpmap" MUST be 48000, and the // RFC7587: The RTP clock rate in "a=rtpmap" MUST be 48000, and the
// number of channels MUST be 2. // number of channels MUST be 2.

View File

@@ -26,7 +26,9 @@ func TestOpusMediaDescription(t *testing.T) {
rtpmap, fmtp := format.Marshal() rtpmap, fmtp := format.Marshal()
require.Equal(t, "opus/48000/2", rtpmap) require.Equal(t, "opus/48000/2", rtpmap)
require.Equal(t, "sprop-stereo=1", fmtp) require.Equal(t, map[string]string{
"sprop-stereo": "1",
}, fmtp)
} }
func TestOpusDecEncoder(t *testing.T) { func TestOpusDecEncoder(t *testing.T) {

View File

@@ -32,7 +32,7 @@ func (t *Vorbis) PayloadType() uint8 {
return t.PayloadTyp return t.PayloadTyp
} }
func (t *Vorbis) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { func (t *Vorbis) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp map[string]string) error {
t.PayloadTyp = payloadType t.PayloadTyp = payloadType
tmp := strings.SplitN(clock, "/", 2) tmp := strings.SplitN(clock, "/", 2)
@@ -52,42 +52,33 @@ func (t *Vorbis) unmarshal(payloadType uint8, clock string, codec string, rtpmap
} }
t.ChannelCount = int(channelCount) t.ChannelCount = int(channelCount)
if fmtp != "" { for key, val := range fmtp {
for _, kv := range strings.Split(fmtp, ";") { if key == "configuration" {
kv = strings.Trim(kv, " ") conf, err := base64.StdEncoding.DecodeString(val)
if len(kv) == 0 {
continue
}
tmp := strings.SplitN(kv, "=", 2)
if len(tmp) != 2 {
return fmt.Errorf("invalid fmtp (%v)", fmtp)
}
if tmp[0] == "configuration" {
conf, err := base64.StdEncoding.DecodeString(tmp[1])
if err != nil { if err != nil {
return fmt.Errorf("invalid AAC config (%v)", tmp[1]) return fmt.Errorf("invalid AAC config (%v)", val)
} }
t.Configuration = conf t.Configuration = conf
} }
} }
}
if t.Configuration == nil { if t.Configuration == nil {
return fmt.Errorf("config is missing (%v)", fmtp) return fmt.Errorf("config is missing")
} }
return nil return nil
} }
// Marshal implements Format. // Marshal implements Format.
func (t *Vorbis) Marshal() (string, string) { func (t *Vorbis) Marshal() (string, map[string]string) {
fmtp := map[string]string{
"configuration": base64.StdEncoding.EncodeToString(t.Configuration),
}
return "VORBIS/" + strconv.FormatInt(int64(t.SampleRate), 10) + return "VORBIS/" + strconv.FormatInt(int64(t.SampleRate), 10) +
"/" + strconv.FormatInt(int64(t.ChannelCount), 10), "/" + strconv.FormatInt(int64(t.ChannelCount), 10),
"configuration=" + base64.StdEncoding.EncodeToString(t.Configuration) fmtp
} }
// PTSEqualsDTS implements Format. // PTSEqualsDTS implements Format.

View File

@@ -30,5 +30,7 @@ func TestVorbisMediaDescription(t *testing.T) {
rtpmap, fmtp := format.Marshal() rtpmap, fmtp := format.Marshal()
require.Equal(t, "VORBIS/48000/2", rtpmap) require.Equal(t, "VORBIS/48000/2", rtpmap)
require.Equal(t, "configuration=AQIDBA==", fmtp) require.Equal(t, map[string]string{
"configuration": "AQIDBA==",
}, fmtp)
} }

View File

@@ -3,7 +3,6 @@ package format
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"github.com/pion/rtp" "github.com/pion/rtp"
@@ -32,57 +31,40 @@ func (t *VP8) PayloadType() uint8 {
return t.PayloadTyp return t.PayloadTyp
} }
func (t *VP8) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { func (t *VP8) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp map[string]string) error {
t.PayloadTyp = payloadType t.PayloadTyp = payloadType
if fmtp != "" { for key, val := range fmtp {
for _, kv := range strings.Split(fmtp, ";") { switch key {
kv = strings.Trim(kv, " ")
if len(kv) == 0 {
continue
}
tmp := strings.SplitN(kv, "=", 2)
if len(tmp) != 2 {
return fmt.Errorf("invalid fmtp attribute (%v)", fmtp)
}
switch tmp[0] {
case "max-fr": case "max-fr":
val, err := strconv.ParseUint(tmp[1], 10, 64) n, err := strconv.ParseUint(val, 10, 64)
if err != nil { if err != nil {
return fmt.Errorf("invalid max-fr (%v)", tmp[1]) return fmt.Errorf("invalid max-fr (%v)", val)
} }
v2 := int(val) v2 := int(n)
t.MaxFR = &v2 t.MaxFR = &v2
case "max-fs": case "max-fs":
val, err := strconv.ParseUint(tmp[1], 10, 64) n, err := strconv.ParseUint(val, 10, 64)
if err != nil { if err != nil {
return fmt.Errorf("invalid max-fs (%v)", tmp[1]) return fmt.Errorf("invalid max-fs (%v)", val)
} }
v2 := int(val) v2 := int(n)
t.MaxFS = &v2 t.MaxFS = &v2
} }
} }
}
return nil return nil
} }
// Marshal implements Format. // Marshal implements Format.
func (t *VP8) Marshal() (string, string) { func (t *VP8) Marshal() (string, map[string]string) {
var tmp []string fmtp := make(map[string]string)
if t.MaxFR != nil { if t.MaxFR != nil {
tmp = append(tmp, "max-fr="+strconv.FormatInt(int64(*t.MaxFR), 10)) fmtp["max-fr"] = strconv.FormatInt(int64(*t.MaxFR), 10)
} }
if t.MaxFS != nil { if t.MaxFS != nil {
tmp = append(tmp, "max-fs="+strconv.FormatInt(int64(*t.MaxFS), 10)) fmtp["max-fs"] = strconv.FormatInt(int64(*t.MaxFS), 10)
}
var fmtp string
if tmp != nil {
fmtp = strings.Join(tmp, ";")
} }
return "VP8/90000", fmtp return "VP8/90000", fmtp

View File

@@ -28,7 +28,10 @@ func TestVP8MediaDescription(t *testing.T) {
rtpmap, fmtp := format.Marshal() rtpmap, fmtp := format.Marshal()
require.Equal(t, "VP8/90000", rtpmap) require.Equal(t, "VP8/90000", rtpmap)
require.Equal(t, "max-fr=123;max-fs=456", fmtp) require.Equal(t, map[string]string{
"max-fr": "123",
"max-fs": "456",
}, fmtp)
} }
func TestVP8DecEncoder(t *testing.T) { func TestVP8DecEncoder(t *testing.T) {

View File

@@ -3,7 +3,6 @@ package format
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"github.com/pion/rtp" "github.com/pion/rtp"
@@ -33,68 +32,51 @@ func (t *VP9) PayloadType() uint8 {
return t.PayloadTyp return t.PayloadTyp
} }
func (t *VP9) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { func (t *VP9) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp map[string]string) error {
t.PayloadTyp = payloadType t.PayloadTyp = payloadType
if fmtp != "" { for key, val := range fmtp {
for _, kv := range strings.Split(fmtp, ";") { switch key {
kv = strings.Trim(kv, " ")
if len(kv) == 0 {
continue
}
tmp := strings.SplitN(kv, "=", 2)
if len(tmp) != 2 {
return fmt.Errorf("invalid fmtp attribute (%v)", fmtp)
}
switch tmp[0] {
case "max-fr": case "max-fr":
val, err := strconv.ParseUint(tmp[1], 10, 64) n, err := strconv.ParseUint(val, 10, 64)
if err != nil { if err != nil {
return fmt.Errorf("invalid max-fr (%v)", tmp[1]) return fmt.Errorf("invalid max-fr (%v)", val)
} }
v2 := int(val) v2 := int(n)
t.MaxFR = &v2 t.MaxFR = &v2
case "max-fs": case "max-fs":
val, err := strconv.ParseUint(tmp[1], 10, 64) n, err := strconv.ParseUint(val, 10, 64)
if err != nil { if err != nil {
return fmt.Errorf("invalid max-fs (%v)", tmp[1]) return fmt.Errorf("invalid max-fs (%v)", val)
} }
v2 := int(val) v2 := int(n)
t.MaxFS = &v2 t.MaxFS = &v2
case "profile-id": case "profile-id":
val, err := strconv.ParseUint(tmp[1], 10, 64) n, err := strconv.ParseUint(val, 10, 64)
if err != nil { if err != nil {
return fmt.Errorf("invalid profile-id (%v)", tmp[1]) return fmt.Errorf("invalid profile-id (%v)", val)
} }
v2 := int(val) v2 := int(n)
t.ProfileID = &v2 t.ProfileID = &v2
} }
} }
}
return nil return nil
} }
// Marshal implements Format. // Marshal implements Format.
func (t *VP9) Marshal() (string, string) { func (t *VP9) Marshal() (string, map[string]string) {
var tmp []string fmtp := make(map[string]string)
if t.MaxFR != nil { if t.MaxFR != nil {
tmp = append(tmp, "max-fr="+strconv.FormatInt(int64(*t.MaxFR), 10)) fmtp["max-fr"] = strconv.FormatInt(int64(*t.MaxFR), 10)
} }
if t.MaxFS != nil { if t.MaxFS != nil {
tmp = append(tmp, "max-fs="+strconv.FormatInt(int64(*t.MaxFS), 10)) fmtp["max-fs"] = strconv.FormatInt(int64(*t.MaxFS), 10)
} }
if t.ProfileID != nil { if t.ProfileID != nil {
tmp = append(tmp, "profile-id="+strconv.FormatInt(int64(*t.ProfileID), 10)) fmtp["profile-id"] = strconv.FormatInt(int64(*t.ProfileID), 10)
}
var fmtp string
if tmp != nil {
fmtp = strings.Join(tmp, ";")
} }
return "VP9/90000", fmtp return "VP9/90000", fmtp

View File

@@ -30,7 +30,11 @@ func TestVP9MediaDescription(t *testing.T) {
rtpmap, fmtp := format.Marshal() rtpmap, fmtp := format.Marshal()
require.Equal(t, "VP9/90000", rtpmap) require.Equal(t, "VP9/90000", rtpmap)
require.Equal(t, "max-fr=123;max-fs=456;profile-id=789", fmtp) require.Equal(t, map[string]string{
"max-fr": "123",
"max-fs": "456",
"profile-id": "789",
}, fmtp)
} }
func TestVP9DecEncoder(t *testing.T) { func TestVP9DecEncoder(t *testing.T) {

View File

@@ -4,6 +4,7 @@ package media
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"sort"
"strconv" "strconv"
"strings" "strings"
@@ -130,10 +131,23 @@ func (m Media) Marshal() *psdp.MediaDescription {
}) })
} }
if fmtp != "" { if len(fmtp) != 0 {
keys := make([]string, len(fmtp))
i := 0
for key := range fmtp {
keys[i] = key
i++
}
sort.Strings(keys)
tmp := make([]string, len(fmtp))
for i, key := range keys {
tmp[i] = key + "=" + fmtp[key]
}
md.Attributes = append(md.Attributes, psdp.Attribute{ md.Attributes = append(md.Attributes, psdp.Attribute{
Key: "fmtp", Key: "fmtp",
Value: typ + " " + fmtp, Value: typ + " " + strings.Join(tmp, "; "),
}) })
} }
} }

View File

@@ -49,7 +49,7 @@ var casesMedias = []struct {
"m=video 0 RTP/AVP 97\r\n" + "m=video 0 RTP/AVP 97\r\n" +
"a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=v\r\n" + "a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=v\r\n" +
"a=rtpmap:97 H264/90000\r\n" + "a=rtpmap:97 H264/90000\r\n" +
"a=fmtp:97 packetization-mode=1; sprop-parameter-sets=Z2QAKKy0A8ARPyo=,aO4Bniw=; profile-level-id=640028\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" + "m=audio 0 RTP/AVP 0\r\n" +
"a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=a\r\n" + "a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=a\r\n" +
"a=recvonly\r\n" + "a=recvonly\r\n" +
@@ -302,7 +302,9 @@ var casesMedias = []struct {
&format.Generic{ &format.Generic{
PayloadTyp: 97, PayloadTyp: 97,
RTPMap: "rtx/90000", RTPMap: "rtx/90000",
FMTP: "apt=96", FMTP: map[string]string{
"apt": "96",
},
ClockRat: 90000, ClockRat: 90000,
}, },
&format.VP9{ &format.VP9{
@@ -311,7 +313,9 @@ var casesMedias = []struct {
&format.Generic{ &format.Generic{
PayloadTyp: 99, PayloadTyp: 99,
RTPMap: "rtx/90000", RTPMap: "rtx/90000",
FMTP: "apt=98", FMTP: map[string]string{
"apt": "98",
},
ClockRat: 90000, ClockRat: 90000,
}, },
&format.H264{ &format.H264{
@@ -321,7 +325,9 @@ var casesMedias = []struct {
&format.Generic{ &format.Generic{
PayloadTyp: 101, PayloadTyp: 101,
RTPMap: "rtx/90000", RTPMap: "rtx/90000",
FMTP: "apt=100", FMTP: map[string]string{
"apt": "100",
},
ClockRat: 90000, ClockRat: 90000,
}, },
&format.Generic{ &format.Generic{
@@ -332,7 +338,9 @@ var casesMedias = []struct {
&format.Generic{ &format.Generic{
PayloadTyp: 124, PayloadTyp: 124,
RTPMap: "rtx/90000", RTPMap: "rtx/90000",
FMTP: "apt=127", FMTP: map[string]string{
"apt": "127",
},
ClockRat: 90000, ClockRat: 90000,
}, },
&format.Generic{ &format.Generic{
@@ -364,8 +372,8 @@ var casesMedias = []struct {
"m=video 0 RTP/AVP 96 98\r\n" + "m=video 0 RTP/AVP 96 98\r\n" +
"a=control\r\n" + "a=control\r\n" +
"a=rtpmap:96 H264/90000\r\n" + "a=rtpmap:96 H264/90000\r\n" +
"a=fmtp:96 packetization-mode=1;" + "a=fmtp:96 packetization-mode=1; profile-level-id=4D002A; " +
" sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==; profile-level-id=4D002A\r\n" + "sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==\r\n" +
"a=rtpmap:98 MetaData\r\n", "a=rtpmap:98 MetaData\r\n",
Medias{ Medias{
{ {
@@ -511,7 +519,9 @@ func TestMediasFindFormat(t *testing.T) {
tr := &format.Generic{ tr := &format.Generic{
PayloadTyp: 97, PayloadTyp: 97,
RTPMap: "rtx/90000", RTPMap: "rtx/90000",
FMTP: "apt=96", FMTP: map[string]string{
"apt": "96",
},
ClockRat: 90000, ClockRat: 90000,
} }