mirror of
https://github.com/aler9/gortsplib
synced 2025-10-05 07:06:58 +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-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)
|
||||||
|
@@ -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)
|
||||||
|
@@ -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)
|
||||||
|
@@ -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)
|
||||||
|
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
|
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
234
track.go
@@ -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
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 (
|
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())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user