client: support publishing with opus

This commit is contained in:
aler9
2021-10-30 15:45:13 +02:00
parent e9044bc6a5
commit cab3fe270e
14 changed files with 1208 additions and 855 deletions

View File

@@ -57,6 +57,7 @@ Features:
* [client-read-save-to-disk](examples/client-read-save-to-disk/main.go) * [client-read-save-to-disk](examples/client-read-save-to-disk/main.go)
* [client-publish-h264](examples/client-publish-h264/main.go) * [client-publish-h264](examples/client-publish-h264/main.go)
* [client-publish-aac](examples/client-publish-aac/main.go) * [client-publish-aac](examples/client-publish-aac/main.go)
* [client-publish-opus](examples/client-publish-opus/main.go)
* [client-publish-options](examples/client-publish-options/main.go) * [client-publish-options](examples/client-publish-options/main.go)
* [client-publish-pause](examples/client-publish-pause/main.go) * [client-publish-pause](examples/client-publish-pause/main.go)
* [server](examples/server/main.go) * [server](examples/server/main.go)

View File

@@ -55,7 +55,7 @@ func main() {
panic(err) panic(err)
} }
// write RTP packets // route RTP packets to the server
err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n]) err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n])
if err != nil { if err != nil {
panic(err) panic(err)

View File

@@ -21,7 +21,7 @@ func main() {
} }
defer pc.Close() defer pc.Close()
fmt.Println("Waiting for a RTP/h264 stream on UDP port 9000 - you can send one with Gstreamer:\n" + fmt.Println("Waiting for a RTP/H264 stream on UDP port 9000 - you can send one with Gstreamer:\n" +
"gst-launch-1.0 videotestsrc ! video/x-raw,width=1920,height=1080" + "gst-launch-1.0 videotestsrc ! video/x-raw,width=1920,height=1080" +
" ! x264enc speed-preset=veryfast tune=zerolatency bitrate=600000" + " ! x264enc speed-preset=veryfast tune=zerolatency bitrate=600000" +
" ! rtph264pay ! udpsink host=127.0.0.1 port=9000") " ! rtph264pay ! udpsink host=127.0.0.1 port=9000")
@@ -56,7 +56,7 @@ func main() {
panic(err) panic(err)
} }
// write RTP packets // route RTP packets to the server
err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n]) err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n])
if err != nil { if err != nil {
panic(err) panic(err)

View File

@@ -67,7 +67,7 @@ func main() {
panic(err) panic(err)
} }
// write RTP packets // route RTP packets to the server
err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n]) err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n])
if err != nil { if err != nil {
panic(err) panic(err)

View File

@@ -0,0 +1,64 @@
package main
import (
"fmt"
"net"
"github.com/aler9/gortsplib"
)
// This example shows how to
// 1. generate RTP/Opus packets with Gstreamer
// 2. connect to a RTSP server, announce an Opus track
// 3. route the packets from Gstreamer to the server
func main() {
// open a listener to receive RTP/Opus packets
pc, err := net.ListenPacket("udp", "localhost:9000")
if err != nil {
panic(err)
}
defer pc.Close()
fmt.Println("Waiting for a RTP/Opus stream on UDP port 9000 - you can send one with Gstreamer:\n" +
"gst-launch-1.0 audiotestsrc freq=300 ! audioconvert ! audioresample ! audio/x-raw,rate=48000" +
" ! opusenc" +
" ! rtpopuspay ! udpsink host=127.0.0.1 port=9000")
// wait for first packet
buf := make([]byte, 2048)
_, _, err = pc.ReadFrom(buf)
if err != nil {
panic(err)
}
fmt.Println("stream connected")
// create an Opus track
track, err := gortsplib.NewTrackOpus(96, &gortsplib.TrackConfigOpus{SampleRate: 48000, ChannelCount: 2})
if err != nil {
panic(err)
}
// connect to the server and start publishing the track
conn, err := gortsplib.DialPublish("rtsp://localhost:8554/mystream",
gortsplib.Tracks{track})
if err != nil {
panic(err)
}
defer conn.Close()
buf = make([]byte, 2048)
for {
// read RTP packets from the source
n, _, err := pc.ReadFrom(buf)
if err != nil {
panic(err)
}
// route RTP packets to the server
err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n])
if err != nil {
panic(err)
}
}
}

View File

