Initial commit

This commit is contained in:
Alexey Khit
2022-08-18 09:19:00 +03:00
commit 3e77835583
65 changed files with 6372 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
package streams
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
type Handler func(url string) (streamer.Producer, error)
var handlers map[string]Handler
func HandleFunc(scheme string, handler Handler) {
if handlers == nil {
handlers = make(map[string]Handler)
}
handlers[scheme] = handler
}
func GetProducer(url string) (streamer.Producer, error) {
i := strings.IndexByte(url, ':')
handler := handlers[url[:i]]
if handler == nil {
return nil, fmt.Errorf("unsupported scheme: %s", url)
}
return handler(url)
}
+89
View File
@@ -0,0 +1,89 @@
package streams
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
type state byte
const (
stateNone state = iota
stateMedias
stateTracks
stateStart
)
type Producer struct {
streamer.Element
url string
element streamer.Producer
tracks []*streamer.Track
state state
}
func (p *Producer) GetMedias() []*streamer.Media {
if p.state == stateNone {
i := strings.IndexByte(p.url, ':')
handler := handlers[p.url[:i]]
if handler == nil {
log.Warn().Str("url", p.url).Msg("[streams] unsupported scheme")
return nil
}
log.Debug().Str("url", p.url).Msg("[streams] probe producer")
var err error
p.element, err = handler(p.url)
if err != nil {
log.Error().Err(err).Str("url", p.url).Msg("[streams] probe producer")
return nil
}
p.state = stateMedias
}
return p.element.GetMedias()
}
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
if p.state == stateMedias {
p.state = stateTracks
}
track := p.element.GetTrack(media, codec)
for _, t := range p.tracks {
if track == t {
return track
}
}
p.tracks = append(p.tracks, track)
return track
}
// internals
func (p *Producer) start() {
if p.state != stateTracks {
return
}
log.Debug().Str("url", p.url).Msg("[streams] start producer")
p.state = stateStart
go p.element.Start()
}
func (p *Producer) stop() {
log.Debug().Str("url", p.url).Msg("[streams] stop producer")
_ = p.element.Stop()
p.element = nil
p.tracks = nil
p.state = stateNone
}
+164
View File
@@ -0,0 +1,164 @@
package streams
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
type Consumer struct {
element streamer.Consumer
tracks []*streamer.Track
}
type Stream struct {
producers []*Producer
consumers []*Consumer
}
func newStream(source interface{}) *Stream {
s := new(Stream)
switch source := source.(type) {
case string:
prod := &Producer{url: source}
s.producers = append(s.producers, prod)
case []interface{}:
for _, source := range source {
prod := &Producer{url: source.(string)}
s.producers = append(s.producers, prod)
}
case map[string]interface{}:
return newStream(source["url"])
default:
panic("wrong source type")
}
return s
}
func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
ic := len(s.consumers)
consumer := &Consumer{element: cons}
// Step 1. Get consumer medias
for icc, consMedia := range cons.GetMedias() {
log.Trace().Stringer("media", consMedia).
Msgf("[streams] consumer:%d:%d candidate", ic, icc)
producers:
for ip, prod := range s.producers {
// Step 2. Get producer medias (not tracks yet)
for ipc, prodMedia := range prod.GetMedias() {
log.Trace().Stringer("media", prodMedia).
Msgf("[streams] producer:%d:%d candidate", ip, ipc)
// Step 3. Match consumer/producer codecs list
prodCodec := prodMedia.MatchMedia(consMedia)
if prodCodec != nil {
log.Trace().Stringer("codec", prodCodec).
Msgf("[streams] match producer:%d:%d => consumer:%d:%d", ip, ipc, ic, icc)
// Step 4. Get producer track
prodTrack := prod.GetTrack(prodMedia, prodCodec)
// Step 5. Add track to consumer and get new track
consTrack := consumer.element.AddTrack(consMedia, prodTrack)
consumer.tracks = append(consumer.tracks, consTrack)
break producers
}
}
}
}
// can't match tracks for consumer
if len(consumer.tracks) == 0 {
return nil
}
s.consumers = append(s.consumers, consumer)
for _, prod := range s.producers {
prod.start()
}
return nil
}
func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
for i, consumer := range s.consumers {
if consumer.element == cons {
// remove consumer pads from all producers
for _, track := range consumer.tracks {
track.Unbind()
}
// remove consumer from slice
s.removeConsumer(i)
break
}
}
for _, producer := range s.producers {
var sink bool
for _, track := range producer.tracks {
if len(track.Sink) > 0 {
sink = true
}
}
if !sink {
producer.stop()
}
}
}
func (s *Stream) AddProducer(prod streamer.Producer) {
panic("not implemented")
}
func (s *Stream) RemoveProducer(prod streamer.Producer) {
panic("not implemented")
}
func (s *Stream) MarshalJSON() ([]byte, error) {
var v []interface{}
for _, prod := range s.producers {
if prod.element != nil {
v = append(v, prod.element)
}
}
for _, cons := range s.consumers {
// cons.element always not nil
v = append(v, cons.element)
}
if len(v) == 0 {
v = nil
}
return json.Marshal(v)
}
func (s *Stream) removeConsumer(i int) {
switch {
case len(s.consumers) == 1: // only one element
s.consumers = nil
case i == 0: // first element
s.consumers = s.consumers[1:]
case i == len(s.consumers)-1: // last element
s.consumers = s.consumers[:i]
default: // middle element
s.consumers = append(s.consumers[:i], s.consumers[i+1:]...)
}
}
func (s *Stream) removeProducer(i int) {
switch {
case len(s.producers) == 1: // only one element
s.producers = nil
case i == 0: // first element
s.producers = s.producers[1:]
case i == len(s.producers)-1: // last element
s.producers = s.producers[:i]
default: // middle element
s.producers = append(s.producers[:i], s.producers[i+1:]...)
}
}
+134
View File
@@ -0,0 +1,134 @@
package streams
import (
"github.com/AlexxIT/go2rtc/pkg/fake"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
// Google Chrome 104.0.5112.79
const chrome = `v=0
o=- 0 0 IN IP4 0.0.0.0
s=-
t=0 0
m=audio 9 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8 110 112 113 126
a=sendrecv
a=rtpmap:111 opus/48000/2
a=rtpmap:63 red/48000/2
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:110 telephone-event/48000
a=rtpmap:112 telephone-event/32000
a=rtpmap:113 telephone-event/16000
a=rtpmap:126 telephone-event/8000
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 122 127 121 125 107 108 109 124 120 123 119 35 36 37 38 39 40 41 42 114 115 116 117 118 43
a=recvonly
a=rtpmap:96 VP8/90000
a=rtpmap:97 rtx/90000
a=rtpmap:98 VP9/90000
a=rtpmap:99 rtx/90000
a=rtpmap:100 VP9/90000
a=rtpmap:101 rtx/90000
a=rtpmap:102 VP9/90000
a=rtpmap:122 rtx/90000
a=rtpmap:127 H264/90000
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtpmap:121 rtx/90000
a=rtpmap:125 H264/90000
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtpmap:107 rtx/90000
a=rtpmap:108 H264/90000
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:109 rtx/90000
a=rtpmap:124 H264/90000
a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtpmap:120 rtx/90000
a=rtpmap:123 H264/90000
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f
a=rtpmap:119 rtx/90000
a=rtpmap:35 H264/90000
a=fmtp:35 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f
a=rtpmap:36 rtx/90000
a=rtpmap:37 H264/90000
a=fmtp:37 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=f4001f
a=rtpmap:38 rtx/90000
a=rtpmap:39 H264/90000
a=fmtp:39 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=f4001f
a=rtpmap:40 rtx/90000
a=rtpmap:41 AV1/90000
a=rtpmap:42 rtx/90000
a=rtpmap:114 H264/90000
a=fmtp:114 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f
a=rtpmap:115 rtx/90000
a=rtpmap:116 red/90000
a=rtpmap:117 rtx/90000
a=rtpmap:118 ulpfec/90000
a=rtpmap:43 flexfec-03/90000
`
const dahuaSimple = `v=0
o=- 0 0 IN IP4 0.0.0.0
s=-
t=0 0
m=video 0 RTP/AVP 96
a=control:trackID=0
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1;profile-level-id=42401E;sprop-parameter-sets=Z0JAHqaAoD2QAA==,aM48gAA=
a=recvonly
m=audio 0 RTP/AVP 97
a=control:trackID=1
a=rtpmap:97 MPEG4-GENERIC/16000
a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
a=recvonly
m=audio 0 RTP/AVP 8
a=control:trackID=5
a=rtpmap:8 PCMA/8000
a=sendonly
`
const ffmpegPCMU48000 = `v=0
o=- 0 0 IN IP4 127.0.0.1
s=-
t=0 0
m=audio 0 RTP/AVP 96
b=AS:384
a=rtpmap:96 PCMU/48000/1
a=control:streamid=0
`
func TestRouting(t *testing.T) {
prod := &fake.Producer{}
prod.Medias, _ = streamer.UnmarshalRTSPSDP([]byte(dahuaSimple))
assert.Len(t, prod.Medias, 3)
HandleFunc("fake", func(url string) (streamer.Producer, error) {
return prod, nil
})
cons := &fake.Consumer{}
cons.Medias, _ = streamer.UnmarshalSDP([]byte(chrome))
assert.Len(t, cons.Medias, 3)
// setup stream with one producer
stream := newStream("fake:")
// main check:
err := stream.AddConsumer(cons)
assert.Nil(t, err)
assert.Len(t, prod.Tracks, 2)
assert.Len(t, cons.Tracks, 2)
time.Sleep(time.Second)
assert.Greater(t, prod.SendPackets,0)
assert.Greater(t, cons.RecvPackets,0)
assert.Greater(t, prod.RecvPackets,0)
assert.Greater(t, cons.SendPackets,0)
}
+28
View File
@@ -0,0 +1,28 @@
package streams
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/rs/zerolog"
)
var Streams = map[string]*Stream{}
func Init() {
var cfg struct {
Mod map[string]interface{} `yaml:"streams"`
}
app.LoadConfig(&cfg)
log = app.GetLogger("streams")
for name, item := range cfg.Mod {
Streams[name] = newStream(item)
}
}
func Get(name string) *Stream {
return Streams[name]
}
var log zerolog.Logger