mirror of
https://github.com/aler9/gortsplib
synced 2025-10-21 14:19:36 +08:00
allow setting additional properties of streams through description.Stream
This commit is contained in:
272
pkg/description/media.go
Normal file
272
pkg/description/media.go
Normal file
@@ -0,0 +1,272 @@
|
||||
// Package description contains objects to describe streams.
|
||||
package description
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
psdp "github.com/pion/sdp/v3"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/url"
|
||||
)
|
||||
|
||||
var smartRegexp = regexp.MustCompile("^([0-9]+) (.*?)/90000")
|
||||
|
||||
func getControlAttribute(attributes []psdp.Attribute) string {
|
||||
for _, attr := range attributes {
|
||||
if attr.Key == "control" {
|
||||
return attr.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getDirection(attributes []psdp.Attribute) MediaDirection {
|
||||
for _, attr := range attributes {
|
||||
switch attr.Key {
|
||||
case "sendonly":
|
||||
return MediaDirectionSendonly
|
||||
|
||||
case "recvonly":
|
||||
return MediaDirectionRecvonly
|
||||
|
||||
case "sendrecv":
|
||||
return MediaDirectionSendrecv
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getFormatAttribute(attributes []psdp.Attribute, payloadType uint8, key string) string {
|
||||
for _, attr := range attributes {
|
||||
if attr.Key == key {
|
||||
v := strings.TrimSpace(attr.Value)
|
||||
if parts := strings.SplitN(v, " ", 2); len(parts) == 2 {
|
||||
if tmp, err := strconv.ParseUint(parts[0], 10, 8); err == nil && uint8(tmp) == payloadType {
|
||||
return parts[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func decodeFMTP(enc string) map[string]string {
|
||||
if enc == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := make(map[string]string)
|
||||
|
||||
for _, kv := range strings.Split(enc, ";") {
|
||||
kv = strings.Trim(kv, " ")
|
||||
|
||||
if len(kv) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
tmp := strings.SplitN(kv, "=", 2)
|
||||
if len(tmp) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
ret[strings.ToLower(tmp[0])] = tmp[1]
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func sortedKeys(fmtp map[string]string) []string {
|
||||
keys := make([]string, len(fmtp))
|
||||
i := 0
|
||||
for key := range fmtp {
|
||||
keys[i] = key
|
||||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// MediaDirection is the direction of a media stream.
|
||||
type MediaDirection string
|
||||
|
||||
// standard directions.
|
||||
const (
|
||||
MediaDirectionSendonly MediaDirection = "sendonly"
|
||||
MediaDirectionRecvonly MediaDirection = "recvonly"
|
||||
MediaDirectionSendrecv MediaDirection = "sendrecv"
|
||||
)
|
||||
|
||||
// MediaType is the type of a media stream.
|
||||
type MediaType string
|
||||
|
||||
// standard media stream types.
|
||||
const (
|
||||
MediaTypeVideo MediaType = "video"
|
||||
MediaTypeAudio MediaType = "audio"
|
||||
MediaTypeApplication MediaType = "application"
|
||||
)
|
||||
|
||||
// Media is a media stream.
|
||||
// It contains one or more formats.
|
||||
type Media struct {
|
||||
// Media type.
|
||||
Type MediaType
|
||||
|
||||
// Direction of the stream.
|
||||
Direction MediaDirection
|
||||
|
||||
// Control attribute.
|
||||
Control string
|
||||
|
||||
// Formats contained into the media.
|
||||
Formats []format.Format
|
||||
}
|
||||
|
||||
// Unmarshal decodes the media from the SDP format.
|
||||
func (m *Media) Unmarshal(md *psdp.MediaDescription) error {
|
||||
m.Type = MediaType(md.MediaName.Media)
|
||||
m.Direction = getDirection(md.Attributes)
|
||||
m.Control = getControlAttribute(md.Attributes)
|
||||
|
||||
m.Formats = nil
|
||||
for _, payloadType := range md.MediaName.Formats {
|
||||
if payloadType == "smart/1/90000" {
|
||||
for _, attr := range md.Attributes {
|
||||
if attr.Key == "rtpmap" {
|
||||
sm := smartRegexp.FindStringSubmatch(attr.Value)
|
||||
if sm != nil {
|
||||
payloadType = sm[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tmp, err := strconv.ParseUint(payloadType, 10, 8)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payloadTypeInt := uint8(tmp)
|
||||
|
||||
rtpMap := getFormatAttribute(md.Attributes, payloadTypeInt, "rtpmap")
|
||||
fmtp := decodeFMTP(getFormatAttribute(md.Attributes, payloadTypeInt, "fmtp"))
|
||||
|
||||
format, err := format.Unmarshal(string(m.Type), payloadTypeInt, rtpMap, fmtp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Formats = append(m.Formats, format)
|
||||
}
|
||||
|
||||
if m.Formats == nil {
|
||||
return fmt.Errorf("no formats found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Marshal encodes the media in SDP format.
|
||||
func (m Media) Marshal() *psdp.MediaDescription {
|
||||
md := &psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: string(m.Type),
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "control",
|
||||
Value: m.Control,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if m.Direction != "" {
|
||||
md.Attributes = append(md.Attributes, psdp.Attribute{
|
||||
Key: string(m.Direction),
|
||||
})
|
||||
}
|
||||
|
||||
for _, forma := range m.Formats {
|
||||
typ := strconv.FormatUint(uint64(forma.PayloadType()), 10)
|
||||
md.MediaName.Formats = append(md.MediaName.Formats, typ)
|
||||
|
||||
rtpmap := forma.RTPMap()
|
||||
if rtpmap != "" {
|
||||
md.Attributes = append(md.Attributes, psdp.Attribute{
|
||||
Key: "rtpmap",
|
||||
Value: typ + " " + rtpmap,
|
||||
})
|
||||
}
|
||||
|
||||
fmtp := forma.FMTP()
|
||||
if len(fmtp) != 0 {
|
||||
tmp := make([]string, len(fmtp))
|
||||
for i, key := range sortedKeys(fmtp) {
|
||||
tmp[i] = key + "=" + fmtp[key]
|
||||
}
|
||||
|
||||
md.Attributes = append(md.Attributes, psdp.Attribute{
|
||||
Key: "fmtp",
|
||||
Value: typ + " " + strings.Join(tmp, "; "),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return md
|
||||
}
|
||||
|
||||
// URL returns the absolute URL of the media.
|
||||
func (m Media) URL(contentBase *url.URL) (*url.URL, error) {
|
||||
if contentBase == nil {
|
||||
return nil, fmt.Errorf("Content-Base header not provided")
|
||||
}
|
||||
|
||||
// no control attribute, use base URL
|
||||
if m.Control == "" {
|
||||
return contentBase, nil
|
||||
}
|
||||
|
||||
// control attribute contains an absolute path
|
||||
if strings.HasPrefix(m.Control, "rtsp://") ||
|
||||
strings.HasPrefix(m.Control, "rtsps://") {
|
||||
ur, err := url.Parse(m.Control)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// copy host and credentials
|
||||
ur.Host = contentBase.Host
|
||||
ur.User = contentBase.User
|
||||
return ur, nil
|
||||
}
|
||||
|
||||
// control attribute contains a relative control attribute
|
||||
// insert the control attribute at the end of the URL
|
||||
// if there's a query, insert it after the query
|
||||
// otherwise insert it after the path
|
||||
strURL := contentBase.String()
|
||||
if m.Control[0] != '?' && !strings.HasSuffix(strURL, "/") {
|
||||
strURL += "/"
|
||||
}
|
||||
|
||||
ur, _ := url.Parse(strURL + m.Control)
|
||||
return ur, nil
|
||||
}
|
||||
|
||||
// FindFormat finds a certain format among all the formats in the media.
|
||||
func (m Media) FindFormat(forma interface{}) bool {
|
||||
for _, formak := range m.Formats {
|
||||
if reflect.TypeOf(formak) == reflect.TypeOf(forma).Elem() {
|
||||
reflect.ValueOf(forma).Elem().Set(reflect.ValueOf(formak))
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
154
pkg/description/media_test.go
Normal file
154
pkg/description/media_test.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package description
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/sdp"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/url"
|
||||
)
|
||||
|
||||
func mustParseURL(s string) *url.URL {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func TestMediaURL(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
sdp []byte
|
||||
baseURL *url.URL
|
||||
ur *url.URL
|
||||
}{
|
||||
{
|
||||
"missing control",
|
||||
[]byte("v=0\r\n" +
|
||||
"m=video 0 RTP/AVP 96\r\n" +
|
||||
"a=rtpmap:96 H264/90000\r\n"),
|
||||
mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/"),
|
||||
mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/"),
|
||||
},
|
||||
{
|
||||
"absolute control",
|
||||
[]byte("v=0\r\n" +
|
||||
"m=video 0 RTP/AVP 96\r\n" +
|
||||
"a=rtpmap:96 H264/90000\r\n" +
|
||||
"a=control:rtsp://localhost/path/trackID=7"),
|
||||
mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/"),
|
||||
mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/trackID=7"),
|
||||
},
|
||||
{
|
||||
"absolute control rtsps",
|
||||
[]byte("v=0\r\n" +
|
||||
"m=video 0 RTP/AVP 96\r\n" +
|
||||
"a=rtpmap:96 H264/90000\r\n" +
|
||||
"a=control:rtsps://localhost/path/trackID=7"),
|
||||
mustParseURL("rtsps://myuser:mypass@192.168.1.99:554/"),
|
||||
mustParseURL("rtsps://myuser:mypass@192.168.1.99:554/path/trackID=7"),
|
||||
},
|
||||
{
|
||||
"relative control",
|
||||
[]byte("v=0\r\n" +
|
||||
"m=video 0 RTP/AVP 96\r\n" +
|
||||
"a=rtpmap:96 H264/90000\r\n" +
|
||||
"a=control:trackID=5"),
|
||||
mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/"),
|
||||
mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/trackID=5"),
|
||||
},
|
||||
{
|
||||
"relative control, subpath",
|
||||
[]byte("v=0\r\n" +
|
||||
"m=video 0 RTP/AVP 96\r\n" +
|
||||
"a=rtpmap:96 H264/90000\r\n" +
|
||||
"a=control:trackID=5"),
|
||||
mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/sub/path/"),
|
||||
mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/sub/path/trackID=5"),
|
||||
},
|
||||
{
|
||||
"relative control, subpath, without slash",
|
||||
[]byte("v=0\r\n" +
|
||||
"m=video 0 RTP/AVP 96\r\n" +
|
||||
"a=rtpmap:96 H264/90000\r\n" +
|
||||
"a=control:trackID=5"),
|
||||
mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/sub/path"),
|
||||
mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/sub/path/trackID=5"),
|
||||
},
|
||||
{
|
||||
"relative control, url with query",
|
||||
[]byte("v=0\r\n" +
|
||||
"m=video 0 RTP/AVP 96\r\n" +
|
||||
"a=rtpmap:96 H264/90000\r\n" +
|
||||
"a=control:trackID=5"),
|
||||
mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/" +
|
||||
"test?user=tmp&password=BagRep1&channel=1&stream=0.sdp"),
|
||||
mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/" +
|
||||
"test?user=tmp&password=BagRep1&channel=1&stream=0.sdp/trackID=5"),
|
||||
},
|
||||
{
|
||||
"relative control, url with special chars and query",
|
||||
[]byte("v=0\r\n" +
|
||||
"m=video 0 RTP/AVP 96\r\n" +
|
||||
"a=rtpmap:96 H264/90000\r\n" +
|
||||
"a=control:trackID=5"),
|
||||
mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/" +
|
||||
"te!st?user=tmp&password=BagRep1!&channel=1&stream=0.sdp"),
|
||||
mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/" +
|
||||
"te!st?user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=5"),
|
||||
},
|
||||
{
|
||||
"relative control, url with query without question mark",
|
||||
[]byte("v=0\r\n" +
|
||||
"m=video 0 RTP/AVP 96\r\n" +
|
||||
"a=rtpmap:96 H264/90000\r\n" +
|
||||
"a=control:trackID=5"),
|
||||
mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/user=tmp&password=BagRep1!&channel=1&stream=0.sdp"),
|
||||
mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=5"),
|
||||
},
|
||||
{
|
||||
"relative control, control is query",
|
||||
[]byte("v=0\r\n" +
|
||||
"m=video 0 RTP/AVP 96\r\n" +
|
||||
"a=rtpmap:96 H264/90000\r\n" +
|
||||
"a=control:?ctype=video"),
|
||||
mustParseURL("rtsp://192.168.1.99:554/test"),
|
||||
mustParseURL("rtsp://192.168.1.99:554/test?ctype=video"),
|
||||
},
|
||||
{
|
||||
"relative control, control is query and no path",
|
||||
[]byte("v=0\r\n" +
|
||||
"m=video 0 RTP/AVP 96\r\n" +
|
||||
"a=rtpmap:96 H264/90000\r\n" +
|
||||
"a=control:?ctype=video"),
|
||||
mustParseURL("rtsp://192.168.1.99:554/"),
|
||||
mustParseURL("rtsp://192.168.1.99:554/?ctype=video"),
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
var sd sdp.SessionDescription
|
||||
err := sd.Unmarshal(ca.sdp)
|
||||
require.NoError(t, err)
|
||||
|
||||
var media Media
|
||||
err = media.Unmarshal(sd.MediaDescriptions[0])
|
||||
require.NoError(t, err)
|
||||
|
||||
ur, err := media.URL(ca.baseURL)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ca.ur, ur)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaURLError(t *testing.T) {
|
||||
media := &Media{
|
||||
Type: "video",
|
||||
Formats: []format.Format{&format.H264{}},
|
||||
}
|
||||
_, err := media.URL(nil)
|
||||
require.EqualError(t, err, "Content-Base header not provided")
|
||||
}
|
83
pkg/description/session.go
Normal file
83
pkg/description/session.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package description
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
psdp "github.com/pion/sdp/v3"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/sdp"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/url"
|
||||
)
|
||||
|
||||
// Session is the description of a RTSP session.
|
||||
type Session struct {
|
||||
// base URL of the stream (read only).
|
||||
BaseURL *url.URL
|
||||
|
||||
// available media streams.
|
||||
Medias []*Media
|
||||
}
|
||||
|
||||
// FindFormat finds a certain format among all the formats in all the medias of the stream.
|
||||
// If the format is found, it is inserted into forma, and its media is returned.
|
||||
func (d *Session) FindFormat(forma interface{}) *Media {
|
||||
for _, media := range d.Medias {
|
||||
ok := media.FindFormat(forma)
|
||||
if ok {
|
||||
return media
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unmarshal decodes the description from SDP.
|
||||
func (d *Session) Unmarshal(ssd *sdp.SessionDescription) error {
|
||||
d.Medias = make([]*Media, len(ssd.MediaDescriptions))
|
||||
|
||||
for i, md := range ssd.MediaDescriptions {
|
||||
var m Media
|
||||
err := m.Unmarshal(md)
|
||||
if err != nil {
|
||||
return fmt.Errorf("media %d is invalid: %v", i+1, err)
|
||||
}
|
||||
d.Medias[i] = &m
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Marshal encodes the description in SDP.
|
||||
func (d Session) Marshal(multicast bool) ([]byte, error) {
|
||||
var address string
|
||||
if multicast {
|
||||
address = "224.1.0.0"
|
||||
} else {
|
||||
address = "0.0.0.0"
|
||||
}
|
||||
|
||||
sout := &sdp.SessionDescription{
|
||||
SessionName: psdp.SessionName("Session"),
|
||||
Origin: psdp.Origin{
|
||||
Username: "-",
|
||||
NetworkType: "IN",
|
||||
AddressType: "IP4",
|
||||
UnicastAddress: "127.0.0.1",
|
||||
},
|
||||
// required by Darwin Sessioning Server
|
||||
ConnectionInformation: &psdp.ConnectionInformation{
|
||||
NetworkType: "IN",
|
||||
AddressType: "IP4",
|
||||
Address: &psdp.Address{Address: address},
|
||||
},
|
||||
TimeDescriptions: []psdp.TimeDescription{
|
||||
{Timing: psdp.Timing{StartTime: 0, StopTime: 0}},
|
||||
},
|
||||
MediaDescriptions: make([]*psdp.MediaDescription, len(d.Medias)),
|
||||
}
|
||||
|
||||
for i, media := range d.Medias {
|
||||
sout.MediaDescriptions[i] = media.Marshal()
|
||||
}
|
||||
|
||||
return sout.Marshal()
|
||||
}
|
724
pkg/description/session_test.go
Normal file
724
pkg/description/session_test.go
Normal file
@@ -0,0 +1,724 @@
|
||||
package description
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/sdp"
|
||||
)
|
||||
|
||||
var casesSession = []struct {
|
||||
name string
|
||||
in string
|
||||
out string
|
||||
desc Session
|
||||
}{
|
||||
{
|
||||
"one format for each media, absolute",
|
||||
"v=0\r\n" +
|
||||
"o=- 0 0 IN IP4 10.0.0.131\r\n" +
|
||||
"s=Media Presentation\r\n" +
|
||||
"i=samsung\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\n" +
|
||||
"b=AS:2632\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"a=control:rtsp://10.0.100.50/profile5/media.smp\r\n" +
|
||||
"a=range:npt=now-\r\n" +
|
||||
"m=video 42504 RTP/AVP 97\r\n" +
|
||||
"b=AS:2560\r\n" +
|
||||
"a=rtpmap:97 H264/90000\r\n" +
|
||||
"a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=v\r\n" +
|
||||
"a=cliprect:0,0,1080,1920\r\n" +
|
||||
"a=framesize:97 1920-1080\r\n" +
|
||||
"a=framerate:30.0\r\n" +
|
||||
"a=fmtp:97 packetization-mode=1;profile-level-id=640028;sprop-parameter-sets=Z2QAKKy0A8ARPyo=,aO4Bniw=\r\n" +
|
||||
"m=audio 42506 RTP/AVP 0\r\n" +
|
||||
"b=AS:64\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=a\r\n" +
|
||||
"a=recvonly\r\n" +
|
||||
"m=application 42508 RTP/AVP 107\r\n" +
|
||||
"b=AS:8\r\n",
|
||||
"v=0\r\n" +
|
||||
"o=- 0 0 IN IP4 127.0.0.1\r\n" +
|
||||
"s=Session\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=video 0 RTP/AVP 97\r\n" +
|
||||
"a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=v\r\n" +
|
||||
"a=rtpmap:97 H264/90000\r\n" +
|
||||
"a=fmtp:97 packetization-mode=1; profile-level-id=640028; sprop-parameter-sets=Z2QAKKy0A8ARPyo=,aO4Bniw=\r\n" +
|
||||
"m=audio 0 RTP/AVP 0\r\n" +
|
||||
"a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=a\r\n" +
|
||||
"a=recvonly\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"m=application 0 RTP/AVP 107\r\n" +
|
||||
"a=control\r\n",
|
||||
Session{
|
||||
Medias: []*Media{
|
||||
{
|
||||
Type: "video",
|
||||
Control: "rtsp://10.0.100.50/profile5/media.smp/trackID=v",
|
||||
Formats: []format.Format{&format.H264{
|
||||
PayloadTyp: 97,
|
||||
PacketizationMode: 1,
|
||||
SPS: []byte{0x67, 0x64, 0x00, 0x28, 0xac, 0xb4, 0x03, 0xc0, 0x11, 0x3f, 0x2a},
|
||||
PPS: []byte{0x68, 0xee, 0x01, 0x9e, 0x2c},
|
||||
}},
|
||||
},
|
||||
{
|
||||
Type: "audio",
|
||||
Direction: MediaDirectionRecvonly,
|
||||
Control: "rtsp://10.0.100.50/profile5/media.smp/trackID=a",
|
||||
Formats: []format.Format{&format.G711{
|
||||
MULaw: true,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Type: "application",
|
||||
Formats: []format.Format{&format.Generic{
|
||||
PayloadTyp: 107,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"one format for each media, relative",
|
||||
"v=0\r\n" +
|
||||
"o=- 0 0 IN IP4 10.0.0.131\r\n" +
|
||||
"s=Media Presentation\r\n" +
|
||||
"i=samsung\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\n" +
|
||||
"b=AS:2632\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"a=range:npt=now-\r\n" +
|
||||
"m=video 42504 RTP/AVP 97\r\n" +
|
||||
"b=AS:2560\r\n" +
|
||||
"a=rtpmap:97 H264/90000\r\n" +
|
||||
"a=control:trackID=1\r\n" +
|
||||
"a=cliprect:0,0,1080,1920\r\n" +
|
||||
"a=framesize:97 1920-1080\r\n" +
|
||||
"a=framerate:30.0\r\n" +
|
||||
"a=fmtp:97 packetization-mode=1;profile-level-id=640028;sprop-parameter-sets=Z2QAKKy0A8ARPyo=,aO4Bniw=\r\n" +
|
||||
"m=audio 42506 RTP/AVP 0\r\n" +
|
||||
"b=AS:64\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"a=control:trackID=2\r\n" +
|
||||
"a=recvonly\r\n" +
|
||||
"m=application 42508 RTP/AVP 107\r\n" +
|
||||
"b=AS:8\r\n",
|
||||
"v=0\r\n" +
|
||||
"o=- 0 0 IN IP4 127.0.0.1\r\n" +
|
||||
"s=Session\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=video 0 RTP/AVP 97\r\n" +
|
||||
"a=control:trackID=1\r\n" +
|
||||
"a=rtpmap:97 H264/90000\r\n" +
|
||||
"a=fmtp:97 packetization-mode=1; profile-level-id=640028; sprop-parameter-sets=Z2QAKKy0A8ARPyo=,aO4Bniw=\r\n" +
|
||||
"m=audio 0 RTP/AVP 0\r\n" +
|
||||
"a=control:trackID=2\r\n" +
|
||||
"a=recvonly\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"m=application 0 RTP/AVP 107\r\n" +
|
||||
"a=control\r\n",
|
||||
Session{
|
||||
Medias: []*Media{
|
||||
{
|
||||
Type: "video",
|
||||
Control: "trackID=1",
|
||||
Formats: []format.Format{&format.H264{
|
||||
PayloadTyp: 97,
|
||||
PacketizationMode: 1,
|
||||
SPS: []byte{0x67, 0x64, 0x00, 0x28, 0xac, 0xb4, 0x03, 0xc0, 0x11, 0x3f, 0x2a},
|
||||
PPS: []byte{0x68, 0xee, 0x01, 0x9e, 0x2c},
|
||||
}},
|
||||
},
|
||||
{
|
||||
Type: "audio",
|
||||
Direction: MediaDirectionRecvonly,
|
||||
Control: "trackID=2",
|
||||
Formats: []format.Format{&format.G711{
|
||||
MULaw: true,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Type: "application",
|
||||
Formats: []format.Format{&format.Generic{
|
||||
PayloadTyp: 107,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"multiple formats for each media",
|
||||
"v=0\r\n" +
|
||||
"o=- 4158123474391860926 2 IN IP4 127.0.0.1\r\n" +
|
||||
"s=-\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"a=group:BUNDLE audio video\r\n" +
|
||||
"a=msid-semantic: WMS mediaSessionLocal\r\n" +
|
||||
"m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\n" +
|
||||
"a=rtcp:9 IN IP4 0.0.0.0\r\n" +
|
||||
"a=ice-ufrag:0D6Y\r\n" +
|
||||
"a=ice-pwd:V3YEqLGAJJhUDUa13C/pKbWe\r\n" +
|
||||
"a=ice-options:trickle renomination\r\n" +
|
||||
"a=fingerprint:sha-256" +
|
||||
" 5E:B5:97:8B:B4:D8:AE:2B:89:F6:82:44:47:69:77:83:05:29:C5:C8:EE:67:50:C3:77:6B:A7:BA:10:E3:08:B8\r\n" +
|
||||
"a=setup:actpass\r\n" +
|
||||
"a=mid:audio\r\n" +
|
||||
"a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" +
|
||||
"a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" +
|
||||
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" +
|
||||
"a=sendonly\r\n" +
|
||||
"a=rtcp-mux\r\n" +
|
||||
"a=rtpmap:111 opus/48000/2\r\n" +
|
||||
"a=rtcp-fb:111 transport-cc\r\n" +
|
||||
"a=fmtp:111 minptime=10;useinbandfec=1\r\n" +
|
||||
"a=rtpmap:103 ISAC/16000\r\n" +
|
||||
"a=rtpmap:104 ISAC/32000\r\n" +
|
||||
"a=rtpmap:9 G722/8000\r\n" +
|
||||
"a=rtpmap:102 ILBC/8000\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"a=rtpmap:8 PCMA/8000\r\n" +
|
||||
"a=rtpmap:106 CN/32000\r\n" +
|
||||
"a=rtpmap:105 CN/16000\r\n" +
|
||||
"a=rtpmap:13 CN/8000\r\n" +
|
||||
"a=rtpmap:110 telephone-event/48000\r\n" +
|
||||
"a=rtpmap:112 telephone-event/32000\r\n" +
|
||||
"a=rtpmap:113 telephone-event/16000\r\n" +
|
||||
"a=rtpmap:126 telephone-event/8000\r\n" +
|
||||
"a=ssrc:3754810229 cname:CvU1TYqkVsjj5XOt\r\n" +
|
||||
"a=ssrc:3754810229 msid:mediaSessionLocal 101\r\n" +
|
||||
"a=ssrc:3754810229 mslabel:mediaSessionLocal\r\n" +
|
||||
"a=ssrc:3754810229 label:101\r\n" +
|
||||
"m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 124 125\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\n" +
|
||||
"a=rtcp:9 IN IP4 0.0.0.0\r\n" +
|
||||
"a=ice-ufrag:0D6Y\r\n" +
|
||||
"a=ice-pwd:V3YEqLGAJJhUDUa13C/pKbWe\r\n" +
|
||||
"a=ice-options:trickle renomination\r\n" +
|
||||
"a=fingerprint:sha-256" +
|
||||
" 5E:B5:97:8B:B4:D8:AE:2B:89:F6:82:44:47:69:77:83:05:29:C5:C8:EE:67:50:C3:77:6B:A7:BA:10:E3:08:B8\r\n" +
|
||||
"a=setup:actpass\r\n" +
|
||||
"a=mid:video\r\n" +
|
||||
"a=extmap:14 urn:ietf:params:rtp-hdrext:toffset\r\n" +
|
||||
"a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" +
|
||||
"a=extmap:13 urn:3gpp:video-orientation\r\n" +
|
||||
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" +
|
||||
"a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\n" +
|
||||
"a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\n" +
|
||||
"a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\n" +
|
||||
"a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\n" +
|
||||
"a=sendonly\r\n" +
|
||||
"a=rtcp-mux\r\n" +
|
||||
"a=rtcp-rsize\r\n" +
|
||||
"a=rtpmap:96 VP8/90000\r\n" +
|
||||
"a=rtcp-fb:96 goog-remb\r\n" +
|
||||
"a=rtcp-fb:96 transport-cc\r\n" +
|
||||
"a=rtcp-fb:96 ccm fir\r\n" +
|
||||
"a=rtcp-fb:96 nack\r\n" +
|
||||
"a=rtcp-fb:96 nack pli\r\n" +
|
||||
"a=rtpmap:97 rtx/90000\r\n" +
|
||||
"a=fmtp:97 apt=96\r\n" +
|
||||
"a=rtpmap:98 VP9/90000\r\n" +
|
||||
"a=rtcp-fb:98 goog-remb\r\n" +
|
||||
"a=rtcp-fb:98 transport-cc\r\n" +
|
||||
"a=rtcp-fb:98 ccm fir\r\n" +
|
||||
"a=rtcp-fb:98 nack\r\n" +
|
||||
"a=rtcp-fb:98 nack pli\r\n" +
|
||||
"a=rtpmap:99 rtx/90000\r\n" +
|
||||
"a=fmtp:99 apt=98\r\n" +
|
||||
"a=rtpmap:100 H264/90000\r\n" +
|
||||
"a=rtcp-fb:100 goog-remb\r\n" +
|
||||
"a=rtcp-fb:100 transport-cc\r\n" +
|
||||
"a=rtcp-fb:100 ccm fir\r\n" +
|
||||
"a=rtcp-fb:100 nack\r\n" +
|
||||
"a=rtcp-fb:100 nack pli\r\n" +
|
||||
"a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n" +
|
||||
"a=rtpmap:101 rtx/90000\r\n" +
|
||||
"a=fmtp:101 apt=100\r\n" +
|
||||
"a=rtpmap:127 red/90000\r\n" +
|
||||
"a=rtpmap:124 rtx/90000\r\n" +
|
||||
"a=fmtp:124 apt=127\r\n" +
|
||||
"a=rtpmap:125 ulpfec/90000\r\n" +
|
||||
"a=ssrc-group:FID 2712436124 1733091158\r\n" +
|
||||
"a=ssrc:2712436124 cname:CvU1TYqkVsjj5XOt\r\n" +
|
||||
"a=ssrc:2712436124 msid:mediaSessionLocal 100\r\n" +
|
||||
"a=ssrc:2712436124 mslabel:mediaSessionLocal\r\n" +
|
||||
"a=ssrc:2712436124 label:100\r\n" +
|
||||
"a=ssrc:1733091158 cname:CvU1TYqkVsjj5XOt\r\n" +
|
||||
"a=ssrc:1733091158 msid:mediaSessionLocal 100\r\n" +
|
||||
"a=ssrc:1733091158 mslabel:mediaSessionLocal\r\n" +
|
||||
"a=ssrc:1733091158 label:100\r\n",
|
||||
"v=0\r\n" +
|
||||
"o=- 0 0 IN IP4 127.0.0.1\r\n" +
|
||||
"s=Session\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=audio 0 RTP/AVP 111 103 104 9 102 0 8 106 105 13 110 112 113 126\r\n" +
|
||||
"a=control\r\n" +
|
||||
"a=sendonly\r\n" +
|
||||
"a=rtpmap:111 opus/48000/2\r\n" +
|
||||
"a=fmtp:111 sprop-stereo=0\r\n" +
|
||||
"a=rtpmap:103 ISAC/16000\r\n" +
|
||||
"a=rtpmap:104 ISAC/32000\r\n" +
|
||||
"a=rtpmap:9 G722/8000\r\n" +
|
||||
"a=rtpmap:102 ILBC/8000\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"a=rtpmap:8 PCMA/8000\r\n" +
|
||||
"a=rtpmap:106 CN/32000\r\n" +
|
||||
"a=rtpmap:105 CN/16000\r\n" +
|
||||
"a=rtpmap:13 CN/8000\r\n" +
|
||||
"a=rtpmap:110 telephone-event/48000\r\n" +
|
||||
"a=rtpmap:112 telephone-event/32000\r\n" +
|
||||
"a=rtpmap:113 telephone-event/16000\r\n" +
|
||||
"a=rtpmap:126 telephone-event/8000\r\n" +
|
||||
"m=video 0 RTP/AVP 96 97 98 99 100 101 127 124 125\r\n" +
|
||||
"a=control\r\n" +
|
||||
"a=sendonly\r\n" +
|
||||
"a=rtpmap:96 VP8/90000\r\n" +
|
||||
"a=rtpmap:97 rtx/90000\r\n" +
|
||||
"a=fmtp:97 apt=96\r\n" +
|
||||
"a=rtpmap:98 VP9/90000\r\n" +
|
||||
"a=rtpmap:99 rtx/90000\r\n" +
|
||||
"a=fmtp:99 apt=98\r\n" +
|
||||
"a=rtpmap:100 H264/90000\r\n" +
|
||||
"a=fmtp:100 packetization-mode=1\r\n" +
|
||||
"a=rtpmap:101 rtx/90000\r\n" +
|
||||
"a=fmtp:101 apt=100\r\n" +
|
||||
"a=rtpmap:127 red/90000\r\n" +
|
||||
"a=rtpmap:124 rtx/90000\r\n" +
|
||||
"a=fmtp:124 apt=127\r\na=rtpmap:125 ulpfec/90000\r\n",
|
||||
Session{
|
||||
Medias: []*Media{
|
||||
{
|
||||
Type: "audio",
|
||||
Direction: MediaDirectionSendonly,
|
||||
Formats: []format.Format{
|
||||
&format.Opus{
|
||||
PayloadTyp: 111,
|
||||
IsStereo: false,
|
||||
},
|
||||
&format.Generic{
|
||||
PayloadTyp: 103,
|
||||
RTPMa: "ISAC/16000",
|
||||
ClockRat: 16000,
|
||||
},
|
||||
&format.Generic{
|
||||
PayloadTyp: 104,
|
||||
RTPMa: "ISAC/32000",
|
||||
ClockRat: 32000,
|
||||
},
|
||||
&format.G722{},
|
||||
&format.Generic{
|
||||
PayloadTyp: 102,
|
||||
RTPMa: "ILBC/8000",
|
||||
ClockRat: 8000,
|
||||
},
|
||||
&format.G711{
|
||||
MULaw: true,
|
||||
},
|
||||
&format.G711{
|
||||
MULaw: false,
|
||||
},
|
||||
&format.Generic{
|
||||
PayloadTyp: 106,
|
||||
RTPMa: "CN/32000",
|
||||
ClockRat: 32000,
|
||||
},
|
||||
&format.Generic{
|
||||
PayloadTyp: 105,
|
||||
RTPMa: "CN/16000",
|
||||
ClockRat: 16000,
|
||||
},
|
||||
&format.Generic{
|
||||
PayloadTyp: 13,
|
||||
RTPMa: "CN/8000",
|
||||
ClockRat: 8000,
|
||||
},
|
||||
&format.Generic{
|
||||
PayloadTyp: 110,
|
||||
RTPMa: "telephone-event/48000",
|
||||
ClockRat: 48000,
|
||||
},
|
||||
&format.Generic{
|
||||
PayloadTyp: 112,
|
||||
RTPMa: "telephone-event/32000",
|
||||
ClockRat: 32000,
|
||||
},
|
||||
&format.Generic{
|
||||
PayloadTyp: 113,
|
||||
RTPMa: "telephone-event/16000",
|
||||
ClockRat: 16000,
|
||||
},
|
||||
&format.Generic{
|
||||
PayloadTyp: 126,
|
||||
RTPMa: "telephone-event/8000",
|
||||
ClockRat: 8000,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "video",
|
||||
Direction: MediaDirectionSendonly,
|
||||
Formats: []format.Format{
|
||||
&format.VP8{
|
||||
PayloadTyp: 96,
|
||||
},
|
||||
&format.Generic{
|
||||
PayloadTyp: 97,
|
||||
RTPMa: "rtx/90000",
|
||||
FMT: map[string]string{
|
||||
"apt": "96",
|
||||
},
|
||||
ClockRat: 90000,
|
||||
},
|
||||
&format.VP9{
|
||||
PayloadTyp: 98,
|
||||
},
|
||||
&format.Generic{
|
||||
PayloadTyp: 99,
|
||||
RTPMa: "rtx/90000",
|
||||
FMT: map[string]string{
|
||||
"apt": "98",
|
||||
},
|
||||
ClockRat: 90000,
|
||||
},
|
||||
&format.H264{
|
||||
PayloadTyp: 100,
|
||||
PacketizationMode: 1,
|
||||
},
|
||||
&format.Generic{
|
||||
PayloadTyp: 101,
|
||||
RTPMa: "rtx/90000",
|
||||
FMT: map[string]string{
|
||||
"apt": "100",
|
||||
},
|
||||
ClockRat: 90000,
|
||||
},
|
||||
&format.Generic{
|
||||
PayloadTyp: 127,
|
||||
RTPMa: "red/90000",
|
||||
ClockRat: 90000,
|
||||
},
|
||||
&format.Generic{
|
||||
PayloadTyp: 124,
|
||||
RTPMa: "rtx/90000",
|
||||
FMT: map[string]string{
|
||||
"apt": "127",
|
||||
},
|
||||
ClockRat: 90000,
|
||||
},
|
||||
&format.Generic{
|
||||
PayloadTyp: 125,
|
||||
RTPMa: "ulpfec/90000",
|
||||
ClockRat: 90000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"multiple formats for each media 2",
|
||||
"v=0\r\n" +
|
||||
"o=- 4158123474391860926 2 IN IP4 127.0.0.1\r\n" +
|
||||
"s=-\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=video 42504 RTP/AVP 96 98\r\n" +
|
||||
"a=rtpmap:96 H264/90000\r\n" +
|
||||
"a=rtpmap:98 MetaData\r\n" +
|
||||
"a=rtcp-mux\r\n" +
|
||||
"a=fmtp:96 packetization-mode=1;profile-level-id=4d002a;" +
|
||||
"sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==\r\n",
|
||||
"v=0\r\n" +
|
||||
"o=- 0 0 IN IP4 127.0.0.1\r\n" +
|
||||
"s=Session\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=video 0 RTP/AVP 96 98\r\n" +
|
||||
"a=control\r\n" +
|
||||
"a=rtpmap:96 H264/90000\r\n" +
|
||||
"a=fmtp:96 packetization-mode=1; profile-level-id=4D002A; " +
|
||||
"sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==\r\n" +
|
||||
"a=rtpmap:98 MetaData\r\n",
|
||||
Session{
|
||||
Medias: []*Media{
|
||||
{
|
||||
Type: "video",
|
||||
Formats: []format.Format{
|
||||
&format.H264{
|
||||
PayloadTyp: 96,
|
||||
SPS: []byte{
|
||||
0x67, 0x4d, 0x00, 0x2a, 0x9d, 0xa8, 0x1e, 0x00,
|
||||
0x89, 0xf9, 0x66, 0xe0, 0x20, 0x20, 0x20, 0x40,
|
||||
},
|
||||
PPS: []byte{0x68, 0xee, 0x3c, 0x80},
|
||||
PacketizationMode: 1,
|
||||
},
|
||||
&format.Generic{
|
||||
PayloadTyp: 98,
|
||||
RTPMa: "MetaData",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"onvif back channel",
|
||||
"v=0\r\n" +
|
||||
"o= 2890842807 IN IP4 192.168.0.1\r\n" +
|
||||
"s=RTSP Session with audiobackchannel\r\n" +
|
||||
"m=video 0 RTP/AVP 26\r\n" +
|
||||
"a=control:rtsp://192.168.0.1/video\r\n" +
|
||||
"a=recvonly\r\n" +
|
||||
"m=audio 0 RTP/AVP 0\r\n" +
|
||||
"a=control:rtsp://192.168.0.1/audio\r\n" +
|
||||
"a=recvonly\r\n" +
|
||||
"m=audio 0 RTP/AVP 0\r\n" +
|
||||
"a=control:rtsp://192.168.0.1/audioback\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"a=sendonly\r\n",
|
||||
"v=0\r\n" +
|
||||
"o=- 0 0 IN IP4 127.0.0.1\r\n" +
|
||||
"s=Session\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=video 0 RTP/AVP 26\r\n" +
|
||||
"a=control:rtsp://192.168.0.1/video\r\n" +
|
||||
"a=recvonly\r\n" +
|
||||
"a=rtpmap:26 JPEG/90000\r\n" +
|
||||
"m=audio 0 RTP/AVP 0\r\n" +
|
||||
"a=control:rtsp://192.168.0.1/audio\r\n" +
|
||||
"a=recvonly\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"m=audio 0 RTP/AVP 0\r\n" +
|
||||
"a=control:rtsp://192.168.0.1/audioback\r\n" +
|
||||
"a=sendonly\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n",
|
||||
Session{
|
||||
Medias: []*Media{
|
||||
{
|
||||
Type: "video",
|
||||
Direction: MediaDirectionRecvonly,
|
||||
Control: "rtsp://192.168.0.1/video",
|
||||
Formats: []format.Format{&format.MJPEG{}},
|
||||
},
|
||||
{
|
||||
Type: "audio",
|
||||
Direction: MediaDirectionRecvonly,
|
||||
Control: "rtsp://192.168.0.1/audio",
|
||||
Formats: []format.Format{&format.G711{MULaw: true}},
|
||||
},
|
||||
{
|
||||
Type: "audio",
|
||||
Direction: MediaDirectionSendonly,
|
||||
Control: "rtsp://192.168.0.1/audioback",
|
||||
Formats: []format.Format{&format.G711{MULaw: true}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"tp-link",
|
||||
"v=0\r\n" +
|
||||
"o=- 4158123474391860926 2 IN IP4 127.0.0.1\r\n" +
|
||||
"s=-\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=application/TP-LINK 0 RTP/AVP smart/1/90000\r\n" +
|
||||
"a=rtpmap:95 TP-LINK/90000\r\n",
|
||||
"v=0\r\n" +
|
||||
"o=- 0 0 IN IP4 127.0.0.1\r\n" +
|
||||
"s=Session\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=application/TP-LINK 0 RTP/AVP 95\r\n" +
|
||||
"a=control\r\n" +
|
||||
"a=rtpmap:95 TP-LINK/90000\r\n",
|
||||
Session{
|
||||
Medias: []*Media{
|
||||
{
|
||||
Type: "application/TP-LINK",
|
||||
Formats: []format.Format{&format.Generic{
|
||||
PayloadTyp: 95,
|
||||
RTPMa: "TP-LINK/90000",
|
||||
ClockRat: 90000,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"mercury",
|
||||
"v=0\n" +
|
||||
"o=- 14665860 31787219 1 IN IP4 192.168.0.60\n" +
|
||||
"s=Session streamed by \"MERCURY RTSP Server\"\n" +
|
||||
"t=0 0\n" +
|
||||
"a=smart_encoder:virtualIFrame=1\n" +
|
||||
"m=application/MERCURY 0 RTP/AVP smart/1/90000\n" +
|
||||
"a=rtpmap:95 MERCURY/90000\n",
|
||||
"v=0\r\n" +
|
||||
"o=- 0 0 IN IP4 127.0.0.1\r\n" +
|
||||
"s=Session\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=application/MERCURY 0 RTP/AVP 95\r\n" +
|
||||
"a=control\r\n" +
|
||||
"a=rtpmap:95 MERCURY/90000\r\n",
|
||||
Session{
|
||||
Medias: []*Media{
|
||||
{
|
||||
Type: "application/MERCURY",
|
||||
Formats: []format.Format{&format.Generic{
|
||||
PayloadTyp: 95,
|
||||
RTPMa: "MERCURY/90000",
|
||||
ClockRat: 90000,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"h264 with space at end",
|
||||
"v=0\r\n" +
|
||||
"o=- 4158123474391860926 2 IN IP4 127.0.0.1\r\n" +
|
||||
"s=-\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=video 42504 RTP/AVP 96\r\n" +
|
||||
"a=rtpmap:96 H264/90000 \r\n" +
|
||||
"a=fmtp:96 packetization-mode=1\r\n",
|
||||
"v=0\r\n" +
|
||||
"o=- 0 0 IN IP4 127.0.0.1\r\n" +
|
||||
"s=Session\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=video 0 RTP/AVP 96\r\n" +
|
||||
"a=control\r\n" +
|
||||
"a=rtpmap:96 H264/90000\r\n" +
|
||||
"a=fmtp:96 packetization-mode=1\r\n",
|
||||
Session{
|
||||
Medias: []*Media{
|
||||
{
|
||||
Type: "video",
|
||||
Formats: []format.Format{
|
||||
&format.H264{
|
||||
PayloadTyp: 96,
|
||||
PacketizationMode: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestSessionUnmarshal(t *testing.T) {
|
||||
for _, ca := range casesSession {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
var sdp sdp.SessionDescription
|
||||
err := sdp.Unmarshal([]byte(ca.in))
|
||||
require.NoError(t, err)
|
||||
|
||||
var desc Session
|
||||
err = desc.Unmarshal(&sdp)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ca.desc, desc)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionUnmarshalErrors(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
sdp string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
"invalid track",
|
||||
"v=0\r\n" +
|
||||
"o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" +
|
||||
"s=SDP Seminar\r\n" +
|
||||
"m=video 0 RTP/AVP/TCP 96\r\n" +
|
||||
"a=rtpmap:96 H265/90000\r\n" +
|
||||
"a=fmtp:96 sprop-vps=QAEMAf//AWAAAAMAsAAAAwAAAwB4FwJA; " +
|
||||
"sprop-sps=QgEBAWAAAAMAsAAAAwAAAwB4oAKggC8c1YgXuRZFL/y5/E/qbgQEBAE=; sprop-pps=RAHAcvBTJA==;\r\n" +
|
||||
"a=control:streamid=0\r\n" +
|
||||
"m=audio 0 RTP/AVP/TCP 97\r\n" +
|
||||
"a=rtpmap:97 mpeg4-generic/44100/2\r\n" +
|
||||
"a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=zzz1210\r\n" +
|
||||
"a=control:streamid=1\r\n",
|
||||
"media 2 is invalid: invalid AAC config: zzz1210",
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
var sd sdp.SessionDescription
|
||||
err := sd.Unmarshal([]byte(ca.sdp))
|
||||
require.NoError(t, err)
|
||||
|
||||
var desc Session
|
||||
err = desc.Unmarshal(&sd)
|
||||
require.EqualError(t, err, ca.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionMarshal(t *testing.T) {
|
||||
for _, ca := range casesSession {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
byts, err := ca.desc.Marshal(false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ca.out, string(byts))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionFindFormat(t *testing.T) {
|
||||
tr := &format.Generic{
|
||||
PayloadTyp: 97,
|
||||
RTPMa: "rtx/90000",
|
||||
FMT: map[string]string{
|
||||
"apt": "96",
|
||||
},
|
||||
ClockRat: 90000,
|
||||
}
|
||||
|
||||
md := &Media{
|
||||
Type: MediaTypeVideo,
|
||||
Formats: []format.Format{
|
||||
&format.VP8{
|
||||
PayloadTyp: 96,
|
||||
},
|
||||
tr,
|
||||
&format.VP9{
|
||||
PayloadTyp: 98,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
desc := &Session{
|
||||
Medias: []*Media{
|
||||
{
|
||||
Type: MediaTypeAudio,
|
||||
Formats: []format.Format{
|
||||
&format.Opus{
|
||||
PayloadTyp: 111,
|
||||
IsStereo: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
md,
|
||||
},
|
||||
}
|
||||
|
||||
var forma *format.Generic
|
||||
me := desc.FindFormat(&forma)
|
||||
require.Equal(t, md, me)
|
||||
require.Equal(t, tr, forma)
|
||||
}
|
Reference in New Issue
Block a user