@@ -63,7 +63,7 @@ func main() {
break break
} }
// write RTP packets // route RTP packets to the server
err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n]) err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n])
if err != nil { if err != nil {
break break

234
track.go
View File

@@ -1,15 +1,12 @@
package gortsplib package gortsplib
import ( import (
"encoding/base64"
"encoding/hex"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
psdp "github.com/pion/sdp/v3" psdp "github.com/pion/sdp/v3"
"github.com/aler9/gortsplib/pkg/aac"
"github.com/aler9/gortsplib/pkg/base" "github.com/aler9/gortsplib/pkg/base"
"github.com/aler9/gortsplib/pkg/sdp" "github.com/aler9/gortsplib/pkg/sdp"
) )
@@ -20,20 +17,6 @@ type Track struct {
Media *psdp.MediaDescription Media *psdp.MediaDescription
} }
// TrackConfigH264 is the configuration of an H264 track.
type TrackConfigH264 struct {
SPS []byte
PPS []byte
}
// TrackConfigAAC is the configuration of an AAC track.
type TrackConfigAAC struct {
Type int
SampleRate int
ChannelCount int
AOTSpecificConfig []byte
}
func (t *Track) hasControlAttribute() bool { func (t *Track) hasControlAttribute() bool {
for _, attr := range t.Media.Attributes { for _, attr := range t.Media.Attributes {
if attr.Key == "control" { if attr.Key == "control" {
@@ -140,223 +123,6 @@ func (t *Track) ClockRate() (int, error) {
return 0, fmt.Errorf("attribute 'rtpmap' not found") return 0, fmt.Errorf("attribute 'rtpmap' not found")
} }
// NewTrackH264 initializes an H264 track.
func NewTrackH264(payloadType uint8, conf *TrackConfigH264) (*Track, error) {
if len(conf.SPS) < 4 {
return nil, fmt.Errorf("invalid SPS")
}
spropParameterSets := base64.StdEncoding.EncodeToString(conf.SPS) +
"," + base64.StdEncoding.EncodeToString(conf.PPS)
profileLevelID := strings.ToUpper(hex.EncodeToString(conf.SPS[1:4]))
typ := strconv.FormatInt(int64(payloadType), 10)
return &Track{
Media: &psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "video",
Protos: []string{"RTP", "AVP"},
Formats: []string{typ},
},
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: typ + " H264/90000",
},
{
Key: "fmtp",
Value: typ + " packetization-mode=1; " +
"sprop-parameter-sets=" + spropParameterSets + "; " +
"profile-level-id=" + profileLevelID,
},
},
},
}, nil
}
// IsH264 checks whether the track is an H264 track.
func (t *Track) IsH264() bool {
if t.Media.MediaName.Media != "video" {
return false
}
v, ok := t.Media.Attribute("rtpmap")
if !ok {
return false
}
v = strings.TrimSpace(v)
vals := strings.Split(v, " ")
if len(vals) != 2 {
return false
}
return vals[1] == "H264/90000"
}
// ExtractConfigH264 extracts the configuration of an H264 track.
func (t *Track) ExtractConfigH264() (*TrackConfigH264, error) {
v, ok := t.Media.Attribute("fmtp")
if !ok {
return nil, fmt.Errorf("fmtp attribute is missing")
}
tmp := strings.SplitN(v, " ", 2)
if len(tmp) != 2 {
return nil, fmt.Errorf("invalid fmtp attribute (%v)", v)
}
for _, kv := range strings.Split(tmp[1], ";") {
kv = strings.Trim(kv, " ")
if len(kv) == 0 {
continue
}
tmp := strings.SplitN(kv, "=", 2)
if len(tmp) != 2 {
return nil, fmt.Errorf("invalid fmtp attribute (%v)", v)
}
if tmp[0] == "sprop-parameter-sets" {
tmp := strings.SplitN(tmp[1], ",", 2)
if len(tmp) != 2 {
return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v)
}
sps, err := base64.StdEncoding.DecodeString(tmp[0])
if err != nil {
return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v)
}
pps, err := base64.StdEncoding.DecodeString(tmp[1])
if err != nil {
return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v)
}
conf := &TrackConfigH264{
SPS: sps,
PPS: pps,
}
return conf, nil
}
}
return nil, fmt.Errorf("sprop-parameter-sets is missing (%v)", v)
}
// NewTrackAAC initializes an AAC track.
func NewTrackAAC(payloadType uint8, conf *TrackConfigAAC) (*Track, error) {
mpegConf, err := aac.MPEG4AudioConfig{
Type: aac.MPEG4AudioType(conf.Type),
SampleRate: conf.SampleRate,
ChannelCount: conf.ChannelCount,
AOTSpecificConfig: conf.AOTSpecificConfig,
}.Encode()
if err != nil {
return nil, err
}
typ := strconv.FormatInt(int64(payloadType), 10)
return &Track{
Media: &psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "audio",
Protos: []string{"RTP", "AVP"},
Formats: []string{typ},
},
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: typ + " mpeg4-generic/" + strconv.FormatInt(int64(conf.SampleRate), 10) +
"/" + strconv.FormatInt(int64(conf.ChannelCount), 10),
},
{
Key: "fmtp",
Value: typ + " profile-level-id=1; " +
"mode=AAC-hbr; " +
"sizelength=13; " +
"indexlength=3; " +
"indexdeltalength=3; " +
"config=" + hex.EncodeToString(mpegConf),
},
},
},
}, nil
}
// IsAAC checks whether the track is an AAC track.
func (t *Track) IsAAC() bool {
if t.Media.MediaName.Media != "audio" {
return false
}
v, ok := t.Media.Attribute("rtpmap")
if !ok {
return false
}
vals := strings.Split(v, " ")
if len(vals) != 2 {
return false
}
return strings.HasPrefix(strings.ToLower(vals[1]), "mpeg4-generic/")
}
// ExtractConfigAAC extracts the configuration of an AAC track.
func (t *Track) ExtractConfigAAC() (*TrackConfigAAC, error) {
v, ok := t.Media.Attribute("fmtp")
if !ok {
return nil, fmt.Errorf("fmtp attribute is missing")
}
tmp := strings.SplitN(v, " ", 2)
if len(tmp) != 2 {
return nil, fmt.Errorf("invalid fmtp (%v)", v)
}
for _, kv := range strings.Split(tmp[1], ";") {
kv = strings.Trim(kv, " ")
if len(kv) == 0 {
continue
}
tmp := strings.SplitN(kv, "=", 2)
if len(tmp) != 2 {
return nil, fmt.Errorf("invalid fmtp (%v)", v)
}
if tmp[0] == "config" {
enc, err := hex.DecodeString(tmp[1])
if err != nil {
return nil, fmt.Errorf("invalid AAC config (%v)", tmp[1])
}
var mpegConf aac.MPEG4AudioConfig
err = mpegConf.Decode(enc)
if err != nil {
return nil, fmt.Errorf("invalid AAC config (%v)", tmp[1])
}
conf := &TrackConfigAAC{
Type: int(mpegConf.Type),
SampleRate: mpegConf.SampleRate,
ChannelCount: mpegConf.ChannelCount,
AOTSpecificConfig: mpegConf.AOTSpecificConfig,
}
return conf, nil
}
}
return nil, fmt.Errorf("config is missing (%v)", v)
}
// Tracks is a list of tracks. // Tracks is a list of tracks.
type Tracks []*Track type Tracks []*Track

