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]
}
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 {

View File

@@ -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{

View File

@@ -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.

View File

@@ -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)
})
}

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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)
})
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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)
})
}
}

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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)
}

View File

@@ -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.

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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) {

View File

@@ -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, "; "),
})
}
}

View File

@@ -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{