diff --git a/pkg/format/format.go b/pkg/format/format.go index c07877fb..be59453c 100644 --- a/pkg/format/format.go +++ b/pkg/format/format.go @@ -32,6 +32,31 @@ func getCodecAndClock(rtpMap string) (string, string) { 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. // It defines a codec and a payload type used to ship the media. type Format interface { @@ -44,10 +69,10 @@ type Format interface { // PayloadType returns the payload type. 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() (string, string) + Marshal() (string, map[string]string) // PTSEqualsDTS checks whether PTS is equal to DTS in RTP packets. PTSEqualsDTS(*rtp.Packet) bool @@ -74,7 +99,7 @@ func Unmarshal(md *psdp.MediaDescription, payloadTypeStr string) (Format, error) rtpMap := getFormatAttribute(md.Attributes, payloadType, "rtpmap") codec, clock := getCodecAndClock(rtpMap) codec = strings.ToLower(codec) - fmtp := getFormatAttribute(md.Attributes, payloadType, "fmtp") + fmtp := decodeFMTP(getFormatAttribute(md.Attributes, payloadType, "fmtp")) format := func() Format { switch { diff --git a/pkg/format/format_test.go b/pkg/format/format_test.go index b62176f2..2fbaa23a 100644 --- a/pkg/format/format_test.go +++ b/pkg/format/format_test.go @@ -721,27 +721,6 @@ func TestNewFromMediaDescriptionErrors(t *testing.T) { }, "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", &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", @@ -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", @@ -908,7 +887,7 @@ func TestNewFromMediaDescriptionErrors(t *testing.T) { }, }, }, - "config is missing (aa=bb)", + "config is missing", }, { "audio opus invalid 1", @@ -961,27 +940,6 @@ func TestNewFromMediaDescriptionErrors(t *testing.T) { }, "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", &psdp.MediaDescription{ diff --git a/pkg/format/g711.go b/pkg/format/g711.go index f93167a7..bae4e30d 100644 --- a/pkg/format/g711.go +++ b/pkg/format/g711.go @@ -30,17 +30,17 @@ func (t *G711) PayloadType() uint8 { 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) return nil } // Marshal implements Format. -func (t *G711) Marshal() (string, string) { +func (t *G711) Marshal() (string, map[string]string) { if t.MULaw { - return "PCMU/8000", "" + return "PCMU/8000", nil } - return "PCMA/8000", "" + return "PCMA/8000", nil } // PTSEqualsDTS implements Format. diff --git a/pkg/format/g711_test.go b/pkg/format/g711_test.go index 4ed4f94a..136c1e5f 100644 --- a/pkg/format/g711_test.go +++ b/pkg/format/g711_test.go @@ -28,7 +28,7 @@ func TestG711MediaDescription(t *testing.T) { rtpmap, fmtp := format.Marshal() 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) { @@ -38,7 +38,7 @@ func TestG711MediaDescription(t *testing.T) { rtpmap, fmtp := format.Marshal() require.Equal(t, "PCMU/8000", rtpmap) - require.Equal(t, "", fmtp) + require.Equal(t, map[string]string(nil), fmtp) }) } diff --git a/pkg/format/g722.go b/pkg/format/g722.go index bea86999..5c086c66 100644 --- a/pkg/format/g722.go +++ b/pkg/format/g722.go @@ -24,13 +24,13 @@ func (t *G722) PayloadType() uint8 { 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 } // Marshal implements Format. -func (t *G722) Marshal() (string, string) { - return "G722/8000", "" +func (t *G722) Marshal() (string, map[string]string) { + return "G722/8000", nil } // PTSEqualsDTS implements Format. diff --git a/pkg/format/g722_test.go b/pkg/format/g722_test.go index d08006fc..7e902a99 100644 --- a/pkg/format/g722_test.go +++ b/pkg/format/g722_test.go @@ -20,7 +20,7 @@ func TestG722MediaDescription(t *testing.T) { rtpmap, fmtp := format.Marshal() require.Equal(t, "G722/8000", rtpmap) - require.Equal(t, "", fmtp) + require.Equal(t, map[string]string(nil), fmtp) } func TestG722DecEncoder(t *testing.T) { diff --git a/pkg/format/generic.go b/pkg/format/generic.go index ca4fea34..2b0be3cd 100644 --- a/pkg/format/generic.go +++ b/pkg/format/generic.go @@ -55,7 +55,7 @@ func findClockRate(payloadType uint8, rtpMap string) (int, error) { type Generic struct { PayloadTyp uint8 RTPMap string - FMTP string + FMTP map[string]string // clock rate of the format. Filled automatically. ClockRat int @@ -82,7 +82,10 @@ func (t *Generic) PayloadType() uint8 { 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.RTPMap = rtpmap t.FMTP = fmtp @@ -91,7 +94,7 @@ func (t *Generic) unmarshal(payloadType uint8, clock string, codec string, rtpma } // Marshal implements Format. -func (t *Generic) Marshal() (string, string) { +func (t *Generic) Marshal() (string, map[string]string) { return t.RTPMap, t.FMTP } diff --git a/pkg/format/generic_test.go b/pkg/format/generic_test.go index c2ce5fae..06a2cef5 100644 --- a/pkg/format/generic_test.go +++ b/pkg/format/generic_test.go @@ -11,8 +11,12 @@ func TestGenericAttributes(t *testing.T) { format := &Generic{ PayloadTyp: 98, RTPMap: "H265/90000", - FMTP: "profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + - "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", + FMTP: map[string]string{ + "profile-id": "1", + "sprop-vps": "QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ", + "sprop-sps": "QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=", + "sprop-pps": "RAHgdrAwxmQ=", + }, } err := format.Init() require.NoError(t, err) @@ -27,14 +31,22 @@ func TestGenericMediaDescription(t *testing.T) { format := &Generic{ PayloadTyp: 98, RTPMap: "H265/90000", - FMTP: "profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + - "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", + FMTP: map[string]string{ + "profile-id": "1", + "sprop-vps": "QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ", + "sprop-sps": "QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=", + "sprop-pps": "RAHgdrAwxmQ=", + }, } err := format.Init() require.NoError(t, err) rtpmap, fmtp := format.Marshal() require.Equal(t, "H265/90000", rtpmap) - require.Equal(t, "profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; "+ - "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", fmtp) + require.Equal(t, map[string]string{ + "profile-id": "1", + "sprop-vps": "QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ", + "sprop-sps": "QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=", + "sprop-pps": "RAHgdrAwxmQ=", + }, fmtp) } diff --git a/pkg/format/h264.go b/pkg/format/h264.go index bf8453eb..263bc511 100644 --- a/pkg/format/h264.go +++ b/pkg/format/h264.go @@ -95,48 +95,35 @@ func (t *H264) PayloadType() uint8 { 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 - if fmtp != "" { - for _, kv := range strings.Split(fmtp, ";") { - 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": - tmp2 := strings.Split(tmp[1], ",") - if len(tmp2) >= 2 { - sps, err := base64.StdEncoding.DecodeString(tmp2[0]) - if err != nil { - return fmt.Errorf("invalid sprop-parameter-sets (%v)", tmp[1]) - } - - pps, err := base64.StdEncoding.DecodeString(tmp2[1]) - if err != nil { - return fmt.Errorf("invalid sprop-parameter-sets (%v)", tmp[1]) - } - - t.SPS = sps - t.PPS = pps - } - - case "packetization-mode": - tmp2, err := strconv.ParseInt(tmp[1], 10, 64) + for key, val := range fmtp { + switch key { + case "sprop-parameter-sets": + tmp2 := strings.Split(val, ",") + if len(tmp2) >= 2 { + sps, err := base64.StdEncoding.DecodeString(tmp2[0]) if err != nil { - return fmt.Errorf("invalid packetization-mode (%v)", tmp[1]) + return fmt.Errorf("invalid sprop-parameter-sets (%v)", val) } - t.PacketizationMode = int(tmp2) + pps, err := base64.StdEncoding.DecodeString(tmp2[1]) + if err != nil { + return fmt.Errorf("invalid sprop-parameter-sets (%v)", val) + } + + t.SPS = sps + t.PPS = pps } + + case "packetization-mode": + tmp2, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return fmt.Errorf("invalid packetization-mode (%v)", val) + } + + t.PacketizationMode = int(tmp2) } } @@ -144,14 +131,16 @@ func (t *H264) unmarshal(payloadType uint8, clock string, codec string, rtpmap s } // Marshal implements Format. -func (t *H264) Marshal() (string, string) { +func (t *H264) Marshal() (string, map[string]string) { t.mutex.RLock() defer t.mutex.RUnlock() - var tmp []string + fmtp := make(map[string]string) + 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 if t.SPS != nil { 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)) } if tmp2 != nil { - tmp = append(tmp, "sprop-parameter-sets="+strings.Join(tmp2, ",")) + fmtp["sprop-parameter-sets"] = strings.Join(tmp2, ",") } if len(t.SPS) >= 4 { - tmp = append(tmp, "profile-level-id="+strings.ToUpper(hex.EncodeToString(t.SPS[1:4]))) - } - var fmtp string - if tmp != nil { - fmtp = strings.Join(tmp, "; ") + fmtp["profile-level-id"] = strings.ToUpper(hex.EncodeToString(t.SPS[1:4])) } return "H264/90000", fmtp diff --git a/pkg/format/h264_test.go b/pkg/format/h264_test.go index b5c3bcad..c225e9ef 100644 --- a/pkg/format/h264_test.go +++ b/pkg/format/h264_test.go @@ -59,8 +59,11 @@ func TestH264MediaDescription(t *testing.T) { rtpmap, fmtp := format.Marshal() require.Equal(t, "H264/90000", rtpmap) - require.Equal(t, "packetization-mode=1; "+ - "sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C", fmtp) + require.Equal(t, map[string]string{ + "packetization-mode": "1", + "sprop-parameter-sets": "Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==", + "profile-level-id": "64000C", + }, fmtp) }) t.Run("no sps/pps", func(t *testing.T) { @@ -71,7 +74,9 @@ func TestH264MediaDescription(t *testing.T) { rtpmap, fmtp := format.Marshal() require.Equal(t, "H264/90000", rtpmap) - require.Equal(t, "packetization-mode=1", fmtp) + require.Equal(t, map[string]string{ + "packetization-mode": "1", + }, fmtp) }) } diff --git a/pkg/format/h265.go b/pkg/format/h265.go index 6e5c5f72..bf8a818b 100644 --- a/pkg/format/h265.go +++ b/pkg/format/h265.go @@ -4,7 +4,6 @@ import ( "encoding/base64" "fmt" "strconv" - "strings" "sync" "github.com/pion/rtp" @@ -38,51 +37,38 @@ func (t *H265) PayloadType() uint8 { 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 - if fmtp != "" { - for _, kv := range strings.Split(fmtp, ";") { - kv = strings.Trim(kv, " ") - - if len(kv) == 0 { - continue + for key, val := range fmtp { + switch key { + case "sprop-vps": + var err error + t.VPS, err = base64.StdEncoding.DecodeString(val) + if err != nil { + return fmt.Errorf("invalid sprop-vps (%v)", fmtp) } - tmp := strings.SplitN(kv, "=", 2) - if len(tmp) != 2 { - return fmt.Errorf("invalid fmtp attribute (%v)", fmtp) + case "sprop-sps": + var err error + t.SPS, err = base64.StdEncoding.DecodeString(val) + if err != nil { + return fmt.Errorf("invalid sprop-sps (%v)", fmtp) } - switch tmp[0] { - case "sprop-vps": - var err error - t.VPS, err = base64.StdEncoding.DecodeString(tmp[1]) - if err != nil { - return fmt.Errorf("invalid sprop-vps (%v)", fmtp) - } - - case "sprop-sps": - var err error - t.SPS, err = base64.StdEncoding.DecodeString(tmp[1]) - if err != nil { - return fmt.Errorf("invalid sprop-sps (%v)", fmtp) - } - - case "sprop-pps": - var err error - t.PPS, err = base64.StdEncoding.DecodeString(tmp[1]) - if err != nil { - return fmt.Errorf("invalid sprop-pps (%v)", fmtp) - } - - case "sprop-max-don-diff": - tmp, err := strconv.ParseInt(tmp[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid sprop-max-don-diff (%v)", fmtp) - } - t.MaxDONDiff = int(tmp) + case "sprop-pps": + var err error + t.PPS, err = base64.StdEncoding.DecodeString(val) + if err != nil { + return fmt.Errorf("invalid sprop-pps (%v)", fmtp) } + + case "sprop-max-don-diff": + tmp, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return fmt.Errorf("invalid sprop-max-don-diff (%v)", fmtp) + } + t.MaxDONDiff = int(tmp) } } @@ -90,26 +76,22 @@ func (t *H265) unmarshal(payloadType uint8, clock string, codec string, rtpmap s } // Marshal implements Format. -func (t *H265) Marshal() (string, string) { +func (t *H265) Marshal() (string, map[string]string) { t.mutex.RLock() defer t.mutex.RUnlock() - var tmp []string + fmtp := make(map[string]string) 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 { - tmp = append(tmp, "sprop-sps="+base64.StdEncoding.EncodeToString(t.SPS)) + fmtp["sprop-sps"] = base64.StdEncoding.EncodeToString(t.SPS) } 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 { - tmp = append(tmp, "sprop-max-don-diff="+strconv.FormatInt(int64(t.MaxDONDiff), 10)) - } - var fmtp string - if tmp != nil { - fmtp = strings.Join(tmp, "; ") + fmtp["sprop-max-don-diff"] = strconv.FormatInt(int64(t.MaxDONDiff), 10) } return "H265/90000", fmtp diff --git a/pkg/format/h265_test.go b/pkg/format/h265_test.go index 472cb01b..a4e77eae 100644 --- a/pkg/format/h265_test.go +++ b/pkg/format/h265_test.go @@ -40,7 +40,11 @@ func TestH265MediaDescription(t *testing.T) { rtpmap, fmtp := format.Marshal() 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) { diff --git a/pkg/format/lpcm.go b/pkg/format/lpcm.go index b58822dc..0a6beebb 100644 --- a/pkg/format/lpcm.go +++ b/pkg/format/lpcm.go @@ -32,7 +32,7 @@ func (t *LPCM) PayloadType() uint8 { 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 switch codec { @@ -68,7 +68,7 @@ func (t *LPCM) unmarshal(payloadType uint8, clock string, codec string, rtpmap s } // Marshal implements Format. -func (t *LPCM) Marshal() (string, string) { +func (t *LPCM) Marshal() (string, map[string]string) { var codec string switch t.BitDepth { case 8: @@ -82,7 +82,7 @@ func (t *LPCM) Marshal() (string, string) { } return codec + "/" + strconv.FormatInt(int64(t.SampleRate), 10) + - "/" + strconv.FormatInt(int64(t.ChannelCount), 10), "" + "/" + strconv.FormatInt(int64(t.ChannelCount), 10), nil } // PTSEqualsDTS implements Format. diff --git a/pkg/format/lpcm_test.go b/pkg/format/lpcm_test.go index 47cbefc9..2734ee63 100644 --- a/pkg/format/lpcm_test.go +++ b/pkg/format/lpcm_test.go @@ -34,7 +34,7 @@ func TestLPCMMediaDescription(t *testing.T) { rtpmap, fmtp := format.Marshal() require.Equal(t, fmt.Sprintf("L%d/96000/2", ca), rtpmap) - require.Equal(t, "", fmtp) + require.Equal(t, map[string]string(nil), fmtp) }) } } diff --git a/pkg/format/mjpeg.go b/pkg/format/mjpeg.go index 4c2481f5..4d1302ff 100644 --- a/pkg/format/mjpeg.go +++ b/pkg/format/mjpeg.go @@ -24,13 +24,13 @@ func (t *MJPEG) PayloadType() uint8 { 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 } // Marshal implements Format. -func (t *MJPEG) Marshal() (string, string) { - return "JPEG/90000", "" +func (t *MJPEG) Marshal() (string, map[string]string) { + return "JPEG/90000", nil } // PTSEqualsDTS implements Format. diff --git a/pkg/format/mjpeg_test.go b/pkg/format/mjpeg_test.go index 64013a50..b398af49 100644 --- a/pkg/format/mjpeg_test.go +++ b/pkg/format/mjpeg_test.go @@ -20,7 +20,7 @@ func TestMJPEGMediaDescription(t *testing.T) { rtpmap, fmtp := format.Marshal() require.Equal(t, "JPEG/90000", rtpmap) - require.Equal(t, "", fmtp) + require.Equal(t, map[string]string(nil), fmtp) } func TestMJPEGDecEncoder(t *testing.T) { diff --git a/pkg/format/mpeg2_audio.go b/pkg/format/mpeg2_audio.go index f1460877..755f5f31 100644 --- a/pkg/format/mpeg2_audio.go +++ b/pkg/format/mpeg2_audio.go @@ -22,13 +22,16 @@ func (t *MPEG2Audio) PayloadType() uint8 { 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 } // Marshal implements Format. -func (t *MPEG2Audio) Marshal() (string, string) { - return "", "" +func (t *MPEG2Audio) Marshal() (string, map[string]string) { + return "", nil } // PTSEqualsDTS implements Format. diff --git a/pkg/format/mpeg2_audio_test.go b/pkg/format/mpeg2_audio_test.go index 9333315a..fef3f808 100644 --- a/pkg/format/mpeg2_audio_test.go +++ b/pkg/format/mpeg2_audio_test.go @@ -20,5 +20,5 @@ func TestMPEG2AudioMediaDescription(t *testing.T) { rtpmap, fmtp := format.Marshal() require.Equal(t, "", rtpmap) - require.Equal(t, "", fmtp) + require.Equal(t, map[string]string(nil), fmtp) } diff --git a/pkg/format/mpeg2_video.go b/pkg/format/mpeg2_video.go index 89b74df9..d0b6e1dd 100644 --- a/pkg/format/mpeg2_video.go +++ b/pkg/format/mpeg2_video.go @@ -22,13 +22,16 @@ func (t *MPEG2Video) PayloadType() uint8 { 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 } // Marshal implements Format. -func (t *MPEG2Video) Marshal() (string, string) { - return "", "" +func (t *MPEG2Video) Marshal() (string, map[string]string) { + return "", nil } // PTSEqualsDTS implements Format. diff --git a/pkg/format/mpeg2_video_test.go b/pkg/format/mpeg2_video_test.go index 80956dc6..9bba89da 100644 --- a/pkg/format/mpeg2_video_test.go +++ b/pkg/format/mpeg2_video_test.go @@ -20,5 +20,5 @@ func TestMPEG2VideoMediaDescription(t *testing.T) { rtpmap, fmtp := format.Marshal() require.Equal(t, "", rtpmap) - require.Equal(t, "", fmtp) + require.Equal(t, map[string]string(nil), fmtp) } diff --git a/pkg/format/mpeg4_audio.go b/pkg/format/mpeg4_audio.go index e878097b..2d606988 100644 --- a/pkg/format/mpeg4_audio.go +++ b/pkg/format/mpeg4_audio.go @@ -4,7 +4,6 @@ import ( "encoding/hex" "fmt" "strconv" - "strings" "github.com/pion/rtp" @@ -36,75 +35,65 @@ func (t *MPEG4Audio) PayloadType() uint8 { 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 - if fmtp != "" { - for _, kv := range strings.Split(fmtp, ";") { - kv = strings.Trim(kv, " ") - - if len(kv) == 0 { - continue + for key, val := range fmtp { + switch key { + case "config": + enc, err := hex.DecodeString(val) + if err != nil { + return fmt.Errorf("invalid AAC config (%v)", val) } - tmp := strings.SplitN(kv, "=", 2) - if len(tmp) != 2 { - return fmt.Errorf("invalid fmtp (%v)", fmtp) + t.Config = &mpeg4audio.Config{} + err = t.Config.Unmarshal(enc) + if err != nil { + return fmt.Errorf("invalid AAC config (%v)", val) } - switch strings.ToLower(tmp[0]) { - case "config": - enc, err := hex.DecodeString(tmp[1]) - if err != nil { - return fmt.Errorf("invalid AAC config (%v)", tmp[1]) - } - - t.Config = &mpeg4audio.Config{} - err = t.Config.Unmarshal(enc) - if err != nil { - return fmt.Errorf("invalid AAC config (%v)", tmp[1]) - } - - case "sizelength": - val, err := strconv.ParseUint(tmp[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid AAC SizeLength (%v)", tmp[1]) - } - t.SizeLength = int(val) - - case "indexlength": - val, err := strconv.ParseUint(tmp[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid AAC IndexLength (%v)", tmp[1]) - } - t.IndexLength = int(val) - - case "indexdeltalength": - val, err := strconv.ParseUint(tmp[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid AAC IndexDeltaLength (%v)", tmp[1]) - } - t.IndexDeltaLength = int(val) + case "sizelength": + n, err := strconv.ParseUint(val, 10, 64) + if err != nil { + return fmt.Errorf("invalid AAC SizeLength (%v)", val) } + t.SizeLength = int(n) + + case "indexlength": + n, err := strconv.ParseUint(val, 10, 64) + if err != nil { + return fmt.Errorf("invalid AAC IndexLength (%v)", val) + } + t.IndexLength = int(n) + + case "indexdeltalength": + n, err := strconv.ParseUint(val, 10, 64) + if err != nil { + return fmt.Errorf("invalid AAC IndexDeltaLength (%v)", val) + } + t.IndexDeltaLength = int(n) } } if t.Config == nil { - return fmt.Errorf("config is missing (%v)", fmtp) + return fmt.Errorf("config is missing") } if t.SizeLength == 0 { - return fmt.Errorf("sizelength is missing (%v)", fmtp) + return fmt.Errorf("sizelength is missing") } return nil } // Marshal implements Format. -func (t *MPEG4Audio) Marshal() (string, string) { +func (t *MPEG4Audio) Marshal() (string, map[string]string) { enc, err := t.Config.Marshal() if err != nil { - return "", "" + return "", nil } sampleRate := t.Config.SampleRate @@ -112,27 +101,20 @@ func (t *MPEG4Audio) Marshal() (string, string) { sampleRate = t.Config.ExtensionSampleRate } - fmtp := "profile-level-id=1; " + - "mode=AAC-hbr; " + - func() string { - if t.SizeLength > 0 { - return fmt.Sprintf("sizelength=%d; ", t.SizeLength) - } - return "" - }() + - func() string { - if t.IndexLength > 0 { - return fmt.Sprintf("indexlength=%d; ", t.IndexLength) - } - return "" - }() + - func() string { - if t.IndexDeltaLength > 0 { - return fmt.Sprintf("indexdeltalength=%d; ", t.IndexDeltaLength) - } - return "" - }() + - "config=" + hex.EncodeToString(enc) + fmtp := make(map[string]string) + + fmtp["profile-level-id"] = "1" + fmtp["mode"] = "AAC-hbr" + if t.SizeLength > 0 { + fmtp["sizelength"] = strconv.FormatInt(int64(t.SizeLength), 10) + } + if t.IndexLength > 0 { + fmtp["indexlength"] = strconv.FormatInt(int64(t.IndexLength), 10) + } + if t.IndexDeltaLength > 0 { + fmtp["indexdeltalength"] = strconv.FormatInt(int64(t.IndexDeltaLength), 10) + } + fmtp["config"] = hex.EncodeToString(enc) return "mpeg4-generic/" + strconv.FormatInt(int64(sampleRate), 10) + "/" + strconv.FormatInt(int64(t.Config.ChannelCount), 10), fmtp diff --git a/pkg/format/mpeg4_audio_test.go b/pkg/format/mpeg4_audio_test.go index 99268eb5..7617729b 100644 --- a/pkg/format/mpeg4_audio_test.go +++ b/pkg/format/mpeg4_audio_test.go @@ -42,8 +42,14 @@ func TestMPEG4AudioMediaDescription(t *testing.T) { rtpmap, fmtp := format.Marshal() require.Equal(t, "mpeg4-generic/48000/2", rtpmap) - require.Equal(t, "profile-level-id=1; mode=AAC-hbr; sizelength=13;"+ - " indexlength=3; indexdeltalength=3; config=1190", fmtp) + require.Equal(t, map[string]string{ + "profile-level-id": "1", + "mode": "AAC-hbr", + "sizelength": "13", + "indexlength": "3", + "indexdeltalength": "3", + "config": "1190", + }, fmtp) } func TestMPEG4AudioDecEncoder(t *testing.T) { diff --git a/pkg/format/opus.go b/pkg/format/opus.go index 7c28254f..ab43ce3c 100644 --- a/pkg/format/opus.go +++ b/pkg/format/opus.go @@ -33,7 +33,7 @@ func (t *Opus) PayloadType() uint8 { 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 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) } - if fmtp != "" { - for _, kv := range strings.Split(fmtp, ";") { - 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) - } - - if strings.ToLower(tmp[0]) == "sprop-stereo" { - t.IsStereo = (tmp[1] == "1") - } + for key, val := range fmtp { + if key == "sprop-stereo" { + t.IsStereo = (val == "1") } } @@ -80,13 +67,15 @@ func (t *Opus) unmarshal(payloadType uint8, clock string, codec string, rtpmap s } // Marshal implements Format. -func (t *Opus) Marshal() (string, string) { - fmtp := "sprop-stereo=" + func() string { - if t.IsStereo { - return "1" - } - return "0" - }() +func (t *Opus) Marshal() (string, map[string]string) { + fmtp := map[string]string{ + "sprop-stereo": func() string { + if t.IsStereo { + return "1" + } + return "0" + }(), + } // RFC7587: The RTP clock rate in "a=rtpmap" MUST be 48000, and the // number of channels MUST be 2. diff --git a/pkg/format/opus_test.go b/pkg/format/opus_test.go index bdcf78f6..549a0461 100644 --- a/pkg/format/opus_test.go +++ b/pkg/format/opus_test.go @@ -26,7 +26,9 @@ func TestOpusMediaDescription(t *testing.T) { rtpmap, fmtp := format.Marshal() 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) { diff --git a/pkg/format/vorbis.go b/pkg/format/vorbis.go index 987382de..dbca38c8 100644 --- a/pkg/format/vorbis.go +++ b/pkg/format/vorbis.go @@ -32,7 +32,7 @@ func (t *Vorbis) PayloadType() uint8 { 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 tmp := strings.SplitN(clock, "/", 2) @@ -52,42 +52,33 @@ func (t *Vorbis) unmarshal(payloadType uint8, clock string, codec string, rtpmap } t.ChannelCount = int(channelCount) - if fmtp != "" { - for _, kv := range strings.Split(fmtp, ";") { - kv = strings.Trim(kv, " ") - - if len(kv) == 0 { - continue + for key, val := range fmtp { + if key == "configuration" { + conf, err := base64.StdEncoding.DecodeString(val) + if err != nil { + return fmt.Errorf("invalid AAC config (%v)", val) } - 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 { - return fmt.Errorf("invalid AAC config (%v)", tmp[1]) - } - - t.Configuration = conf - } + t.Configuration = conf } } if t.Configuration == nil { - return fmt.Errorf("config is missing (%v)", fmtp) + return fmt.Errorf("config is missing") } return nil } // 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) + "/" + strconv.FormatInt(int64(t.ChannelCount), 10), - "configuration=" + base64.StdEncoding.EncodeToString(t.Configuration) + fmtp } // PTSEqualsDTS implements Format. diff --git a/pkg/format/vorbis_test.go b/pkg/format/vorbis_test.go index 7d56e694..cdc16441 100644 --- a/pkg/format/vorbis_test.go +++ b/pkg/format/vorbis_test.go @@ -30,5 +30,7 @@ func TestVorbisMediaDescription(t *testing.T) { rtpmap, fmtp := format.Marshal() require.Equal(t, "VORBIS/48000/2", rtpmap) - require.Equal(t, "configuration=AQIDBA==", fmtp) + require.Equal(t, map[string]string{ + "configuration": "AQIDBA==", + }, fmtp) } diff --git a/pkg/format/vp8.go b/pkg/format/vp8.go index 20df8bc7..1ffb0f60 100644 --- a/pkg/format/vp8.go +++ b/pkg/format/vp8.go @@ -3,7 +3,6 @@ package format import ( "fmt" "strconv" - "strings" "github.com/pion/rtp" @@ -32,39 +31,26 @@ func (t *VP8) PayloadType() uint8 { 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 - if fmtp != "" { - for _, kv := range strings.Split(fmtp, ";") { - kv = strings.Trim(kv, " ") - - if len(kv) == 0 { - continue + for key, val := range fmtp { + switch key { + case "max-fr": + n, err := strconv.ParseUint(val, 10, 64) + if err != nil { + return fmt.Errorf("invalid max-fr (%v)", val) } + v2 := int(n) + t.MaxFR = &v2 - tmp := strings.SplitN(kv, "=", 2) - if len(tmp) != 2 { - return fmt.Errorf("invalid fmtp attribute (%v)", fmtp) - } - - switch tmp[0] { - case "max-fr": - val, err := strconv.ParseUint(tmp[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid max-fr (%v)", tmp[1]) - } - v2 := int(val) - t.MaxFR = &v2 - - case "max-fs": - val, err := strconv.ParseUint(tmp[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid max-fs (%v)", tmp[1]) - } - v2 := int(val) - t.MaxFS = &v2 + case "max-fs": + n, err := strconv.ParseUint(val, 10, 64) + if err != nil { + return fmt.Errorf("invalid max-fs (%v)", val) } + v2 := int(n) + t.MaxFS = &v2 } } @@ -72,17 +58,13 @@ func (t *VP8) unmarshal(payloadType uint8, clock string, codec string, rtpmap st } // Marshal implements Format. -func (t *VP8) Marshal() (string, string) { - var tmp []string +func (t *VP8) Marshal() (string, map[string]string) { + fmtp := make(map[string]string) 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 { - tmp = append(tmp, "max-fs="+strconv.FormatInt(int64(*t.MaxFS), 10)) - } - var fmtp string - if tmp != nil { - fmtp = strings.Join(tmp, ";") + fmtp["max-fs"] = strconv.FormatInt(int64(*t.MaxFS), 10) } return "VP8/90000", fmtp diff --git a/pkg/format/vp8_test.go b/pkg/format/vp8_test.go index b1b6b4c4..fb4fcd7e 100644 --- a/pkg/format/vp8_test.go +++ b/pkg/format/vp8_test.go @@ -28,7 +28,10 @@ func TestVP8MediaDescription(t *testing.T) { rtpmap, fmtp := format.Marshal() 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) { diff --git a/pkg/format/vp9.go b/pkg/format/vp9.go index ca5c141a..abc92875 100644 --- a/pkg/format/vp9.go +++ b/pkg/format/vp9.go @@ -3,7 +3,6 @@ package format import ( "fmt" "strconv" - "strings" "github.com/pion/rtp" @@ -33,47 +32,34 @@ func (t *VP9) PayloadType() uint8 { 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 - if fmtp != "" { - for _, kv := range strings.Split(fmtp, ";") { - kv = strings.Trim(kv, " ") - - if len(kv) == 0 { - continue + for key, val := range fmtp { + switch key { + case "max-fr": + n, err := strconv.ParseUint(val, 10, 64) + if err != nil { + return fmt.Errorf("invalid max-fr (%v)", val) } + v2 := int(n) + t.MaxFR = &v2 - tmp := strings.SplitN(kv, "=", 2) - if len(tmp) != 2 { - return fmt.Errorf("invalid fmtp attribute (%v)", fmtp) + case "max-fs": + n, err := strconv.ParseUint(val, 10, 64) + if err != nil { + return fmt.Errorf("invalid max-fs (%v)", val) } + v2 := int(n) + t.MaxFS = &v2 - switch tmp[0] { - case "max-fr": - val, err := strconv.ParseUint(tmp[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid max-fr (%v)", tmp[1]) - } - v2 := int(val) - t.MaxFR = &v2 - - case "max-fs": - val, err := strconv.ParseUint(tmp[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid max-fs (%v)", tmp[1]) - } - v2 := int(val) - t.MaxFS = &v2 - - case "profile-id": - val, err := strconv.ParseUint(tmp[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid profile-id (%v)", tmp[1]) - } - v2 := int(val) - t.ProfileID = &v2 + case "profile-id": + n, err := strconv.ParseUint(val, 10, 64) + if err != nil { + return fmt.Errorf("invalid profile-id (%v)", val) } + v2 := int(n) + t.ProfileID = &v2 } } @@ -81,20 +67,16 @@ func (t *VP9) unmarshal(payloadType uint8, clock string, codec string, rtpmap st } // Marshal implements Format. -func (t *VP9) Marshal() (string, string) { - var tmp []string +func (t *VP9) Marshal() (string, map[string]string) { + fmtp := make(map[string]string) 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 { - tmp = append(tmp, "max-fs="+strconv.FormatInt(int64(*t.MaxFS), 10)) + fmtp["max-fs"] = strconv.FormatInt(int64(*t.MaxFS), 10) } if t.ProfileID != nil { - tmp = append(tmp, "profile-id="+strconv.FormatInt(int64(*t.ProfileID), 10)) - } - var fmtp string - if tmp != nil { - fmtp = strings.Join(tmp, ";") + fmtp["profile-id"] = strconv.FormatInt(int64(*t.ProfileID), 10) } return "VP9/90000", fmtp diff --git a/pkg/format/vp9_test.go b/pkg/format/vp9_test.go index 77297151..1e4d9e30 100644 --- a/pkg/format/vp9_test.go +++ b/pkg/format/vp9_test.go @@ -30,7 +30,11 @@ func TestVP9MediaDescription(t *testing.T) { rtpmap, fmtp := format.Marshal() 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) { diff --git a/pkg/media/media.go b/pkg/media/media.go index 774a2061..a115d410 100644 --- a/pkg/media/media.go +++ b/pkg/media/media.go @@ -4,6 +4,7 @@ package media import ( "fmt" "reflect" + "sort" "strconv" "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{ Key: "fmtp", - Value: typ + " " + fmtp, + Value: typ + " " + strings.Join(tmp, "; "), }) } } diff --git a/pkg/media/medias_test.go b/pkg/media/medias_test.go index 98d94b82..2baf33a8 100644 --- a/pkg/media/medias_test.go +++ b/pkg/media/medias_test.go @@ -49,7 +49,7 @@ var casesMedias = []struct { "m=video 0 RTP/AVP 97\r\n" + "a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=v\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" + "a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=a\r\n" + "a=recvonly\r\n" + @@ -302,8 +302,10 @@ var casesMedias = []struct { &format.Generic{ PayloadTyp: 97, RTPMap: "rtx/90000", - FMTP: "apt=96", - ClockRat: 90000, + FMTP: map[string]string{ + "apt": "96", + }, + ClockRat: 90000, }, &format.VP9{ PayloadTyp: 98, @@ -311,8 +313,10 @@ var casesMedias = []struct { &format.Generic{ PayloadTyp: 99, RTPMap: "rtx/90000", - FMTP: "apt=98", - ClockRat: 90000, + FMTP: map[string]string{ + "apt": "98", + }, + ClockRat: 90000, }, &format.H264{ PayloadTyp: 100, @@ -321,8 +325,10 @@ var casesMedias = []struct { &format.Generic{ PayloadTyp: 101, RTPMap: "rtx/90000", - FMTP: "apt=100", - ClockRat: 90000, + FMTP: map[string]string{ + "apt": "100", + }, + ClockRat: 90000, }, &format.Generic{ PayloadTyp: 127, @@ -332,8 +338,10 @@ var casesMedias = []struct { &format.Generic{ PayloadTyp: 124, RTPMap: "rtx/90000", - FMTP: "apt=127", - ClockRat: 90000, + FMTP: map[string]string{ + "apt": "127", + }, + ClockRat: 90000, }, &format.Generic{ PayloadTyp: 125, @@ -364,8 +372,8 @@ var casesMedias = []struct { "m=video 0 RTP/AVP 96 98\r\n" + "a=control\r\n" + "a=rtpmap:96 H264/90000\r\n" + - "a=fmtp:96 packetization-mode=1;" + - " sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==; profile-level-id=4D002A\r\n" + + "a=fmtp:96 packetization-mode=1; profile-level-id=4D002A; " + + "sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==\r\n" + "a=rtpmap:98 MetaData\r\n", Medias{ { @@ -511,8 +519,10 @@ func TestMediasFindFormat(t *testing.T) { tr := &format.Generic{ PayloadTyp: 97, RTPMap: "rtx/90000", - FMTP: "apt=96", - ClockRat: 90000, + FMTP: map[string]string{ + "apt": "96", + }, + ClockRat: 90000, } md := &Media{