130
track_aac.go Normal file
View File

@@ -0,0 +1,130 @@
package gortsplib
import (
"encoding/hex"
"fmt"
"strconv"
"strings"
psdp "github.com/pion/sdp/v3"
"github.com/aler9/gortsplib/pkg/aac"
)
// TrackConfigAAC is the configuration of an AAC track.
type TrackConfigAAC struct {
Type int
SampleRate int
ChannelCount int
AOTSpecificConfig []byte
}
// NewTrackAAC initializes an AAC track.
func NewTrackAAC(payloadType uint8, conf *TrackConfigAAC) (*Track, error) {
mpegConf, err := aac.MPEG4AudioConfig{
Type: aac.MPEG4AudioType(conf.Type),
SampleRate: conf.SampleRate,
ChannelCount: conf.ChannelCount,
AOTSpecificConfig: conf.AOTSpecificConfig,
}.Encode()
if err != nil {
return nil, err
}
typ := strconv.FormatInt(int64(payloadType), 10)
return &Track{
Media: &psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "audio",
Protos: []string{"RTP", "AVP"},
Formats: []string{typ},
},
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: typ + " mpeg4-generic/" + strconv.FormatInt(int64(conf.SampleRate), 10) +
"/" + strconv.FormatInt(int64(conf.ChannelCount), 10),
},
{
Key: "fmtp",
Value: typ + " profile-level-id=1; " +
"mode=AAC-hbr; " +
"sizelength=13; " +
"indexlength=3; " +
"indexdeltalength=3; " +
"config=" + hex.EncodeToString(mpegConf),
},
},
},
}, nil
}
// IsAAC checks whether the track is an AAC track.
func (t *Track) IsAAC() bool {
if t.Media.MediaName.Media != "audio" {
return false
}
v, ok := t.Media.Attribute("rtpmap")
if !ok {
return false
}
vals := strings.Split(v, " ")
if len(vals) != 2 {
return false
}
return strings.HasPrefix(strings.ToLower(vals[1]), "mpeg4-generic/")
}
// ExtractConfigAAC extracts the configuration of an AAC track.
func (t *Track) ExtractConfigAAC() (*TrackConfigAAC, error) {
v, ok := t.Media.Attribute("fmtp")
if !ok {
return nil, fmt.Errorf("fmtp attribute is missing")
}
tmp := strings.SplitN(v, " ", 2)
if len(tmp) != 2 {
return nil, fmt.Errorf("invalid fmtp (%v)", v)
}
for _, kv := range strings.Split(tmp[1], ";") {
kv = strings.Trim(kv, " ")
if len(kv) == 0 {
continue
}
tmp := strings.SplitN(kv, "=", 2)
if len(tmp) != 2 {
return nil, fmt.Errorf("invalid fmtp (%v)", v)
}
if tmp[0] == "config" {
enc, err := hex.DecodeString(tmp[1])
if err != nil {
return nil, fmt.Errorf("invalid AAC config (%v)", tmp[1])
}
var mpegConf aac.MPEG4AudioConfig
err = mpegConf.Decode(enc)
if err != nil {
return nil, fmt.Errorf("invalid AAC config (%v)", tmp[1])
}
conf := &TrackConfigAAC{
Type: int(mpegConf.Type),
SampleRate: mpegConf.SampleRate,
ChannelCount: mpegConf.ChannelCount,
AOTSpecificConfig: mpegConf.AOTSpecificConfig,
}
return conf, nil
}
}
return nil, fmt.Errorf("config is missing (%v)", v)
}

