mirror of
https://github.com/aler9/gortsplib
synced 2025-10-05 15:16:51 +08:00
store media IDs in description.Session
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
psdp "github.com/pion/sdp/v3"
|
psdp "github.com/pion/sdp/v3"
|
||||||
|
|
||||||
@@ -17,9 +18,23 @@ import (
|
|||||||
|
|
||||||
var smartRegexp = regexp.MustCompile("^([0-9]+) (.*?)/90000")
|
var smartRegexp = regexp.MustCompile("^([0-9]+) (.*?)/90000")
|
||||||
|
|
||||||
func getControlAttribute(attributes []psdp.Attribute) string {
|
func replaceSmartPayloadType(payloadType string, attributes []psdp.Attribute) string {
|
||||||
|
if payloadType == "smart/1/90000" {
|
||||||
|
for _, attr := range attributes {
|
||||||
|
if attr.Key == "rtpmap" {
|
||||||
|
sm := smartRegexp.FindStringSubmatch(attr.Value)
|
||||||
|
if sm != nil {
|
||||||
|
return sm[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return payloadType
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAttribute(attributes []psdp.Attribute, key string) string {
|
||||||
for _, attr := range attributes {
|
for _, attr := range attributes {
|
||||||
if attr.Key == "control" {
|
if attr.Key == key {
|
||||||
return attr.Value
|
return attr.Value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,6 +107,15 @@ func sortedKeys(fmtp map[string]string) []string {
|
|||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isAlphaNumeric(v string) bool {
|
||||||
|
for _, r := range v {
|
||||||
|
if !unicode.IsLetter(r) && !unicode.IsNumber(r) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// MediaDirection is the direction of a media stream.
|
// MediaDirection is the direction of a media stream.
|
||||||
type MediaDirection string
|
type MediaDirection string
|
||||||
|
|
||||||
@@ -118,7 +142,10 @@ type Media struct {
|
|||||||
// Media type.
|
// Media type.
|
||||||
Type MediaType
|
Type MediaType
|
||||||
|
|
||||||
// Direction of the stream.
|
// Media ID (optional).
|
||||||
|
ID string
|
||||||
|
|
||||||
|
// Direction of the stream (optional).
|
||||||
Direction MediaDirection
|
Direction MediaDirection
|
||||||
|
|
||||||
// Control attribute.
|
// Control attribute.
|
||||||
@@ -131,22 +158,18 @@ type Media struct {
|
|||||||
// Unmarshal decodes the media from the SDP format.
|
// Unmarshal decodes the media from the SDP format.
|
||||||
func (m *Media) Unmarshal(md *psdp.MediaDescription) error {
|
func (m *Media) Unmarshal(md *psdp.MediaDescription) error {
|
||||||
m.Type = MediaType(md.MediaName.Media)
|
m.Type = MediaType(md.MediaName.Media)
|
||||||
|
|
||||||
|
m.ID = getAttribute(md.Attributes, "mid")
|
||||||
|
if m.ID != "" && !isAlphaNumeric(m.ID) {
|
||||||
|
return fmt.Errorf("invalid mid: %v", m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
m.Direction = getDirection(md.Attributes)
|
m.Direction = getDirection(md.Attributes)
|
||||||
m.Control = getControlAttribute(md.Attributes)
|
m.Control = getAttribute(md.Attributes, "control")
|
||||||
|
|
||||||
m.Formats = nil
|
m.Formats = nil
|
||||||
for _, payloadType := range md.MediaName.Formats {
|
for _, payloadType := range md.MediaName.Formats {
|
||||||
if payloadType == "smart/1/90000" {
|
payloadType = replaceSmartPayloadType(payloadType, md.Attributes)
|
||||||
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)
|
tmp, err := strconv.ParseUint(payloadType, 10, 8)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -179,12 +202,13 @@ func (m Media) Marshal() *psdp.MediaDescription {
|
|||||||
Media: string(m.Type),
|
Media: string(m.Type),
|
||||||
Protos: []string{"RTP", "AVP"},
|
Protos: []string{"RTP", "AVP"},
|
||||||
},
|
},
|
||||||
Attributes: []psdp.Attribute{
|
}
|
||||||
{
|
|
||||||
Key: "control",
|
if m.ID != "" {
|
||||||
Value: m.Control,
|
md.Attributes = append(md.Attributes, psdp.Attribute{
|
||||||
},
|
Key: "mid",
|
||||||
},
|
Value: m.ID,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.Direction != "" {
|
if m.Direction != "" {
|
||||||
@@ -193,6 +217,11 @@ func (m Media) Marshal() *psdp.MediaDescription {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
md.Attributes = append(md.Attributes, psdp.Attribute{
|
||||||
|
Key: "control",
|
||||||
|
Value: m.Control,
|
||||||
|
})
|
||||||
|
|
||||||
for _, forma := range m.Formats {
|
for _, forma := range m.Formats {
|
||||||
typ := strconv.FormatUint(uint64(forma.PayloadType()), 10)
|
typ := strconv.FormatUint(uint64(forma.PayloadType()), 10)
|
||||||
md.MediaName.Formats = append(md.MediaName.Formats, typ)
|
md.MediaName.Formats = append(md.MediaName.Formats, typ)
|
||||||
|
@@ -9,6 +9,33 @@ import (
|
|||||||
"github.com/bluenviron/gortsplib/v4/pkg/url"
|
"github.com/bluenviron/gortsplib/v4/pkg/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func atLeastOneHasMID(medias []*Media) bool {
|
||||||
|
for _, media := range medias {
|
||||||
|
if media.ID != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func atLeastOneDoesntHaveMID(medias []*Media) bool {
|
||||||
|
for _, media := range medias {
|
||||||
|
if media.ID == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasMediaWithID(medias []*Media, id string) bool {
|
||||||
|
for _, media := range medias {
|
||||||
|
if media.ID == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Session is the description of a RTSP stream.
|
// Session is the description of a RTSP stream.
|
||||||
type Session struct {
|
type Session struct {
|
||||||
// base URL of the stream (read only).
|
// base URL of the stream (read only).
|
||||||
@@ -48,9 +75,18 @@ func (d *Session) Unmarshal(ssd *sdp.SessionDescription) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("media %d is invalid: %v", i+1, err)
|
return fmt.Errorf("media %d is invalid: %v", i+1, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.ID != "" && hasMediaWithID(d.Medias[:i], m.ID) {
|
||||||
|
return fmt.Errorf("duplicate media IDs")
|
||||||
|
}
|
||||||
|
|
||||||
d.Medias[i] = &m
|
d.Medias[i] = &m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if atLeastOneHasMID(d.Medias) && atLeastOneDoesntHaveMID(d.Medias) {
|
||||||
|
return fmt.Errorf("media IDs sent partially")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -51,8 +51,8 @@ var casesSession = []struct {
|
|||||||
"a=rtpmap:97 H264/90000\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" +
|
"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" +
|
"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=recvonly\r\n" +
|
||||||
|
"a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=a\r\n" +
|
||||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||||
"m=application 0 RTP/AVP 107\r\n" +
|
"m=application 0 RTP/AVP 107\r\n" +
|
||||||
"a=control\r\n",
|
"a=control\r\n",
|
||||||
@@ -121,8 +121,8 @@ var casesSession = []struct {
|
|||||||
"a=rtpmap:97 H264/90000\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" +
|
"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" +
|
"m=audio 0 RTP/AVP 0\r\n" +
|
||||||
"a=control:trackID=2\r\n" +
|
|
||||||
"a=recvonly\r\n" +
|
"a=recvonly\r\n" +
|
||||||
|
"a=control:trackID=2\r\n" +
|
||||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||||
"m=application 0 RTP/AVP 107\r\n" +
|
"m=application 0 RTP/AVP 107\r\n" +
|
||||||
"a=control\r\n",
|
"a=control\r\n",
|
||||||
@@ -264,8 +264,9 @@ var casesSession = []struct {
|
|||||||
"c=IN IP4 0.0.0.0\r\n" +
|
"c=IN IP4 0.0.0.0\r\n" +
|
||||||
"t=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" +
|
"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=mid:audio\r\n" +
|
||||||
"a=sendonly\r\n" +
|
"a=sendonly\r\n" +
|
||||||
|
"a=control\r\n" +
|
||||||
"a=rtpmap:111 opus/48000/2\r\n" +
|
"a=rtpmap:111 opus/48000/2\r\n" +
|
||||||
"a=fmtp:111 sprop-stereo=0\r\n" +
|
"a=fmtp:111 sprop-stereo=0\r\n" +
|
||||||
"a=rtpmap:103 ISAC/16000\r\n" +
|
"a=rtpmap:103 ISAC/16000\r\n" +
|
||||||
@@ -282,8 +283,9 @@ var casesSession = []struct {
|
|||||||
"a=rtpmap:113 telephone-event/16000\r\n" +
|
"a=rtpmap:113 telephone-event/16000\r\n" +
|
||||||
"a=rtpmap:126 telephone-event/8000\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" +
|
"m=video 0 RTP/AVP 96 97 98 99 100 101 127 124 125\r\n" +
|
||||||
"a=control\r\n" +
|
"a=mid:video\r\n" +
|
||||||
"a=sendonly\r\n" +
|
"a=sendonly\r\n" +
|
||||||
|
"a=control\r\n" +
|
||||||
"a=rtpmap:96 VP8/90000\r\n" +
|
"a=rtpmap:96 VP8/90000\r\n" +
|
||||||
"a=rtpmap:97 rtx/90000\r\n" +
|
"a=rtpmap:97 rtx/90000\r\n" +
|
||||||
"a=fmtp:97 apt=96\r\n" +
|
"a=fmtp:97 apt=96\r\n" +
|
||||||
@@ -301,6 +303,7 @@ var casesSession = []struct {
|
|||||||
Title: ``,
|
Title: ``,
|
||||||
Medias: []*Media{
|
Medias: []*Media{
|
||||||
{
|
{
|
||||||
|
ID: "audio",
|
||||||
Type: MediaTypeAudio,
|
Type: MediaTypeAudio,
|
||||||
Direction: MediaDirectionSendonly,
|
Direction: MediaDirectionSendonly,
|
||||||
Formats: []format.Format{
|
Formats: []format.Format{
|
||||||
@@ -368,6 +371,7 @@ var casesSession = []struct {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
ID: "video",
|
||||||
Type: MediaTypeVideo,
|
Type: MediaTypeVideo,
|
||||||
Direction: MediaDirectionSendonly,
|
Direction: MediaDirectionSendonly,
|
||||||
Formats: []format.Format{
|
Formats: []format.Format{
|
||||||
@@ -496,16 +500,16 @@ var casesSession = []struct {
|
|||||||
"c=IN IP4 0.0.0.0\r\n" +
|
"c=IN IP4 0.0.0.0\r\n" +
|
||||||
"t=0 0\r\n" +
|
"t=0 0\r\n" +
|
||||||
"m=video 0 RTP/AVP 26\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=recvonly\r\n" +
|
||||||
|
"a=control:rtsp://192.168.0.1/video\r\n" +
|
||||||
"a=rtpmap:26 JPEG/90000\r\n" +
|
"a=rtpmap:26 JPEG/90000\r\n" +
|
||||||
"m=audio 0 RTP/AVP 0\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=recvonly\r\n" +
|
||||||
|
"a=control:rtsp://192.168.0.1/audio\r\n" +
|
||||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||||
"m=audio 0 RTP/AVP 0\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=sendonly\r\n" +
|
||||||
|
"a=control:rtsp://192.168.0.1/audioback\r\n" +
|
||||||
"a=rtpmap:0 PCMU/8000\r\n",
|
"a=rtpmap:0 PCMU/8000\r\n",
|
||||||
Session{
|
Session{
|
||||||
Title: `RTSP Session with audiobackchannel`,
|
Title: `RTSP Session with audiobackchannel`,
|
||||||
@@ -710,6 +714,27 @@ func FuzzSessionUnmarshalErrors(f *testing.F) {
|
|||||||
"a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=zzz1210\r\n" +
|
"a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=zzz1210\r\n" +
|
||||||
"a=control:streamid=1\r\n")
|
"a=control:streamid=1\r\n")
|
||||||
|
|
||||||
|
f.Add("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" +
|
||||||
|
"m=audio 0 RTP/AVP/TCP 0\r\n" +
|
||||||
|
"a=mid:2\r\n")
|
||||||
|
|
||||||
|
f.Add("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=mid:2\r\n" +
|
||||||
|
"a=rtpmap:96 H264/90000\r\n" +
|
||||||
|
"a=fmtp:96 packetization-mode=1\r\n" +
|
||||||
|
"m=audio 0 RTP/AVP/TCP 0\r\n" +
|
||||||
|
"a=mid:2\r\n")
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, enc string) {
|
f.Fuzz(func(t *testing.T, enc string) {
|
||||||
var sd sdp.SessionDescription
|
var sd sdp.SessionDescription
|
||||||
err := sd.Unmarshal([]byte(enc))
|
err := sd.Unmarshal([]byte(enc))
|
||||||
|
2
pkg/description/testdata/fuzz/FuzzSessionUnmarshalErrors/b0432aab46b15405
vendored
Normal file
2
pkg/description/testdata/fuzz/FuzzSessionUnmarshalErrors/b0432aab46b15405
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
string("v=0\ns=0\nm=video 0 AVP 0\na=mid: ")
|
Reference in New Issue
Block a user