mirror of
https://github.com/aler9/gortsplib
synced 2025-10-05 15:16:51 +08:00
client: support publishing with opus
This commit is contained in:
@@ -57,6 +57,7 @@ Features:
|
||||
* [client-read-save-to-disk](examples/client-read-save-to-disk/main.go)
|
||||
* [client-publish-h264](examples/client-publish-h264/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-pause](examples/client-publish-pause/main.go)
|
||||
* [server](examples/server/main.go)
|
||||
|
@@ -55,7 +55,7 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// write RTP packets
|
||||
// route RTP packets to the server
|
||||
err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@@ -21,7 +21,7 @@ func main() {
|
||||
}
|
||||
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" +
|
||||
" ! x264enc speed-preset=veryfast tune=zerolatency bitrate=600000" +
|
||||
" ! rtph264pay ! udpsink host=127.0.0.1 port=9000")
|
||||
@@ -56,7 +56,7 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// write RTP packets
|
||||
// route RTP packets to the server
|
||||
err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@@ -67,7 +67,7 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// write RTP packets
|
||||
// route RTP packets to the server
|
||||
err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
64
examples/client-publish-opus/main.go
Normal file
64
examples/client-publish-opus/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -63,7 +63,7 @@ func main() {
|
||||
break
|
||||
}
|
||||
|
||||
// write RTP packets
|
||||
// route RTP packets to the server
|
||||
err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n])
|
||||
if err != nil {
|
||||
break
|
||||
|
234
track.go
234
track.go
@@ -1,15 +1,12 @@
|
||||
package gortsplib
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
psdp "github.com/pion/sdp/v3"
|
||||
|
||||
"github.com/aler9/gortsplib/pkg/aac"
|
||||
"github.com/aler9/gortsplib/pkg/base"
|
||||
"github.com/aler9/gortsplib/pkg/sdp"
|
||||
)
|
||||
@@ -20,20 +17,6 @@ type Track struct {
|
||||
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 {
|
||||
for _, attr := range t.Media.Attributes {
|
||||
if attr.Key == "control" {
|
||||
@@ -140,223 +123,6 @@ func (t *Track) ClockRate() (int, error) {
|
||||
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.
|
||||
type Tracks []*Track
|
||||
|
||||
|
130
track_aac.go
Normal file
130
track_aac.go
Normal 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
286
track_aac_test.go
Normal 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
124
track_h264.go
Normal 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
348
track_h264_test.go
Normal 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
93
track_opus.go
Normal 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
157
track_opus_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
}
|
616
track_test.go
616
track_test.go
@@ -3,7 +3,6 @@ package gortsplib
|
||||
import (
|
||||
"testing"
|
||||
|
||||
psdp "github.com/pion/sdp/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user