286
track_aac_test.go Normal file
View File

@@ -0,0 +1,286 @@
package gortsplib
import (
"testing"
psdp "github.com/pion/sdp/v3"
"github.com/stretchr/testify/require"
)
func TestTrackAACNew(t *testing.T) {
track, err := NewTrackAAC(96, &TrackConfigAAC{
Type: 2,
SampleRate: 48000,
ChannelCount: 2,
})
require.NoError(t, err)
require.Equal(t, &Track{
Media: &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=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190",
},
},
},
}, track)
}
func TestTrackIsAAC(t *testing.T) {
for _, ca := range []struct {
name string
track *Track
}{
{
"standard",
&Track{
Media: &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=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190",
},
},
},
},
},
{
"uppercase",
&Track{
Media: &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=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190",
},
},
},
},
},
} {
t.Run(ca.name, func(t *testing.T) {
require.Equal(t, true, ca.track.IsAAC())
})
}
}
func TestTrackExtractConfigAAC(t *testing.T) {
for _, ca := range []struct {
name string
track *Track
conf *TrackConfigAAC
}{
{
"generic",
&Track{
Media: &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=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190",
},
},
},
},
&TrackConfigAAC{
Type: 2,
SampleRate: 48000,
ChannelCount: 2,
},
},
{
"vlc rtsp server",
&Track{
Media: &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=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190;",
},
},
},
},
&TrackConfigAAC{
Type: 2,
SampleRate: 48000,
ChannelCount: 2,
},
},
} {
t.Run(ca.name, func(t *testing.T) {
conf, err := ca.track.ExtractConfigAAC()
require.NoError(t, err)
require.Equal(t, ca.conf, conf)
})
}
}
func TestTrackConfigAACErrors(t *testing.T) {
for _, ca := range []struct {
name string
track *Track
err string
}{
{
"missing fmtp",
&Track{
Media: &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",
},
},
},
},
"fmtp attribute is missing",
},
{
"invalid fmtp",
&Track{
Media: &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",
},
},
},
},
"invalid fmtp (96)",
},
{
"fmtp without key",
&Track{
Media: &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 (96 profile-level-id)",
},
{
"missing config",
&Track{
Media: &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=1",
},
},
},
},
"config is missing (96 profile-level-id=1)",
},
{
"invalid config",
&Track{
Media: &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=1; config=zz",
},
},
},
},
"invalid AAC config (zz)",
},
} {
t.Run(ca.name, func(t *testing.T) {
_, err := ca.track.ExtractConfigAAC()
require.Equal(t, ca.err, err.Error())
})
}
}

124
track_h264.go Normal file
View File

