mirror of
https://github.com/aler9/gortsplib
synced 2025-10-05 07:06:58 +08:00
add MP4A-LATM format (#237)
This commit is contained in:
@@ -142,7 +142,10 @@ func Unmarshal(md *psdp.MediaDescription, payloadTypeStr string) (Format, error)
|
|||||||
return &LPCM{}
|
return &LPCM{}
|
||||||
|
|
||||||
case codec == "mpeg4-generic":
|
case codec == "mpeg4-generic":
|
||||||
return &MPEG4Audio{}
|
return &MPEG4AudioGeneric{}
|
||||||
|
|
||||||
|
case codec == "mp4a-latm":
|
||||||
|
return &MPEG4AudioLATM{}
|
||||||
|
|
||||||
case codec == "vorbis":
|
case codec == "vorbis":
|
||||||
return &Vorbis{}
|
return &Vorbis{}
|
||||||
|
@@ -9,7 +9,15 @@ import (
|
|||||||
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
|
"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 {
|
for _, ca := range []struct {
|
||||||
name string
|
name string
|
||||||
md *psdp.MediaDescription
|
md *psdp.MediaDescription
|
||||||
@@ -250,6 +258,68 @@ func TestNewFromMediaDescription(t *testing.T) {
|
|||||||
IndexDeltaLength: 0,
|
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",
|
"audio vorbis",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
@@ -562,14 +632,8 @@ func TestNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
&VP8{
|
&VP8{
|
||||||
PayloadTyp: 96,
|
PayloadTyp: 96,
|
||||||
MaxFR: func() *int {
|
MaxFR: intPtr(123),
|
||||||
v := 123
|
MaxFS: intPtr(456),
|
||||||
return &v
|
|
||||||
}(),
|
|
||||||
MaxFS: func() *int {
|
|
||||||
v := 456
|
|
||||||
return &v
|
|
||||||
}(),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -593,18 +657,9 @@ func TestNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
&VP9{
|
&VP9{
|
||||||
PayloadTyp: 96,
|
PayloadTyp: 96,
|
||||||
MaxFR: func() *int {
|
MaxFR: intPtr(123),
|
||||||
v := 123
|
MaxFS: intPtr(456),
|
||||||
return &v
|
ProfileID: intPtr(789),
|
||||||
}(),
|
|
||||||
MaxFS: func() *int {
|
|
||||||
v := 456
|
|
||||||
return &v
|
|
||||||
}(),
|
|
||||||
ProfileID: func() *int {
|
|
||||||
v := 789
|
|
||||||
return &v
|
|
||||||
}(),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -714,7 +769,7 @@ func TestNewFromMediaDescription(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewFromMediaDescriptionErrors(t *testing.T) {
|
func TestUnmarshalErrors(t *testing.T) {
|
||||||
for _, ca := range []struct {
|
for _, ca := range []struct {
|
||||||
name string
|
name string
|
||||||
md *psdp.MediaDescription
|
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",
|
"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",
|
"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",
|
"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",
|
"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",
|
"audio vorbis missing configuration",
|
||||||
|
@@ -11,9 +11,12 @@ import (
|
|||||||
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
|
"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
|
// Specification: https://datatracker.ietf.org/doc/html/rfc3640
|
||||||
type MPEG4Audio struct {
|
type MPEG4AudioGeneric struct {
|
||||||
PayloadTyp uint8
|
PayloadTyp uint8
|
||||||
Config *mpeg4audio.Config
|
Config *mpeg4audio.Config
|
||||||
SizeLength int
|
SizeLength int
|
||||||
@@ -22,21 +25,21 @@ type MPEG4Audio struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// String implements Format.
|
// String implements Format.
|
||||||
func (f *MPEG4Audio) String() string {
|
func (f *MPEG4AudioGeneric) String() string {
|
||||||
return "MPEG4-audio"
|
return "MPEG4-audio-gen"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClockRate implements Format.
|
// ClockRate implements Format.
|
||||||
func (f *MPEG4Audio) ClockRate() int {
|
func (f *MPEG4AudioGeneric) ClockRate() int {
|
||||||
return f.Config.SampleRate
|
return f.Config.SampleRate
|
||||||
}
|
}
|
||||||
|
|
||||||
// PayloadType implements Format.
|
// PayloadType implements Format.
|
||||||
func (f *MPEG4Audio) PayloadType() uint8 {
|
func (f *MPEG4AudioGeneric) PayloadType() uint8 {
|
||||||
return f.PayloadTyp
|
return f.PayloadTyp
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *MPEG4Audio) unmarshal(
|
func (f *MPEG4AudioGeneric) unmarshal(
|
||||||
payloadType uint8, clock string, codec string,
|
payloadType uint8, clock string, codec string,
|
||||||
rtpmap string, fmtp map[string]string,
|
rtpmap string, fmtp map[string]string,
|
||||||
) error {
|
) error {
|
||||||
@@ -47,33 +50,33 @@ func (f *MPEG4Audio) unmarshal(
|
|||||||
case "config":
|
case "config":
|
||||||
enc, err := hex.DecodeString(val)
|
enc, err := hex.DecodeString(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid AAC config (%v)", val)
|
return fmt.Errorf("invalid AAC config: %v", val)
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Config = &mpeg4audio.Config{}
|
f.Config = &mpeg4audio.Config{}
|
||||||
err = f.Config.Unmarshal(enc)
|
err = f.Config.Unmarshal(enc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid AAC config (%v)", val)
|
return fmt.Errorf("invalid AAC config: %v", val)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "sizelength":
|
case "sizelength":
|
||||||
n, err := strconv.ParseUint(val, 10, 64)
|
n, err := strconv.ParseUint(val, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid AAC SizeLength (%v)", val)
|
return fmt.Errorf("invalid AAC SizeLength: %v", val)
|
||||||
}
|
}
|
||||||
f.SizeLength = int(n)
|
f.SizeLength = int(n)
|
||||||
|
|
||||||
case "indexlength":
|
case "indexlength":
|
||||||
n, err := strconv.ParseUint(val, 10, 64)
|
n, err := strconv.ParseUint(val, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid AAC IndexLength (%v)", val)
|
return fmt.Errorf("invalid AAC IndexLength: %v", val)
|
||||||
}
|
}
|
||||||
f.IndexLength = int(n)
|
f.IndexLength = int(n)
|
||||||
|
|
||||||
case "indexdeltalength":
|
case "indexdeltalength":
|
||||||
n, err := strconv.ParseUint(val, 10, 64)
|
n, err := strconv.ParseUint(val, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid AAC IndexDeltaLength (%v)", val)
|
return fmt.Errorf("invalid AAC IndexDeltaLength: %v", val)
|
||||||
}
|
}
|
||||||
f.IndexDeltaLength = int(n)
|
f.IndexDeltaLength = int(n)
|
||||||
}
|
}
|
||||||
@@ -91,7 +94,7 @@ func (f *MPEG4Audio) unmarshal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Marshal implements Format.
|
// Marshal implements Format.
|
||||||
func (f *MPEG4Audio) Marshal() (string, map[string]string) {
|
func (f *MPEG4AudioGeneric) Marshal() (string, map[string]string) {
|
||||||
enc, err := f.Config.Marshal()
|
enc, err := f.Config.Marshal()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil
|
return "", nil
|
||||||
@@ -102,19 +105,23 @@ func (f *MPEG4Audio) Marshal() (string, map[string]string) {
|
|||||||
sampleRate = f.Config.ExtensionSampleRate
|
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 {
|
if f.SizeLength > 0 {
|
||||||
fmtp["sizelength"] = strconv.FormatInt(int64(f.SizeLength), 10)
|
fmtp["sizelength"] = strconv.FormatInt(int64(f.SizeLength), 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.IndexLength > 0 {
|
if f.IndexLength > 0 {
|
||||||
fmtp["indexlength"] = strconv.FormatInt(int64(f.IndexLength), 10)
|
fmtp["indexlength"] = strconv.FormatInt(int64(f.IndexLength), 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.IndexDeltaLength > 0 {
|
if f.IndexDeltaLength > 0 {
|
||||||
fmtp["indexdeltalength"] = strconv.FormatInt(int64(f.IndexDeltaLength), 10)
|
fmtp["indexdeltalength"] = strconv.FormatInt(int64(f.IndexDeltaLength), 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmtp["config"] = hex.EncodeToString(enc)
|
fmtp["config"] = hex.EncodeToString(enc)
|
||||||
|
|
||||||
return "mpeg4-generic/" + strconv.FormatInt(int64(sampleRate), 10) +
|
return "mpeg4-generic/" + strconv.FormatInt(int64(sampleRate), 10) +
|
||||||
@@ -122,12 +129,12 @@ func (f *MPEG4Audio) Marshal() (string, map[string]string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PTSEqualsDTS implements Format.
|
// PTSEqualsDTS implements Format.
|
||||||
func (f *MPEG4Audio) PTSEqualsDTS(*rtp.Packet) bool {
|
func (f *MPEG4AudioGeneric) PTSEqualsDTS(*rtp.Packet) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateDecoder creates a decoder able to decode the content of the format.
|
// 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{
|
d := &rtpmpeg4audio.Decoder{
|
||||||
SampleRate: f.Config.SampleRate,
|
SampleRate: f.Config.SampleRate,
|
||||||
SizeLength: f.SizeLength,
|
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.
|
// 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{
|
e := &rtpmpeg4audio.Encoder{
|
||||||
PayloadType: f.PayloadTyp,
|
PayloadType: f.PayloadTyp,
|
||||||
SampleRate: f.Config.SampleRate,
|
SampleRate: f.Config.SampleRate,
|
@@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
|
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMPEG4AudioAttributes(t *testing.T) {
|
func TestMPEG4AudioGenericAttributes(t *testing.T) {
|
||||||
format := &MPEG4Audio{
|
format := &MPEG4AudioGeneric{
|
||||||
PayloadTyp: 96,
|
PayloadTyp: 96,
|
||||||
Config: &mpeg4audio.Config{
|
Config: &mpeg4audio.Config{
|
||||||
Type: mpeg4audio.ObjectTypeAACLC,
|
Type: mpeg4audio.ObjectTypeAACLC,
|
||||||
@@ -21,14 +21,14 @@ func TestMPEG4AudioAttributes(t *testing.T) {
|
|||||||
IndexLength: 3,
|
IndexLength: 3,
|
||||||
IndexDeltaLength: 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, 48000, format.ClockRate())
|
||||||
require.Equal(t, uint8(96), format.PayloadType())
|
require.Equal(t, uint8(96), format.PayloadType())
|
||||||
require.Equal(t, true, format.PTSEqualsDTS(&rtp.Packet{}))
|
require.Equal(t, true, format.PTSEqualsDTS(&rtp.Packet{}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMPEG4AudioMediaDescription(t *testing.T) {
|
func TestMPEG4AudioGenericMediaDescription(t *testing.T) {
|
||||||
format := &MPEG4Audio{
|
format := &MPEG4AudioGeneric{
|
||||||
PayloadTyp: 96,
|
PayloadTyp: 96,
|
||||||
Config: &mpeg4audio.Config{
|
Config: &mpeg4audio.Config{
|
||||||
Type: mpeg4audio.ObjectTypeAACLC,
|
Type: mpeg4audio.ObjectTypeAACLC,
|
||||||
@@ -52,8 +52,8 @@ func TestMPEG4AudioMediaDescription(t *testing.T) {
|
|||||||
}, fmtp)
|
}, fmtp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMPEG4AudioDecEncoder(t *testing.T) {
|
func TestMPEG4AudioGenericDecEncoder(t *testing.T) {
|
||||||
format := &MPEG4Audio{
|
format := &MPEG4AudioGeneric{
|
||||||
PayloadTyp: 96,
|
PayloadTyp: 96,
|
||||||
Config: &mpeg4audio.Config{
|
Config: &mpeg4audio.Config{
|
||||||
Type: mpeg4audio.ObjectTypeAACLC,
|
Type: mpeg4audio.ObjectTypeAACLC,
|
164
pkg/formats/mpeg4_audio_latm.go
Normal file
164
pkg/formats/mpeg4_audio_latm.go
Normal 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
|
||||||
|
}
|
42
pkg/formats/mpeg4_audio_latm_test.go
Normal file
42
pkg/formats/mpeg4_audio_latm_test.go
Normal 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)
|
||||||
|
}
|
@@ -9,39 +9,38 @@ import (
|
|||||||
"github.com/pion/rtp"
|
"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
|
// Specification: https://datatracker.ietf.org/doc/html/rfc6416#section-7.1
|
||||||
type MPEG4Video struct {
|
type MPEG4VideoES struct {
|
||||||
PayloadTyp uint8
|
PayloadTyp uint8
|
||||||
ProfileLevelID int
|
ProfileLevelID int
|
||||||
Config []byte
|
Config []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// String implements Format.
|
// String implements Format.
|
||||||
func (f *MPEG4Video) String() string {
|
func (f *MPEG4VideoES) String() string {
|
||||||
return "MPEG4-video"
|
return "MPEG4-video-es"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClockRate implements Format.
|
// ClockRate implements Format.
|
||||||
func (f *MPEG4Video) ClockRate() int {
|
func (f *MPEG4VideoES) ClockRate() int {
|
||||||
return 90000
|
return 90000
|
||||||
}
|
}
|
||||||
|
|
||||||
// PayloadType implements Format.
|
// PayloadType implements Format.
|
||||||
func (f *MPEG4Video) PayloadType() uint8 {
|
func (f *MPEG4VideoES) PayloadType() uint8 {
|
||||||
return f.PayloadTyp
|
return f.PayloadTyp
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *MPEG4Video) unmarshal(
|
func (f *MPEG4VideoES) unmarshal(
|
||||||
payloadType uint8, clock string, codec string,
|
payloadType uint8, clock string, codec string,
|
||||||
rtpmap string, fmtp map[string]string,
|
rtpmap string, fmtp map[string]string,
|
||||||
) error {
|
) error {
|
||||||
f.PayloadTyp = payloadType
|
f.PayloadTyp = payloadType
|
||||||
|
f.ProfileLevelID = 1 // default value defined by specification
|
||||||
// If this parameter is not specified by
|
|
||||||
// the procedure, its default value of 1 (Simple Profile/Level 1) is
|
|
||||||
// used.
|
|
||||||
f.ProfileLevelID = 1
|
|
||||||
|
|
||||||
for key, val := range fmtp {
|
for key, val := range fmtp {
|
||||||
switch key {
|
switch key {
|
||||||
@@ -66,7 +65,7 @@ func (f *MPEG4Video) unmarshal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Marshal implements Format.
|
// Marshal implements Format.
|
||||||
func (f *MPEG4Video) Marshal() (string, map[string]string) {
|
func (f *MPEG4VideoES) Marshal() (string, map[string]string) {
|
||||||
fmtp := map[string]string{
|
fmtp := map[string]string{
|
||||||
"profile-level-id": strconv.FormatInt(int64(f.ProfileLevelID), 10),
|
"profile-level-id": strconv.FormatInt(int64(f.ProfileLevelID), 10),
|
||||||
"config": strings.ToUpper(hex.EncodeToString(f.Config)),
|
"config": strings.ToUpper(hex.EncodeToString(f.Config)),
|
||||||
@@ -76,6 +75,6 @@ func (f *MPEG4Video) Marshal() (string, map[string]string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PTSEqualsDTS implements Format.
|
// PTSEqualsDTS implements Format.
|
||||||
func (f *MPEG4Video) PTSEqualsDTS(*rtp.Packet) bool {
|
func (f *MPEG4VideoES) PTSEqualsDTS(*rtp.Packet) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
@@ -7,20 +7,20 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMPEG4VideoAttributes(t *testing.T) {
|
func TestMPEG4VideoESAttributes(t *testing.T) {
|
||||||
format := &MPEG4Video{
|
format := &MPEG4VideoES{
|
||||||
PayloadTyp: 96,
|
PayloadTyp: 96,
|
||||||
ProfileLevelID: 1,
|
ProfileLevelID: 1,
|
||||||
Config: []byte{0x01, 0x02, 0x03},
|
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, 90000, format.ClockRate())
|
||||||
require.Equal(t, uint8(96), format.PayloadType())
|
require.Equal(t, uint8(96), format.PayloadType())
|
||||||
require.Equal(t, true, format.PTSEqualsDTS(&rtp.Packet{}))
|
require.Equal(t, true, format.PTSEqualsDTS(&rtp.Packet{}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMPEG4VideoMediaDescription(t *testing.T) {
|
func TestMPEG4VideoESMediaDescription(t *testing.T) {
|
||||||
format := &MPEG4Video{
|
format := &MPEG4VideoES{
|
||||||
PayloadTyp: 96,
|
PayloadTyp: 96,
|
||||||
ProfileLevelID: 1,
|
ProfileLevelID: 1,
|
||||||
Config: []byte{0x0a, 0x0b, 0x03},
|
Config: []byte{0x0a, 0x0b, 0x03},
|
3
pkg/formats/mpeg4_video_generic.go
Normal file
3
pkg/formats/mpeg4_video_generic.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
// TODO
|
3
pkg/formats/mpeg4_video_generic_test.go
Normal file
3
pkg/formats/mpeg4_video_generic_test.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
// TODO
|
@@ -553,7 +553,7 @@ func TestMediasUnmarshalErrors(t *testing.T) {
|
|||||||
"a=rtpmap:97 mpeg4-generic/44100/2\r\n" +
|
"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=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",
|
"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) {
|
t.Run(ca.name, func(t *testing.T) {
|
||||||
|
Reference in New Issue
Block a user