282 lines
5.8 KiB
Go
282 lines
5.8 KiB
Go
package streamer
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/pion/sdp/v3"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
const (
|
|
DirectionRecvonly = "recvonly"
|
|
DirectionSendonly = "sendonly"
|
|
DirectionSendRecv = "sendrecv"
|
|
)
|
|
|
|
const (
|
|
KindVideo = "video"
|
|
KindAudio = "audio"
|
|
)
|
|
|
|
const (
|
|
CodecH264 = "H264" // payloadType: 96
|
|
CodecH265 = "H265"
|
|
CodecVP8 = "VP8"
|
|
CodecVP9 = "VP9"
|
|
CodecAV1 = "AV1"
|
|
|
|
CodecPCMU = "PCMU" // payloadType: 0
|
|
CodecPCMA = "PCMA" // payloadType: 8
|
|
CodecAAC = "MPEG4-GENERIC"
|
|
CodecOpus = "OPUS" // payloadType: 111
|
|
CodecG722 = "G722"
|
|
CodecMPA = "MPA" // payload: 14
|
|
)
|
|
|
|
func GetKind(name string) string {
|
|
switch name {
|
|
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1:
|
|
return KindVideo
|
|
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMPA:
|
|
return KindAudio
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Media take best from:
|
|
// - deepch/vdk/format/rtsp/sdp.Media
|
|
// - pion/sdp.MediaDescription
|
|
type Media struct {
|
|
Kind string `json:"kind,omitempty"` // video or audio
|
|
Direction string `json:"direction,omitempty"`
|
|
Codecs []*Codec `json:"codecs,omitempty"`
|
|
|
|
MID string `json:"mid,omitempty"` // TODO: fixme?
|
|
Control string `json:"control,omitempty"` // TODO: fixme?
|
|
Title string `json:"title,omitempty"` // TODO: fixme?
|
|
}
|
|
|
|
func (m *Media) String() string {
|
|
s := fmt.Sprintf("%s, %s", m.Kind, m.Direction)
|
|
for _, codec := range m.Codecs {
|
|
s += ", " + codec.String()
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (m *Media) Clone() *Media {
|
|
clone := *m
|
|
return &clone
|
|
}
|
|
|
|
func (m *Media) AV() bool {
|
|
return m.Kind == KindVideo || m.Kind == KindAudio
|
|
}
|
|
|
|
func (m *Media) MatchCodec(codec *Codec) bool {
|
|
for _, c := range m.Codecs {
|
|
if c.Match(codec) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m *Media) MatchMedia(media *Media) *Codec {
|
|
if m.Kind != media.Kind {
|
|
return nil
|
|
}
|
|
|
|
switch m.Direction {
|
|
case DirectionSendonly:
|
|
if media.Direction != DirectionRecvonly {
|
|
return nil
|
|
}
|
|
case DirectionRecvonly:
|
|
if media.Direction != DirectionSendonly {
|
|
return nil
|
|
}
|
|
default:
|
|
panic("wrong direction")
|
|
}
|
|
|
|
for _, localCodec := range m.Codecs {
|
|
if media.Codecs == nil {
|
|
return localCodec
|
|
}
|
|
|
|
for _, remoteCodec := range media.Codecs {
|
|
if localCodec.Match(remoteCodec) {
|
|
return localCodec
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Codec take best from:
|
|
// - deepch/vdk/av.CodecData
|
|
// - pion/webrtc.RTPCodecCapability
|
|
type Codec struct {
|
|
Name string // H264, PCMU, PCMA, opus...
|
|
ClockRate uint32 // 90000, 8000, 16000...
|
|
Channels uint16 // 0, 1, 2
|
|
FmtpLine string
|
|
PayloadType uint8
|
|
}
|
|
|
|
func NewCodec(name string) *Codec {
|
|
name = strings.ToUpper(name)
|
|
switch name {
|
|
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1:
|
|
return &Codec{Name: name, ClockRate: 90000}
|
|
case CodecPCMU, CodecPCMA:
|
|
return &Codec{Name: name, ClockRate: 8000}
|
|
case CodecOpus:
|
|
return &Codec{Name: name, ClockRate: 48000, Channels: 2}
|
|
}
|
|
|
|
panic(fmt.Sprintf("unsupported codec: %s", name))
|
|
}
|
|
|
|
func (c *Codec) String() string {
|
|
s := fmt.Sprintf("%d %s/%d", c.PayloadType, c.Name, c.ClockRate)
|
|
if c.Channels > 0 {
|
|
s = fmt.Sprintf("%s/%d", s, c.Channels)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (c *Codec) Clone() *Codec {
|
|
clone := *c
|
|
return &clone
|
|
}
|
|
|
|
func (c *Codec) Match(codec *Codec) bool {
|
|
return c.Name == codec.Name &&
|
|
(c.ClockRate == codec.ClockRate || codec.ClockRate == 0) &&
|
|
(c.Channels == codec.Channels || codec.Channels == 0)
|
|
}
|
|
|
|
func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
|
|
sd := &sdp.SessionDescription{}
|
|
if err := sd.Unmarshal(rawSDP); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var medias []*Media
|
|
for _, md := range sd.MediaDescriptions {
|
|
media := UnmarshalMedia(md)
|
|
|
|
if media.Direction == DirectionSendRecv {
|
|
media.Direction = DirectionRecvonly
|
|
medias = append(medias, media)
|
|
|
|
media = media.Clone()
|
|
media.Direction = DirectionSendonly
|
|
}
|
|
|
|
medias = append(medias, media)
|
|
}
|
|
|
|
return medias, nil
|
|
}
|
|
|
|
func MarshalSDP(medias []*Media) ([]byte, error) {
|
|
sd := &sdp.SessionDescription{}
|
|
|
|
payloadType := uint8(96)
|
|
|
|
for _, media := range medias {
|
|
if media.Codecs == nil {
|
|
continue
|
|
}
|
|
|
|
codec := media.Codecs[0]
|
|
md := &sdp.MediaDescription{
|
|
MediaName: sdp.MediaName{
|
|
Media: media.Kind,
|
|
Protos: []string{"RTP", "AVP"},
|
|
},
|
|
}
|
|
md.WithCodec(payloadType, codec.Name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
|
|
|
sd.MediaDescriptions = append(sd.MediaDescriptions, md)
|
|
|
|
payloadType++
|
|
}
|
|
|
|
return sd.Marshal()
|
|
}
|
|
|
|
func UnmarshalMedia(md *sdp.MediaDescription) *Media {
|
|
m := &Media{
|
|
Kind: md.MediaName.Media,
|
|
}
|
|
|
|
for _, attr := range md.Attributes {
|
|
switch attr.Key {
|
|
case DirectionSendonly, DirectionRecvonly, DirectionSendRecv:
|
|
m.Direction = attr.Key
|
|
case "control":
|
|
m.Control = attr.Value
|
|
case "mid":
|
|
m.MID = attr.Value
|
|
}
|
|
}
|
|
|
|
for _, format := range md.MediaName.Formats {
|
|
m.Codecs = append(m.Codecs, UnmarshalCodec(md, format))
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
|
c := &Codec{PayloadType: byte(atoi(payloadType))}
|
|
|
|
for _, attr := range md.Attributes {
|
|
switch {
|
|
case c.Name == "" && attr.Key == "rtpmap" && strings.HasPrefix(attr.Value, payloadType):
|
|
i := strings.IndexByte(attr.Value, ' ')
|
|
ss := strings.Split(attr.Value[i+1:], "/")
|
|
|
|
c.Name = strings.ToUpper(ss[0])
|
|
// fix tailing space: `a=rtpmap:96 H264/90000 `
|
|
c.ClockRate = uint32(atoi(strings.TrimRightFunc(ss[1], unicode.IsSpace)))
|
|
|
|
if len(ss) == 3 && ss[2] == "2" {
|
|
c.Channels = 2
|
|
}
|
|
case c.FmtpLine == "" && attr.Key == "fmtp" && strings.HasPrefix(attr.Value, payloadType):
|
|
if i := strings.IndexByte(attr.Value, ' '); i > 0 {
|
|
c.FmtpLine = attr.Value[i+1:]
|
|
}
|
|
}
|
|
}
|
|
|
|
if c.Name == "" {
|
|
switch payloadType {
|
|
case "0":
|
|
c.Name = CodecPCMU
|
|
c.ClockRate = 8000
|
|
case "8":
|
|
c.Name = CodecPCMA
|
|
c.ClockRate = 8000
|
|
case "14":
|
|
c.Name = CodecMPA
|
|
c.ClockRate = 44100
|
|
default:
|
|
c.Name = payloadType
|
|
}
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
func atoi(s string) (i int) {
|
|
i, _ = strconv.Atoi(s)
|
|
return
|
|
}
|