@@ -0,0 +1,124 @@
package gortsplib
import (
"encoding/base64"
"encoding/hex"
"fmt"
"strconv"
"strings"
psdp "github.com/pion/sdp/v3"
)
// TrackConfigH264 is the configuration of an H264 track.
type TrackConfigH264 struct {
SPS []byte
PPS []byte
}
// NewTrackH264 initializes an H264 track.
func NewTrackH264(payloadType uint8, conf *TrackConfigH264) (*Track, error) {
if len(conf.SPS) < 4 {
return nil, fmt.Errorf("invalid SPS")
}
spropParameterSets := base64.StdEncoding.EncodeToString(conf.SPS) +
"," + base64.StdEncoding.EncodeToString(conf.PPS)
profileLevelID := strings.ToUpper(hex.EncodeToString(conf.SPS[1:4]))
typ := strconv.FormatInt(int64(payloadType), 10)
return &Track{
Media: &psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "video",
Protos: []string{"RTP", "AVP"},
Formats: []string{typ},
},
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: typ + " H264/90000",
},
{
Key: "fmtp",
Value: typ + " packetization-mode=1; " +
"sprop-parameter-sets=" + spropParameterSets + "; " +
"profile-level-id=" + profileLevelID,
},
},
},
}, nil
}
// IsH264 checks whether the track is an H264 track.
func (t *Track) IsH264() bool {
if t.Media.MediaName.Media != "video" {
return false
}
v, ok := t.Media.Attribute("rtpmap")
if !ok {
return false
}
v = strings.TrimSpace(v)
vals := strings.Split(v, " ")
if len(vals) != 2 {
return false
}
return vals[1] == "H264/90000"
}
// ExtractConfigH264 extracts the configuration of an H264 track.
func (t *Track) ExtractConfigH264() (*TrackConfigH264, error) {
v, ok := t.Media.Attribute("fmtp")
if !ok {
return nil, fmt.Errorf("fmtp attribute is missing")
}
tmp := strings.SplitN(v, " ", 2)
if len(tmp) != 2 {
return nil, fmt.Errorf("invalid fmtp attribute (%v)", v)
}
for _, kv := range strings.Split(tmp[1], ";") {
kv = strings.Trim(kv, " ")
if len(kv) == 0 {
continue
}
tmp := strings.SplitN(kv, "=", 2)
if len(tmp) != 2 {
return nil, fmt.Errorf("invalid fmtp attribute (%v)", v)
}
if tmp[0] == "sprop-parameter-sets" {
tmp := strings.SplitN(tmp[1], ",", 2)
if len(tmp) != 2 {
return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v)
}
sps, err := base64.StdEncoding.DecodeString(tmp[0])
if err != nil {
return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v)
}
pps, err := base64.StdEncoding.DecodeString(tmp[1])
if err != nil {
return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v)
}
conf := &TrackConfigH264{
SPS: sps,
PPS: pps,
}
return conf, nil
}
}
return nil, fmt.Errorf("sprop-parameter-sets is missing (%v)", v)
}

348
track_h264_test.go Normal file
View File

