mirror of
https://github.com/aler9/gortsplib
synced 2025-09-27 03:25:52 +08:00
276 lines
5.8 KiB
Go
276 lines
5.8 KiB
Go
// Package description contains objects to describe streams.
|
|
package description
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"reflect"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
psdp "github.com/pion/sdp/v3"
|
|
|
|
"github.com/bluenviron/gortsplib/v4/pkg/base"
|
|
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
|
"github.com/bluenviron/gortsplib/v4/pkg/mikey"
|
|
)
|
|
|
|
func getAttribute(attributes []psdp.Attribute, key string) string {
|
|
for _, attr := range attributes {
|
|
if attr.Key == key {
|
|
return attr.Value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func isBackChannel(attributes []psdp.Attribute) bool {
|
|
for _, attr := range attributes {
|
|
if attr.Key == "sendonly" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func isAlphaNumeric(v string) bool {
|
|
for _, r := range v {
|
|
if !unicode.IsLetter(r) && !unicode.IsNumber(r) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// MediaType is the type of a media stream.
|
|
type MediaType string
|
|
|
|
// media 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
|
|
|
|
// Media ID (optional).
|
|
ID string
|
|
|
|
// Whether this media is a back channel.
|
|
IsBackChannel bool
|
|
|
|
// Control attribute.
|
|
Control string
|
|
|
|
// Whether the transport is secure.
|
|
Secure bool
|
|
|
|
// key-mgmt attribute.
|
|
KeyMgmtMikey *mikey.Message
|
|
|
|
// 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.ID = getAttribute(md.Attributes, "mid")
|
|
if m.ID != "" && !isAlphaNumeric(m.ID) {
|
|
return fmt.Errorf("invalid mid: %v", m.ID)
|
|
}
|
|
|
|
m.IsBackChannel = isBackChannel(md.Attributes)
|
|
m.Control = getAttribute(md.Attributes, "control")
|
|
m.Secure = slices.Contains(md.MediaName.Protos, "SAVP")
|
|
|
|
if enc := getAttribute(md.Attributes, "key-mgmt"); enc != "" {
|
|
if !strings.HasPrefix(enc, "mikey ") {
|
|
return fmt.Errorf("unsupported key-mgmt: %v", enc)
|
|
}
|
|
|
|
enc2, err := base64.StdEncoding.DecodeString(enc[len("mikey "):])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
m.KeyMgmtMikey = &mikey.Message{}
|
|
err = m.KeyMgmtMikey.Unmarshal(enc2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
m.Formats = nil
|
|
|
|
for _, payloadType := range md.MediaName.Formats {
|
|
format, err := format.Unmarshal(md, payloadType)
|
|
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.
|
|
//
|
|
// Deprecated: replaced by Marshal2.
|
|
func (m Media) Marshal() *psdp.MediaDescription {
|
|
ret, err := m.Marshal2()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// Marshal2 encodes the media in SDP format.
|
|
func (m Media) Marshal2() (*psdp.MediaDescription, error) {
|
|
var protos []string
|
|
if !m.Secure {
|
|
protos = []string{"RTP", "AVP"}
|
|
} else {
|
|
protos = []string{"RTP", "SAVP"}
|
|
}
|
|
|
|
md := &psdp.MediaDescription{
|
|
MediaName: psdp.MediaName{
|
|
Media: string(m.Type),
|
|
Protos: protos,
|
|
},
|
|
}
|
|
|
|
if m.ID != "" {
|
|
md.Attributes = append(md.Attributes, psdp.Attribute{
|
|
Key: "mid",
|
|
Value: m.ID,
|
|
})
|
|
}
|
|
|
|
if m.IsBackChannel {
|
|
md.Attributes = append(md.Attributes, psdp.Attribute{
|
|
Key: "sendonly",
|
|
})
|
|
}
|
|
|
|
if m.KeyMgmtMikey != nil {
|
|
keyEnc, err := m.KeyMgmtMikey.Marshal()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
md.Attributes = append(md.Attributes, psdp.Attribute{
|
|
Key: "key-mgmt",
|
|
Value: "mikey " + base64.StdEncoding.EncodeToString(keyEnc),
|
|
})
|
|
}
|
|
|
|
md.Attributes = append(md.Attributes, psdp.Attribute{
|
|
Key: "control",
|
|
Value: m.Control,
|
|
})
|
|
|
|
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, nil
|
|
}
|
|
|
|
// URL returns the absolute URL of the media.
|
|
func (m Media) URL(contentBase *base.URL) (*base.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 := base.ParseURL(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] != '?' && m.Control[0] != '/' && !strings.HasSuffix(strURL, "/") {
|
|
strURL += "/"
|
|
}
|
|
|
|
ur, _ := base.ParseURL(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
|
|
}
|