add MP4A-LATM format (#237)

This commit is contained in:
Alessandro Ros
2023-04-10 13:07:09 +02:00
committed by GitHub
parent 0561810600
commit c0c2dd68d3
11 changed files with 350 additions and 74 deletions

View File

@@ -142,7 +142,10 @@ func Unmarshal(md *psdp.MediaDescription, payloadTypeStr string) (Format, error)
return &LPCM{}
case codec == "mpeg4-generic":
return &MPEG4Audio{}
return &MPEG4AudioGeneric{}
case codec == "mp4a-latm":
return &MPEG4AudioLATM{}
case codec == "vorbis":
return &Vorbis{}

View File

@@ -9,7 +9,15 @@ import (
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
)
func TestNewFromMediaDescription(t *testing.T) {
func intPtr(v int) *int {
return &v
}
func boolPtr(v bool) *bool {
return &v
}
func TestUnmarshal(t *testing.T) {
for _, ca := range []struct {
name string
md *psdp.MediaDescription
@@ -250,6 +258,68 @@ func TestNewFromMediaDescription(t *testing.T) {
IndexDeltaLength: 0,
},
},
{
"audio aac lc latm",
&psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "audio",
Protos: []string{"RTP", "AVP"},
Formats: []string{"96"},
},
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 MP4A-LATM/24000/2",
},
{
Key: "fmtp",
Value: "96 110 profile-level-id=1; bitrate=64000; cpresent=0; " +
"object=2; config=400026203fc0",
},
},
},
&MPEG4AudioLATM{
PayloadTyp: 96,
SampleRate: 24000,
Channels: 2,
ProfileLevelID: 30,
Bitrate: intPtr(64000),
CPresent: boolPtr(false),
Object: 2,
Config: []byte{0x40, 0x00, 0x26, 0x20, 0x3f, 0xc0},
},
},
{
"audio aac v2 latm",
&psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "audio",
Protos: []string{"RTP", "AVP"},
Formats: []string{"110"},
},
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "110 MP4A-LATM/24000/1",
},
{
Key: "fmtp",
Value: "110 profile-level-id=15; object=2; cpresent=0; " +
"config=400026103fc0; SBR-enabled=1",
},
},
},
&MPEG4AudioLATM{
PayloadTyp: 110,
SampleRate: 24000,
Channels: 1,
ProfileLevelID: 15,
CPresent: boolPtr(false),
Object: 2,
SBREnabled: boolPtr(true),
Config: []byte{0x40, 0x00, 0x26, 0x10, 0x3f, 0xc0},
},
},
{
"audio vorbis",
&psdp.MediaDescription{
@@ -562,14 +632,8 @@ func TestNewFromMediaDescription(t *testing.T) {
},
&VP8{
PayloadTyp: 96,
MaxFR: func() *int {
v := 123
return &v
}(),
MaxFS: func() *int {
v := 456
return &v
}(),
MaxFR: intPtr(123),
MaxFS: intPtr(456),
},
},
{
@@ -593,18 +657,9 @@ func TestNewFromMediaDescription(t *testing.T) {
},
&VP9{
PayloadTyp: 96,
MaxFR: func() *int {
v := 123
return &v
}(),
MaxFS: func() *int {
v := 456
return &v
}(),
ProfileID: func() *int {
v := 789
return &v
}(),
MaxFR: intPtr(123),
MaxFS: intPtr(456),
ProfileID: intPtr(789),
},
},
{
@@ -714,7 +769,7 @@ func TestNewFromMediaDescription(t *testing.T) {
}
}
func TestNewFromMediaDescriptionErrors(t *testing.T) {
func TestUnmarshalErrors(t *testing.T) {
for _, ca := range []struct {
name string
md *psdp.MediaDescription
@@ -794,7 +849,7 @@ func TestNewFromMediaDescriptionErrors(t *testing.T) {
},
},
},
"invalid AAC config (zz)",
"invalid AAC config: zz",
},
{
"audio aac invalid config 2",
@@ -815,7 +870,7 @@ func TestNewFromMediaDescriptionErrors(t *testing.T) {
},
},
},
"invalid AAC config (aa)",
"invalid AAC config: aa",
},
{
"audio aac missing sizelength",
@@ -857,7 +912,7 @@ func TestNewFromMediaDescriptionErrors(t *testing.T) {
},
},
},
"invalid AAC SizeLength (aaa)",
"invalid AAC SizeLength: aaa",
},
{
"audio aac invalid indexlength",
@@ -878,7 +933,7 @@ func TestNewFromMediaDescriptionErrors(t *testing.T) {
},
},
},
"invalid AAC IndexLength (aaa)",
"invalid AAC IndexLength: aaa",
},
{
"audio aac invalid indexdeltalength",
@@ -899,7 +954,7 @@ func TestNewFromMediaDescriptionErrors(t *testing.T) {
},
},
},
"invalid AAC IndexDeltaLength (aaa)",
"invalid AAC IndexDeltaLength: aaa",
},
{
"audio vorbis missing configuration",

View File

@@ -11,9 +11,12 @@ import (
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
)
// MPEG4Audio is a RTP format that uses a MPEG-4 audio codec.
// MPEG4Audio is an alias for MPEG4AudioGeneric.
type MPEG4Audio = MPEG4AudioGeneric
// MPEG4AudioGeneric is a RTP format that uses a MPEG-4 audio codec.
// Specification: https://datatracker.ietf.org/doc/html/rfc3640
type MPEG4Audio struct {
type MPEG4AudioGeneric struct {
PayloadTyp uint8
Config *mpeg4audio.Config
SizeLength int
@@ -22,21 +25,21 @@ type MPEG4Audio struct {
}
// String implements Format.
func (f *MPEG4Audio) String() string {
return "MPEG4-audio"
func (f *MPEG4AudioGeneric) String() string {
return "MPEG4-audio-gen"
}
// ClockRate implements Format.
func (f *MPEG4Audio) ClockRate() int {
func (f *MPEG4AudioGeneric) ClockRate() int {
return f.Config.SampleRate
}
// PayloadType implements Format.
func (f *MPEG4Audio) PayloadType() uint8 {
func (f *MPEG4AudioGeneric) PayloadType() uint8 {
return f.PayloadTyp
}
func (f *MPEG4Audio) unmarshal(
func (f *MPEG4AudioGeneric) unmarshal(
payloadType uint8, clock string, codec string,
rtpmap string, fmtp map[string]string,
) error {
@@ -47,33 +50,33 @@ func (f *MPEG4Audio) unmarshal(
case "config":
enc, err := hex.DecodeString(val)
if err != nil {
return fmt.Errorf("invalid AAC config (%v)", val)
return fmt.Errorf("invalid AAC config: %v", val)
}
f.Config = &mpeg4audio.Config{}
err = f.Config.Unmarshal(enc)
if err != nil {
return fmt.Errorf("invalid AAC config (%v)", val)
return fmt.Errorf("invalid AAC config: %v", val)
}
case "sizelength":
n, err := strconv.ParseUint(val, 10, 64)
if err != nil {
return fmt.Errorf("invalid AAC SizeLength (%v)", val)
return fmt.Errorf("invalid AAC SizeLength: %v", val)
}
f.SizeLength = int(n)
case "indexlength":
n, err := strconv.ParseUint(val, 10, 64)
if err != nil {
return fmt.Errorf("invalid AAC IndexLength (%v)", val)
return fmt.Errorf("invalid AAC IndexLength: %v", val)
}
f.IndexLength = int(n)
case "indexdeltalength":
n, err := strconv.ParseUint(val, 10, 64)
if err != nil {
return fmt.Errorf("invalid AAC IndexDeltaLength (%v)", val)
return fmt.Errorf("invalid AAC IndexDeltaLength: %v", val)
}
f.IndexDeltaLength = int(n)
}
@@ -91,7 +94,7 @@ func (f *MPEG4Audio) unmarshal(
}
// Marshal implements Format.
func (f *MPEG4Audio) Marshal() (string, map[string]string) {
func (f *MPEG4AudioGeneric) Marshal() (string, map[string]string) {
enc, err := f.Config.Marshal()
if err != nil {
return "", nil
@@ -102,19 +105,23 @@ func (f *MPEG4Audio) Marshal() (string, map[string]string) {
sampleRate = f.Config.ExtensionSampleRate
}
fmtp := make(map[string]string)
fmtp := map[string]string{
"profile-level-id": "1",
"mode": "AAC-hbr",
}
fmtp["profile-level-id"] = "1"
fmtp["mode"] = "AAC-hbr"
if f.SizeLength > 0 {
fmtp["sizelength"] = strconv.FormatInt(int64(f.SizeLength), 10)
}
if f.IndexLength > 0 {
fmtp["indexlength"] = strconv.FormatInt(int64(f.IndexLength), 10)
}
if f.IndexDeltaLength > 0 {
fmtp["indexdeltalength"] = strconv.FormatInt(int64(f.IndexDeltaLength), 10)
}
fmtp["config"] = hex.EncodeToString(enc)
return "mpeg4-generic/" + strconv.FormatInt(int64(sampleRate), 10) +
@@ -122,12 +129,12 @@ func (f *MPEG4Audio) Marshal() (string, map[string]string) {
}
// PTSEqualsDTS implements Format.
func (f *MPEG4Audio) PTSEqualsDTS(*rtp.Packet) bool {
func (f *MPEG4AudioGeneric) PTSEqualsDTS(*rtp.Packet) bool {
return true
}
// CreateDecoder creates a decoder able to decode the content of the format.
func (f *MPEG4Audio) CreateDecoder() *rtpmpeg4audio.Decoder {
func (f *MPEG4AudioGeneric) CreateDecoder() *rtpmpeg4audio.Decoder {
d := &rtpmpeg4audio.Decoder{
SampleRate: f.Config.SampleRate,
SizeLength: f.SizeLength,
@@ -139,7 +146,7 @@ func (f *MPEG4Audio) CreateDecoder() *rtpmpeg4audio.Decoder {
}
// CreateEncoder creates an encoder able to encode the content of the format.
func (f *MPEG4Audio) CreateEncoder() *rtpmpeg4audio.Encoder {
func (f *MPEG4AudioGeneric) CreateEncoder() *rtpmpeg4audio.Encoder {
e := &rtpmpeg4audio.Encoder{
PayloadType: f.PayloadTyp,
SampleRate: f.Config.SampleRate,

View File

@@ -9,8 +9,8 @@ import (
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
)
func TestMPEG4AudioAttributes(t *testing.T) {
format := &MPEG4Audio{
func TestMPEG4AudioGenericAttributes(t *testing.T) {
format := &MPEG4AudioGeneric{
PayloadTyp: 96,
Config: &mpeg4audio.Config{
Type: mpeg4audio.ObjectTypeAACLC,
@@ -21,14 +21,14 @@ func TestMPEG4AudioAttributes(t *testing.T) {
IndexLength: 3,
IndexDeltaLength: 3,
}
require.Equal(t, "MPEG4-audio", format.String())
require.Equal(t, "MPEG4-audio-gen", format.String())
require.Equal(t, 48000, format.ClockRate())
require.Equal(t, uint8(96), format.PayloadType())
require.Equal(t, true, format.PTSEqualsDTS(&rtp.Packet{}))
}
func TestMPEG4AudioMediaDescription(t *testing.T) {
format := &MPEG4Audio{
func TestMPEG4AudioGenericMediaDescription(t *testing.T) {
format := &MPEG4AudioGeneric{
PayloadTyp: 96,
Config: &mpeg4audio.Config{
Type: mpeg4audio.ObjectTypeAACLC,
@@ -52,8 +52,8 @@ func TestMPEG4AudioMediaDescription(t *testing.T) {
}, fmtp)
}
func TestMPEG4AudioDecEncoder(t *testing.T) {
format := &MPEG4Audio{
func TestMPEG4AudioGenericDecEncoder(t *testing.T) {
format := &MPEG4AudioGeneric{
PayloadTyp: 96,
Config: &mpeg4audio.Config{
Type: mpeg4audio.ObjectTypeAACLC,

View File

@@ -0,0 +1,164 @@
package formats
import (
"encoding/hex"
"fmt"
"strconv"
"strings"
"github.com/pion/rtp"
)
// MPEG4AudioLATM is a RTP format that uses a MPEG-4 audio codec.
// Specification: https://datatracker.ietf.org/doc/html/rfc6416#section-7.3
type MPEG4AudioLATM struct {
PayloadTyp uint8
SampleRate int
Channels int
ProfileLevelID int
Bitrate *int
Object int
CPresent *bool
Config []byte
SBREnabled *bool
}
// String implements Format.
func (f *MPEG4AudioLATM) String() string {
return "MPEG4-audio-latm"
}
// ClockRate implements Format.
func (f *MPEG4AudioLATM) ClockRate() int {
return f.SampleRate
}
// PayloadType implements Format.
func (f *MPEG4AudioLATM) PayloadType() uint8 {
return f.PayloadTyp
}
func (f *MPEG4AudioLATM) unmarshal(
payloadType uint8, clock string, codec string,
rtpmap string, fmtp map[string]string,
) error {
tmp := strings.SplitN(clock, "/", 2)
if len(tmp) != 2 {
return fmt.Errorf("invalid clock: %v", clock)
}
tmp2, err := strconv.ParseInt(tmp[0], 10, 64)
if err != nil {
return err
}
f.SampleRate = int(tmp2)
tmp2, err = strconv.ParseInt(tmp[1], 10, 64)
if err != nil {
return err
}
f.Channels = int(tmp2)
f.PayloadTyp = payloadType
f.ProfileLevelID = 30 // default value defined by specification
for key, val := range fmtp {
switch key {
case "profile-level-id":
tmp, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return fmt.Errorf("invalid profile-level-id: %v", val)
}
f.ProfileLevelID = int(tmp)
case "bitrate":
tmp, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return fmt.Errorf("invalid bitrate: %v", val)
}
v := int(tmp)
f.Bitrate = &v
case "object":
tmp, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return fmt.Errorf("invalid object: %v", val)
}
f.Object = int(tmp)
case "cpresent":
tmp, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return fmt.Errorf("invalid cpresent: %v", val)
}
v := (tmp == 1)
f.CPresent = &v
case "config":
var err error
f.Config, err = hex.DecodeString(val)
if err != nil {
return fmt.Errorf("invalid AAC config: %v", val)
}
case "sbr-enabled":
tmp, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return fmt.Errorf("invalid SBR-enabled: %v", val)
}
v := (tmp == 1)
f.SBREnabled = &v
}
}
if f.Object == 0 {
return fmt.Errorf("object is missing")
}
if f.Config == nil {
return fmt.Errorf("config is missing")
}
return nil
}
// Marshal implements Format.
func (f *MPEG4AudioLATM) Marshal() (string, map[string]string) {
fmtp := map[string]string{
"profile-level-id": strconv.FormatInt(int64(f.ProfileLevelID), 10),
"config": hex.EncodeToString(f.Config),
"object": strconv.FormatInt(int64(f.Object), 10),
}
if f.Bitrate != nil {
fmtp["bitrate"] = strconv.FormatInt(int64(*f.Bitrate), 10)
}
if f.CPresent != nil {
if *f.CPresent {
fmtp["cpresent"] = "1"
} else {
fmtp["cpresent"] = "0"
}
}
if f.SBREnabled != nil {
if *f.CPresent {
fmtp["SBR-enabled"] = "1"
} else {
fmtp["SBR-enabled"] = "0"
}
}
return "MP4A-LATM/" + strconv.FormatInt(int64(f.SampleRate), 10) +
"/" + strconv.FormatInt(int64(f.Channels), 10), fmtp
}
// PTSEqualsDTS implements Format.
func (f *MPEG4AudioLATM) PTSEqualsDTS(*rtp.Packet) bool {
return true
}

View File

@@ -0,0 +1,42 @@
package formats
import (
"testing"
"github.com/pion/rtp"
"github.com/stretchr/testify/require"
)
func TestMPEG4AudioLATMAttributes(t *testing.T) {
format := &MPEG4AudioLATM{
PayloadTyp: 96,
SampleRate: 48000,
Channels: 2,
Object: 2,
ProfileLevelID: 1,
Config: []byte{0x01, 0x02, 0x03},
}
require.Equal(t, "MPEG4-audio-latm", format.String())
require.Equal(t, 48000, format.ClockRate())
require.Equal(t, uint8(96), format.PayloadType())
require.Equal(t, true, format.PTSEqualsDTS(&rtp.Packet{}))
}
func TestMPEG4AudioLATMMediaDescription(t *testing.T) {
format := &MPEG4AudioLATM{
PayloadTyp: 96,
SampleRate: 48000,
Channels: 2,
Object: 2,
ProfileLevelID: 1,
Config: []byte{0x01, 0x02, 0x03},
}
rtpmap, fmtp := format.Marshal()
require.Equal(t, "MP4A-LATM/48000/2", rtpmap)
require.Equal(t, map[string]string{
"profile-level-id": "1",
"object": "2",
"config": "010203",
}, fmtp)
}

View File

@@ -9,39 +9,38 @@ import (
"github.com/pion/rtp"
)
// MPEG4Video is a RTP format that uses the video codec defined in MPEG-4 part 2.
// MPEG4Video is an alias for MPEG4VideoES.
type MPEG4Video = MPEG4VideoES
// MPEG4VideoES is a RTP format that uses the video codec defined in MPEG-4 part 2.
// Specification: https://datatracker.ietf.org/doc/html/rfc6416#section-7.1
type MPEG4Video struct {
type MPEG4VideoES struct {
PayloadTyp uint8
ProfileLevelID int
Config []byte
}
// String implements Format.
func (f *MPEG4Video) String() string {
return "MPEG4-video"
func (f *MPEG4VideoES) String() string {
return "MPEG4-video-es"
}
// ClockRate implements Format.
func (f *MPEG4Video) ClockRate() int {
func (f *MPEG4VideoES) ClockRate() int {
return 90000
}
// PayloadType implements Format.
func (f *MPEG4Video) PayloadType() uint8 {
func (f *MPEG4VideoES) PayloadType() uint8 {
return f.PayloadTyp
}
func (f *MPEG4Video) unmarshal(
func (f *MPEG4VideoES) unmarshal(
payloadType uint8, clock string, codec string,
rtpmap string, fmtp map[string]string,
) error {
f.PayloadTyp = payloadType
// If this parameter is not specified by
// the procedure, its default value of 1 (Simple Profile/Level 1) is
// used.
f.ProfileLevelID = 1
f.ProfileLevelID = 1 // default value defined by specification
for key, val := range fmtp {
switch key {
@@ -66,7 +65,7 @@ func (f *MPEG4Video) unmarshal(
}
// Marshal implements Format.
func (f *MPEG4Video) Marshal() (string, map[string]string) {
func (f *MPEG4VideoES) Marshal() (string, map[string]string) {
fmtp := map[string]string{
"profile-level-id": strconv.FormatInt(int64(f.ProfileLevelID), 10),
"config": strings.ToUpper(hex.EncodeToString(f.Config)),
@@ -76,6 +75,6 @@ func (f *MPEG4Video) Marshal() (string, map[string]string) {
}
// PTSEqualsDTS implements Format.
func (f *MPEG4Video) PTSEqualsDTS(*rtp.Packet) bool {
func (f *MPEG4VideoES) PTSEqualsDTS(*rtp.Packet) bool {
return true
}

View File

@@ -7,20 +7,20 @@ import (
"github.com/stretchr/testify/require"
)
func TestMPEG4VideoAttributes(t *testing.T) {
format := &MPEG4Video{
func TestMPEG4VideoESAttributes(t *testing.T) {
format := &MPEG4VideoES{
PayloadTyp: 96,
ProfileLevelID: 1,
Config: []byte{0x01, 0x02, 0x03},
}
require.Equal(t, "MPEG4-video", format.String())
require.Equal(t, "MPEG4-video-es", format.String())
require.Equal(t, 90000, format.ClockRate())
require.Equal(t, uint8(96), format.PayloadType())
require.Equal(t, true, format.PTSEqualsDTS(&rtp.Packet{}))
}
func TestMPEG4VideoMediaDescription(t *testing.T) {
format := &MPEG4Video{
func TestMPEG4VideoESMediaDescription(t *testing.T) {
format := &MPEG4VideoES{
PayloadTyp: 96,
ProfileLevelID: 1,
Config: []byte{0x0a, 0x0b, 0x03},

View File

@@ -0,0 +1,3 @@
package formats
// TODO

View File

@@ -0,0 +1,3 @@
package formats
// TODO

View File

@@ -553,7 +553,7 @@ func TestMediasUnmarshalErrors(t *testing.T) {
"a=rtpmap:97 mpeg4-generic/44100/2\r\n" +
"a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=zzz1210\r\n" +
"a=control:streamid=1\r\n",
"media 2 is invalid: invalid AAC config (zzz1210)",
"media 2 is invalid: invalid AAC config: zzz1210",
},
} {
t.Run(ca.name, func(t *testing.T) {