@@ -0,0 +1,348 @@
package gortsplib
import (
"testing"
psdp "github.com/pion/sdp/v3"
"github.com/stretchr/testify/require"
)
func TestTrackH264New(t *testing.T) {
tr, err := NewTrackH264(96, &TrackConfigH264{
SPS: []byte{
0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0,
0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00,
0x00, 0x03, 0x00, 0x3d, 0x08,
},
PPS: []byte{
0x68, 0xee, 0x3c, 0x80,
},
})
require.NoError(t, err)
require.Equal(t, &Track{
Media: &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 packetization-mode=1; " +
"sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C",
},
},
},
}, tr)
}
func TestTrackIsH264(t *testing.T) {
for _, ca := range []struct {
name string
track *Track
}{
{
"standard",
&Track{
Media: &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 packetization-mode=1; " +
"sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C",
},
},
},
},
},
{
"space at the end rtpmap",
&Track{
Media: &psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "video",
Protos: []string{"RTP", "AVP"},
Formats: []string{"96"},
},
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 H264/90000 ",
},
},
},
},
},
} {
t.Run(ca.name, func(t *testing.T) {
require.Equal(t, true, ca.track.IsH264())
})
}
}
func TestTrackExtractConfigH264(t *testing.T) {
for _, ca := range []struct {
name string
track *Track
conf *TrackConfigH264
}{
{
"generic",
&Track{
Media: &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 packetization-mode=1; " +
"sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C",
},
},
},
},
&TrackConfigH264{
SPS: []byte{
0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0,
0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00,
0x00, 0x03, 0x00, 0x3d, 0x08,
},
PPS: []byte{
0x68, 0xee, 0x3c, 0x80,
},
},
},
{
"vlc rtsp server",
&Track{
Media: &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 packetization-mode=1;profile-level-id=64001f;" +
"sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,aOvjyyLA;",
},
},
},
},
&TrackConfigH264{
SPS: []byte{
0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9, 0x40, 0x50,
0x05, 0xbb, 0x01, 0x6c, 0x80, 0x00, 0x00, 0x03,
0x00, 0x80, 0x00, 0x00, 0x1e, 0x07, 0x8c, 0x18,
0xcb,
},
PPS: []byte{
0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0,
},
},
},
} {
t.Run(ca.name, func(t *testing.T) {
conf, err := ca.track.ExtractConfigH264()
require.NoError(t, err)
require.Equal(t, ca.conf, conf)
})
}
}
func TestTrackConfigH264Errors(t *testing.T) {
for _, ca := range []struct {
name string
track *Track
err string
}{
{
"missing fmtp",
&Track{
Media: &psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "video",
Protos: []string{"RTP", "AVP"},
Formats: []string{"96"},
},
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 H264/90000",
},
},
},
},
"fmtp attribute is missing",
},
{
"invalid fmtp",
&Track{
Media: &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",
},
},
},
},
"invalid fmtp attribute (96)",
},
{
"fmtp without key",
&Track{
Media: &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 packetization-mode",
},
},
},
},
"invalid fmtp attribute (96 packetization-mode)",
},
{
"missing sprop-parameter-set",
&Track{
Media: &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 packetization-mode=1",
},
},
},
},
"sprop-parameter-sets is missing (96 packetization-mode=1)",
},
{
"invalid sprop-parameter-set 1",
&Track{
Media: &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 sprop-parameter-sets=aaaaaa",
},
},
},
},
"invalid sprop-parameter-sets (96 sprop-parameter-sets=aaaaaa)",
},
{
"invalid sprop-parameter-set 2",
&Track{
Media: &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 sprop-parameter-sets=aaaaaa,bbb",
},
},
},
},
"invalid sprop-parameter-sets (96 sprop-parameter-sets=aaaaaa,bbb)",
},
{
"invalid sprop-parameter-set 3",
&Track{
Media: &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 sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,bbb",
},
},
},
},
"invalid sprop-parameter-sets (96 sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,bbb)",
},
} {
t.Run(ca.name, func(t *testing.T) {
_, err := ca.track.ExtractConfigH264()
require.Equal(t, ca.err, err.Error())
})
}
}

93
track_opus.go Normal file
View File

@@ -0,0 +1,93 @@
package gortsplib
import (
"fmt"
"strconv"
"strings"
psdp "github.com/pion/sdp/v3"
)
// TrackConfigOpus is the configuration of an Opus track.
type TrackConfigOpus struct {
SampleRate int
ChannelCount int
}
// NewTrackOpus initializes an Opus track.
func NewTrackOpus(payloadType uint8, conf *TrackConfigOpus) (*Track, error) {
typ := strconv.FormatInt(int64(payloadType), 10)
return &Track{
Media: &psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "audio",
Protos: []string{"RTP", "AVP"},
Formats: []string{typ},
},
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: typ + " opus/" + strconv.FormatInt(int64(conf.SampleRate), 10) +
"/" + strconv.FormatInt(int64(conf.ChannelCount), 10),
},
{
Key: "fmtp",
Value: typ + " sprop-stereo=" + func() string {
if conf.ChannelCount == 2 {
return "1"
}
return "0"
}(),
},
},
},
}, nil
}
// IsOpus checks whether the track is an Opus track.
func (t *Track) IsOpus() bool {
if t.Media.MediaName.Media != "audio" {
return false
}
v, ok := t.Media.Attribute("rtpmap")
if !ok {
return false
}
vals := strings.Split(v, " ")
if len(vals) != 2 {
return false
}
return strings.HasPrefix(vals[1], "opus/")
}
// ExtractConfigOpus extracts the configuration of an Opus track.
func (t *Track) ExtractConfigOpus() (*TrackConfigOpus, error) {
v, ok := t.Media.Attribute("rtpmap")
if !ok {
return nil, fmt.Errorf("rtpmap attribute is missing")
}
tmp := strings.SplitN(v, "/", 3)
if len(tmp) != 3 {
return nil, fmt.Errorf("invalid rtpmap (%v)", v)
}
sampleRate, err := strconv.ParseInt(tmp[1], 10, 64)
if err != nil {
return nil, err
}
channelCount, err := strconv.ParseInt(tmp[2], 10, 64)
if err != nil {
return nil, err
}
return &TrackConfigOpus{
SampleRate: int(sampleRate),
ChannelCount: int(channelCount),
}, nil
}

157
track_opus_test.go Normal file
View File

