Initial commit
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
package streamer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
JSONType = "type"
|
||||
JSONRemoteAddr = "remote_addr"
|
||||
JSONUserAgent = "user_agent"
|
||||
JSONReceive = "receive"
|
||||
JSONSend = "send"
|
||||
)
|
||||
|
||||
// Message - struct for data exchange in Web API
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// other
|
||||
|
||||
func Between(s, sub1, sub2 string) string {
|
||||
i := strings.Index(s, sub1)
|
||||
if i < 0 {
|
||||
return ""
|
||||
}
|
||||
s = s[i+len(sub1):]
|
||||
|
||||
if len(sub2) == 1 {
|
||||
i = strings.IndexByte(s, sub2[0])
|
||||
} else {
|
||||
i = strings.Index(s, sub2)
|
||||
}
|
||||
if i >= 0 {
|
||||
return s[:i]
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func Contains(medias []*Media, media *Media, codec *Codec) bool {
|
||||
var ok1, ok2 bool
|
||||
for _, m := range medias {
|
||||
if m == media {
|
||||
ok1 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, c := range media.Codecs {
|
||||
if c == codec {
|
||||
ok2 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return ok1 && ok2
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package streamer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pion/sdp/v3"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
func GetKind(name string) string {
|
||||
switch name {
|
||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1:
|
||||
return KindVideo
|
||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722:
|
||||
return KindAudio
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Media take best from:
|
||||
// - deepch/vdk/format/rtsp/sdp.Media
|
||||
// - pion/sdp.MediaDescription
|
||||
type Media struct {
|
||||
Kind string // video, audio
|
||||
Direction string
|
||||
Codecs []*Codec
|
||||
|
||||
MID string // TODO: fixme?
|
||||
Control string // 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 &&
|
||||
c.Channels == codec.Channels
|
||||
}
|
||||
|
||||
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 UnmarshalRTSPSDP(rawSDP []byte) ([]*Media, error) {
|
||||
medias, err := UnmarshalSDP(rawSDP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// fix bug in ONVIF spec
|
||||
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
|
||||
for _, media := range medias {
|
||||
switch media.Direction {
|
||||
case DirectionRecvonly, "":
|
||||
media.Direction = DirectionSendonly
|
||||
case DirectionSendonly:
|
||||
media.Direction = DirectionRecvonly
|
||||
}
|
||||
}
|
||||
|
||||
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])
|
||||
c.ClockRate = uint32(atoi(ss[1]))
|
||||
|
||||
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 = "PCMU"
|
||||
c.ClockRate = 8000
|
||||
case "8":
|
||||
c.Name = "PCMA"
|
||||
c.ClockRate = 8000
|
||||
default:
|
||||
panic("unknown codec")
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func atoi(s string) (i int) {
|
||||
i, _ = strconv.Atoi(s)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package streamer
|
||||
|
||||
// States, Queries and Events
|
||||
|
||||
type EventType byte
|
||||
|
||||
const (
|
||||
StateNull EventType = iota
|
||||
StateReady
|
||||
StatePaused
|
||||
StatePlaying
|
||||
)
|
||||
|
||||
// Element base struct for all classes with support feedback
|
||||
type Element struct {
|
||||
events []EventFunc
|
||||
}
|
||||
|
||||
type EventFunc func(msg interface{})
|
||||
|
||||
func (e *Element) Listen(f EventFunc) {
|
||||
e.events = append(e.events, f)
|
||||
}
|
||||
|
||||
func (e *Element) Fire(msg interface{}) {
|
||||
for _, f := range e.events {
|
||||
f(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Element) Push(msg interface{}) {
|
||||
}
|
||||
|
||||
// Producer and Consumer interfaces
|
||||
|
||||
type Producer interface {
|
||||
Listen(f EventFunc)
|
||||
GetMedias() []*Media
|
||||
GetTrack(media *Media, codec *Codec) *Track
|
||||
Start() error
|
||||
Stop() error
|
||||
}
|
||||
|
||||
type Consumer interface {
|
||||
Listen(f EventFunc)
|
||||
GetMedias() []*Media
|
||||
AddTrack(media *Media, track *Track) *Track
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package streamer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type WriterFunc func(packet *rtp.Packet) error
|
||||
type WrapperFunc func(push WriterFunc) WriterFunc
|
||||
|
||||
type Track struct {
|
||||
Codec *Codec
|
||||
Direction string
|
||||
Sink map[*Track]WriterFunc
|
||||
}
|
||||
|
||||
func (t *Track) String() string {
|
||||
s := t.Codec.String()
|
||||
s += fmt.Sprintf(", sinks=%d", len(t.Sink))
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *Track) WriteRTP(p *rtp.Packet) error {
|
||||
for _, f := range t.Sink {
|
||||
_ = f(p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Track) Bind(w WriterFunc) *Track {
|
||||
if t.Sink == nil {
|
||||
t.Sink = map[*Track]WriterFunc{}
|
||||
}
|
||||
|
||||
clone := &Track{
|
||||
Codec: t.Codec, Direction: t.Direction, Sink: t.Sink,
|
||||
}
|
||||
t.Sink[clone] = w
|
||||
return clone
|
||||
}
|
||||
|
||||
func (t *Track) Unbind() {
|
||||
delete(t.Sink, t)
|
||||
}
|
||||
Reference in New Issue
Block a user