@@ -0,0 +1,157 @@
package gortsplib
import (
"testing"
psdp "github.com/pion/sdp/v3"
"github.com/stretchr/testify/require"
)
func TestTrackOpusNew(t *testing.T) {
track, err := NewTrackOpus(96, &TrackConfigOpus{
SampleRate: 48000,
ChannelCount: 2,
})
require.NoError(t, err)
require.Equal(t, &Track{
Media: &psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "audio",
Protos: []string{"RTP", "AVP"},
Formats: []string{"96"},
},
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 opus/48000/2",
},
{
Key: "fmtp",
Value: "96 sprop-stereo=1",
},
},
},
}, track)
}
func TestTrackIsOpus(t *testing.T) {
for _, ca := range []struct {
name string
track *Track
}{
{
"standard",
&Track{
Media: &psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "audio",
Protos: []string{"RTP", "AVP"},
Formats: []string{"96"},
},
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 opus/48000/2",
},
{
Key: "fmtp",
Value: "96 sprop-stereo=1",
},
},
},
},
},
} {
t.Run(ca.name, func(t *testing.T) {
require.Equal(t, true, ca.track.IsOpus())
})
}
}
func TestTrackExtractConfigOpus(t *testing.T) {
for _, ca := range []struct {
name string
track *Track
conf *TrackConfigOpus
}{
{
"generic",
&Track{
Media: &psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "audio",
Protos: []string{"RTP", "AVP"},
Formats: []string{"96"},
},
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 opus/48000/2",
},
{
Key: "fmtp",
Value: "96 sprop-stereo=1",
},
},
},
},
&TrackConfigOpus{
SampleRate: 48000,
ChannelCount: 2,
},
},
} {
t.Run(ca.name, func(t *testing.T) {
conf, err := ca.track.ExtractConfigOpus()
require.NoError(t, err)
require.Equal(t, ca.conf, conf)
})
}
}
func TestTrackConfigOpusErrors(t *testing.T) {
for _, ca := range []struct {
name string
track *Track
err string
}{
{
"missing rtpmap",
&Track{
Media: &psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "audio",
Protos: []string{"RTP", "AVP"},
Formats: []string{"96"},
},
Attributes: []psdp.Attribute{},
},
},
"rtpmap attribute is missing",
},
{
"invalid rtpmap",
&Track{
Media: &psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "audio",
Protos: []string{"RTP", "AVP"},
Formats: []string{"96"},
},
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96",
},
},
},
},
"invalid rtpmap (96)",
},
} {
t.Run(ca.name, func(t *testing.T) {
_, err := ca.track.ExtractConfigOpus()
require.Equal(t, ca.err, err.Error())
})
}
}

View File

@@ -3,7 +3,6 @@ package gortsplib
import ( import (
"testing" "testing"
psdp "github.com/pion/sdp/v3"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/aler9/gortsplib/pkg/base" "github.com/aler9/gortsplib/pkg/base"
@@ -219,618 +218,3 @@ func TestTrackClockRate(t *testing.T) {
}) })
} }
} }
func TestTrackH264New(t *testing.T) {
sps := []byte{
0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0,
0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00,
0x00, 0x03, 0x00, 0x3d, 0x08,
}
pps := []byte{
0x68, 0xee, 0x3c, 0x80,
}
tr, err := NewTrackH264(96, &TrackConfigH264{sps, pps})
require.NoError(t, err)
require.Equal(t, &Track{
Media: &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 packetization-mode=1; " +
"sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C",
},
},
},
}, tr)
}
func TestTrackIsH264(t *testing.T) {
for _, ca := range []struct {
name string
track *Track
}{
{
"standard",
&Track{
Media: &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 packetization-mode=1; " +
"sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C",
},
},
},
},
},
{
"standard with a space at the end rtpmap",
&Track{
Media: &psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "video",
Protos: []string{"RTP", "AVP"},
Formats: []string{"96"},
},
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 H264/90000 ",
},
},
},
},
},
} {
t.Run(ca.name, func(t *testing.T) {
require.Equal(t, true, ca.track.IsH264())
})
}
}
func TestTrackExtractConfigH264(t *testing.T) {
for _, ca := range []struct {
name string
track *Track
conf *TrackConfigH264
}{
{
"generic",
&Track{
Media: &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 packetization-mode=1; " +
"sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C",
},
},
},
},
&TrackConfigH264{
SPS: []byte{
0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0,
0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00,
0x00, 0x03, 0x00, 0x3d, 0x08,
},
PPS: []byte{
0x68, 0xee, 0x3c, 0x80,
},
},
},
{
"vlc rtsp server",
&Track{
Media: &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 packetization-mode=1;profile-level-id=64001f;" +
"sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,aOvjyyLA;",
},
},
},
},
&TrackConfigH264{
SPS: []byte{
0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9, 0x40, 0x50,
0x05, 0xbb, 0x01, 0x6c, 0x80, 0x00, 0x00, 0x03,
0x00, 0x80, 0x00, 0x00, 0x1e, 0x07, 0x8c, 0x18,
0xcb,
},
PPS: []byte{
0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0,
},
},
},
} {
t.Run(ca.name, func(t *testing.T) {
conf, err := ca.track.ExtractConfigH264()
require.NoError(t, err)
require.Equal(t, ca.conf, conf)
})
}
}
func TestTrackConfigH264Errors(t *testing.T) {
for _, ca := range []struct {
name string
track *Track
err string
}{
{
"missing fmtp",
&Track{
Media: &psdp.MediaDescription{
MediaName: psdp.MediaName{
Media: "video",
Protos: []string{"RTP", "AVP"},
Formats: []string{"96"},
},
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 H264/90000",
},
},
},
},
"fmtp attribute is missing",
},
{
"invalid fmtp",
&Track{
Media: &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",
},
},
},
},
"invalid fmtp attribute (96)",
},
{
"fmtp without key",
&Track{
Media: &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 packetization-mode",
},
},
},
},
"invalid fmtp attribute (96 packetization-mode)",
},
{
"missing sprop-parameter-set",
&Track{
Media: &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 packetization-mode=1",
},
},
},
},
"sprop-parameter-sets is missing (96 packetization-mode=1)",
},
{
"invalid sprop-parameter-set 1",
&Track{
Media: &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 sprop-parameter-sets=aaaaaa",
},
},
},
},
"invalid sprop-parameter-sets (96 sprop-parameter-sets=aaaaaa)",
},
{
"invalid sprop-parameter-set 2",
&Track{
Media: &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 sprop-parameter-sets=aaaaaa,bbb",
},
},
},
},
"invalid sprop-parameter-sets (96 sprop-parameter-sets=aaaaaa,bbb)",
},
{
"invalid sprop-parameter-set 3",
&Track{
Media: &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 sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,bbb",
},
},
},
},
"invalid sprop-parameter-sets (96 sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,bbb)",
},
} {
t.Run(ca.name, func(t *testing.T) {
_, err := ca.track.ExtractConfigH264()
require.Equal(t, ca.err, err.Error())
})
}
}
func TestTrackAACNew(t *testing.T) {
track, err := NewTrackAAC(96, &TrackConfigAAC{Type: 2, SampleRate: 48000, ChannelCount: 2})
require.NoError(t, err)
require.Equal(t, &Track{
Media: &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=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190",
},
},
},
}, track)
}
func TestTrackIsAAC(t *testing.T) {
for _, ca := range []struct {
name string
track *Track
}{
{
"standard",
&Track{
Media: &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=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190",
},
},
},
},
},
{
"uppercase",
&Track{
Media: &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=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190",
},
},
},
},
},
} {
t.Run(ca.name, func(t *testing.T) {
require.Equal(t, true, ca.track.IsAAC())
})
}
}
func TestTrackExtractConfigAAC(t *testing.T) {
for _, ca := range []struct {
name string
track *Track
conf *TrackConfigAAC
}{
{
"generic",
&Track{
Media: &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=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190",
},
},
},
},
&TrackConfigAAC{
Type: 2,
SampleRate: 48000,
ChannelCount: 2,
},
},
{
"vlc rtsp server",
&Track{
Media: &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=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190;",
},
},
},
},
&TrackConfigAAC{
Type: 2,
SampleRate: 48000,
ChannelCount: 2,
},
},
} {
t.Run(ca.name, func(t *testing.T) {
conf, err := ca.track.ExtractConfigAAC()
require.NoError(t, err)
require.Equal(t, ca.conf, conf)
})
}
}
func TestTrackConfigAACErrors(t *testing.T) {
for _, ca := range []struct {
name string
track *Track
err string
}{
{
"missing fmtp",
&Track{
Media: &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",
},
},
},
},
"fmtp attribute is missing",
},
{
"invalid fmtp",
&Track{
Media: &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",
},
},
},
},
"invalid fmtp (96)",
},
{
"fmtp without key",
&Track{
Media: &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 (96 profile-level-id)",
},
{
"missing config",
&Track{
Media: &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=1",
},
},
},
},
"config is missing (96 profile-level-id=1)",
},
{
"invalid config",
&Track{
Media: &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=1; config=zz",
},
},
},
},
"invalid AAC config (zz)",
},
} {
t.Run(ca.name, func(t *testing.T) {
_, err := ca.track.ExtractConfigAAC()
require.Equal(t, ca.err, err.Error())
})
}
}