diff --git a/cmd/debug/debug.go b/cmd/debug/debug.go index e2092ee5..90861b37 100644 --- a/cmd/debug/debug.go +++ b/cmd/debug/debug.go @@ -3,7 +3,7 @@ package debug import ( "github.com/AlexxIT/go2rtc/cmd/api" "github.com/AlexxIT/go2rtc/cmd/streams" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" ) func Init() { @@ -12,6 +12,6 @@ func Init() { streams.HandleFunc("null", nullHandler) } -func nullHandler(string) (streamer.Producer, error) { +func nullHandler(string) (core.Producer, error) { return nil, nil } diff --git a/cmd/dvrip/dvrip.go b/cmd/dvrip/dvrip.go index db45999e..0826b009 100644 --- a/cmd/dvrip/dvrip.go +++ b/cmd/dvrip/dvrip.go @@ -2,15 +2,15 @@ package dvrip import ( "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/dvrip" - "github.com/AlexxIT/go2rtc/pkg/streamer" ) func Init() { streams.HandleFunc("dvrip", handle) } -func handle(url string) (streamer.Producer, error) { +func handle(url string) (core.Producer, error) { conn := dvrip.NewClient(url) if err := conn.Dial(); err != nil { return nil, err diff --git a/cmd/echo/echo.go b/cmd/echo/echo.go index 460f5cdc..d0714fc9 100644 --- a/cmd/echo/echo.go +++ b/cmd/echo/echo.go @@ -4,15 +4,15 @@ import ( "bytes" "github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/shell" - "github.com/AlexxIT/go2rtc/pkg/streamer" "os/exec" ) func Init() { log := app.GetLogger("echo") - streams.HandleFunc("echo", func(url string) (streamer.Producer, error) { + streams.HandleFunc("echo", func(url string) (core.Producer, error) { args := shell.QuoteSplit(url[5:]) b, err := exec.Command(args[0], args[1:]...).Output() diff --git a/cmd/exec/exec.go b/cmd/exec/exec.go index 27afbf83..20d2fc14 100644 --- a/cmd/exec/exec.go +++ b/cmd/exec/exec.go @@ -8,9 +8,9 @@ import ( "github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/rtsp" "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/core" pkg "github.com/AlexxIT/go2rtc/pkg/rtsp" "github.com/AlexxIT/go2rtc/pkg/shell" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/rs/zerolog" "os" "os/exec" @@ -48,7 +48,7 @@ func Init() { log = app.GetLogger("exec") } -func Handle(url string) (streamer.Producer, error) { +func Handle(url string) (core.Producer, error) { sum := md5.Sum([]byte(url)) path := "/" + hex.EncodeToString(sum[:]) @@ -67,7 +67,7 @@ func Handle(url string) (streamer.Producer, error) { cmd.Stderr = os.Stderr } - ch := make(chan streamer.Producer) + ch := make(chan core.Producer) waitersMu.Lock() waiters[path] = ch @@ -116,5 +116,5 @@ func Handle(url string) (streamer.Producer, error) { // internal var log zerolog.Logger -var waiters = map[string]chan streamer.Producer{} +var waiters = map[string]chan core.Producer{} var waitersMu sync.Mutex diff --git a/cmd/ffmpeg/device/device_darwin.go b/cmd/ffmpeg/device/device_darwin.go index d277a12b..a22f7e13 100644 --- a/cmd/ffmpeg/device/device_darwin.go +++ b/cmd/ffmpeg/device/device_darwin.go @@ -2,7 +2,7 @@ package device import ( "bytes" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "os/exec" "strings" ) @@ -11,15 +11,15 @@ import ( const deviceInputPrefix = "-f avfoundation" func deviceInputSuffix(videoIdx, audioIdx int) string { - video := findMedia(streamer.KindVideo, videoIdx) - audio := findMedia(streamer.KindAudio, audioIdx) + video := findMedia(core.KindVideo, videoIdx) + audio := findMedia(core.KindAudio, audioIdx) switch { case video != nil && audio != nil: - return `"` + video.MID + `:` + audio.MID + `"` + return `"` + video.ID + `:` + audio.ID + `"` case video != nil: - return `"` + video.MID + `"` + return `"` + video.ID + `"` case audio != nil: - return `"` + audio.MID + `"` + return `"` + audio.ID + `"` } return "" } @@ -40,10 +40,10 @@ process: for _, line := range lines { switch { case strings.HasSuffix(line, "video devices:"): - kind = streamer.KindVideo + kind = core.KindVideo continue case strings.HasSuffix(line, "audio devices:"): - kind = streamer.KindAudio + kind = core.KindAudio continue case strings.HasPrefix(line, "dummy"): break process @@ -56,6 +56,6 @@ process: } } -func loadMedia(kind, name string) *streamer.Media { - return &streamer.Media{Kind: kind, MID: name} +func loadMedia(kind, name string) *core.Media { + return &core.Media{Kind: kind, ID: name} } diff --git a/cmd/ffmpeg/device/device_linux.go b/cmd/ffmpeg/device/device_linux.go index 84736c0c..3ce29a86 100644 --- a/cmd/ffmpeg/device/device_linux.go +++ b/cmd/ffmpeg/device/device_linux.go @@ -2,7 +2,7 @@ package device import ( "bytes" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "io/ioutil" "os/exec" "strings" @@ -12,8 +12,8 @@ import ( const deviceInputPrefix = "-f v4l2" func deviceInputSuffix(videoIdx, audioIdx int) string { - video := findMedia(streamer.KindVideo, videoIdx) - return video.MID + video := findMedia(core.KindVideo, videoIdx) + return video.ID } func loadMedias() { @@ -23,8 +23,8 @@ func loadMedias() { } for _, file := range files { log.Trace().Msg("[ffmpeg] " + file.Name()) - if strings.HasPrefix(file.Name(), streamer.KindVideo) { - media := loadMedia(streamer.KindVideo, "/dev/"+file.Name()) + if strings.HasPrefix(file.Name(), core.KindVideo) { + media := loadMedia(core.KindVideo, "/dev/"+file.Name()) if media != nil { medias = append(medias, media) } @@ -32,7 +32,7 @@ func loadMedias() { } } -func loadMedia(kind, name string) *streamer.Media { +func loadMedia(kind, name string) *core.Media { cmd := exec.Command( Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name, ) @@ -44,5 +44,5 @@ func loadMedia(kind, name string) *streamer.Media { return nil } - return &streamer.Media{Kind: kind, MID: name} + return &core.Media{Kind: kind, ID: name} } diff --git a/cmd/ffmpeg/device/device_windows.go b/cmd/ffmpeg/device/device_windows.go index cb294048..f465386a 100644 --- a/cmd/ffmpeg/device/device_windows.go +++ b/cmd/ffmpeg/device/device_windows.go @@ -2,7 +2,7 @@ package device import ( "bytes" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "os/exec" "strings" ) @@ -11,15 +11,15 @@ import ( const deviceInputPrefix = "-f dshow" func deviceInputSuffix(videoIdx, audioIdx int) string { - video := findMedia(streamer.KindVideo, videoIdx) - audio := findMedia(streamer.KindAudio, audioIdx) + video := findMedia(core.KindVideo, videoIdx) + audio := findMedia(core.KindAudio, audioIdx) switch { case video != nil && audio != nil: - return `video="` + video.MID + `":audio=` + audio.MID + `"` + return `video="` + video.ID + `":audio=` + audio.ID + `"` case video != nil: - return `video="` + video.MID + `"` + return `video="` + video.ID + `"` case audio != nil: - return `audio="` + audio.MID + `"` + return `audio="` + audio.ID + `"` } return "" } @@ -37,9 +37,9 @@ func loadMedias() { for _, line := range lines { var kind string if strings.HasSuffix(line, "(video)") { - kind = streamer.KindVideo + kind = core.KindVideo } else if strings.HasSuffix(line, "(audio)") { - kind = streamer.KindAudio + kind = core.KindAudio } else { continue } @@ -52,6 +52,6 @@ func loadMedias() { } } -func loadMedia(kind, name string) *streamer.Media { - return &streamer.Media{Kind: kind, MID: name} +func loadMedia(kind, name string) *core.Media { + return &core.Media{Kind: kind, ID: name} } diff --git a/cmd/ffmpeg/device/devices.go b/cmd/ffmpeg/device/devices.go index 31f603e9..1a886f7e 100644 --- a/cmd/ffmpeg/device/devices.go +++ b/cmd/ffmpeg/device/devices.go @@ -4,7 +4,7 @@ import ( "encoding/json" "github.com/AlexxIT/go2rtc/cmd/api" "github.com/AlexxIT/go2rtc/cmd/app" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/rs/zerolog" "net/http" "net/url" @@ -52,9 +52,9 @@ func GetInput(src string) (string, error) { var Bin string var log zerolog.Logger -var medias []*streamer.Media +var medias []*core.Media -func findMedia(kind string, index int) *streamer.Media { +func findMedia(kind string, index int) *core.Media { for _, media := range medias { if media.Kind != kind { continue diff --git a/cmd/ffmpeg/ffmpeg.go b/cmd/ffmpeg/ffmpeg.go index 34fd2c53..1489bb84 100644 --- a/cmd/ffmpeg/ffmpeg.go +++ b/cmd/ffmpeg/ffmpeg.go @@ -8,7 +8,7 @@ import ( "github.com/AlexxIT/go2rtc/cmd/ffmpeg/device" "github.com/AlexxIT/go2rtc/cmd/rtsp" "github.com/AlexxIT/go2rtc/cmd/streams" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "net/url" "strconv" "strings" @@ -27,7 +27,7 @@ func Init() { defaults["global"] += " -v error" } - streams.HandleFunc("ffmpeg", func(url string) (streamer.Producer, error) { + streams.HandleFunc("ffmpeg", func(url string) (core.Producer, error) { args := parseArgs(url[7:]) // remove `ffmpeg:` if args == nil { return nil, errors.New("can't generate ffmpeg command") diff --git a/cmd/hass/hass.go b/cmd/hass/hass.go index d3b98a40..c220b4d5 100644 --- a/cmd/hass/hass.go +++ b/cmd/hass/hass.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/streams" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/rs/zerolog" "os" "path" @@ -38,7 +38,7 @@ func Init() { urls := map[string]string{} - streams.HandleFunc("hass", func(url string) (streamer.Producer, error) { + streams.HandleFunc("hass", func(url string) (core.Producer, error) { if hurl := urls[url[5:]]; hurl != "" { return streams.GetProducer(hurl) } diff --git a/cmd/hls/hls.go b/cmd/hls/hls.go index 98f2cb94..dc35c7d3 100644 --- a/cmd/hls/hls.go +++ b/cmd/hls/hls.go @@ -4,9 +4,9 @@ import ( "fmt" "github.com/AlexxIT/go2rtc/cmd/api" "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mp4" "github.com/AlexxIT/go2rtc/pkg/mpegts" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/rs/zerolog/log" "net/http" "strconv" @@ -27,7 +27,7 @@ func Init() { } type Consumer interface { - streamer.Consumer + core.Consumer Init() ([]byte, error) MimeCodecs() string Start() diff --git a/cmd/homekit/homekit.go b/cmd/homekit/homekit.go index e83fe429..1e7d0756 100644 --- a/cmd/homekit/homekit.go +++ b/cmd/homekit/homekit.go @@ -5,8 +5,8 @@ import ( "github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/srtp" "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/homekit" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/rs/zerolog" ) @@ -20,12 +20,12 @@ func Init() { var log zerolog.Logger -func streamHandler(url string) (streamer.Producer, error) { +func streamHandler(url string) (core.Producer, error) { conn, err := homekit.NewClient(url, srtp.Server) if err != nil { return nil, err } - if err = conn.Dial();err!=nil{ + if err = conn.Dial(); err != nil { return nil, err } return conn, nil diff --git a/cmd/http/http.go b/cmd/http/http.go index e7d9d4a8..a261b0c6 100644 --- a/cmd/http/http.go +++ b/cmd/http/http.go @@ -4,10 +4,10 @@ import ( "errors" "fmt" "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mjpeg" "github.com/AlexxIT/go2rtc/pkg/mpegts" "github.com/AlexxIT/go2rtc/pkg/rtmp" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/tcp" "net/http" "strings" @@ -18,7 +18,7 @@ func Init() { streams.HandleFunc("https", handle) } -func handle(url string) (streamer.Producer, error) { +func handle(url string) (core.Producer, error) { // first we get the Content-Type to define supported producer req, err := http.NewRequest("GET", url, nil) if err != nil { diff --git a/cmd/isapi/init.go b/cmd/isapi/init.go index 895e05d0..33bb85a7 100644 --- a/cmd/isapi/init.go +++ b/cmd/isapi/init.go @@ -2,15 +2,15 @@ package isapi import ( "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/isapi" - "github.com/AlexxIT/go2rtc/pkg/streamer" ) func Init() { streams.HandleFunc("isapi", handle) } -func handle(url string) (streamer.Producer, error) { +func handle(url string) (core.Producer, error) { conn, err := isapi.NewClient(url) if err != nil { return nil, err diff --git a/cmd/ivideon/ivideon.go b/cmd/ivideon/ivideon.go index a6e17a60..d63edfd5 100644 --- a/cmd/ivideon/ivideon.go +++ b/cmd/ivideon/ivideon.go @@ -2,13 +2,13 @@ package ivideon import ( "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/ivideon" - "github.com/AlexxIT/go2rtc/pkg/streamer" "strings" ) func Init() { - streams.HandleFunc("ivideon", func(url string) (streamer.Producer, error) { + streams.HandleFunc("ivideon", func(url string) (core.Producer, error) { id := strings.Replace(url[8:], "/", ":", 1) prod := ivideon.NewClient(id) if err := prod.Dial(); err != nil { diff --git a/cmd/mp4/mp4.go b/cmd/mp4/mp4.go index ee3a1c89..c98ff9a6 100644 --- a/cmd/mp4/mp4.go +++ b/cmd/mp4/mp4.go @@ -4,8 +4,8 @@ import ( "github.com/AlexxIT/go2rtc/cmd/api" "github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mp4" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/rs/zerolog" "net/http" "strconv" @@ -105,10 +105,10 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { cons := &mp4.Consumer{ RemoteAddr: r.RemoteAddr, UserAgent: r.UserAgent(), - Medias: streamer.ParseQuery(r.URL.Query()), + Medias: core.ParseQuery(r.URL.Query()), } - cons.Listen(func(msg interface{}) { + cons.Listen(func(msg any) { if data, ok := msg.([]byte); ok { if _, err := w.Write(data); err != nil && exit != nil { exit <- err diff --git a/cmd/mp4/ws.go b/cmd/mp4/ws.go index afb6b8a3..35a94cb3 100644 --- a/cmd/mp4/ws.go +++ b/cmd/mp4/ws.go @@ -4,8 +4,8 @@ import ( "errors" "github.com/AlexxIT/go2rtc/cmd/api" "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mp4" - "github.com/AlexxIT/go2rtc/pkg/streamer" "strings" ) @@ -94,40 +94,40 @@ func handlerWSMP4(tr *api.Transport, msg *api.Message) error { return nil } -func parseMedias(codecs string, parseAudio bool) (medias []*streamer.Media) { - var videos []*streamer.Codec - var audios []*streamer.Codec +func parseMedias(codecs string, parseAudio bool) (medias []*core.Media) { + var videos []*core.Codec + var audios []*core.Codec for _, name := range strings.Split(codecs, ",") { switch name { case mp4.MimeH264: - codec := &streamer.Codec{Name: streamer.CodecH264} + codec := &core.Codec{Name: core.CodecH264} videos = append(videos, codec) case mp4.MimeH265: - codec := &streamer.Codec{Name: streamer.CodecH265} + codec := &core.Codec{Name: core.CodecH265} videos = append(videos, codec) case mp4.MimeAAC: - codec := &streamer.Codec{Name: streamer.CodecAAC} + codec := &core.Codec{Name: core.CodecAAC} audios = append(audios, codec) case mp4.MimeOpus: - codec := &streamer.Codec{Name: streamer.CodecOpus} + codec := &core.Codec{Name: core.CodecOpus} audios = append(audios, codec) } } if videos != nil { - media := &streamer.Media{ - Kind: streamer.KindVideo, - Direction: streamer.DirectionRecvonly, + media := &core.Media{ + Kind: core.KindVideo, + Direction: core.DirectionSendonly, Codecs: videos, } medias = append(medias, media) } if audios != nil && parseAudio { - media := &streamer.Media{ - Kind: streamer.KindAudio, - Direction: streamer.DirectionRecvonly, + media := &core.Media{ + Kind: core.KindAudio, + Direction: core.DirectionSendonly, Codecs: audios, } medias = append(medias, media) diff --git a/cmd/rtmp/rtmp.go b/cmd/rtmp/rtmp.go index 7be03d8a..e3042981 100644 --- a/cmd/rtmp/rtmp.go +++ b/cmd/rtmp/rtmp.go @@ -3,8 +3,8 @@ package rtmp import ( "github.com/AlexxIT/go2rtc/cmd/api" "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/rtmp" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/rs/zerolog/log" "io" "net/http" @@ -16,7 +16,7 @@ func Init() { api.HandleFunc("api/stream.flv", apiHandle) } -func streamsHandle(url string) (streamer.Producer, error) { +func streamsHandle(url string) (core.Producer, error) { conn := rtmp.NewClient(url) if err := conn.Dial(); err != nil { return nil, err diff --git a/cmd/rtsp/rtsp.go b/cmd/rtsp/rtsp.go index 86041677..e1c78bd6 100644 --- a/cmd/rtsp/rtsp.go +++ b/cmd/rtsp/rtsp.go @@ -3,9 +3,9 @@ package rtsp import ( "github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mp4" "github.com/AlexxIT/go2rtc/pkg/rtsp" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog" "net" @@ -86,9 +86,9 @@ var Port string var log zerolog.Logger var handlers []Handler -var defaultMedias []*streamer.Media +var defaultMedias []*core.Media -func rtspHandler(url string) (streamer.Producer, error) { +func rtspHandler(url string) (core.Producer, error) { backchannel := true if i := strings.IndexByte(url, '#'); i > 0 { @@ -98,11 +98,7 @@ func rtspHandler(url string) (streamer.Producer, error) { url = url[:i] } - conn, err := rtsp.NewClient(url) - if err != nil { - return nil, err - } - + conn := rtsp.NewClient(url) conn.UserAgent = app.UserAgent if log.Trace().Enabled() { @@ -118,12 +114,12 @@ func rtspHandler(url string) (streamer.Producer, error) { }) } - if err = conn.Dial(); err != nil { + if err := conn.Dial(); err != nil { return nil, err } conn.Backchannel = backchannel - if err = conn.Describe(); err != nil { + if err := conn.Describe(); err != nil { if !backchannel { return nil, err } @@ -211,9 +207,6 @@ func tcpHandler(conn *rtsp.Conn) { closer = func() { stream.RemoveProducer(conn) } - - case streamer.StatePlaying: - log.Debug().Str("stream", name).Msg("[rtsp] start") } }) diff --git a/cmd/streams/consumer.go b/cmd/streams/consumer.go deleted file mode 100644 index 9a855af2..00000000 --- a/cmd/streams/consumer.go +++ /dev/null @@ -1,15 +0,0 @@ -package streams - -import ( - "encoding/json" - "github.com/AlexxIT/go2rtc/pkg/streamer" -) - -type Consumer struct { - element streamer.Consumer - tracks []*streamer.Track -} - -func (c *Consumer) MarshalJSON() ([]byte, error) { - return json.Marshal(c.element) -} diff --git a/cmd/streams/handlers.go b/cmd/streams/handlers.go index 5a5fbfdf..62c9129f 100644 --- a/cmd/streams/handlers.go +++ b/cmd/streams/handlers.go @@ -2,12 +2,12 @@ package streams import ( "fmt" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "strings" "sync" ) -type Handler func(url string) (streamer.Producer, error) +type Handler func(url string) (core.Producer, error) var handlers = map[string]Handler{} var handlersMu sync.Mutex @@ -32,7 +32,7 @@ func HasProducer(url string) bool { return getHandler(url) != nil } -func GetProducer(url string) (streamer.Producer, error) { +func GetProducer(url string) (core.Producer, error) { handler := getHandler(url) if handler == nil { return nil, fmt.Errorf("unsupported scheme: %s", url) diff --git a/cmd/streams/play.go b/cmd/streams/play.go index 5f34d73e..748130f4 100644 --- a/cmd/streams/play.go +++ b/cmd/streams/play.go @@ -2,14 +2,14 @@ package streams import ( "errors" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" ) func (s *Stream) Play(source string) error { s.mu.Lock() for _, producer := range s.producers { - if producer.state == stateInternal && producer.element != nil { - _ = producer.element.Stop() + if producer.state == stateInternal && producer.conn != nil { + _ = producer.conn.Stop() } } s.mu.Unlock() @@ -18,14 +18,14 @@ func (s *Stream) Play(source string) error { return nil } - var src streamer.Producer + var src core.Producer for _, producer := range s.producers { - if producer.element == nil { + if producer.conn == nil { continue } - cons, ok := producer.element.(streamer.Consumer) + cons, ok := producer.conn.(core.Consumer) if !ok { continue } @@ -59,7 +59,7 @@ func (s *Stream) Play(source string) error { } // check if client support consumer interface - cons, ok := dst.(streamer.Consumer) + cons, ok := dst.(core.Consumer) if !ok { _ = dst.Stop() continue @@ -98,50 +98,49 @@ func (s *Stream) Play(source string) error { return errors.New("can't find consumer") } -func (s *Stream) AddInternalProducer(prod streamer.Producer) { - producer := &Producer{element: prod, state: stateInternal} +func (s *Stream) AddInternalProducer(conn core.Producer) { + producer := &Producer{conn: conn, state: stateInternal} s.mu.Lock() s.producers = append(s.producers, producer) s.mu.Unlock() } -func (s *Stream) AddInternalConsumer(cons streamer.Consumer) { - consumer := &Consumer{element: cons} +func (s *Stream) AddInternalConsumer(conn core.Consumer) { s.mu.Lock() - s.consumers = append(s.consumers, consumer) + s.consumers = append(s.consumers, conn) s.mu.Unlock() } -func (s *Stream) RemoveInternalConsumer(cons streamer.Consumer) { +func (s *Stream) RemoveInternalConsumer(conn core.Consumer) { s.mu.Lock() for i, consumer := range s.consumers { - if consumer.element == cons { - s.removeConsumer(i) + if consumer == conn { + s.consumers = append(s.consumers[:i], s.consumers[i+1:]...) break } } s.mu.Unlock() } -func matchMedia(prod streamer.Producer, cons streamer.Consumer) bool { +func matchMedia(prod core.Producer, cons core.Consumer) bool { for _, consMedia := range cons.GetMedias() { for _, prodMedia := range prod.GetMedias() { - // codec negotiation - prodCodec := prodMedia.MatchMedia(consMedia) + if prodMedia.Direction != core.DirectionRecvonly { + continue + } + + prodCodec, consCodec := prodMedia.MatchMedia(consMedia) if prodCodec == nil { continue } - // setup producer track - prodTrack := prod.GetTrack(prodMedia, prodCodec) - if prodTrack == nil { - return false + track, err := prod.GetTrack(prodMedia, prodCodec) + if err != nil { + continue } - // setup consumer track - consTrack := cons.AddTrack(consMedia, prodTrack) - if consTrack == nil { - return false + if err = cons.AddTrack(consMedia, consCodec, track); err != nil { + continue } return true diff --git a/cmd/streams/producer.go b/cmd/streams/producer.go index bd8a8ebc..3c86961e 100644 --- a/cmd/streams/producer.go +++ b/cmd/streams/producer.go @@ -2,7 +2,8 @@ package streams import ( "encoding/json" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "errors" + "github.com/AlexxIT/go2rtc/pkg/core" "strings" "sync" "time" @@ -20,20 +21,95 @@ const ( ) type Producer struct { - streamer.Element + core.Listener url string template string - element streamer.Producer + conn core.Producer + receivers []*core.Receiver + senders []*core.Receiver + lastErr error - tracks []*streamer.Track state state mu sync.Mutex workerID int } +func (p *Producer) Dial() error { + p.mu.Lock() + defer p.mu.Unlock() + + if p.state == stateNone { + conn, err := GetProducer(p.url) + if err != nil { + return err + } + + p.conn = conn + p.state = stateMedias + } + + return nil +} + +func (p *Producer) GetMedias() []*core.Media { + p.mu.Lock() + defer p.mu.Unlock() + + return p.conn.GetMedias() +} + +func (p *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + p.mu.Lock() + defer p.mu.Unlock() + + if p.state == stateNone { + return nil, errors.New("get track from none state") + } + + for _, track := range p.receivers { + if track.Codec == codec { + return track, nil + } + } + + track, err := p.conn.GetTrack(media, codec) + if err != nil { + return nil, err + } + + p.receivers = append(p.receivers, track) + + if p.state == stateMedias { + p.state = stateTracks + } + + return track, nil +} + +func (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { + p.mu.Lock() + defer p.mu.Unlock() + + if p.state == stateNone { + return errors.New("add track from none state") + } + + if err := p.conn.(core.Consumer).AddTrack(media, codec, track); err != nil { + return err + } + + p.senders = append(p.senders, track) + + if p.state == stateMedias { + p.state = stateTracks + } + + return nil +} + func (p *Producer) SetSource(s string) { if p.template == "" { p.template = p.url @@ -41,64 +117,12 @@ func (p *Producer) SetSource(s string) { p.url = strings.Replace(p.template, "{input}", s, 1) } -func (p *Producer) GetMedias() []*streamer.Media { - p.mu.Lock() - defer p.mu.Unlock() - - if p.state == stateNone { - log.Debug().Msgf("[streams] probe producer url=%s", p.url) - - p.element, p.lastErr = GetProducer(p.url) - if p.lastErr != nil || p.element == nil { - log.Error().Err(p.lastErr).Str("url", p.url).Caller().Send() - return nil - } - - p.state = stateMedias - } - - // if element in reconnect state - if p.element == nil { - return nil - } - - return p.element.GetMedias() -} - -func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track { - p.mu.Lock() - defer p.mu.Unlock() - - if p.state == stateNone { - return nil - } - - for _, track := range p.tracks { - if track.Codec == codec { - return track - } - } - - track := p.element.GetTrack(media, codec) - if track == nil { - return nil - } - - p.tracks = append(p.tracks, track) - - if p.state == stateMedias { - p.state = stateTracks - } - - return track -} - func (p *Producer) MarshalJSON() ([]byte, error) { - if p.element != nil { - return json.Marshal(p.element) + if p.conn != nil { + return json.Marshal(p.conn) } - info := streamer.Info{URL: p.url} + info := core.Info{URL: p.url} return json.Marshal(info) } @@ -117,11 +141,11 @@ func (p *Producer) start() { p.state = stateStart p.workerID++ - go p.worker(p.element, p.workerID) + go p.worker(p.conn, p.workerID) } -func (p *Producer) worker(element streamer.Producer, workerID int) { - if err := element.Start(); err != nil { +func (p *Producer) worker(conn core.Producer, workerID int) { + if err := conn.Start(); err != nil { p.mu.Lock() closed := p.workerID != workerID p.mu.Unlock() @@ -147,9 +171,8 @@ func (p *Producer) reconnect(workerID int) { log.Debug().Msgf("[streams] reconnect to url=%s", p.url) - p.element, p.lastErr = GetProducer(p.url) - if p.lastErr != nil || p.element == nil { - log.Debug().Msgf("[streams] producer=%s", p.lastErr) + if err := p.Dial(); err != nil { + log.Debug().Msgf("[streams] producer=%s", err) // TODO: dynamic timeout time.AfterFunc(30*time.Second, func() { p.reconnect(workerID) @@ -157,27 +180,37 @@ func (p *Producer) reconnect(workerID int) { return } - medias := p.element.GetMedias() + for _, media := range p.conn.GetMedias() { + switch media.Direction { + case core.DirectionRecvonly: + for _, receiver := range p.receivers { + codec := media.MatchCodec(receiver.Codec) + if codec == nil { + continue + } - // convert all old producer tracks to new tracks - for i, oldTrack := range p.tracks { - // match new element medias with old track codec - for _, media := range medias { - codec := media.MatchCodec(oldTrack.Codec) - if codec == nil { - continue + track, err := p.conn.GetTrack(media, codec) + if err != nil { + continue + } + + receiver.Replace(track) + break } - // move sink from old track to new track - newTrack := p.element.GetTrack(media, codec) - newTrack.GetSink(oldTrack) - p.tracks[i] = newTrack + case core.DirectionSendonly: + for _, sender := range p.senders { + codec := media.MatchCodec(sender.Codec) + if codec == nil { + continue + } - break + _ = p.conn.(core.Consumer).AddTrack(media, codec, sender) + } } } - go p.worker(p.element, workerID) + go p.worker(p.conn, workerID) } func (p *Producer) stop() { @@ -197,11 +230,12 @@ func (p *Producer) stop() { log.Debug().Msgf("[streams] stop producer url=%s", p.url) - if p.element != nil { - _ = p.element.Stop() - p.element = nil + if p.conn != nil { + _ = p.conn.Stop() + p.conn = nil } p.state = stateNone - p.tracks = nil + p.receivers = nil + p.senders = nil } diff --git a/cmd/streams/stream.go b/cmd/streams/stream.go index f6da32b8..0e2d5d43 100644 --- a/cmd/streams/stream.go +++ b/cmd/streams/stream.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" "fmt" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "strings" "sync" "sync/atomic" @@ -12,12 +12,12 @@ import ( type Stream struct { producers []*Producer - consumers []*Consumer + consumers []core.Consumer mu sync.Mutex requests int32 } -func NewStream(source interface{}) *Stream { +func NewStream(source any) *Stream { switch source := source.(type) { case string: s := new(Stream) @@ -38,7 +38,7 @@ func NewStream(source interface{}) *Stream { case nil: return new(Stream) default: - panic("wrong source type") + panic(core.Caller()) } } @@ -48,57 +48,71 @@ func (s *Stream) SetSource(source string) { } } -func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) { +func (s *Stream) AddConsumer(cons core.Consumer) (err error) { // support for multiple simultaneous requests from different consumers atomic.AddInt32(&s.requests, 1) - ic := len(s.consumers) - - consumer := &Consumer{element: cons} var producers []*Producer // matched producers for consumer var codecs string // Step 1. Get consumer medias - for icc, consMedia := range cons.GetMedias() { - log.Trace().Stringer("media", consMedia). - Msgf("[streams] consumer=%d candidate=%d", ic, icc) + for _, consMedia := range cons.GetMedias() { 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 candidate=%d", ip, ipc) + for _, prod := range s.producers { + if err = prod.Dial(); err != nil { + continue + } + // Step 2. Get producer medias (not tracks yet) + for _, prodMedia := range prod.GetMedias() { collectCodecs(prodMedia, &codecs) // 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) + prodCodec, consCodec := prodMedia.MatchMedia(consMedia) + if prodCodec == nil { + continue + } - // Step 4. Get producer track - prodTrack := prod.GetTrack(prodMedia, prodCodec) - if prodTrack == nil { - log.Warn().Str("url", prod.url).Msg("[streams] can't get track") + var track *core.Receiver + + switch prodMedia.Direction { + case core.DirectionRecvonly: + // Step 4. Get recvonly track from producer + if track, err = prod.GetTrack(prodMedia, prodCodec); err != nil { + log.Info().Err(err).Msg("[streams] can't get track") + continue + } + // Step 5. Add track to consumer + if err = cons.AddTrack(consMedia, consCodec, track); err != nil { + log.Info().Err(err).Msg("[streams] can't add track") continue } - // Step 5. Add track to consumer and get new track - consTrack := consumer.element.AddTrack(consMedia, prodTrack) - - consumer.tracks = append(consumer.tracks, consTrack) - producers = append(producers, prod) - if !consMedia.MatchAll() { - break producers + case core.DirectionSendonly: + // Step 4. Get recvonly track from consumer (backchannel) + if track, err = cons.(core.Producer).GetTrack(consMedia, consCodec); err != nil { + log.Info().Err(err).Msg("[streams] can't get track") + continue } + // Step 5. Add track to producer + if err = prod.AddTrack(prodMedia, prodCodec, track); err != nil { + log.Info().Err(err).Msg("[streams] can't add track") + continue + } + } + + producers = append(producers, prod) + + if !consMedia.MatchAll() { + break producers } } } } + // stop producers if they don't have readers if atomic.AddInt32(&s.requests, -1) == 0 { s.stopProducers() } @@ -118,7 +132,7 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) { } s.mu.Lock() - s.consumers = append(s.consumers, consumer) + s.consumers = append(s.consumers, cons) s.mu.Unlock() // there may be duplicates, but that's not a problem @@ -129,16 +143,13 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) { return nil } -func (s *Stream) RemoveConsumer(cons streamer.Consumer) { +func (s *Stream) RemoveConsumer(cons core.Consumer) { + _ = cons.Stop() + s.mu.Lock() 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) + if consumer == cons { + s.consumers = append(s.consumers[:i], s.consumers[i+1:]...) break } } @@ -147,18 +158,18 @@ func (s *Stream) RemoveConsumer(cons streamer.Consumer) { s.stopProducers() } -func (s *Stream) AddProducer(prod streamer.Producer) { - producer := &Producer{element: prod, state: stateExternal} +func (s *Stream) AddProducer(prod core.Producer) { + producer := &Producer{conn: prod, state: stateExternal} s.mu.Lock() s.producers = append(s.producers, producer) s.mu.Unlock() } -func (s *Stream) RemoveProducer(prod streamer.Producer) { +func (s *Stream) RemoveProducer(prod core.Producer) { s.mu.Lock() for i, producer := range s.producers { - if producer.element == prod { - s.removeProducer(i) + if producer.conn == prod { + s.producers = append(s.producers[:i], s.producers[i+1:]...) break } } @@ -169,8 +180,8 @@ func (s *Stream) stopProducers() { s.mu.Lock() producers: for _, producer := range s.producers { - for _, track := range producer.tracks { - if track.HasSink() { + for _, track := range producer.receivers { + if len(track.Senders()) > 0 { continue producers } } @@ -179,20 +190,6 @@ producers: s.mu.Unlock() } -//func (s *Stream) Active() bool { -// if len(s.consumers) > 0 { -// return true -// } -// -// for _, prod := range s.producers { -// if prod.element != nil { -// return true -// } -// } -// -// return false -//} - func (s *Stream) MarshalJSON() ([]byte, error) { if !s.mu.TryLock() { log.Warn().Msgf("[streams] json locked") @@ -200,8 +197,8 @@ func (s *Stream) MarshalJSON() ([]byte, error) { } var info struct { - Producers []*Producer `json:"producers"` - Consumers []*Consumer `json:"consumers"` + Producers []*Producer `json:"producers"` + Consumers []core.Consumer `json:"consumers"` } info.Producers = s.producers info.Consumers = s.consumers @@ -211,40 +208,14 @@ func (s *Stream) MarshalJSON() ([]byte, error) { return json.Marshal(info) } -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:]...) - } -} - -func collectCodecs(media *streamer.Media, codecs *string) { - if media.Direction == streamer.DirectionRecvonly { +func collectCodecs(media *core.Media, codecs *string) { + if media.Direction == core.DirectionRecvonly { return } for _, codec := range media.Codecs { name := codec.Name - if name == streamer.CodecAAC { + if name == core.CodecAAC { name = "AAC" } if strings.Contains(*codecs, name) { diff --git a/cmd/tapo/tapo.go b/cmd/tapo/tapo.go index 527cc898..07c278a5 100644 --- a/cmd/tapo/tapo.go +++ b/cmd/tapo/tapo.go @@ -2,7 +2,7 @@ package tapo import ( "github.com/AlexxIT/go2rtc/cmd/streams" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/tapo" ) @@ -10,7 +10,7 @@ func Init() { streams.HandleFunc("tapo", handle) } -func handle(url string) (streamer.Producer, error) { +func handle(url string) (core.Producer, error) { conn := tapo.NewClient(url) if err := conn.Dial(); err != nil { return nil, err diff --git a/cmd/webrtc/client.go b/cmd/webrtc/client.go index 49726789..bad44da8 100644 --- a/cmd/webrtc/client.go +++ b/cmd/webrtc/client.go @@ -4,7 +4,6 @@ import ( "errors" "github.com/AlexxIT/go2rtc/cmd/api" "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/webrtc" "github.com/gorilla/websocket" pion "github.com/pion/webrtc/v3" @@ -14,7 +13,7 @@ import ( "time" ) -func streamsHandler(url string) (streamer.Producer, error) { +func streamsHandler(url string) (core.Producer, error) { url = url[7:] if i := strings.Index(url, "://"); i > 0 { switch url[:i] { @@ -29,7 +28,7 @@ func streamsHandler(url string) (streamer.Producer, error) { // asyncClient can connect only to go2rtc server // ex: ws://localhost:1984/api/ws?src=camera1 -func asyncClient(url string) (streamer.Producer, error) { +func asyncClient(url string) (core.Producer, error) { // 1. Connect to signalign server ws, _, err := websocket.DefaultDialer.Dial(url, nil) if err != nil { @@ -52,7 +51,7 @@ func asyncClient(url string) (streamer.Producer, error) { prod := webrtc.NewConn(pc) prod.Desc = "WebRTC/WebSocket async" - prod.Mode = streamer.ModeActiveProducer + prod.Mode = core.ModeActiveProducer prod.Listen(func(msg any) { switch msg := msg.(type) { case pion.PeerConnectionState: @@ -67,10 +66,10 @@ func asyncClient(url string) (streamer.Producer, error) { } }) - medias := []*streamer.Media{ - {Kind: streamer.KindVideo, Direction: streamer.DirectionRecvonly}, - {Kind: streamer.KindAudio, Direction: streamer.DirectionRecvonly}, - {Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly}, + medias := []*core.Media{ + {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, + {Kind: core.KindAudio, Direction: core.DirectionRecvonly}, + {Kind: core.KindAudio, Direction: core.DirectionSendonly}, } // 3. Create offer @@ -129,7 +128,7 @@ func asyncClient(url string) (streamer.Producer, error) { // syncClient - support WebRTC-HTTP Egress Protocol (WHEP) // ex: http://localhost:1984/api/webrtc?src=camera1 -func syncClient(url string) (streamer.Producer, error) { +func syncClient(url string) (core.Producer, error) { // 2. Create PeerConnection pc, err := PeerConnection(true) if err != nil { @@ -139,11 +138,11 @@ func syncClient(url string) (streamer.Producer, error) { prod := webrtc.NewConn(pc) prod.Desc = "WebRTC/WHEP sync" - prod.Mode = streamer.ModeActiveProducer + prod.Mode = core.ModeActiveProducer - medias := []*streamer.Media{ - {Kind: streamer.KindVideo, Direction: streamer.DirectionRecvonly}, - {Kind: streamer.KindAudio, Direction: streamer.DirectionRecvonly}, + medias := []*core.Media{ + {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, + {Kind: core.KindAudio, Direction: core.DirectionRecvonly}, } // 3. Create offer diff --git a/cmd/webrtc/init.go b/cmd/webrtc/init.go index 43f81e76..73d73503 100644 --- a/cmd/webrtc/init.go +++ b/cmd/webrtc/init.go @@ -6,7 +6,6 @@ import ( "github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/webrtc" pion "github.com/pion/webrtc/v3" "github.com/rs/zerolog" @@ -87,16 +86,16 @@ var PeerConnection func(active bool) (*pion.PeerConnection, error) func asyncHandler(tr *api.Transport, msg *api.Message) error { var stream *streams.Stream - var mode streamer.Mode + var mode core.Mode query := tr.Request.URL.Query() if name := query.Get("src"); name != "" { stream = streams.GetOrNew(name) - mode = streamer.ModePassiveConsumer + mode = core.ModePassiveConsumer log.Debug().Str("src", name).Msg("[webrtc] new consumer") } else if name = query.Get("dst"); name != "" { stream = streams.Get(name) - mode = streamer.ModePassiveProducer + mode = core.ModePassiveProducer log.Debug().Str("src", name).Msg("[webrtc] new producer") } @@ -124,9 +123,9 @@ func asyncHandler(tr *api.Transport, msg *api.Message) error { return } switch mode { - case streamer.ModePassiveConsumer: + case core.ModePassiveConsumer: stream.RemoveConsumer(conn) - case streamer.ModePassiveProducer: + case core.ModePassiveProducer: stream.RemoveProducer(conn) } @@ -158,14 +157,14 @@ func asyncHandler(tr *api.Transport, msg *api.Message) error { } switch mode { - case streamer.ModePassiveConsumer: + case core.ModePassiveConsumer: // 2. AddConsumer, so we get new tracks if err = stream.AddConsumer(conn); err != nil { log.Debug().Err(err).Msg("[webrtc] add consumer") _ = conn.Close() return err } - case streamer.ModePassiveProducer: + case core.ModePassiveProducer: stream.AddProducer(conn) } @@ -202,9 +201,9 @@ func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer // create new webrtc instance conn := webrtc.NewConn(pc) conn.Desc = desc - conn.Mode = streamer.ModePassiveConsumer + conn.Mode = core.ModePassiveConsumer conn.UserAgent = userAgent - conn.Listen(func(msg interface{}) { + conn.Listen(func(msg any) { switch msg := msg.(type) { case pion.PeerConnectionState: if msg == pion.PeerConnectionStateClosed { diff --git a/cmd/webrtc/server.go b/cmd/webrtc/server.go index 10e9394f..f66cab00 100644 --- a/cmd/webrtc/server.go +++ b/cmd/webrtc/server.go @@ -3,7 +3,7 @@ package webrtc import ( "encoding/json" "github.com/AlexxIT/go2rtc/cmd/streams" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/webrtc" pion "github.com/pion/webrtc/v3" "io" @@ -161,7 +161,7 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) { // create new webrtc instance prod := webrtc.NewConn(pc) prod.Desc = "WebRTC/WHIP sync" - prod.Mode = streamer.ModePassiveProducer + prod.Mode = core.ModePassiveProducer prod.UserAgent = r.UserAgent() if err = prod.SetOffer(string(offer)); err != nil { diff --git a/cmd/webtorrent/init.go b/cmd/webtorrent/init.go index f8b905a8..13c7dfd0 100644 --- a/cmd/webtorrent/init.go +++ b/cmd/webtorrent/init.go @@ -8,7 +8,6 @@ import ( "github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/webrtc" "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/webtorrent" "github.com/rs/zerolog" "net/http" @@ -142,7 +141,7 @@ func apiHandle(w http.ResponseWriter, r *http.Request) { } } -func streamHandle(rawURL string) (streamer.Producer, error) { +func streamHandle(rawURL string) (core.Producer, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err diff --git a/pkg/aac/rtp.go b/pkg/aac/rtp.go index 74565ea7..ba3dfce9 100644 --- a/pkg/aac/rtp.go +++ b/pkg/aac/rtp.go @@ -2,62 +2,59 @@ package aac import ( "encoding/binary" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/rtp" ) const RTPPacketVersionAAC = 0 -func RTPDepay(track *streamer.Track) streamer.WrapperFunc { - return func(push streamer.WriterFunc) streamer.WriterFunc { - return func(packet *rtp.Packet) error { - // support ONLY 2 bytes header size! - // streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408 - headersSize := binary.BigEndian.Uint16(packet.Payload) >> 3 +func RTPDepay(handler core.HandlerFunc) core.HandlerFunc { + return func(packet *rtp.Packet) { + // support ONLY 2 bytes header size! + // streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408 + headersSize := binary.BigEndian.Uint16(packet.Payload) >> 3 - //log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker) + //log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker) - data := packet.Payload[2+headersSize:] - if IsADTS(data) { - data = data[7:] - } - - clone := *packet - clone.Version = RTPPacketVersionAAC - clone.Payload = data - return push(&clone) + data := packet.Payload[2+headersSize:] + if IsADTS(data) { + data = data[7:] } + + clone := *packet + clone.Version = RTPPacketVersionAAC + clone.Payload = data + handler(&clone) } } -func RTPPay(mtu uint16) streamer.WrapperFunc { +func RTPPay(handler core.HandlerFunc) core.HandlerFunc { sequencer := rtp.NewRandomSequencer() - return func(push streamer.WriterFunc) streamer.WriterFunc { - return func(packet *rtp.Packet) error { - if packet.Version != RTPPacketVersionAAC { - return push(packet) - } - - // support ONLY one unit in payload - size := uint16(len(packet.Payload)) - // 2 bytes header size + 2 bytes first payload size - payload := make([]byte, 2+2+size) - payload[1] = 16 // header size in bits - binary.BigEndian.PutUint16(payload[2:], size<<3) - copy(payload[4:], packet.Payload) - - clone := rtp.Packet{ - Header: rtp.Header{ - Version: 2, - Marker: true, - SequenceNumber: sequencer.NextSequenceNumber(), - Timestamp: packet.Timestamp, - }, - Payload: payload, - } - return push(&clone) + return func(packet *rtp.Packet) { + if packet.Version != RTPPacketVersionAAC { + handler(packet) + return } + + // support ONLY one unit in payload + size := uint16(len(packet.Payload)) + // 2 bytes header size + 2 bytes first payload size + payload := make([]byte, 2+2+size) + payload[1] = 16 // header size in bits + binary.BigEndian.PutUint16(payload[2:], size<<3) + copy(payload[4:], packet.Payload) + + clone := rtp.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: true, + SequenceNumber: sequencer.NextSequenceNumber(), + Timestamp: packet.Timestamp, + }, + Payload: payload, + } + handler(&clone) } } diff --git a/pkg/core/codec.go b/pkg/core/codec.go new file mode 100644 index 00000000..67c6d2cb --- /dev/null +++ b/pkg/core/codec.go @@ -0,0 +1,142 @@ +package core + +import ( + "encoding/base64" + "fmt" + "github.com/pion/sdp/v3" + "strconv" + "strings" + "unicode" +) + +type Codec struct { + Name string // H264, PCMU, PCMA, opus... + ClockRate uint32 // 90000, 8000, 16000... + Channels uint16 // 0, 1, 2 + FmtpLine string + PayloadType uint8 +} + +func (c *Codec) String() string { + s := fmt.Sprintf("%d %s", c.PayloadType, c.Name) + if c.ClockRate != 0 && c.ClockRate != 90000 { + s = fmt.Sprintf("%s/%d", s, c.ClockRate) + } + if c.Channels > 0 { + s = fmt.Sprintf("%s/%d", s, c.Channels) + } + return s +} + +func (c *Codec) Text() string { + switch c.Name { + case CodecH264: + if profile := DecodeH264(c.FmtpLine); profile != "" { + return "H.264 " + profile + } + return c.Name + } + + s := c.Name + if c.ClockRate != 0 && c.ClockRate != 90000 { + s += "/" + strconv.Itoa(int(c.ClockRate)) + } + if c.Channels > 0 { + s += "/" + strconv.Itoa(int(c.Channels)) + } + return s +} + +func (c *Codec) IsRTP() bool { + return c.PayloadType != PayloadTypeRAW +} + +func (c *Codec) Clone() *Codec { + clone := *c + return &clone +} + +func (c *Codec) Match(remote *Codec) bool { + switch remote.Name { + case CodecAll, CodecAny: + return true + } + + return c.Name == remote.Name && + (c.ClockRate == remote.ClockRate || remote.ClockRate == 0) && + (c.Channels == remote.Channels || remote.Channels == 0) +} + +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 == "" { + // https://en.wikipedia.org/wiki/RTP_payload_formats + switch payloadType { + case "0": + c.Name = CodecPCMU + c.ClockRate = 8000 + case "8": + c.Name = CodecPCMA + c.ClockRate = 8000 + case "14": + c.Name = CodecMP3 + c.ClockRate = 44100 + case "26": + c.Name = CodecJPEG + c.ClockRate = 90000 + default: + c.Name = payloadType + } + } + + return c +} + +func atoi(s string) (i int) { + i, _ = strconv.Atoi(s) + return +} + +func DecodeH264(fmtp string) string { + if ps := Between(fmtp, "sprop-parameter-sets=", ","); ps != "" { + if sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 { + var profile string + switch sps[1] { + case 0x42: + profile = "Baseline" + case 0x4D: + profile = "Main" + case 0x58: + profile = "Extended" + case 0x64: + profile = "High" + default: + profile = fmt.Sprintf("0x%02X", sps[1]) + } + + return fmt.Sprintf("%s %d.%d", profile, sps[3]/10, sps[3]%10) + } + } + return "" +} diff --git a/pkg/core/core.go b/pkg/core/core.go new file mode 100644 index 00000000..9e6fbb98 --- /dev/null +++ b/pkg/core/core.go @@ -0,0 +1,99 @@ +package core + +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" + CodecJPEG = "JPEG" // payloadType: 26 + + CodecPCMU = "PCMU" // payloadType: 0 + CodecPCMA = "PCMA" // payloadType: 8 + CodecAAC = "MPEG4-GENERIC" + CodecOpus = "OPUS" // payloadType: 111 + CodecG722 = "G722" + CodecMP3 = "MPA" // payload: 14, aka MPEG-1 Layer III + + CodecELD = "ELD" // AAC-ELD + + CodecAll = "ALL" + CodecAny = "ANY" +) + +const PayloadTypeRAW byte = 255 + +type Producer interface { + // GetMedias - return Media(s) with local Media.Direction: + // - recvonly for Producer Video/Audio + // - sendonly for Producer backchannel + GetMedias() []*Media + + // GetTrack - return Receiver, that can only produce rtp.Packet(s) + GetTrack(media *Media, codec *Codec) (*Receiver, error) + + Start() error + Stop() error +} + +type Consumer interface { + // GetMedias - return Media(s) with local Media.Direction: + // - sendonly for Consumer Video/Audio + // - recvonly for Consumer backchannel + GetMedias() []*Media + + AddTrack(media *Media, codec *Codec, track *Receiver) error + + Stop() error +} + +type Mode byte + +const ( + ModeActiveProducer Mode = iota + 1 // typical source (client) + ModePassiveConsumer + ModePassiveProducer + ModeActiveConsumer +) + +func (m Mode) String() string { + switch m { + case ModeActiveProducer: + return "active producer" + case ModePassiveConsumer: + return "passive consumer" + case ModePassiveProducer: + return "passive producer" + case ModeActiveConsumer: + return "active consumer" + } + return "unknown" +} + +type Info struct { + Type string `json:"type,omitempty"` + URL string `json:"url,omitempty"` + RemoteAddr string `json:"remote_addr,omitempty"` + UserAgent string `json:"user_agent,omitempty"` + Medias []*Media `json:"medias,omitempty"` + Receivers []*Receiver `json:"receivers,omitempty"` + Senders []*Sender `json:"senders,omitempty"` + Recv int `json:"recv,omitempty"` + Send int `json:"send,omitempty"` +} + +const ( + UnsupportedCodec = "unsupported codec" + WrongMediaDirection = "wrong media direction" +) diff --git a/pkg/core/helpers.go b/pkg/core/helpers.go index 2813e399..69dedb20 100644 --- a/pkg/core/helpers.go +++ b/pkg/core/helpers.go @@ -2,6 +2,10 @@ package core import ( cryptorand "crypto/rand" + "github.com/rs/zerolog/log" + "runtime" + "strconv" + "strings" ) const digits = "0123456789abcdefghijklmnopqrstuvwxyz" @@ -17,3 +21,35 @@ func RandString(size byte) string { } return string(b) } + +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 Assert(ok bool) { + if !ok { + _, file, line, _ := runtime.Caller(1) + panic(file + ":" + strconv.Itoa(line)) + } +} + +func Caller() string { + log.Error().Caller(0).Send() + _, file, line, _ := runtime.Caller(1) + return file + ":" + strconv.Itoa(line) +} diff --git a/pkg/core/listener.go b/pkg/core/listener.go new file mode 100644 index 00000000..75d9202a --- /dev/null +++ b/pkg/core/listener.go @@ -0,0 +1,18 @@ +package core + +type EventFunc func(msg any) + +// Listener base struct for all classes with support feedback +type Listener struct { + events []EventFunc +} + +func (l *Listener) Listen(f EventFunc) { + l.events = append(l.events, f) +} + +func (l *Listener) Fire(msg any) { + for _, f := range l.events { + f(msg) + } +} diff --git a/pkg/core/media.go b/pkg/core/media.go new file mode 100644 index 00000000..1e972350 --- /dev/null +++ b/pkg/core/media.go @@ -0,0 +1,191 @@ +package core + +import ( + "encoding/json" + "fmt" + "github.com/pion/sdp/v3" + "strings" +) + +// 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"` // sendonly, recvonly + Codecs []*Codec `json:"codecs,omitempty"` + + ID string `json:"id,omitempty"` // MID for WebRTC, Control for RTSP +} + +func (m *Media) String() string { + s := fmt.Sprintf("%s, %s", m.Kind, m.Direction) + for _, codec := range m.Codecs { + name := codec.Text() + + if strings.Contains(s, name) { + continue + } + + s += ", " + name + } + return s +} + +func (m *Media) MarshalJSON() ([]byte, error) { + return json.Marshal(m.String()) +} + +func (m *Media) Clone() *Media { + clone := *m + clone.Codecs = make([]*Codec, len(m.Codecs)) + for i, codec := range m.Codecs { + clone.Codecs[i] = codec.Clone() + } + return &clone +} + +func (m *Media) MatchMedia(remote *Media) (codec, remoteCodec *Codec) { + // check same kind and opposite dirrection + if m.Kind != remote.Kind || + m.Direction == DirectionSendonly && remote.Direction != DirectionRecvonly || + m.Direction == DirectionRecvonly && remote.Direction != DirectionSendonly { + return nil, nil + } + + for _, codec = range m.Codecs { + for _, remoteCodec = range remote.Codecs { + if codec.Match(remoteCodec) { + return + } + } + } + + return nil, nil +} + +func (m *Media) MatchCodec(remote *Codec) *Codec { + for _, codec := range m.Codecs { + if codec.Match(remote) { + return codec + } + } + return nil +} + +func (m *Media) MatchAll() bool { + for _, codec := range m.Codecs { + if codec.Name == CodecAll { + return true + } + } + return false +} + +func GetKind(name string) string { + switch name { + case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG: + return KindVideo + case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecELD: + return KindAudio + } + return "" +} + +func MarshalSDP(name string, medias []*Media) ([]byte, error) { + sd := &sdp.SessionDescription{ + Origin: sdp.Origin{ + Username: "-", SessionID: 1, SessionVersion: 1, + NetworkType: "IN", AddressType: "IP4", UnicastAddress: "0.0.0.0", + }, + SessionName: sdp.SessionName(name), + ConnectionInformation: &sdp.ConnectionInformation{ + NetworkType: "IN", AddressType: "IP4", Address: &sdp.Address{ + Address: "0.0.0.0", + }, + }, + TimeDescriptions: []sdp.TimeDescription{ + {Timing: sdp.Timing{}}, + }, + } + + for _, media := range medias { + if media.Codecs == nil { + continue + } + + codec := media.Codecs[0] + + name := codec.Name + if name == CodecELD { + name = CodecAAC + } + + md := &sdp.MediaDescription{ + MediaName: sdp.MediaName{ + Media: media.Kind, + Protos: []string{"RTP", "AVP"}, + }, + } + md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine) + + sd.MediaDescriptions = append(sd.MediaDescriptions, md) + } + + 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", "mid": + m.ID = attr.Value + } + } + + for _, format := range md.MediaName.Formats { + m.Codecs = append(m.Codecs, UnmarshalCodec(md, format)) + } + + return m +} + +func ParseQuery(query map[string][]string) (medias []*Media) { + // set media candidates from query list + for key, values := range query { + switch key { + case KindVideo, KindAudio: + for _, value := range values { + media := &Media{Kind: key, Direction: DirectionSendonly} + + for _, name := range strings.Split(value, ",") { + name = strings.ToUpper(name) + + // check aliases + switch name { + case "", "COPY": + name = CodecAny + case "MJPEG": + name = CodecJPEG + case "AAC": + name = CodecAAC + case "MP3": + name = CodecMP3 + } + + media.Codecs = append(media.Codecs, &Codec{Name: name}) + } + + medias = append(medias, media) + } + } + } + + return +} diff --git a/pkg/streamer/media_test.go b/pkg/core/media_test.go similarity index 98% rename from pkg/streamer/media_test.go rename to pkg/core/media_test.go index f283882d..caf064bd 100644 --- a/pkg/streamer/media_test.go +++ b/pkg/core/media_test.go @@ -1,4 +1,4 @@ -package streamer +package core import ( "fmt" diff --git a/pkg/core/probe.go b/pkg/core/probe.go new file mode 100644 index 00000000..2a9f515a --- /dev/null +++ b/pkg/core/probe.go @@ -0,0 +1,31 @@ +package core + +import "time" + +type Probe struct { + deadline time.Time + items map[any]struct{} +} + +func NewProbe(enable bool) *Probe { + if enable { + return &Probe{ + deadline: time.Now().Add(time.Second * 3), + items: map[any]struct{}{}, + } + } else { + return nil + } +} + +// Active return true if probe enabled and not finish +func (p *Probe) Active() bool { + return len(p.items) < 2 && time.Now().Before(p.deadline) +} + +// Append safe to run if Probe is nil +func (p *Probe) Append(v any) { + if p != nil { + p.items[v] = struct{}{} + } +} diff --git a/pkg/core/track.go b/pkg/core/track.go new file mode 100644 index 00000000..82c8cc8a --- /dev/null +++ b/pkg/core/track.go @@ -0,0 +1,188 @@ +package core + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/pion/rtp" + "strconv" + "sync" +) + +var ErrCantGetTrack = errors.New("can't get track") + +type Receiver struct { + Codec *Codec + Media *Media + + ID byte // Channel for RTSP, PayloadType for MPEG-TS + + senders map[*Sender]chan *rtp.Packet + mu sync.Mutex + bytes int +} + +func NewReceiver(media *Media, codec *Codec) *Receiver { + Assert(codec != nil) + return &Receiver{Codec: codec, Media: media} +} + +// WriteRTP - fast and non blocking write to all readers buffers +func (t *Receiver) WriteRTP(packet *rtp.Packet) { + t.mu.Lock() + t.bytes += len(packet.Payload) + for sender, buffer := range t.senders { + if len(buffer) < cap(buffer) { + buffer <- packet + } else { + sender.overflow++ + } + } + t.mu.Unlock() +} + +func (t *Receiver) Senders() (senders []*Sender) { + t.mu.Lock() + for sender := range t.senders { + senders = append(senders, sender) + } + t.mu.Unlock() + return +} + +func (t *Receiver) Close() { + t.mu.Lock() + // close all sender channel buffers and erase senders list + for _, buffer := range t.senders { + close(buffer) + } + t.senders = nil + t.mu.Unlock() +} + +func (t *Receiver) Replace(target *Receiver) { + // move this receiver senders to new receiver + t.mu.Lock() + senders := t.senders + t.mu.Unlock() + + target.mu.Lock() + target.senders = senders + target.mu.Unlock() +} + +func (t *Receiver) String() string { + s := t.Codec.String() + ", bytes=" + strconv.Itoa(t.bytes) + if t.mu.TryLock() { + s += fmt.Sprintf(", senders=%d", len(t.senders)) + t.mu.Unlock() + } else { + s += fmt.Sprintf(", senders=?") + } + return s +} + +func (t *Receiver) MarshalJSON() ([]byte, error) { + return json.Marshal(t.String()) +} + +type Sender struct { + Codec *Codec + Media *Media + + Handler HandlerFunc + + receivers []*Receiver + mu sync.Mutex + bytes int + + overflow int +} + +func NewSender(media *Media, codec *Codec) *Sender { + return &Sender{Codec: codec, Media: media} +} + +// HandlerFunc like http.HandlerFunc +type HandlerFunc func(packet *rtp.Packet) + +func (s *Sender) HandleRTP(track *Receiver) { + bufferSize := 100 + + if GetKind(track.Codec.Name) == KindVideo { + if track.Codec.IsRTP() { + // H.264 2560x1440 4096kbs can have 700+ packets between 25 frames + // H.265 5120x1440 can have 700+ packets between two keyframes + bufferSize = 1000 + } else { + bufferSize = 50 + } + } + + buffer := make(chan *rtp.Packet, bufferSize) + + track.mu.Lock() + if track.senders == nil { + track.senders = map[*Sender]chan *rtp.Packet{} + } + track.senders[s] = buffer + track.mu.Unlock() + + s.mu.Lock() + s.receivers = append(s.receivers, track) + s.mu.Unlock() + + go func() { + // read packets from buffer channel until it will be closed + for packet := range buffer { + s.bytes += len(packet.Payload) + s.Handler(packet) + } + + // remove current receiver from list + // it can only happen when receiver close buffer channel + s.mu.Lock() + for i, receiver := range s.receivers { + if receiver == track { + s.receivers = append(s.receivers[:i], s.receivers[i+1:]...) + break + } + } + s.mu.Unlock() + }() +} + +func (s *Sender) Close() { + s.mu.Lock() + // remove this sender from all receivers list + for _, receiver := range s.receivers { + receiver.mu.Lock() + if buffer := receiver.senders[s]; buffer != nil { + // remove channel from list + delete(receiver.senders, s) + // close channel + close(buffer) + } + receiver.mu.Unlock() + } + s.receivers = nil + s.mu.Unlock() +} + +func (s *Sender) String() string { + info := s.Codec.String() + ", bytes=" + strconv.Itoa(s.bytes) + if s.mu.TryLock() { + info += ", receivers=" + strconv.Itoa(len(s.receivers)) + s.mu.Unlock() + } else { + info += ", receivers=?" + } + if s.overflow > 0 { + info += ", overflow=" + strconv.Itoa(s.overflow) + } + return info +} + +func (s *Sender) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} diff --git a/pkg/dvrip/client.go b/pkg/dvrip/client.go index abcd5ddb..e8b99583 100644 --- a/pkg/dvrip/client.go +++ b/pkg/dvrip/client.go @@ -8,9 +8,9 @@ import ( "encoding/json" "errors" "fmt" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h265" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/pion/rtp" "io" "net" @@ -19,7 +19,7 @@ import ( ) type Client struct { - streamer.Element + core.Listener uri string conn net.Conn @@ -28,14 +28,17 @@ type Client struct { seq uint32 stream string - medias []*streamer.Media - videoTrack *streamer.Track - audioTrack *streamer.Track + medias []*core.Media + receivers []*core.Receiver + videoTrack *core.Receiver + audioTrack *core.Receiver videoTS uint32 videoDT uint32 audioTS uint32 audioSeq uint16 + + recv uint32 } type Response map[string]any @@ -196,7 +199,7 @@ func (c *Client) Handle() error { //log.Printf("[AVC] %v, len: %d, ts: %10d", h265.Types(payload), len(payload), packet.Timestamp) - _ = c.videoTrack.WriteRTP(packet) + c.videoTrack.WriteRTP(packet) } case 0x1FD: // PFrame @@ -210,7 +213,7 @@ func (c *Client) Handle() error { //log.Printf("[DVR] %v, len: %d, ts: %10d", h265.Types(packet.Payload), len(packet.Payload), packet.Timestamp) - _ = c.videoTrack.WriteRTP(packet) + c.videoTrack.WriteRTP(packet) } case 0x1FA, 0x1F9: // audio @@ -245,7 +248,7 @@ func (c *Client) Handle() error { //log.Printf("[DVR] len: %d, ts: %10d", len(packet.Payload), packet.Timestamp) - _ = c.audioTrack.WriteRTP(packet) + c.audioTrack.WriteRTP(packet) } } } @@ -295,6 +298,8 @@ func (c *Client) Response() (b []byte, err error) { return } + c.recv += 20 + if b[0] != 255 { return nil, errors.New("read error") } @@ -307,6 +312,8 @@ func (c *Client) Response() (b []byte, err error) { return } + c.recv += size + return } @@ -328,21 +335,21 @@ func (c *Client) ResponseJSON() (res Response, err error) { } func (c *Client) AddVideoTrack(mediaCode byte, payload []byte) { - var codec *streamer.Codec + var codec *core.Codec switch mediaCode { case 2: - codec = &streamer.Codec{ - Name: streamer.CodecH264, + codec = &core.Codec{ + Name: core.CodecH264, ClockRate: 90000, - PayloadType: streamer.PayloadTypeRAW, + PayloadType: core.PayloadTypeRAW, FmtpLine: h264.GetFmtpLine(payload), } case 0x03, 0x13: - codec = &streamer.Codec{ - Name: streamer.CodecH265, + codec = &core.Codec{ + Name: core.CodecH265, ClockRate: 90000, - PayloadType: streamer.PayloadTypeRAW, + PayloadType: core.PayloadTypeRAW, FmtpLine: "profile-id=1", } @@ -369,14 +376,15 @@ func (c *Client) AddVideoTrack(mediaCode byte, payload []byte) { return } - media := &streamer.Media{ - Kind: streamer.KindVideo, - Direction: streamer.DirectionSendonly, - Codecs: []*streamer.Codec{codec}, + media := &core.Media{ + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{codec}, } c.medias = append(c.medias, media) - c.videoTrack = streamer.NewTrack(media, codec) + c.videoTrack = core.NewReceiver(media, codec) + c.receivers = append(c.receivers, c.videoTrack) } var sampleRates = []uint32{4000, 8000, 11025, 16000, 20000, 22050, 32000, 44100, 48000} @@ -384,15 +392,15 @@ var sampleRates = []uint32{4000, 8000, 11025, 16000, 20000, 22050, 32000, 44100, func (c *Client) AddAudioTrack(mediaCode byte, sampleRate byte) { // https://github.com/vigoss30611/buildroot-ltc/blob/master/system/qm/ipc/ProtocolService/src/ZhiNuo/inc/zn_dh_base_type.h // PCM8 = 7, G729, IMA_ADPCM, G711U, G721, PCM8_VWIS, MS_ADPCM, G711A, PCM16 - var codec *streamer.Codec + var codec *core.Codec switch mediaCode { case 10: // G711U - codec = &streamer.Codec{ - Name: streamer.CodecPCMU, + codec = &core.Codec{ + Name: core.CodecPCMU, } case 14: // G711A - codec = &streamer.Codec{ - Name: streamer.CodecPCMA, + codec = &core.Codec{ + Name: core.CodecPCMA, } default: println("[DVRIP] unsupported audio codec:", mediaCode) @@ -403,14 +411,15 @@ func (c *Client) AddAudioTrack(mediaCode byte, sampleRate byte) { codec.ClockRate = sampleRates[sampleRate-1] } - media := &streamer.Media{ - Kind: streamer.KindAudio, - Direction: streamer.DirectionSendonly, - Codecs: []*streamer.Codec{codec}, + media := &core.Media{ + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{codec}, } c.medias = append(c.medias, media) - c.audioTrack = streamer.NewTrack(media, codec) + c.audioTrack = core.NewReceiver(media, codec) + c.receivers = append(c.receivers, c.audioTrack) } func SofiaHash(password string) string { diff --git a/pkg/dvrip/producer.go b/pkg/dvrip/producer.go index 0ab04edc..6c1ffe4d 100644 --- a/pkg/dvrip/producer.go +++ b/pkg/dvrip/producer.go @@ -1,19 +1,21 @@ package dvrip -import "github.com/AlexxIT/go2rtc/pkg/streamer" +import ( + "encoding/json" + "github.com/AlexxIT/go2rtc/pkg/core" +) -func (c *Client) GetMedias() []*streamer.Media { +func (c *Client) GetMedias() []*core.Media { return c.medias } -func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track { - if c.videoTrack != nil && c.videoTrack.Codec == codec { - return c.videoTrack +func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + for _, track := range c.receivers { + if track.Codec == codec { + return track, nil + } } - if c.audioTrack != nil && c.audioTrack.Codec == codec { - return c.audioTrack - } - return nil + return nil, core.ErrCantGetTrack } func (c *Client) Start() error { @@ -21,5 +23,19 @@ func (c *Client) Start() error { } func (c *Client) Stop() error { + for _, receiver := range c.receivers { + receiver.Close() + } return c.Close() } + +func (c *Client) MarshalJSON() ([]byte, error) { + info := &core.Info{ + Type: "DVRIP active producer", + RemoteAddr: c.conn.RemoteAddr().String(), + Medias: c.medias, + Receivers: c.receivers, + Recv: int(c.recv), + } + return json.Marshal(info) +} diff --git a/pkg/fake/consumer.go b/pkg/fake/.consumer.go similarity index 100% rename from pkg/fake/consumer.go rename to pkg/fake/.consumer.go diff --git a/pkg/fake/producer.go b/pkg/fake/.producer.go similarity index 100% rename from pkg/fake/producer.go rename to pkg/fake/.producer.go diff --git a/pkg/h264/avc.go b/pkg/h264/avc.go index 37bf7258..99fd4598 100644 --- a/pkg/h264/avc.go +++ b/pkg/h264/avc.go @@ -3,7 +3,7 @@ package h264 import ( "bytes" "encoding/binary" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/rtp" ) @@ -164,17 +164,15 @@ func EncodeAVC(nals ...[]byte) (avc []byte) { return } -func RepairAVC(track *streamer.Track) streamer.WrapperFunc { - sps, pps := GetParameterSet(track.Codec.FmtpLine) +func RepairAVC(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { + sps, pps := GetParameterSet(codec.FmtpLine) ps := EncodeAVC(sps, pps) - return func(push streamer.WriterFunc) streamer.WriterFunc { - return func(packet *rtp.Packet) (err error) { - if NALUType(packet.Payload) == NALUTypeIFrame { - packet.Payload = Join(ps, packet.Payload) - } - return push(packet) + return func(packet *rtp.Packet) { + if NALUType(packet.Payload) == NALUTypeIFrame { + packet.Payload = Join(ps, packet.Payload) } + handler(packet) } } diff --git a/pkg/h264/helper.go b/pkg/h264/helper.go index 3fa012ec..8d0ef9a9 100644 --- a/pkg/h264/helper.go +++ b/pkg/h264/helper.go @@ -5,7 +5,7 @@ import ( "encoding/binary" "encoding/hex" "fmt" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "strings" ) @@ -62,11 +62,11 @@ func GetProfileLevelID(fmtp string) string { var conf []byte // some cameras has wrong profile-level-id // https://github.com/AlexxIT/go2rtc/issues/155 - if s := streamer.Between(fmtp, "sprop-parameter-sets=", ","); s != "" { + if s := core.Between(fmtp, "sprop-parameter-sets=", ","); s != "" { if sps, _ := base64.StdEncoding.DecodeString(s); len(sps) >= 4 { conf = sps[1:4] } - } else if s = streamer.Between(fmtp, "profile-level-id=", ";"); s != "" { + } else if s = core.Between(fmtp, "profile-level-id=", ";"); s != "" { conf, _ = hex.DecodeString(s) } @@ -89,7 +89,7 @@ func GetParameterSet(fmtp string) (sps, pps []byte) { return } - s := streamer.Between(fmtp, "sprop-parameter-sets=", ";") + s := core.Between(fmtp, "sprop-parameter-sets=", ";") if s == "" { return } diff --git a/pkg/h264/rtp.go b/pkg/h264/rtp.go index 8900e790..8cccb637 100644 --- a/pkg/h264/rtp.go +++ b/pkg/h264/rtp.go @@ -2,7 +2,7 @@ package h264 import ( "encoding/binary" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/rtp" "github.com/pion/rtp/codecs" ) @@ -11,119 +11,112 @@ const RTPPacketVersionAVC = 0 const PSMaxSize = 128 // the biggest SPS I've seen is 48 (EZVIZ CS-CV210) -func RTPDepay(track *streamer.Track) streamer.WrapperFunc { +func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { depack := &codecs.H264Packet{IsAVC: true} - sps, pps := GetParameterSet(track.Codec.FmtpLine) + sps, pps := GetParameterSet(codec.FmtpLine) ps := EncodeAVC(sps, pps) buf := make([]byte, 0, 512*1024) // 512K - return func(push streamer.WriterFunc) streamer.WriterFunc { - return func(packet *rtp.Packet) error { - //log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) + return func(packet *rtp.Packet) { + //log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) - payload, err := depack.Unmarshal(packet.Payload) - if len(payload) == 0 || err != nil { - return nil - } - - // Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true - // Reolink Duo 2: sends SPS with Marker and PPS without - if packet.Marker && len(payload) < PSMaxSize { - switch NALUType(payload) { - case NALUTypeSPS, NALUTypePPS: - buf = append(buf, payload...) - return nil - case NALUTypeSEI: - // RtspServer https://github.com/AlexxIT/go2rtc/issues/244 - // sends, marked SPS, marked PPS, marked SEI, marked IFrame - return nil - } - } - - if len(buf) == 0 { - for { - // Amcrest IP4M-1051: 9, 7, 8, 6, 28... - // Amcrest IP4M-1051: 9, 6, 1 - switch NALUType(payload) { - case NALUTypeIFrame: - // fix IFrame without SPS,PPS - buf = append(buf, ps...) - case NALUTypeSEI, NALUTypeAUD: - // fix ffmpeg with transcoding first frame - i := int(4 + binary.BigEndian.Uint32(payload)) - - // check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A) - if i == len(payload) { - return nil - } - - payload = payload[i:] - continue - } - break - } - } - - // collect all NALs for Access Unit - if !packet.Marker { - buf = append(buf, payload...) - return nil - } - - if len(buf) > 0 { - payload = append(buf, payload...) - buf = buf[:0] - } - - // should not be that huge SPS - if NALUType(payload) == NALUTypeSPS && binary.BigEndian.Uint32(payload) >= PSMaxSize { - // some Chinese buggy cameras has single packet with SPS+PPS+IFrame separated by 00 00 00 01 - // https://github.com/AlexxIT/WebRTC/issues/391 - // https://github.com/AlexxIT/WebRTC/issues/392 - AnnexB2AVC(payload) - } - - //log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber) - - clone := *packet - clone.Version = RTPPacketVersionAVC - clone.Payload = payload - return push(&clone) + payload, err := depack.Unmarshal(packet.Payload) + if len(payload) == 0 || err != nil { + return } + + // Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true + // Reolink Duo 2: sends SPS with Marker and PPS without + if packet.Marker && len(payload) < PSMaxSize { + switch NALUType(payload) { + case NALUTypeSPS, NALUTypePPS: + buf = append(buf, payload...) + return + case NALUTypeSEI: + // RtspServer https://github.com/AlexxIT/go2rtc/issues/244 + // sends, marked SPS, marked PPS, marked SEI, marked IFrame + return + } + } + + if len(buf) == 0 { + for { + // Amcrest IP4M-1051: 9, 7, 8, 6, 28... + // Amcrest IP4M-1051: 9, 6, 1 + switch NALUType(payload) { + case NALUTypeIFrame: + // fix IFrame without SPS,PPS + buf = append(buf, ps...) + case NALUTypeSEI, NALUTypeAUD: + // fix ffmpeg with transcoding first frame + i := int(4 + binary.BigEndian.Uint32(payload)) + + // check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A) + if i == len(payload) { + return + } + + payload = payload[i:] + continue + } + break + } + } + + // collect all NALs for Access Unit + if !packet.Marker { + buf = append(buf, payload...) + return + } + + if len(buf) > 0 { + payload = append(buf, payload...) + buf = buf[:0] + } + + // should not be that huge SPS + if NALUType(payload) == NALUTypeSPS && binary.BigEndian.Uint32(payload) >= PSMaxSize { + // some Chinese buggy cameras has single packet with SPS+PPS+IFrame separated by 00 00 00 01 + // https://github.com/AlexxIT/WebRTC/issues/391 + // https://github.com/AlexxIT/WebRTC/issues/392 + AnnexB2AVC(payload) + } + + //log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber) + + clone := *packet + clone.Version = RTPPacketVersionAVC + clone.Payload = payload + handler(&clone) } } -func RTPPay(mtu uint16) streamer.WrapperFunc { +func RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc { payloader := &Payloader{IsAVC: true} sequencer := rtp.NewRandomSequencer() mtu -= 12 // rtp.Header size - return func(push streamer.WriterFunc) streamer.WriterFunc { - return func(packet *rtp.Packet) error { - if packet.Version != RTPPacketVersionAVC { - return push(packet) - } + return func(packet *rtp.Packet) { + if packet.Version != RTPPacketVersionAVC { + handler(packet) + return + } - payloads := payloader.Payload(mtu, packet.Payload) - last := len(payloads) - 1 - for i, payload := range payloads { - clone := rtp.Packet{ - Header: rtp.Header{ - Version: 2, - Marker: i == last, - SequenceNumber: sequencer.NextSequenceNumber(), - Timestamp: packet.Timestamp, - }, - Payload: payload, - } - if err := push(&clone); err != nil { - return err - } + payloads := payloader.Payload(mtu, packet.Payload) + last := len(payloads) - 1 + for i, payload := range payloads { + clone := rtp.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: i == last, + SequenceNumber: sequencer.NextSequenceNumber(), + Timestamp: packet.Timestamp, + }, + Payload: payload, } - - return nil + handler(&clone) } } } diff --git a/pkg/h265/helper.go b/pkg/h265/helper.go index 345b9687..44605bde 100644 --- a/pkg/h265/helper.go +++ b/pkg/h265/helper.go @@ -3,7 +3,7 @@ package h265 import ( "encoding/base64" "encoding/binary" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" ) const ( @@ -62,13 +62,13 @@ func GetParameterSet(fmtp string) (vps, sps, pps []byte) { return } - s := streamer.Between(fmtp, "sprop-vps=", ";") + s := core.Between(fmtp, "sprop-vps=", ";") vps, _ = base64.StdEncoding.DecodeString(s) - s = streamer.Between(fmtp, "sprop-sps=", ";") + s = core.Between(fmtp, "sprop-sps=", ";") sps, _ = base64.StdEncoding.DecodeString(s) - s = streamer.Between(fmtp, "sprop-pps=", ";") + s = core.Between(fmtp, "sprop-pps=", ";") pps, _ = base64.StdEncoding.DecodeString(s) return diff --git a/pkg/h265/rtp.go b/pkg/h265/rtp.go index e0906c04..333ca6d4 100644 --- a/pkg/h265/rtp.go +++ b/pkg/h265/rtp.go @@ -2,189 +2,177 @@ package h265 import ( "encoding/binary" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/pion/rtp" ) -func RTPDepay(track *streamer.Track) streamer.WrapperFunc { - //vps, sps, pps := GetParameterSet(track.Codec.FmtpLine) +func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { + //vps, sps, pps := GetParameterSet(codec.FmtpLine) //ps := h264.EncodeAVC(vps, sps, pps) buf := make([]byte, 0, 512*1024) // 512K var nuStart int - return func(push streamer.WriterFunc) streamer.WriterFunc { - return func(packet *rtp.Packet) error { - data := packet.Payload - nuType := (data[0] >> 1) & 0x3F - //log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, nuType, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) + return func(packet *rtp.Packet) { + data := packet.Payload + nuType := (data[0] >> 1) & 0x3F + //log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, nuType, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) - // Fix for RtspServer https://github.com/AlexxIT/go2rtc/issues/244 - if packet.Marker && len(data) < h264.PSMaxSize { - switch nuType { - case NALUTypeVPS, NALUTypeSPS, NALUTypePPS: - packet.Marker = false - case NALUTypePrefixSEI, NALUTypeSuffixSEI: - return nil - } + // Fix for RtspServer https://github.com/AlexxIT/go2rtc/issues/244 + if packet.Marker && len(data) < h264.PSMaxSize { + switch nuType { + case NALUTypeVPS, NALUTypeSPS, NALUTypePPS: + packet.Marker = false + case NALUTypePrefixSEI, NALUTypeSuffixSEI: + return } + } - if nuType == NALUTypeFU { - switch data[2] >> 6 { - case 2: // begin - nuType = data[2] & 0x3F + if nuType == NALUTypeFU { + switch data[2] >> 6 { + case 2: // begin + nuType = data[2] & 0x3F - // push PS data before keyframe - //if len(buf) == 0 && nuType >= 19 && nuType <= 21 { - // buf = append(buf, ps...) - //} + // push PS data before keyframe + //if len(buf) == 0 && nuType >= 19 && nuType <= 21 { + // buf = append(buf, ps...) + //} - nuStart = len(buf) - buf = append(buf, 0, 0, 0, 0) // NAL unit size - buf = append(buf, (data[0]&0x81)|(nuType<<1), data[1]) - buf = append(buf, data[3:]...) - return nil - case 0: // continue - buf = append(buf, data[3:]...) - return nil - case 1: // end - buf = append(buf, data[3:]...) - binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4)) - } - } else { nuStart = len(buf) buf = append(buf, 0, 0, 0, 0) // NAL unit size - buf = append(buf, data...) - binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(data))) + buf = append(buf, (data[0]&0x81)|(nuType<<1), data[1]) + buf = append(buf, data[3:]...) + return + case 0: // continue + buf = append(buf, data[3:]...) + return + case 1: // end + buf = append(buf, data[3:]...) + binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4)) } - - // collect all NAL Units for Access Unit - if !packet.Marker { - return nil - } - - //log.Printf("[HEVC] %v, len: %d", Types(buf), len(buf)) - - clone := *packet - clone.Version = h264.RTPPacketVersionAVC - clone.Payload = buf - - buf = buf[:0] - - return push(&clone) + } else { + nuStart = len(buf) + buf = append(buf, 0, 0, 0, 0) // NAL unit size + buf = append(buf, data...) + binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(data))) } + + // collect all NAL Units for Access Unit + if !packet.Marker { + return + } + + //log.Printf("[HEVC] %v, len: %d", Types(buf), len(buf)) + + clone := *packet + clone.Version = h264.RTPPacketVersionAVC + clone.Payload = buf + + buf = buf[:0] + + handler(&clone) } } -func RTPPay(mtu uint16) streamer.WrapperFunc { +func RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc { payloader := &Payloader{} sequencer := rtp.NewRandomSequencer() mtu -= 12 // rtp.Header size - return func(push streamer.WriterFunc) streamer.WriterFunc { - return func(packet *rtp.Packet) error { - if packet.Version != h264.RTPPacketVersionAVC { - return push(packet) - } + return func(packet *rtp.Packet) { + if packet.Version != h264.RTPPacketVersionAVC { + handler(packet) + return + } - payloads := payloader.Payload(mtu, packet.Payload) - last := len(payloads) - 1 - for i, payload := range payloads { - clone := rtp.Packet{ - Header: rtp.Header{ - Version: 2, - Marker: i == last, - SequenceNumber: sequencer.NextSequenceNumber(), - Timestamp: packet.Timestamp, - }, - Payload: payload, - } - if err := push(&clone); err != nil { - return err - } + payloads := payloader.Payload(mtu, packet.Payload) + last := len(payloads) - 1 + for i, payload := range payloads { + clone := rtp.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: i == last, + SequenceNumber: sequencer.NextSequenceNumber(), + Timestamp: packet.Timestamp, + }, + Payload: payload, } - - return nil + handler(&clone) } } } // SafariPay - generate Safari friendly payload for H265 // https://github.com/AlexxIT/Blog/issues/5 -func SafariPay(mtu uint16) streamer.WrapperFunc { +func SafariPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc { sequencer := rtp.NewRandomSequencer() size := int(mtu - 12) // rtp.Header size - return func(push streamer.WriterFunc) streamer.WriterFunc { - return func(packet *rtp.Packet) error { - if packet.Version != h264.RTPPacketVersionAVC { - return push(packet) + return func(packet *rtp.Packet) { + if packet.Version != h264.RTPPacketVersionAVC { + handler(packet) + return + } + + // protect original packets from modification + au := make([]byte, len(packet.Payload)) + copy(au, packet.Payload) + + var start byte + + for i := 0; i < len(au); { + size := int(binary.BigEndian.Uint32(au[i:])) + 4 + + // convert AVC to Annex-B + au[i] = 0 + au[i+1] = 0 + au[i+2] = 0 + au[i+3] = 1 + + switch NALUType(au[i:]) { + case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3: + start = 3 + default: + if start == 0 { + start = 2 + } } - // protect original packets from modification - au := make([]byte, len(packet.Payload)) - copy(au, packet.Payload) + i += size + } - var start byte + // rtp.Packet payload + b := make([]byte, 1, size) + size-- // minus header byte - for i := 0; i < len(au); { - size := int(binary.BigEndian.Uint32(au[i:])) + 4 + for au != nil { + b[0] = start - // convert AVC to Annex-B - au[i] = 0 - au[i+1] = 0 - au[i+2] = 0 - au[i+3] = 1 - - switch NALUType(au[i:]) { - case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3: - start = 3 - default: - if start == 0 { - start = 2 - } - } - - i += size + if start > 1 { + start -= 2 } - // rtp.Packet payload - b := make([]byte, 1, size) - size-- // minus header byte - - for au != nil { - b[0] = start - - if start > 1 { - start -= 2 - } - - if len(au) > size { - b = append(b, au[:size]...) - au = au[size:] - } else { - b = append(b, au...) - au = nil - } - - clone := rtp.Packet{ - Header: rtp.Header{ - Version: 2, - Marker: au == nil, - SequenceNumber: sequencer.NextSequenceNumber(), - Timestamp: packet.Timestamp, - }, - Payload: b, - } - if err := push(&clone); err != nil { - return err - } - - b = b[:1] // clear buffer + if len(au) > size { + b = append(b, au[:size]...) + au = au[size:] + } else { + b = append(b, au...) + au = nil } - return nil + clone := rtp.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: au == nil, + SequenceNumber: sequencer.NextSequenceNumber(), + Timestamp: packet.Timestamp, + }, + Payload: b, + } + handler(&clone) + + b = b[:1] // clear buffer } } } diff --git a/pkg/hap/character.go b/pkg/hap/character.go index a19eb7a8..d7c68337 100644 --- a/pkg/hap/character.go +++ b/pkg/hap/character.go @@ -11,14 +11,14 @@ import ( ) type Character struct { - AID int `json:"aid,omitempty"` - IID int `json:"iid"` - Type string `json:"type,omitempty"` - Format string `json:"format,omitempty"` - Value interface{} `json:"value,omitempty"` - Event interface{} `json:"ev,omitempty"` - Perms []string `json:"perms,omitempty"` - Description string `json:"description,omitempty"` + AID int `json:"aid,omitempty"` + IID int `json:"iid"` + Type string `json:"type,omitempty"` + Format string `json:"format,omitempty"` + Value any `json:"value,omitempty"` + Event any `json:"ev,omitempty"` + Perms []string `json:"perms,omitempty"` + Description string `json:"description,omitempty"` //MaxDataLen int `json:"maxDataLen"` listeners map[io.Writer]bool diff --git a/pkg/hap/conn.go b/pkg/hap/conn.go index 0b51c6d9..48fb50b8 100644 --- a/pkg/hap/conn.go +++ b/pkg/hap/conn.go @@ -7,8 +7,8 @@ import ( "encoding/json" "errors" "fmt" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/hap/mdns" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/brutella/hap" "github.com/brutella/hap/chacha20poly1305" "github.com/brutella/hap/curve25519" @@ -26,7 +26,7 @@ import ( // Conn for HomeKit. DevicePublic can be null. type Conn struct { - streamer.Element + core.Listener DeviceAddress string // including port DeviceID string diff --git a/pkg/homekit/client.go b/pkg/homekit/client.go index 72f95666..3314efaf 100644 --- a/pkg/homekit/client.go +++ b/pkg/homekit/client.go @@ -4,10 +4,10 @@ import ( "encoding/json" "errors" "fmt" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/hap" "github.com/AlexxIT/go2rtc/pkg/hap/camera" "github.com/AlexxIT/go2rtc/pkg/srtp" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/brutella/hap/characteristic" "github.com/brutella/hap/rtp" "net" @@ -16,15 +16,15 @@ import ( ) type Client struct { - streamer.Element + core.Listener conn *hap.Conn exit chan error server *srtp.Server url string - medias []*streamer.Media - tracks []*streamer.Track + medias []*core.Media + receivers []*core.Receiver sessions []*srtp.Session } @@ -62,7 +62,7 @@ func (c *Client) Dial() error { return nil } -func (c *Client) GetMedias() []*streamer.Media { +func (c *Client) GetMedias() []*core.Media { if c.medias == nil { c.medias = c.getMedias() } @@ -70,20 +70,20 @@ func (c *Client) GetMedias() []*streamer.Media { return c.medias } -func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track { - for _, track := range c.tracks { +func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + for _, track := range c.receivers { if track.Codec == codec { - return track + return track, nil } } - track := streamer.NewTrack(media, codec) - c.tracks = append(c.tracks, track) - return track + track := core.NewReceiver(media, codec) + c.receivers = append(c.receivers, track) + return track, nil } func (c *Client) Start() error { - if c.tracks == nil { + if c.receivers == nil { return errors.New("producer without tracks") } @@ -161,11 +161,11 @@ func (c *Client) Start() error { return err } - for _, track := range c.tracks { + for _, track := range c.receivers { switch track.Codec.Name { - case streamer.CodecH264: + case core.CodecH264: vs.Track = track - case streamer.CodecELD: + case core.CodecELD: as.Track = track } } @@ -188,8 +188,8 @@ func (c *Client) Stop() error { return err } -func (c *Client) getMedias() []*streamer.Media { - var medias []*streamer.Media +func (c *Client) getMedias() []*core.Media { + var medias []*core.Media accs, err := c.conn.GetAccessories() if err != nil { @@ -206,20 +206,20 @@ func (c *Client) getMedias() []*streamer.Media { } for _, hkCodec := range v1.Codecs { - codec := &streamer.Codec{ClockRate: 90000} + codec := &core.Codec{ClockRate: 90000} switch hkCodec.Type { case rtp.VideoCodecType_H264: - codec.Name = streamer.CodecH264 + codec.Name = core.CodecH264 codec.FmtpLine = "profile-level-id=420029" default: fmt.Printf("unknown codec: %d", hkCodec.Type) continue } - media := &streamer.Media{ - Kind: streamer.KindVideo, Direction: streamer.DirectionSendonly, - Codecs: []*streamer.Codec{codec}, + media := &core.Media{ + Kind: core.KindVideo, Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{codec}, } medias = append(medias, media) } @@ -231,7 +231,7 @@ func (c *Client) getMedias() []*streamer.Media { } for _, hkCodec := range v2.Codecs { - codec := &streamer.Codec{ + codec := &core.Codec{ Channels: uint16(hkCodec.Parameters.Channels), } @@ -248,7 +248,7 @@ func (c *Client) getMedias() []*streamer.Media { switch hkCodec.Type { case rtp.AudioCodecType_AAC_ELD: - codec.Name = streamer.CodecELD + codec.Name = core.CodecELD // only this value supported by FFmpeg codec.FmtpLine = "profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=F8EC3000" default: @@ -256,9 +256,9 @@ func (c *Client) getMedias() []*streamer.Media { continue } - media := &streamer.Media{ - Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly, - Codecs: []*streamer.Codec{codec}, + media := &core.Media{ + Kind: core.KindAudio, Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{codec}, } medias = append(medias, media) } @@ -272,12 +272,12 @@ func (c *Client) MarshalJSON() ([]byte, error) { recv += atomic.LoadUint32(&session.Recv) } - info := &streamer.Info{ - Type: "HomeKit source", - URL: c.conn.URL(), - Medias: c.medias, - Tracks: c.tracks, - Recv: recv, + info := &core.Info{ + Type: "HomeKit active producer", + URL: c.conn.URL(), + Medias: c.medias, + Receivers: c.receivers, + Recv: int(recv), } return json.Marshal(info) } diff --git a/pkg/isapi/client.go b/pkg/isapi/client.go index 04ca22ce..e00c5504 100644 --- a/pkg/isapi/client.go +++ b/pkg/isapi/client.go @@ -2,7 +2,7 @@ package isapi import ( "errors" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/tcp" "io" "net" @@ -11,16 +11,15 @@ import ( ) type Client struct { - streamer.Element - - url string - - medias []*streamer.Media - tracks []*streamer.Track + core.Listener + url string channel string conn net.Conn - send int + + medias []*core.Media + sender *core.Sender + send int } func NewClient(rawURL string) (*Client, error) { @@ -60,22 +59,22 @@ func (c *Client) Dial() (err error) { xml := string(b) - codec := streamer.Between(xml, ``, `<`) + codec := core.Between(xml, ``, `<`) switch codec { case "G.711ulaw": - codec = streamer.CodecPCMU + codec = core.CodecPCMU case "G.711alaw": - codec = streamer.CodecPCMA + codec = core.CodecPCMA default: return nil } - c.channel = streamer.Between(xml, ``, `<`) + c.channel = core.Between(xml, ``, `<`) - media := &streamer.Media{ - Kind: streamer.KindAudio, - Direction: streamer.DirectionRecvonly, - Codecs: []*streamer.Codec{ + media := &core.Media{ + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ {Name: codec, ClockRate: 8000}, }, } diff --git a/pkg/isapi/consumer.go b/pkg/isapi/consumer.go index 4784e981..c7b51c9d 100644 --- a/pkg/isapi/consumer.go +++ b/pkg/isapi/consumer.go @@ -1,18 +1,63 @@ package isapi import ( - "github.com/AlexxIT/go2rtc/pkg/streamer" + "encoding/json" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/rtp" ) -func (c *Client) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { - consCodec := media.MatchCodec(track.Codec) - consTrack := c.GetTrack(media, consCodec) - if consTrack == nil { - return nil +func (c *Client) GetMedias() []*core.Media { + return c.medias +} + +func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + return nil, core.ErrCantGetTrack +} + +func (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { + if c.sender == nil { + c.sender = core.NewSender(media, track.Codec) + c.sender.Handler = func(packet *rtp.Packet) { + if c.conn == nil { + return + } + c.send += len(packet.Payload) + _, _ = c.conn.Write(packet.Payload) + } } - return track.Bind(func(packet *rtp.Packet) error { - return consTrack.WriteRTP(packet) - }) + c.sender.HandleRTP(track) + return nil +} + +func (c *Client) Start() (err error) { + if err = c.Open(); err != nil { + return + } + return +} + +func (c *Client) Stop() (err error) { + if c.sender != nil { + c.sender.Close() + } + + if c.conn != nil { + _ = c.Close() + return c.conn.Close() + } + + return nil +} + +func (c *Client) MarshalJSON() ([]byte, error) { + info := &core.Info{ + Type: "ISAPI active consumer", + Medias: c.medias, + Send: c.send, + } + if c.sender != nil { + info.Senders = []*core.Sender{c.sender} + } + return json.Marshal(info) } diff --git a/pkg/isapi/producer.go b/pkg/isapi/producer.go deleted file mode 100644 index 305aa61f..00000000 --- a/pkg/isapi/producer.go +++ /dev/null @@ -1,56 +0,0 @@ -package isapi - -import ( - "encoding/json" - "github.com/AlexxIT/go2rtc/pkg/streamer" - "github.com/pion/rtp" -) - -func (c *Client) GetMedias() []*streamer.Media { - return c.medias -} - -func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track { - for _, track := range c.tracks { - if track.Codec == codec { - return track - } - } - - track := streamer.NewTrack(media, codec) - track = track.Bind(func(packet *rtp.Packet) (err error) { - if c.conn != nil { - c.send += len(packet.Payload) - _, err = c.conn.Write(packet.Payload) - } - return - }) - c.tracks = append(c.tracks, track) - - return track -} - -func (c *Client) Start() (err error) { - if err = c.Open(); err != nil { - return - } - return -} - -func (c *Client) Stop() (err error) { - if c.conn == nil { - return - } - _ = c.Close() - return c.conn.Close() -} - -func (c *Client) MarshalJSON() ([]byte, error) { - info := &streamer.Info{ - Type: "ISAPI", - Medias: c.medias, - Tracks: c.tracks, - Send: uint32(c.send), - } - return json.Marshal(info) -} diff --git a/pkg/iso/codecs.go b/pkg/iso/codecs.go index ecb251d0..fe1d6093 100644 --- a/pkg/iso/codecs.go +++ b/pkg/iso/codecs.go @@ -1,13 +1,15 @@ package iso -import "github.com/AlexxIT/go2rtc/pkg/streamer" +import ( + "github.com/AlexxIT/go2rtc/pkg/core" +) func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) { // https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html switch codec { - case streamer.CodecH264: + case core.CodecH264: m.StartAtom("avc1") - case streamer.CodecH265: + case core.CodecH265: m.StartAtom("hev1") default: panic("unsupported iso video: " + codec) @@ -30,9 +32,9 @@ func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) { m.WriteUint16(0xFFFF) // color table id (-1) switch codec { - case streamer.CodecH264: + case core.CodecH264: m.StartAtom("avcC") - case streamer.CodecH265: + case core.CodecH265: m.StartAtom("hvcC") } m.Write(conf) @@ -43,13 +45,13 @@ func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) { func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, conf []byte) { switch codec { - case streamer.CodecAAC, streamer.CodecMP3: + case core.CodecAAC, core.CodecMP3: m.StartAtom("mp4a") - case streamer.CodecOpus: + case core.CodecOpus: m.StartAtom("Opus") - case streamer.CodecPCMU: + case core.CodecPCMU: m.StartAtom("ulaw") - case streamer.CodecPCMA: + case core.CodecPCMA: m.StartAtom("alaw") default: panic("unsupported iso audio: " + codec) @@ -66,16 +68,16 @@ func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, con m.WriteFloat32(float64(sampleRate)) // sample_rate switch codec { - case streamer.CodecAAC: + case core.CodecAAC: m.WriteEsdsAAC(conf) - case streamer.CodecMP3: + case core.CodecMP3: m.WriteEsdsMP3() - case streamer.CodecOpus: + case core.CodecOpus: // don't know what means this magic m.StartAtom("dOps") m.WriteBytes(0, 0x02, 0x01, 0x38, 0, 0, 0xBB, 0x80, 0, 0, 0) m.EndAtom() - case streamer.CodecPCMU, streamer.CodecPCMA: + case core.CodecPCMU, core.CodecPCMA: // don't know what means this magic m.StartAtom("chan") m.WriteBytes(0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0) diff --git a/pkg/ivideon/client.go b/pkg/ivideon/client.go index e07496e1..875dbab8 100644 --- a/pkg/ivideon/client.go +++ b/pkg/ivideon/client.go @@ -6,7 +6,7 @@ import ( "encoding/binary" "encoding/json" "fmt" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/deepch/vdk/codec/h264parser" "github.com/deepch/vdk/format/fmp4/fmp4io" "github.com/gorilla/websocket" @@ -15,7 +15,6 @@ import ( "net/http" "strings" "sync" - "sync/atomic" "time" ) @@ -28,13 +27,14 @@ const ( ) type Client struct { - streamer.Element + core.Listener ID string - conn *websocket.Conn - medias []*streamer.Media - tracks map[byte]*streamer.Track + conn *websocket.Conn + + medias []*core.Media + receiver *core.Receiver msg *message t0 time.Time @@ -43,7 +43,7 @@ type Client struct { state State mu sync.Mutex - recv uint32 + recv int } func NewClient(id string) *Client { @@ -107,12 +107,11 @@ func (c *Client) Handle() error { return err } - track := c.tracks[c.msg.Track] - if track != nil { + if c.receiver != nil && c.receiver.ID == c.msg.Track { c.mu.Lock() if c.state == StateHandle { c.buffer <- data - atomic.AddUint32(&c.recv, uint32(len(data))) + c.recv += len(data) } c.mu.Unlock() } @@ -139,12 +138,11 @@ func (c *Client) Handle() error { return err } - track = c.tracks[msg.Track] - if track != nil { + if c.receiver != nil && c.receiver.ID == msg.Track { c.mu.Lock() if c.state == StateHandle { c.buffer <- data - atomic.AddUint32(&c.recv, uint32(len(data))) + c.recv += len(data) } c.mu.Unlock() } @@ -173,8 +171,6 @@ func (c *Client) Close() error { } func (c *Client) getTracks() error { - c.tracks = map[byte]*streamer.Track{} - for { _, data, err := c.conn.ReadMessage() if err != nil { @@ -197,15 +193,15 @@ func (c *Client) getTracks() error { switch s { case "avc1": // avc1.4d0029 // skip multiple identical init - if c.tracks[msg.TrackID] != nil { + if c.receiver != nil { continue } - codec := &streamer.Codec{ - Name: streamer.CodecH264, + codec := &core.Codec{ + Name: core.CodecH264, ClockRate: 90000, FmtpLine: "profile-level-id=" + msg.CodecString[i+1:], - PayloadType: streamer.PayloadTypeRAW, + PayloadType: core.PayloadTypeRAW, } i = bytes.Index(msg.Data, []byte("avcC")) - 4 @@ -225,15 +221,15 @@ func (c *Client) getTracks() error { base64.StdEncoding.EncodeToString(record.SPS[0]) + "," + base64.StdEncoding.EncodeToString(record.PPS[0]) - media := &streamer.Media{ - Kind: streamer.KindVideo, - Direction: streamer.DirectionSendonly, - Codecs: []*streamer.Codec{codec}, + media := &core.Media{ + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{codec}, } c.medias = append(c.medias, media) - track := streamer.NewTrack(media, codec) - c.tracks[msg.TrackID] = track + c.receiver = core.NewReceiver(media, codec) + c.receiver.ID = msg.TrackID case "mp4a": // mp4a.40.2 } @@ -249,11 +245,6 @@ func (c *Client) getTracks() error { } func (c *Client) worker(buffer chan []byte) { - var track *streamer.Track - for _, track = range c.tracks { - break - } - for data := range buffer { moof := &fmp4io.MovieFrag{} if _, err := moof.Unmarshal(data, 0); err != nil { @@ -289,7 +280,7 @@ func (c *Client) worker(buffer chan []byte) { Header: rtp.Header{Timestamp: ts * 90}, Payload: data[:entry.Size], } - _ = track.WriteRTP(packet) + c.receiver.WriteRTP(packet) data = data[entry.Size:] ts += entry.Duration diff --git a/pkg/ivideon/producer.go b/pkg/ivideon/producer.go index bd40514a..d0a8fcba 100644 --- a/pkg/ivideon/producer.go +++ b/pkg/ivideon/producer.go @@ -2,22 +2,18 @@ package ivideon import ( "encoding/json" - "fmt" - "github.com/AlexxIT/go2rtc/pkg/streamer" - "sync/atomic" + "github.com/AlexxIT/go2rtc/pkg/core" ) -func (c *Client) GetMedias() []*streamer.Media { +func (c *Client) GetMedias() []*core.Media { return c.medias } -func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track { - for _, track := range c.tracks { - if track.Codec == codec { - return track - } +func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + if c.receiver != nil { + return c.receiver, nil } - panic(fmt.Sprintf("wrong media/codec: %+v %+v", media, codec)) + return nil, core.ErrCantGetTrack } func (c *Client) Start() error { @@ -29,21 +25,21 @@ func (c *Client) Start() error { } func (c *Client) Stop() error { + if c.receiver != nil { + c.receiver.Close() + } return c.Close() } func (c *Client) MarshalJSON() ([]byte, error) { - var tracks []*streamer.Track - for _, track := range c.tracks { - tracks = append(tracks, track) - } - - info := &streamer.Info{ - Type: "Ivideon source", + info := &core.Info{ + Type: "Ivideon active producer", URL: c.ID, Medias: c.medias, - Tracks: tracks, - Recv: atomic.LoadUint32(&c.recv), + Recv: c.recv, + } + if c.receiver != nil { + info.Receivers = []*core.Receiver{c.receiver} } return json.Marshal(info) } diff --git a/pkg/mjpeg/client.go b/pkg/mjpeg/client.go index f6cda026..3940166f 100644 --- a/pkg/mjpeg/client.go +++ b/pkg/mjpeg/client.go @@ -3,7 +3,7 @@ package mjpeg import ( "bufio" "errors" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/pion/rtp" "io" @@ -11,12 +11,11 @@ import ( "net/textproto" "strconv" "strings" - "sync/atomic" "time" ) type Client struct { - streamer.Element + core.Listener UserAgent string RemoteAddr string @@ -24,9 +23,10 @@ type Client struct { closed bool res *http.Response - medias []*streamer.Media - track *streamer.Track - recv uint32 + medias []*core.Media + receiver *core.Receiver + + recv int } func NewClient(res *http.Response) *Client { @@ -40,9 +40,9 @@ func (c *Client) startJPEG() error { } packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf} - _ = c.track.WriteRTP(packet) + c.receiver.WriteRTP(packet) - atomic.AddUint32(&c.recv, uint32(len(buf))) + c.recv += len(buf) req := c.res.Request @@ -61,10 +61,12 @@ func (c *Client) startJPEG() error { return err } - packet = &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf} - _ = c.track.WriteRTP(packet) + if c.receiver != nil { + packet = &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf} + c.receiver.WriteRTP(packet) + } - atomic.AddUint32(&c.recv, uint32(len(buf))) + c.recv += len(buf) } return nil @@ -109,10 +111,12 @@ func (c *Client) startMJPEG(boundary string) error { return err } - packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf} - _ = c.track.WriteRTP(packet) + if c.receiver != nil { + packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf} + c.receiver.WriteRTP(packet) + } - atomic.AddUint32(&c.recv, uint32(len(buf))) + c.recv += len(buf) if _, err = r.Discard(2); err != nil { return err diff --git a/pkg/mjpeg/consumer.go b/pkg/mjpeg/consumer.go index df4aa680..88244337 100644 --- a/pkg/mjpeg/consumer.go +++ b/pkg/mjpeg/consumer.go @@ -2,52 +2,71 @@ package mjpeg import ( "encoding/json" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/rtp" - "sync/atomic" ) type Consumer struct { - streamer.Element + core.Listener UserAgent string RemoteAddr string - codecs []*streamer.Codec - start bool + medias []*core.Media + sender *core.Sender - send uint32 + send int } -func (c *Consumer) GetMedias() []*streamer.Media { - return []*streamer.Media{{ - Kind: streamer.KindVideo, - Direction: streamer.DirectionRecvonly, - Codecs: []*streamer.Codec{{Name: streamer.CodecJPEG}}, - }} +func (c *Consumer) GetMedias() []*core.Media { + if c.medias == nil { + c.medias = []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecJPEG}, + }, + }, + } + } + return c.medias } -func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { - push := func(packet *rtp.Packet) error { - c.Fire(packet.Payload) - atomic.AddUint32(&c.send, uint32(len(packet.Payload))) - return nil +func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { + if c.sender == nil { + c.sender = core.NewSender(media, track.Codec) + c.sender.Handler = func(packet *rtp.Packet) { + c.Fire(packet.Payload) + c.send += len(packet.Payload) + } + + if track.Codec.IsRTP() { + c.sender.Handler = RTPDepay(c.sender.Handler) + } } - if track.Codec.IsRTP() { - wrapper := RTPDepay(track) - push = wrapper(push) - } + c.sender.HandleRTP(track) + return nil +} - return track.Bind(push) +func (c *Consumer) Stop() error { + if c.sender != nil { + c.sender.Close() + } + return nil } func (c *Consumer) MarshalJSON() ([]byte, error) { - info := &streamer.Info{ - Type: "MJPEG client", + info := &core.Info{ + Type: "MJPEG passive consumer", RemoteAddr: c.RemoteAddr, UserAgent: c.UserAgent, - Send: atomic.LoadUint32(&c.send), + Medias: c.medias, + Send: c.send, + } + if c.sender != nil { + info.Senders = []*core.Sender{c.sender} } return json.Marshal(info) } diff --git a/pkg/mjpeg/producer.go b/pkg/mjpeg/producer.go index b23cfcac..cbb59afb 100644 --- a/pkg/mjpeg/producer.go +++ b/pkg/mjpeg/producer.go @@ -3,19 +3,18 @@ package mjpeg import ( "encoding/json" "errors" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "strings" - "sync/atomic" ) -func (c *Client) GetMedias() []*streamer.Media { +func (c *Client) GetMedias() []*core.Media { if c.medias == nil { - c.medias = []*streamer.Media{{ - Kind: streamer.KindVideo, - Direction: streamer.DirectionSendonly, - Codecs: []*streamer.Codec{ + c.medias = []*core.Media{{ + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ { - Name: streamer.CodecJPEG, ClockRate: 90000, PayloadType: streamer.PayloadTypeRAW, + Name: core.CodecJPEG, ClockRate: 90000, PayloadType: core.PayloadTypeRAW, }, }, }} @@ -23,11 +22,11 @@ func (c *Client) GetMedias() []*streamer.Media { return c.medias } -func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track { - if c.track == nil { - c.track = streamer.NewTrack(media, codec) +func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + if c.receiver == nil { + c.receiver = core.NewReceiver(media, codec) } - return c.track + return c.receiver, nil } func (c *Client) Start() error { @@ -46,6 +45,9 @@ func (c *Client) Start() error { } func (c *Client) Stop() error { + if c.receiver != nil { + c.receiver.Close() + } // important for close reader/writer gorutines _ = c.res.Body.Close() c.closed = true @@ -53,12 +55,16 @@ func (c *Client) Stop() error { } func (c *Client) MarshalJSON() ([]byte, error) { - info := &streamer.Info{ - Type: "MJPEG source", + info := &core.Info{ + Type: "MJPEG active producer", URL: c.res.Request.URL.String(), RemoteAddr: c.RemoteAddr, UserAgent: c.UserAgent, - Recv: atomic.LoadUint32(&c.recv), + Medias: c.medias, + Recv: c.recv, + } + if c.receiver != nil { + info.Receivers = []*core.Receiver{c.receiver} } return json.Marshal(info) } diff --git a/pkg/mjpeg/rtp.go b/pkg/mjpeg/rtp.go index ae983329..6f137f3e 100644 --- a/pkg/mjpeg/rtp.go +++ b/pkg/mjpeg/rtp.go @@ -3,86 +3,84 @@ package mjpeg import ( "bytes" "encoding/binary" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/rtp" "image" "image/jpeg" ) -func RTPDepay(track *streamer.Track) streamer.WrapperFunc { +func RTPDepay(handlerFunc core.HandlerFunc) core.HandlerFunc { buf := make([]byte, 0, 512*1024) // 512K - return func(push streamer.WriterFunc) streamer.WriterFunc { - return func(packet *rtp.Packet) error { - //log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", track.Codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) + return func(packet *rtp.Packet) { + //log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", track.Codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) - // https://www.rfc-editor.org/rfc/rfc2435#section-3.1 - b := packet.Payload + // https://www.rfc-editor.org/rfc/rfc2435#section-3.1 + b := packet.Payload - // 3.1. JPEG header - t := b[4] + // 3.1. JPEG header + t := b[4] - // 3.1.7. Restart Marker header - if 64 <= t && t <= 127 { - b = b[12:] // skip it - } else { - b = b[8:] - } - - if len(buf) == 0 { - var lqt, cqt []byte - - // 3.1.8. Quantization Table header - q := packet.Payload[5] - if q >= 128 { - lqt = b[4:68] - cqt = b[68:132] - b = b[132:] - } else { - lqt, cqt = MakeTables(q) - } - - // https://www.rfc-editor.org/rfc/rfc2435#section-3.1.5 - // The maximum width is 2040 pixels. - w := uint16(packet.Payload[6]) << 3 - h := uint16(packet.Payload[7]) << 3 - - // fix sizes more than 2040 - switch { - // 512x1920 512x1440 - case w == cutSize(2560) && (h == 1920 || h == 1440): - w = 2560 - // 1792x112 - case w == cutSize(3840) && h == cutSize(2160): - w = 3840 - h = 2160 - // 256x1296 - case w == cutSize(2304) && h == 1296: - w = 2304 - } - - //fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h) - buf = MakeHeaders(buf, t, w, h, lqt, cqt) - } - - // 3.1.9. JPEG Payload - buf = append(buf, b...) - - if !packet.Marker { - return nil - } - - if end := buf[len(buf)-2:]; end[0] != 0xFF && end[1] != 0xD9 { - buf = append(buf, 0xFF, 0xD9) - } - - clone := *packet - clone.Payload = buf - - buf = buf[:0] // clear buffer - - return push(&clone) + // 3.1.7. Restart Marker header + if 64 <= t && t <= 127 { + b = b[12:] // skip it + } else { + b = b[8:] } + + if len(buf) == 0 { + var lqt, cqt []byte + + // 3.1.8. Quantization Table header + q := packet.Payload[5] + if q >= 128 { + lqt = b[4:68] + cqt = b[68:132] + b = b[132:] + } else { + lqt, cqt = MakeTables(q) + } + + // https://www.rfc-editor.org/rfc/rfc2435#section-3.1.5 + // The maximum width is 2040 pixels. + w := uint16(packet.Payload[6]) << 3 + h := uint16(packet.Payload[7]) << 3 + + // fix sizes more than 2040 + switch { + // 512x1920 512x1440 + case w == cutSize(2560) && (h == 1920 || h == 1440): + w = 2560 + // 1792x112 + case w == cutSize(3840) && h == cutSize(2160): + w = 3840 + h = 2160 + // 256x1296 + case w == cutSize(2304) && h == 1296: + w = 2304 + } + + //fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h) + buf = MakeHeaders(buf, t, w, h, lqt, cqt) + } + + // 3.1.9. JPEG Payload + buf = append(buf, b...) + + if !packet.Marker { + return + } + + if end := buf[len(buf)-2:]; end[0] != 0xFF && end[1] != 0xD9 { + buf = append(buf, 0xFF, 0xD9) + } + + clone := *packet + clone.Payload = buf + + buf = buf[:0] // clear buffer + + handlerFunc(&clone) } } @@ -90,102 +88,96 @@ func cutSize(size uint16) uint16 { return ((size >> 3) & 0xFF) << 3 } -func RTPPay() streamer.WrapperFunc { +func RTPPay(handlerFunc core.HandlerFunc) core.HandlerFunc { const packetSize = 1436 sequencer := rtp.NewRandomSequencer() - return func(push streamer.WriterFunc) streamer.WriterFunc { - return func(packet *rtp.Packet) error { - // reincode image to more common form - p, err := Transcode(packet.Payload) - if err != nil { - return err + return func(packet *rtp.Packet) { + // reincode image to more common form + p, err := Transcode(packet.Payload) + if err != nil { + return + } + + h1 := make([]byte, 8) + h1[4] = 1 // Type + h1[5] = 255 // Q + + // MBZ=0, Precision=0, Length=128 + h2 := make([]byte, 4, 132) + h2[3] = 128 + + var jpgData []byte + for jpgData == nil { + // 2 bytes h1 + if p[0] != 0xFF { + return } - h1 := make([]byte, 8) - h1[4] = 1 // Type - h1[5] = 255 // Q + size := binary.BigEndian.Uint16(p[2:]) + 2 - // MBZ=0, Precision=0, Length=128 - h2 := make([]byte, 4, 132) - h2[3] = 128 - - var jpgData []byte - for jpgData == nil { - // 2 bytes h1 - if p[0] != 0xFF { - return nil + // 2 bytes payload size (include 2 bytes) + switch p[1] { + case 0xD8: // 0. Start Of Image (size=0) + p = p[2:] + continue + case 0xDB: // 1. Define Quantization Table (size=130) + for i := uint16(4 + 1); i < size; i += 1 + 64 { + h2 = append(h2, p[i:i+64]...) } - - size := binary.BigEndian.Uint16(p[2:]) + 2 - - // 2 bytes payload size (include 2 bytes) - switch p[1] { - case 0xD8: // 0. Start Of Image (size=0) - p = p[2:] - continue - case 0xDB: // 1. Define Quantization Table (size=130) - for i := uint16(4 + 1); i < size; i += 1 + 64 { - h2 = append(h2, p[i:i+64]...) - } - case 0xC0: // 2. Start Of Frame (size=15) - if p[4] != 8 { - return nil - } - h := binary.BigEndian.Uint16(p[5:]) - w := binary.BigEndian.Uint16(p[7:]) - h1[6] = uint8(w >> 3) - h1[7] = uint8(h >> 3) - case 0xC4: // 3. Define Huffman Table (size=416) - case 0xDA: // 4. Start Of Scan (size=10) - jpgData = p[size:] + case 0xC0: // 2. Start Of Frame (size=15) + if p[4] != 8 { + return } - - p = p[size:] + h := binary.BigEndian.Uint16(p[5:]) + w := binary.BigEndian.Uint16(p[7:]) + h1[6] = uint8(w >> 3) + h1[7] = uint8(h >> 3) + case 0xC4: // 3. Define Huffman Table (size=416) + case 0xDA: // 4. Start Of Scan (size=10) + jpgData = p[size:] } - offset := 0 - p = make([]byte, 0) + p = p[size:] + } - for jpgData != nil { - p = p[:0] + offset := 0 + p = make([]byte, 0) - if offset > 0 { - h1[1] = byte(offset >> 16) - h1[2] = byte(offset >> 8) - h1[3] = byte(offset) - p = append(p, h1...) - } else { - p = append(p, h1...) - p = append(p, h2...) - } + for jpgData != nil { + p = p[:0] - dataLen := packetSize - len(p) - if dataLen < len(jpgData) { - p = append(p, jpgData[:dataLen]...) - jpgData = jpgData[dataLen:] - offset += dataLen - } else { - p = append(p, jpgData...) - jpgData = nil - } - - clone := rtp.Packet{ - Header: rtp.Header{ - Version: 2, - Marker: jpgData == nil, - SequenceNumber: sequencer.NextSequenceNumber(), - Timestamp: packet.Timestamp, - }, - Payload: p, - } - if err := push(&clone); err != nil { - return err - } + if offset > 0 { + h1[1] = byte(offset >> 16) + h1[2] = byte(offset >> 8) + h1[3] = byte(offset) + p = append(p, h1...) + } else { + p = append(p, h1...) + p = append(p, h2...) } - return nil + dataLen := packetSize - len(p) + if dataLen < len(jpgData) { + p = append(p, jpgData[:dataLen]...) + jpgData = jpgData[dataLen:] + offset += dataLen + } else { + p = append(p, jpgData...) + jpgData = nil + } + + clone := rtp.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: jpgData == nil, + SequenceNumber: sequencer.NextSequenceNumber(), + Timestamp: packet.Timestamp, + }, + Payload: p, + } + handlerFunc(&clone) } } } diff --git a/pkg/mp4/consumer.go b/pkg/mp4/consumer.go index 496ae65b..47555540 100644 --- a/pkg/mp4/consumer.go +++ b/pkg/mp4/consumer.go @@ -3,176 +3,165 @@ package mp4 import ( "encoding/json" "github.com/AlexxIT/go2rtc/pkg/aac" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h265" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/pion/rtp" - "sync/atomic" ) type Consumer struct { - streamer.Element + core.Listener - Medias []*streamer.Media + Medias []*core.Media UserAgent string RemoteAddr string - muxer *Muxer - codecs []*streamer.Codec - wait byte + senders []*core.Sender - send uint32 + muxer *Muxer + wait byte + + send int } -// ParseQuery - like usual parse, but with mp4 param handler -func ParseQuery(query map[string][]string) []*streamer.Media { - if query["mp4"] != nil { - cons := Consumer{} - return cons.GetMedias() - } - - return streamer.ParseQuery(query) -} - -const ( - waitNone byte = iota - waitKeyframe - waitInit -) - -func (c *Consumer) GetMedias() []*streamer.Media { - if c.Medias != nil { - return c.Medias - } - - // default medias - return []*streamer.Media{ - { - Kind: streamer.KindVideo, - Direction: streamer.DirectionRecvonly, - Codecs: []*streamer.Codec{ - {Name: streamer.CodecH264}, - {Name: streamer.CodecH265}, +func (c *Consumer) GetMedias() []*core.Media { + if c.Medias == nil { + // default local medias + c.Medias = []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecH264}, + {Name: core.CodecH265}, + }, }, - }, - { - Kind: streamer.KindAudio, - Direction: streamer.DirectionRecvonly, - Codecs: []*streamer.Codec{ - {Name: streamer.CodecAAC}, + { + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecAAC}, + }, }, - }, + } } + + return c.Medias } -func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { - trackID := byte(len(c.codecs)) - c.codecs = append(c.codecs, track.Codec) +func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { + trackID := byte(len(c.senders)) - codec := track.Codec - switch codec.Name { - case streamer.CodecH264: + handler := core.NewSender(media, track.Codec) + + switch track.Codec.Name { + case core.CodecH264: c.wait = waitInit - push := func(packet *rtp.Packet) error { + handler.Handler = func(packet *rtp.Packet) { if packet.Version != h264.RTPPacketVersionAVC { - return nil + return } if c.wait != waitNone { if c.wait == waitInit || !h264.IsKeyframe(packet.Payload) { - return nil + return } c.wait = waitNone } buf := c.muxer.Marshal(trackID, packet) - atomic.AddUint32(&c.send, uint32(len(buf))) c.Fire(buf) - return nil + c.send += len(buf) } - var wrapper streamer.WrapperFunc - if codec.IsRTP() { - wrapper = h264.RTPDepay(track) + if track.Codec.IsRTP() { + handler.Handler = h264.RTPDepay(track.Codec, handler.Handler) } else { - wrapper = h264.RepairAVC(track) + handler.Handler = h264.RepairAVC(track.Codec, handler.Handler) } - push = wrapper(push) - return track.Bind(push) - - case streamer.CodecH265: + case core.CodecH265: c.wait = waitInit - push := func(packet *rtp.Packet) error { + handler.Handler = func(packet *rtp.Packet) { if packet.Version != h264.RTPPacketVersionAVC { - return nil + return } if c.wait != waitNone { if c.wait == waitInit || !h265.IsKeyframe(packet.Payload) { - return nil + return } c.wait = waitNone } buf := c.muxer.Marshal(trackID, packet) - atomic.AddUint32(&c.send, uint32(len(buf))) c.Fire(buf) - return nil + c.send += len(buf) } - if codec.IsRTP() { - wrapper := h265.RTPDepay(track) - push = wrapper(push) + if track.Codec.IsRTP() { + handler.Handler = h265.RTPDepay(track.Codec, handler.Handler) } - return track.Bind(push) - - case streamer.CodecAAC: - push := func(packet *rtp.Packet) error { + case core.CodecAAC: + handler.Handler = func(packet *rtp.Packet) { if c.wait != waitNone { - return nil + return } buf := c.muxer.Marshal(trackID, packet) - atomic.AddUint32(&c.send, uint32(len(buf))) c.Fire(buf) - return nil + c.send += len(buf) } - if codec.IsRTP() { - wrapper := aac.RTPDepay(track) - push = wrapper(push) + if track.Codec.IsRTP() { + handler.Handler = aac.RTPDepay(handler.Handler) } - return track.Bind(push) - - case streamer.CodecOpus, streamer.CodecMP3, streamer.CodecPCMU, streamer.CodecPCMA: - push := func(packet *rtp.Packet) error { + case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA: + handler.Handler = func(packet *rtp.Packet) { if c.wait != waitNone { - return nil + return } buf := c.muxer.Marshal(trackID, packet) - atomic.AddUint32(&c.send, uint32(len(buf))) c.Fire(buf) - return nil + c.send += len(buf) } - return track.Bind(push) + default: + panic("unsupported codec") } - panic("unsupported codec") + handler.HandleRTP(track) + c.senders = append(c.senders, handler) + + return nil +} + +func (c *Consumer) Stop() error { + for _, sender := range c.senders { + sender.Close() + } + return nil +} + +func (c *Consumer) Codecs() []*core.Codec { + codecs := make([]*core.Codec, len(c.senders)) + for i, sender := range c.senders { + codecs[i] = sender.Codec + } + return codecs } func (c *Consumer) MimeCodecs() string { - return c.muxer.MimeCodecs(c.codecs) + return c.muxer.MimeCodecs(c.Codecs()) } func (c *Consumer) MimeType() string { @@ -181,7 +170,7 @@ func (c *Consumer) MimeType() string { func (c *Consumer) Init() ([]byte, error) { c.muxer = &Muxer{} - return c.muxer.GetInit(c.codecs) + return c.muxer.GetInit(c.Codecs()) } func (c *Consumer) Start() { @@ -190,14 +179,14 @@ func (c *Consumer) Start() { } } -// - func (c *Consumer) MarshalJSON() ([]byte, error) { - info := &streamer.Info{ - Type: "MP4 client", + info := &core.Info{ + Type: "MP4 passive consumer", RemoteAddr: c.RemoteAddr, UserAgent: c.UserAgent, - Send: atomic.LoadUint32(&c.send), + Medias: c.Medias, + Senders: c.senders, + Send: c.send, } return json.Marshal(info) } diff --git a/pkg/mp4/helpers.go b/pkg/mp4/helpers.go new file mode 100644 index 00000000..909b59cb --- /dev/null +++ b/pkg/mp4/helpers.go @@ -0,0 +1,19 @@ +package mp4 + +import "github.com/AlexxIT/go2rtc/pkg/core" + +// ParseQuery - like usual parse, but with mp4 param handler +func ParseQuery(query map[string][]string) []*core.Media { + if query["mp4"] != nil { + cons := Consumer{} + return cons.GetMedias() + } + + return core.ParseQuery(query) +} + +const ( + waitNone byte = iota + waitKeyframe + waitInit +) diff --git a/pkg/mp4/muxer.go b/pkg/mp4/muxer.go index cd7d9762..067902a8 100644 --- a/pkg/mp4/muxer.go +++ b/pkg/mp4/muxer.go @@ -2,10 +2,10 @@ package mp4 import ( "encoding/hex" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h265" "github.com/AlexxIT/go2rtc/pkg/iso" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/deepch/vdk/codec/h264parser" "github.com/deepch/vdk/codec/h265parser" "github.com/pion/rtp" @@ -24,7 +24,7 @@ const ( MimeOpus = "opus" ) -func (m *Muxer) MimeCodecs(codecs []*streamer.Codec) string { +func (m *Muxer) MimeCodecs(codecs []*core.Codec) string { var s string for i, codec := range codecs { @@ -33,15 +33,15 @@ func (m *Muxer) MimeCodecs(codecs []*streamer.Codec) string { } switch codec.Name { - case streamer.CodecH264: + case core.CodecH264: s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine) - case streamer.CodecH265: + case core.CodecH265: // H.265 profile=main level=5.1 // hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome s += MimeH265 - case streamer.CodecAAC: + case core.CodecAAC: s += MimeAAC - case streamer.CodecOpus: + case core.CodecOpus: s += MimeOpus } } @@ -49,7 +49,7 @@ func (m *Muxer) MimeCodecs(codecs []*streamer.Codec) string { return s } -func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) { +func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) { mv := iso.NewMovie(1024) mv.WriteFileType() @@ -58,7 +58,7 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) { for i, codec := range codecs { switch codec.Name { - case streamer.CodecH264: + case core.CodecH264: sps, pps := h264.GetParameterSet(codec.FmtpLine) if sps == nil { // some dummy SPS and PPS not a problem @@ -77,7 +77,7 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) { codecData.AVCDecoderConfRecordBytes(), ) - case streamer.CodecH265: + case core.CodecH265: vps, sps, pps := h265.GetParameterSet(codec.FmtpLine) if sps == nil { // some dummy SPS and PPS not a problem @@ -97,8 +97,8 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) { codecData.AVCDecoderConfRecordBytes(), ) - case streamer.CodecAAC: - s := streamer.Between(codec.FmtpLine, "config=", ";") + case core.CodecAAC: + s := core.Between(codec.FmtpLine, "config=", ";") b, err := hex.DecodeString(s) if err != nil { return nil, err @@ -108,7 +108,7 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) { uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, b, ) - case streamer.CodecOpus, streamer.CodecMP3, streamer.CodecPCMU, streamer.CodecPCMA: + case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA: mv.WriteAudioTrack( uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil, ) diff --git a/pkg/mp4/segment.go b/pkg/mp4/segment.go index 6fac7a40..f6ca82ec 100644 --- a/pkg/mp4/segment.go +++ b/pkg/mp4/segment.go @@ -2,48 +2,49 @@ package mp4 import ( "encoding/json" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h265" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/pion/rtp" - "sync/atomic" ) type Segment struct { - streamer.Element + core.Listener - Medias []*streamer.Media + Medias []*core.Media UserAgent string RemoteAddr string + senders []*core.Sender + MimeType string OnlyKeyframe bool - send uint32 + send int } -func (c *Segment) GetMedias() []*streamer.Media { +func (c *Segment) GetMedias() []*core.Media { if c.Medias != nil { return c.Medias } - // default medias - return []*streamer.Media{ + // default local medias + return []*core.Media{ { - Kind: streamer.KindVideo, - Direction: streamer.DirectionRecvonly, - Codecs: []*streamer.Codec{ - {Name: streamer.CodecH264}, - {Name: streamer.CodecH265}, + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecH264}, + {Name: core.CodecH265}, }, }, } } -func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { +func (c *Segment) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { muxer := &Muxer{} - codecs := []*streamer.Codec{track.Codec} + codecs := []*core.Codec{track.Codec} init, err := muxer.GetInit(codecs) if err != nil { @@ -52,26 +53,26 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream c.MimeType = `video/mp4; codecs="` + muxer.MimeCodecs(codecs) + `"` + handler := core.NewSender(media, track.Codec) + switch track.Codec.Name { - case streamer.CodecH264: - var push streamer.WriterFunc + case core.CodecH264: if c.OnlyKeyframe { - push = func(packet *rtp.Packet) error { + handler.Handler = func(packet *rtp.Packet) { if !h264.IsKeyframe(packet.Payload) { - return nil + return } buf := muxer.Marshal(0, packet) - atomic.AddUint32(&c.send, uint32(len(buf))) c.Fire(append(init, buf...)) - return nil + c.send += len(buf) } } else { var buf []byte - push = func(packet *rtp.Packet) error { + handler.Handler = func(packet *rtp.Packet) { if h264.IsKeyframe(packet.Payload) { // fist frame - send only IFrame // other frames - send IFrame and all PFrames @@ -81,9 +82,10 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream buf = append(buf, b...) } - atomic.AddUint32(&c.send, uint32(len(buf))) c.Fire(buf) + c.send += len(buf) + buf = buf[:0] buf = append(buf, init...) muxer.Reset() @@ -93,51 +95,56 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream b := muxer.Marshal(0, packet) buf = append(buf, b...) } - - return nil } } - var wrapper streamer.WrapperFunc if track.Codec.IsRTP() { - wrapper = h264.RTPDepay(track) + handler.Handler = h264.RTPDepay(track.Codec, handler.Handler) } else { - wrapper = h264.RepairAVC(track) + handler.Handler = h264.RepairAVC(track.Codec, handler.Handler) } - push = wrapper(push) - return track.Bind(push) - - case streamer.CodecH265: - push := func(packet *rtp.Packet) error { + case core.CodecH265: + handler.Handler = func(packet *rtp.Packet) { if !h265.IsKeyframe(packet.Payload) { - return nil + return } buf := muxer.Marshal(0, packet) - atomic.AddUint32(&c.send, uint32(len(buf))) c.Fire(append(init, buf...)) - return nil + c.send += len(buf) } if track.Codec.IsRTP() { - wrapper := h265.RTPDepay(track) - push = wrapper(push) + handler.Handler = h265.RTPDepay(track.Codec, handler.Handler) } - return track.Bind(push) + default: + panic(core.UnsupportedCodec) } - panic("unsupported codec") + handler.HandleRTP(track) + c.senders = append(c.senders, handler) + + return nil +} + +func (c *Segment) Stop() error { + for _, sender := range c.senders { + sender.Close() + } + return nil } func (c *Segment) MarshalJSON() ([]byte, error) { - info := &streamer.Info{ - Type: "WS/MP4 client", + info := &core.Info{ + Type: "MP4/WebSocket passive consumer", RemoteAddr: c.RemoteAddr, UserAgent: c.UserAgent, - Send: atomic.LoadUint32(&c.send), + Medias: c.Medias, + Senders: c.senders, + Send: c.send, } return json.Marshal(info) } diff --git a/pkg/mp4/v1/consumer.go b/pkg/mp4/v1/.consumer.go similarity index 80% rename from pkg/mp4/v1/consumer.go rename to pkg/mp4/v1/.consumer.go index 491b668a..b09c5ff9 100644 --- a/pkg/mp4/v1/consumer.go +++ b/pkg/mp4/v1/.consumer.go @@ -3,8 +3,8 @@ package mp4 import ( "encoding/hex" "github.com/AlexxIT/go2rtc/pkg/aac" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/deepch/vdk/av" "github.com/deepch/vdk/codec/aacparser" "github.com/deepch/vdk/codec/h264parser" @@ -14,9 +14,9 @@ import ( ) type Consumer struct { - streamer.Element + core.Listener - Medias []*streamer.Media + Medias []*core.Media UserAgent string RemoteAddr string @@ -28,35 +28,35 @@ type Consumer struct { send int } -func (c *Consumer) GetMedias() []*streamer.Media { +func (c *Consumer) GetMedias() []*core.Media { if c.Medias != nil { return c.Medias } - return []*streamer.Media{ + return []*core.Media{ { - Kind: streamer.KindVideo, - Direction: streamer.DirectionRecvonly, - Codecs: []*streamer.Codec{ - {Name: streamer.CodecH264, ClockRate: 90000}, + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecH264, ClockRate: 90000}, }, }, { - Kind: streamer.KindAudio, - Direction: streamer.DirectionRecvonly, - Codecs: []*streamer.Codec{ - {Name: streamer.CodecAAC, ClockRate: 16000}, + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecAAC, ClockRate: 16000}, }, }, } } -func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { +func (c *Consumer) AddTrack(media *core.Media, track *core.Track) *core.Track { codec := track.Codec trackID := int8(len(c.streams)) switch codec.Name { - case streamer.CodecH264: + case core.CodecH264: sps, pps := h264.GetParameterSet(codec.FmtpLine) stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps) if err != nil { @@ -102,8 +102,8 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea return track.Bind(push) - case streamer.CodecAAC: - s := streamer.Between(codec.FmtpLine, "config=", ";") + case core.CodecAAC: + s := core.Between(codec.FmtpLine, "config=", ";") b, err := hex.DecodeString(s) if err != nil { diff --git a/pkg/mp4/v2/consumer.go b/pkg/mp4/v2/.consumer.go similarity index 80% rename from pkg/mp4/v2/consumer.go rename to pkg/mp4/v2/.consumer.go index 3df6e355..27b65017 100644 --- a/pkg/mp4/v2/consumer.go +++ b/pkg/mp4/v2/.consumer.go @@ -3,6 +3,7 @@ package mp4 import ( "encoding/json" "github.com/AlexxIT/go2rtc/pkg/aac" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h265" "github.com/AlexxIT/go2rtc/pkg/streamer" @@ -11,14 +12,14 @@ import ( ) type Consumer struct { - streamer.Element + core.Listener - Medias []*streamer.Media + Medias []*core.Media UserAgent string RemoteAddr string muxer *Muxer - codecs []*streamer.Codec + codecs []*core.Codec wait byte send uint32 @@ -30,38 +31,38 @@ const ( waitInit ) -func (c *Consumer) GetMedias() []*streamer.Media { +func (c *Consumer) GetMedias() []*core.Media { if c.Medias != nil { return c.Medias } // default medias - return []*streamer.Media{ + return []*core.Media{ { - Kind: streamer.KindVideo, - Direction: streamer.DirectionRecvonly, - Codecs: []*streamer.Codec{ - {Name: streamer.CodecH264}, - {Name: streamer.CodecH265}, + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecH264}, + {Name: core.CodecH265}, }, }, { - Kind: streamer.KindAudio, - Direction: streamer.DirectionRecvonly, - Codecs: []*streamer.Codec{ - {Name: streamer.CodecAAC}, + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecAAC}, }, }, } } -func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { +func (c *Consumer) AddTrack(media *core.Media, track *core.Track) *core.Track { trackID := byte(len(c.codecs)) c.codecs = append(c.codecs, track.Codec) codec := track.Codec switch codec.Name { - case streamer.CodecH264: + case core.CodecH264: c.wait = waitInit push := func(packet *rtp.Packet) error { @@ -93,7 +94,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea return track.Bind(push) - case streamer.CodecH265: + case core.CodecH265: c.wait = waitInit push := func(packet *rtp.Packet) error { @@ -164,7 +165,7 @@ func (c *Consumer) Start() { // func (c *Consumer) MarshalJSON() ([]byte, error) { - info := &streamer.Info{ + info := &core.Info{ Type: "MP4 client", RemoteAddr: c.RemoteAddr, UserAgent: c.UserAgent, diff --git a/pkg/mp4/v2/segment.go b/pkg/mp4/v2/.segment.go similarity index 77% rename from pkg/mp4/v2/segment.go rename to pkg/mp4/v2/.segment.go index 9cc3a88a..cd473c3d 100644 --- a/pkg/mp4/v2/segment.go +++ b/pkg/mp4/v2/.segment.go @@ -2,17 +2,17 @@ package mp4 import ( "encoding/json" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h265" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/pion/rtp" "sync/atomic" ) type Segment struct { - streamer.Element + core.Listener - Medias []*streamer.Media + Medias []*core.Media UserAgent string RemoteAddr string @@ -22,28 +22,28 @@ type Segment struct { send uint32 } -func (c *Segment) GetMedias() []*streamer.Media { +func (c *Segment) GetMedias() []*core.Media { if c.Medias != nil { return c.Medias } // default medias - return []*streamer.Media{ + return []*core.Media{ { - Kind: streamer.KindVideo, - Direction: streamer.DirectionRecvonly, - Codecs: []*streamer.Codec{ - {Name: streamer.CodecH264}, - {Name: streamer.CodecH265}, + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecH264}, + {Name: core.CodecH265}, }, }, } } -func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { +func (c *Segment) AddTrack(media *core.Media, track *core.Track) *core.Track { muxer := &Muxer{} - codecs := []*streamer.Codec{track.Codec} + codecs := []*core.Codec{track.Codec} init, err := muxer.GetInit(codecs) if err != nil { @@ -53,8 +53,8 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream c.MimeType = muxer.MimeType(codecs) switch track.Codec.Name { - case streamer.CodecH264: - var push streamer.WriterFunc + case core.CodecH264: + var push core.WriterFunc if c.OnlyKeyframe { push = func(packet *rtp.Packet) error { @@ -98,7 +98,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream } } - var wrapper streamer.WrapperFunc + var wrapper core.WrapperFunc if track.Codec.IsRTP() { wrapper = h264.RTPDepay(track) } else { @@ -108,7 +108,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream return track.Bind(push) - case streamer.CodecH265: + case core.CodecH265: push := func(packet *rtp.Packet) error { if !h265.IsKeyframe(packet.Payload) { return nil @@ -133,7 +133,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream } func (c *Segment) MarshalJSON() ([]byte, error) { - info := &streamer.Info{ + info := &core.Info{ Type: "WS/MP4 client", RemoteAddr: c.RemoteAddr, UserAgent: c.UserAgent, diff --git a/pkg/mp4/v2/muxer.go b/pkg/mp4/v2/muxer.go index efd4b912..8c177449 100644 --- a/pkg/mp4/v2/muxer.go +++ b/pkg/mp4/v2/muxer.go @@ -3,9 +3,9 @@ package mp4 import ( "encoding/binary" "encoding/hex" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h265" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/deepch/vdk/av" "github.com/deepch/vdk/codec/h264parser" "github.com/deepch/vdk/codec/h265parser" @@ -21,7 +21,7 @@ type Muxer struct { pts []uint32 } -func (m *Muxer) MimeType(codecs []*streamer.Codec) string { +func (m *Muxer) MimeType(codecs []*core.Codec) string { s := `video/mp4; codecs="` for i, codec := range codecs { @@ -30,13 +30,13 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string { } switch codec.Name { - case streamer.CodecH264: + case core.CodecH264: s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine) - case streamer.CodecH265: + case core.CodecH265: // H.265 profile=main level=5.1 // hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome s += "hvc1.1.6.L153.B0" - case streamer.CodecAAC: + case core.CodecAAC: s += "mp4a.40.2" } } @@ -44,12 +44,12 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string { return s + `"` } -func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) { +func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) { moov := MOOV() for i, codec := range codecs { switch codec.Name { - case streamer.CodecH264: + case core.CodecH264: sps, pps := h264.GetParameterSet(codec.FmtpLine) if sps == nil { // some dummy SPS and PPS not a problem @@ -92,7 +92,7 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) { moov.Tracks = append(moov.Tracks, trak) - case streamer.CodecH265: + case core.CodecH265: vps, sps, pps := h265.GetParameterSet(codec.FmtpLine) if sps == nil { // some dummy SPS and PPS not a problem @@ -136,8 +136,8 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) { moov.Tracks = append(moov.Tracks, trak) - case streamer.CodecAAC: - s := streamer.Between(codec.FmtpLine, "config=", ";") + case core.CodecAAC: + s := core.Between(codec.FmtpLine, "config=", ";") b, err := hex.DecodeString(s) if err != nil { return nil, err diff --git a/pkg/mpegts/client.go b/pkg/mpegts/client.go index f194f3b1..4ef41813 100644 --- a/pkg/mpegts/client.go +++ b/pkg/mpegts/client.go @@ -1,17 +1,19 @@ package mpegts import ( - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "net/http" ) type Client struct { - streamer.Element + core.Listener - medias []*streamer.Media - tracks map[byte]*streamer.Track + medias []*core.Media + receivers []*core.Receiver res *http.Response + + recv int } func NewClient(res *http.Response) *Client { @@ -19,46 +21,50 @@ func NewClient(res *http.Response) *Client { } func (c *Client) Handle() error { - if c.tracks == nil { - c.tracks = map[byte]*streamer.Track{} - } - reader := NewReader() b := make([]byte, 1024*1024*256) // 256K - probe := streamer.NewProbe(c.medias == nil) + probe := core.NewProbe(c.medias == nil) for probe == nil || probe.Active() { n, err := c.res.Body.Read(b) if err != nil { return err } + c.recv += n + reader.AppendBuffer(b[:n]) + reading: for { packet := reader.GetPacket() if packet == nil { break } - track := c.tracks[packet.PayloadType] - if track == nil { - // count track on probe state even if not support it - probe.Append(packet.PayloadType) - - media := GetMedia(packet) - if media == nil { - continue // unsupported codec + for _, receiver := range c.receivers { + if receiver.ID == packet.PayloadType { + receiver.WriteRTP(packet) + continue reading } - - track = streamer.NewTrack(media, nil) - - c.medias = append(c.medias, media) - c.tracks[packet.PayloadType] = track } - _ = track.WriteRTP(packet) + // count track on probe state even if not support it + probe.Append(packet.PayloadType) + + media := GetMedia(packet) + if media == nil { + continue // unsupported codec + } + + c.medias = append(c.medias, media) + + receiver := core.NewReceiver(media, media.Codecs[0]) + receiver.ID = packet.PayloadType + c.receivers = append(c.receivers, receiver) + + receiver.WriteRTP(packet) //log.Printf("[AVC] %v, len: %d, pts: %d ts: %10d", h264.Types(packet.Payload), len(packet.Payload), pkt.PTS, packet.Timestamp) } diff --git a/pkg/mpegts/helpers.go b/pkg/mpegts/helpers.go index 21eb7dee..037078a7 100644 --- a/pkg/mpegts/helpers.go +++ b/pkg/mpegts/helpers.go @@ -1,8 +1,8 @@ package mpegts import ( + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/pion/rtp" "time" ) @@ -13,7 +13,7 @@ const ( ) const ( - StreamTypePrivate = 0x06 // PCMU or PCMA from FFmpeg + StreamTypePrivate = 0x06 // PCMU or PCMA or FLAC from FFmpeg StreamTypeAAC = 0x0F StreamTypeH264 = 0x1B StreamTypePCMATapo = 0x90 @@ -153,34 +153,34 @@ func ParseTime(b []byte) uint32 { return (uint32(b[0]&0x0E) << 29) | (uint32(b[1]) << 22) | (uint32(b[2]&0xFE) << 14) | (uint32(b[3]) << 7) | (uint32(b[4]) >> 1) } -func GetMedia(pkt *rtp.Packet) *streamer.Media { - var codec *streamer.Codec +func GetMedia(pkt *rtp.Packet) *core.Media { + var codec *core.Codec var kind string switch pkt.PayloadType { case StreamTypeH264: - codec = &streamer.Codec{ - Name: streamer.CodecH264, + codec = &core.Codec{ + Name: core.CodecH264, ClockRate: 90000, - PayloadType: streamer.PayloadTypeRAW, + PayloadType: core.PayloadTypeRAW, FmtpLine: h264.GetFmtpLine(pkt.Payload), } - kind = streamer.KindVideo + kind = core.KindVideo case StreamTypePCMATapo: - codec = &streamer.Codec{ - Name: streamer.CodecPCMA, + codec = &core.Codec{ + Name: core.CodecPCMA, ClockRate: 8000, } - kind = streamer.KindAudio + kind = core.KindAudio default: return nil } - return &streamer.Media{ + return &core.Media{ Kind: kind, - Direction: streamer.DirectionSendonly, - Codecs: []*streamer.Codec{codec}, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{codec}, } } diff --git a/pkg/mpegts/mpegts_test.go b/pkg/mpegts/mpegts_test.go index 8dbf4ead..e1cc379f 100644 --- a/pkg/mpegts/mpegts_test.go +++ b/pkg/mpegts/mpegts_test.go @@ -21,28 +21,28 @@ func dec(s string) []byte { return b } -func TestStream(t *testing.T) { - // ffmpeg - annexb := dec("00000001 09f0 00000001 6764001fac2484014016ec0440000003004000000c23c60c92 00000001 68ee32c8b0 000001 6588808003 00000001 09") - avc, i := ParseAVC(annexb) - assert.Equal(t, dec("00000019 6764001fac2484014016ec0440000003004000000c23c60c92 00000005 68ee32c8b0 00000005 6588808003"), avc) - assert.Equal(t, dec("00000001 09"), annexb[i:]) - - // http mpeg ts - annexb = dec("00000001 0950 000001 6764001facd2014016e8400000fa400030e081 000001 68ea8f2c 000001 65b8400eff 00000001 09") - avc, i = ParseAVC(annexb) - assert.Equal(t, dec("00000013 6764001facd2014016e8400000fa400030e081 00000004 68ea8f2c 00000005 65b8400eff"), avc) - assert.Equal(t, dec("00000001 09"), annexb[i:]) - - // tapo TC60 - annexb = dec("00000001 67640028ac1ad00a00b74dc0404050000003001000000301e8f1422a 00000001 68ee04c92240 00000001 45b80000d0 00000001 67") - avc, i = ParseAVC(annexb) - assert.Equal(t, dec("0000001C 67640028ac1ad00a00b74dc0404050000003001000000301e8f1422a 00000006 68ee04c92240 00000005 45b80000d0"), avc) - assert.Equal(t, dec("00000001 67"), annexb[i:]) - - // Tapo ? - annexb = dec("00000001 674d0032e90048014742000007d2000138d108 00000001 68ea8f20 00000001 65b8400cff 00000001 67") - avc, i = ParseAVC(annexb) - assert.Equal(t, dec("00000013 674d0032e90048014742000007d2000138d108 00000004 68ea8f20 00000005 65b8400cff"), avc) - assert.Equal(t, dec("00000001 67"), annexb[i:]) -} +//func TestStream(t *testing.T) { +// // ffmpeg +// annexb := dec("00000001 09f0 00000001 6764001fac2484014016ec0440000003004000000c23c60c92 00000001 68ee32c8b0 000001 6588808003 00000001 09") +// avc, i := ParseAVC(annexb) +// assert.Equal(t, dec("00000019 6764001fac2484014016ec0440000003004000000c23c60c92 00000005 68ee32c8b0 00000005 6588808003"), avc) +// assert.Equal(t, dec("00000001 09"), annexb[i:]) +// +// // http mpeg ts +// annexb = dec("00000001 0950 000001 6764001facd2014016e8400000fa400030e081 000001 68ea8f2c 000001 65b8400eff 00000001 09") +// avc, i = ParseAVC(annexb) +// assert.Equal(t, dec("00000013 6764001facd2014016e8400000fa400030e081 00000004 68ea8f2c 00000005 65b8400eff"), avc) +// assert.Equal(t, dec("00000001 09"), annexb[i:]) +// +// // tapo TC60 +// annexb = dec("00000001 67640028ac1ad00a00b74dc0404050000003001000000301e8f1422a 00000001 68ee04c92240 00000001 45b80000d0 00000001 67") +// avc, i = ParseAVC(annexb) +// assert.Equal(t, dec("0000001C 67640028ac1ad00a00b74dc0404050000003001000000301e8f1422a 00000006 68ee04c92240 00000005 45b80000d0"), avc) +// assert.Equal(t, dec("00000001 67"), annexb[i:]) +// +// // Tapo ? +// annexb = dec("00000001 674d0032e90048014742000007d2000138d108 00000001 68ea8f20 00000001 65b8400cff 00000001 67") +// avc, i = ParseAVC(annexb) +// assert.Equal(t, dec("00000013 674d0032e90048014742000007d2000138d108 00000004 68ea8f20 00000005 65b8400cff"), avc) +// assert.Equal(t, dec("00000001 67"), annexb[i:]) +//} diff --git a/pkg/mpegts/producer.go b/pkg/mpegts/producer.go index 03e39684..d75d7a21 100644 --- a/pkg/mpegts/producer.go +++ b/pkg/mpegts/producer.go @@ -1,20 +1,21 @@ package mpegts import ( - "github.com/AlexxIT/go2rtc/pkg/streamer" + "encoding/json" + "github.com/AlexxIT/go2rtc/pkg/core" ) -func (c *Client) GetMedias() []*streamer.Media { +func (c *Client) GetMedias() []*core.Media { return c.medias } -func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track { - for _, track := range c.tracks { +func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + for _, track := range c.receivers { if track.Codec == codec { - return track + return track, nil } } - return nil + return nil, core.ErrCantGetTrack } func (c *Client) Start() error { @@ -22,5 +23,19 @@ func (c *Client) Start() error { } func (c *Client) Stop() error { + for _, receiver := range c.receivers { + receiver.Close() + } return c.Close() } + +func (c *Client) MarshalJSON() ([]byte, error) { + info := &core.Info{ + Type: "MPEG-TS active producer", + URL: c.res.Request.URL.String(), + Medias: c.medias, + Receivers: c.receivers, + Recv: c.recv, + } + return json.Marshal(info) +} diff --git a/pkg/mpegts/ts.go b/pkg/mpegts/ts.go index e9f0051b..61182916 100644 --- a/pkg/mpegts/ts.go +++ b/pkg/mpegts/ts.go @@ -3,24 +3,26 @@ package mpegts import ( "bytes" "encoding/hex" + "encoding/json" "github.com/AlexxIT/go2rtc/pkg/aac" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/deepch/vdk/av" "github.com/deepch/vdk/codec/aacparser" "github.com/deepch/vdk/codec/h264parser" "github.com/deepch/vdk/format/ts" "github.com/pion/rtp" - "sync/atomic" "time" ) type Consumer struct { - streamer.Element + core.Listener UserAgent string RemoteAddr string + senders []*core.Sender + buf *bytes.Buffer muxer *ts.Muxer mimeType string @@ -28,35 +30,36 @@ type Consumer struct { start bool init []byte - send uint32 + send int } -func (c *Consumer) GetMedias() []*streamer.Media { - return []*streamer.Media{ +func (c *Consumer) GetMedias() []*core.Media { + return []*core.Media{ { - Kind: streamer.KindVideo, - Direction: streamer.DirectionRecvonly, - Codecs: []*streamer.Codec{ - {Name: streamer.CodecH264}, + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecH264}, }, }, //{ - // Kind: streamer.KindAudio, - // Direction: streamer.DirectionRecvonly, - // Codecs: []*streamer.Codec{ - // {Name: streamer.CodecAAC}, + // Kind: core.KindAudio, + // Direction: core.DirectionSendonly, + // Codecs: []*core.Codec{ + // {Name: core.CodecAAC}, // }, //}, } } -func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { - codec := track.Codec +func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { trackID := int8(len(c.streams)) - switch codec.Name { - case streamer.CodecH264: - sps, pps := h264.GetParameterSet(codec.FmtpLine) + handler := core.NewSender(media, track.Codec) + + switch track.Codec.Name { + case core.CodecH264: + sps, pps := h264.GetParameterSet(track.Codec.FmtpLine) stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps) if err != nil { return nil @@ -66,21 +69,21 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea c.mimeType += "," } - c.mimeType += "avc1." + h264.GetProfileLevelID(codec.FmtpLine) + c.mimeType += "avc1." + h264.GetProfileLevelID(track.Codec.FmtpLine) c.streams = append(c.streams, stream) pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond} - ts2time := time.Second / time.Duration(codec.ClockRate) + ts2time := time.Second / time.Duration(track.Codec.ClockRate) - push := func(packet *rtp.Packet) error { + handler.Handler = func(packet *rtp.Packet) { if packet.Version != h264.RTPPacketVersionAVC { - return nil + return } if !c.start { - return nil + return } pkt.Data = packet.Payload @@ -91,28 +94,26 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea pkt.Time = newTime if err = c.muxer.WritePacket(pkt); err != nil { - return err + return } // clone bytes from buffer, so next packet won't overwrite it buf := append([]byte{}, c.buf.Bytes()...) - atomic.AddUint32(&c.send, uint32(len(buf))) c.Fire(buf) + c.send += len(buf) + c.buf.Reset() - - return nil } - if codec.IsRTP() { - wrapper := h264.RTPDepay(track) - push = wrapper(push) + if track.Codec.IsRTP() { + handler.Handler = h264.RTPDepay(track.Codec, handler.Handler) + } else { + handler.Handler = h264.RepairAVC(track.Codec, handler.Handler) } - return track.Bind(push) - - case streamer.CodecAAC: - s := streamer.Between(codec.FmtpLine, "config=", ";") + case core.CodecAAC: + s := core.Between(track.Codec.FmtpLine, "config=", ";") b, err := hex.DecodeString(s) if err != nil { @@ -133,11 +134,11 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond} - ts2time := time.Second / time.Duration(codec.ClockRate) + ts2time := time.Second / time.Duration(track.Codec.ClockRate) - push := func(packet *rtp.Packet) error { + handler.Handler = func(packet *rtp.Packet) { if !c.start { - return nil + return } pkt.Data = packet.Payload @@ -147,29 +148,31 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea } pkt.Time = newTime - if err := c.muxer.WritePacket(pkt); err != nil { - return err + if err = c.muxer.WritePacket(pkt); err != nil { + return } // clone bytes from buffer, so next packet won't overwrite it buf := append([]byte{}, c.buf.Bytes()...) - atomic.AddUint32(&c.send, uint32(len(buf))) c.Fire(buf) + c.send += len(buf) + c.buf.Reset() - - return nil } - if codec.IsRTP() { - wrapper := aac.RTPDepay(track) - push = wrapper(push) + if track.Codec.IsRTP() { + handler.Handler = aac.RTPDepay(handler.Handler) } - return track.Bind(push) + default: + panic("unsupported codec") } - panic("unsupported codec") + handler.HandleRTP(track) + c.senders = append(c.senders, handler) + + return nil } func (c *Consumer) MimeCodecs() string { @@ -192,3 +195,22 @@ func (c *Consumer) Init() ([]byte, error) { func (c *Consumer) Start() { c.start = true } + +func (c *Consumer) Stop() error { + for _, sender := range c.senders { + sender.Close() + } + return nil +} + +func (c *Consumer) MarshalJSON() ([]byte, error) { + info := &core.Info{ + Type: "TS passive consumer", + RemoteAddr: c.RemoteAddr, + UserAgent: c.UserAgent, + Medias: c.GetMedias(), + Senders: c.senders, + Send: c.send, + } + return json.Marshal(info) +} diff --git a/pkg/ngrok/ngrok.go b/pkg/ngrok/ngrok.go index 82773de7..1a07d730 100644 --- a/pkg/ngrok/ngrok.go +++ b/pkg/ngrok/ngrok.go @@ -3,14 +3,14 @@ package ngrok import ( "bufio" "encoding/json" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "io" "os/exec" "strings" ) type Ngrok struct { - streamer.Element + core.Listener Tunnels map[string]string diff --git a/pkg/rtmp/client.go b/pkg/rtmp/client.go index 8ee885d8..dc3ed64d 100644 --- a/pkg/rtmp/client.go +++ b/pkg/rtmp/client.go @@ -4,15 +4,14 @@ import ( "encoding/base64" "encoding/hex" "fmt" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/httpflv" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/deepch/vdk/av" "github.com/deepch/vdk/codec/aacparser" "github.com/deepch/vdk/codec/h264parser" "github.com/deepch/vdk/format/rtmp" "github.com/pion/rtp" "net/http" - "sync/atomic" "time" ) @@ -24,17 +23,17 @@ type Conn interface { } type Client struct { - streamer.Element + core.Listener URI string - medias []*streamer.Media - tracks []*streamer.Track + medias []*core.Media + receivers []*core.Receiver conn Conn closed bool - recv uint32 + recv int } func NewClient(uri string) *Client { @@ -74,61 +73,55 @@ func (c *Client) Describe() (err error) { base64.StdEncoding.EncodeToString(info.PPS[0]), ) - codec := &streamer.Codec{ - Name: streamer.CodecH264, + codec := &core.Codec{ + Name: core.CodecH264, ClockRate: 90000, FmtpLine: fmtp, - PayloadType: streamer.PayloadTypeRAW, + PayloadType: core.PayloadTypeRAW, } - media := &streamer.Media{ - Kind: streamer.KindVideo, - Direction: streamer.DirectionSendonly, - Codecs: []*streamer.Codec{codec}, + media := &core.Media{ + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{codec}, } c.medias = append(c.medias, media) - track := streamer.NewTrack(media, codec) - c.tracks = append(c.tracks, track) + track := core.NewReceiver(media, codec) + c.receivers = append(c.receivers, track) case av.AAC: // TODO: fix support cd := stream.(aacparser.CodecData) - codec := &streamer.Codec{ - Name: streamer.CodecAAC, + codec := &core.Codec{ + Name: core.CodecAAC, ClockRate: uint32(cd.Config.SampleRate), Channels: uint16(cd.Config.ChannelConfig), // a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588 FmtpLine: "streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=" + hex.EncodeToString(cd.ConfigBytes), - PayloadType: streamer.PayloadTypeRAW, + PayloadType: core.PayloadTypeRAW, } - media := &streamer.Media{ - Kind: streamer.KindAudio, - Direction: streamer.DirectionSendonly, - Codecs: []*streamer.Codec{codec}, + media := &core.Media{ + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{codec}, } c.medias = append(c.medias, media) - track := streamer.NewTrack(media, codec) - c.tracks = append(c.tracks, track) + track := core.NewReceiver(media, codec) + c.receivers = append(c.receivers, track) default: fmt.Printf("[rtmp] unsupported codec %+v\n", stream) } } - c.Fire(streamer.StateReady) - return } func (c *Client) Handle() (err error) { - defer c.Fire(streamer.StateNull) - - c.Fire(streamer.StatePlaying) - for { var pkt av.Packet pkt, err = c.conn.ReadPacket() @@ -139,9 +132,9 @@ func (c *Client) Handle() (err error) { return } - atomic.AddUint32(&c.recv, uint32(len(pkt.Data))) + c.recv += len(pkt.Data) - track := c.tracks[int(pkt.Idx)] + track := c.receivers[int(pkt.Idx)] // convert seconds to RTP timestamp timestamp := uint32(pkt.Time * time.Duration(track.Codec.ClockRate) / time.Second) @@ -150,7 +143,7 @@ func (c *Client) Handle() (err error) { Header: rtp.Header{Timestamp: timestamp}, Payload: pkt.Data, } - _ = track.WriteRTP(packet) + track.WriteRTP(packet) } } diff --git a/pkg/rtmp/producer.go b/pkg/rtmp/producer.go new file mode 100644 index 00000000..c74eb586 --- /dev/null +++ b/pkg/rtmp/producer.go @@ -0,0 +1,41 @@ +package rtmp + +import ( + "encoding/json" + "github.com/AlexxIT/go2rtc/pkg/core" +) + +func (c *Client) GetMedias() []*core.Media { + return c.medias +} + +func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + for _, track := range c.receivers { + if track.Codec == codec { + return track, nil + } + } + return nil, core.ErrCantGetTrack +} + +func (c *Client) Start() error { + return c.Handle() +} + +func (c *Client) Stop() error { + for _, receiver := range c.receivers { + receiver.Close() + } + return c.Close() +} + +func (c *Client) MarshalJSON() ([]byte, error) { + info := &core.Info{ + Type: "RTMP active producer", + URL: c.URI, + Medias: c.medias, + Receivers: c.receivers, + Recv: c.recv, + } + return json.Marshal(info) +} diff --git a/pkg/rtmp/streamer.go b/pkg/rtmp/streamer.go deleted file mode 100644 index 2dc38238..00000000 --- a/pkg/rtmp/streamer.go +++ /dev/null @@ -1,40 +0,0 @@ -package rtmp - -import ( - "encoding/json" - "fmt" - "github.com/AlexxIT/go2rtc/pkg/streamer" - "sync/atomic" -) - -func (c *Client) GetMedias() []*streamer.Media { - return c.medias -} - -func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track { - for _, track := range c.tracks { - if track.Codec == codec { - return track - } - } - panic(fmt.Sprintf("wrong media/codec: %+v %+v", media, codec)) -} - -func (c *Client) Start() error { - return c.Handle() -} - -func (c *Client) Stop() error { - return c.Close() -} - -func (c *Client) MarshalJSON() ([]byte, error) { - info := &streamer.Info{ - Type: "RTMP source", - URL: c.URI, - Medias: c.medias, - Tracks: c.tracks, - Recv: atomic.LoadUint32(&c.recv), - } - return json.Marshal(info) -} diff --git a/pkg/rtsp/client.go b/pkg/rtsp/client.go index f720278f..df17d531 100644 --- a/pkg/rtsp/client.go +++ b/pkg/rtsp/client.go @@ -3,115 +3,25 @@ package rtsp import ( "bufio" "crypto/tls" - "encoding/binary" "errors" "fmt" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/tcp" - "github.com/pion/rtcp" - "github.com/pion/rtp" - "io" "net" "net/http" "net/url" "strconv" "strings" - "sync" "time" ) -const ( - ProtoRTSP = "RTSP/1.0" - MethodOptions = "OPTIONS" - MethodSetup = "SETUP" - MethodTeardown = "TEARDOWN" - MethodDescribe = "DESCRIBE" - MethodPlay = "PLAY" - MethodPause = "PAUSE" - MethodAnnounce = "ANNOUNCE" - MethodRecord = "RECORD" -) - -type Mode byte - -const ( - ModeUnknown Mode = iota - ModeClientProducer // conn act as RTSP client that receive data from RTSP server (ex. camera) - ModeServerUnknown - ModeServerProducer // conn act as RTSP server that reseive data from RTSP client (ex. ffmpeg output) - ModeServerConsumer // conn act as RTSP server that send data to RTSP client (ex. ffmpeg input) -) - -type State byte - -func (s State) String() string { - switch s { - case StateNone: - return "NONE" - case StateConn: - return "CONN" - case StateSetup: - return "SETUP" - case StatePlay: - return "PLAY" - case StateHandle: - return "HANDLE" - } - return strconv.Itoa(int(s)) +func NewClient(uri string) *Conn { + return &Conn{uri: uri} } -const ( - StateNone State = iota - StateConn - StateSetup - StatePlay - StateHandle -) - -type Conn struct { - streamer.Element - - // public - - Backchannel bool - SessionName string - - Medias []*streamer.Media - Session string - UserAgent string - URL *url.URL - - // internal - - auth *tcp.Auth - conn net.Conn - mode Mode - state State - stateMu sync.Mutex - reader *bufio.Reader - sequence int - uri string - - tracks []*streamer.Track - channels map[byte]*streamer.Track - - // stats - - receive int - send int -} - -func NewClient(uri string) (*Conn, error) { - c := new(Conn) - c.mode = ModeClientProducer - c.uri = uri - return c, c.parseURI() -} - -func (c *Conn) parseURI() (err error) { - c.URL, err = url.Parse(c.uri) - if err != nil { - return err +func (c *Conn) Dial() (err error) { + if c.URL, err = url.Parse(c.uri); err != nil { + return } if strings.IndexByte(c.URL.Host, ':') < 0 { @@ -122,14 +32,6 @@ func (c *Conn) parseURI() (err error) { c.auth = tcp.NewAuth(c.URL.User) c.URL.User = nil - return nil -} - -func (c *Conn) Dial() (err error) { - if c.conn != nil { - _ = c.parseURI() - } - c.conn, err = net.DialTimeout("tcp", c.URL.Host, time.Second*5) if err != nil { return @@ -314,7 +216,7 @@ func (c *Conn) Describe() error { return err } - c.mode = ModeClientProducer + c.mode = core.ModeActiveProducer return nil } @@ -328,7 +230,7 @@ func (c *Conn) Announce() (err error) { }, } - req.Body, err = streamer.MarshalSDP(c.SessionName, c.Medias) + req.Body, err = core.MarshalSDP(c.SessionName, c.Medias) if err != nil { return err } @@ -342,7 +244,7 @@ func (c *Conn) Announce() (err error) { func (c *Conn) Setup() error { for _, media := range c.Medias { - _, err := c.SetupMedia(media, media.Codecs[0], true) + _, err := c.SetupMedia(media, true) if err != nil { return err } @@ -351,7 +253,7 @@ func (c *Conn) Setup() error { return nil } -func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bool) (*streamer.Track, error) { +func (c *Conn) SetupMedia(media *core.Media, first bool) (byte, error) { // TODO: rewrite recoonection and first flag if first { c.stateMu.Lock() @@ -359,36 +261,45 @@ func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bo } if c.state != StateConn && c.state != StateSetup { - return nil, fmt.Errorf("RTSP SETUP from wrong state: %s", c.state) + return 0, fmt.Errorf("RTSP SETUP from wrong state: %s", c.state) } - ch := c.GetChannel(media) - if ch < 0 { - return nil, fmt.Errorf("wrong media: %v", media) + var transport string + + // try to use media position as channel number + for i, m := range c.Medias { + if m.ID == media.ID { + transport = fmt.Sprintf( + // i - RTP (data channel) + // i+1 - RTCP (control channel) + "RTP/AVP/TCP;unicast;interleaved=%d-%d", i*2, i*2+1, + ) + break + } } - rawURL := media.Control + if transport == "" { + return 0, fmt.Errorf("wrong media: %v", media) + } + + rawURL := media.ID // control if !strings.Contains(rawURL, "://") { rawURL = c.URL.String() if !strings.HasSuffix(rawURL, "/") { rawURL += "/" } - rawURL += media.Control + rawURL += media.ID } trackURL, err := urlParse(rawURL) if err != nil { - return nil, err + return 0, err } req := &tcp.Request{ Method: MethodSetup, URL: trackURL, Header: map[string][]string{ - "Transport": {fmt.Sprintf( - // i - RTP (data channel) - // i+1 - RTCP (control channel) - "RTP/AVP/TCP;unicast;interleaved=%d-%d", ch*2, ch*2+1, - )}, + "Transport": {transport}, }, } @@ -400,20 +311,20 @@ func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bo if c.Backchannel { c.Backchannel = false if err := c.Dial(); err != nil { - return nil, err + return 0, err } if err := c.Describe(); err != nil { - return nil, err + return 0, err } for _, newMedia := range c.Medias { - if newMedia.Control == media.Control { - return c.SetupMedia(newMedia, newMedia.Codecs[0], false) + if newMedia.ID == media.ID { + return c.SetupMedia(newMedia, false) } } } - return nil, err + return 0, err } if c.Session == "" { @@ -426,60 +337,29 @@ func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bo } } - // in case the track has already been setup before - if codec == nil { - c.state = StateSetup - return nil, nil - } - // we send our `interleaved`, but camera can answer with another // Transport: RTP/AVP/TCP;unicast;interleaved=10-11;ssrc=10117CB7 // Transport: RTP/AVP/TCP;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0 // Transport: RTP/AVP/TCP;ssrc=22345682;interleaved=0-1 - s := res.Header.Get("Transport") - // TODO: rewrite - if !strings.HasPrefix(s, "RTP/AVP/TCP;") { + transport = res.Header.Get("Transport") + if !strings.HasPrefix(transport, "RTP/AVP/TCP;") { // Escam Q6 has a bug: // Transport: RTP/AVP;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0-1 - if !strings.Contains(s, ";interleaved=") { - return nil, fmt.Errorf("wrong transport: %s", s) + if !strings.Contains(transport, ";interleaved=") { + return 0, fmt.Errorf("wrong transport: %s", transport) } } - i := strings.Index(s, "interleaved=") - if i < 0 { - return nil, fmt.Errorf("wrong transport: %s", s) - } - - s = s[i+len("interleaved="):] - i = strings.IndexAny(s, "-;") - if i > 0 { - s = s[:i] - } - - ch, err = strconv.Atoi(s) - if err != nil { - return nil, err - } - - track := streamer.NewTrack(media, codec) - - switch track.Direction { - case streamer.DirectionSendonly: - if c.channels == nil { - c.channels = make(map[byte]*streamer.Track) - } - c.channels[byte(ch)] = track - - case streamer.DirectionRecvonly: - track = c.bindTrack(track, byte(ch), codec.PayloadType) - } - c.state = StateSetup - c.tracks = append(c.tracks, track) - return track, nil + channel := core.Between(transport, "interleaved=", "-") + i, err := strconv.Atoi(channel) + if err != nil { + return 0, err + } + + return byte(i), nil } func (c *Conn) Play() (err error) { @@ -516,224 +396,3 @@ func (c *Conn) Close() error { c.state = StateNone return c.conn.Close() } - -func (c *Conn) Handle() (err error) { - c.stateMu.Lock() - - switch c.state { - case StateNone: // Close after PLAY and before Handle is OK (because SETUP after PLAY) - case StatePlay: - c.state = StateHandle - default: - err = fmt.Errorf("RTSP HANDLE from wrong state: %s", c.state) - - c.state = StateNone - _ = c.conn.Close() - } - - ok := c.state == StateHandle - - c.stateMu.Unlock() - - if !ok { - return - } - - defer func() { - c.stateMu.Lock() - defer c.stateMu.Unlock() - - if c.state == StateNone { - err = nil - return - } - - // may have gotten here because of the deadline - // so close the connection to stop keepalive - c.state = StateNone - _ = c.conn.Close() - }() - - var timeout time.Duration - - switch c.mode { - case ModeClientProducer: - // polling frames from remote RTSP Server (ex Camera) - go c.keepalive() - - if c.HasSendTracks() { - // if we receiving video/audio from camera - timeout = time.Second * 5 - } else { - // if we only send audio to camera - timeout = time.Second * 30 - } - - case ModeServerProducer: - // polling frames from remote RTSP Client (ex FFmpeg) - timeout = time.Second * 15 - - case ModeServerConsumer: - // pushing frames to remote RTSP Client (ex VLC) - timeout = time.Second * 60 - - default: - return fmt.Errorf("wrong RTSP conn mode: %d", c.mode) - } - - for { - if c.state == StateNone { - return - } - - if err = c.conn.SetReadDeadline(time.Now().Add(timeout)); err != nil { - return - } - - // we can read: - // 1. RTP interleaved: `$` + 1B channel number + 2B size - // 2. RTSP response: RTSP/1.0 200 OK - // 3. RTSP request: OPTIONS ... - var buf4 []byte // `$` + 1B channel number + 2B size - buf4, err = c.reader.Peek(4) - if err != nil { - return - } - - var channelID byte - var size uint16 - - if buf4[0] != '$' { - switch string(buf4) { - case "RTSP": - var res *tcp.Response - if res, err = tcp.ReadResponse(c.reader); err != nil { - return - } - c.Fire(res) - continue - - case "OPTI", "TEAR", "DESC", "SETU", "PLAY", "PAUS", "RECO", "ANNO", "GET_", "SET_": - var req *tcp.Request - if req, err = tcp.ReadRequest(c.reader); err != nil { - return - } - c.Fire(req) - continue - - default: - for i := 0; ; i++ { - // search next start symbol - if _, err = c.reader.ReadBytes('$'); err != nil { - return err - } - - if channelID, err = c.reader.ReadByte(); err != nil { - return err - } - - // check if channel ID exists - if c.channels[channelID] == nil { - continue - } - - buf4 = make([]byte, 2) - if _, err = io.ReadFull(c.reader, buf4); err != nil { - return err - } - - // check if size good for RTP - size = binary.BigEndian.Uint16(buf4) - if size <= 1500 { - break - } - - // 10 tries to find good packet - if i >= 10 { - return fmt.Errorf("RTSP wrong input") - } - } - - c.Fire("RTSP wrong input") - } - } else { - // hope that the odd channels are always RTCP - channelID = buf4[1] - - // get data size - size = binary.BigEndian.Uint16(buf4[2:]) - - // skip 4 bytes from c.reader.Peek - if _, err = c.reader.Discard(4); err != nil { - return - } - } - - // init memory for data - buf := make([]byte, size) - if _, err = io.ReadFull(c.reader, buf); err != nil { - return - } - - c.receive += int(size) - - if channelID&1 == 0 { - packet := &rtp.Packet{} - if err = packet.Unmarshal(buf); err != nil { - return - } - - track := c.channels[channelID] - if track != nil { - _ = track.WriteRTP(packet) - } else { - //c.Fire("wrong channelID: " + strconv.Itoa(int(channelID))) - } - } else { - msg := &RTCP{Channel: channelID} - - if err = msg.Header.Unmarshal(buf); err != nil { - continue - } - - msg.Packets, err = rtcp.Unmarshal(buf) - if err != nil { - continue - } - - c.Fire(msg) - } - } -} - -func (c *Conn) keepalive() { - // TODO: rewrite to RTCP - req := &tcp.Request{Method: MethodOptions, URL: c.URL} - for { - time.Sleep(time.Second * 25) - if c.state == StateNone { - return - } - if err := c.Request(req); err != nil { - return - } - } -} - -func (c *Conn) GetChannel(media *streamer.Media) int { - for i, m := range c.Medias { - if m == media { - return i - } - } - return -1 -} - -func (c *Conn) HasSendTracks() bool { - for _, track := range c.tracks { - if track.Direction == streamer.DirectionSendonly { - return true - } - } - return false -} diff --git a/pkg/rtsp/conn.go b/pkg/rtsp/conn.go new file mode 100644 index 00000000..2a0add62 --- /dev/null +++ b/pkg/rtsp/conn.go @@ -0,0 +1,274 @@ +package rtsp + +import ( + "bufio" + "encoding/binary" + "fmt" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/tcp" + "github.com/pion/rtcp" + "github.com/pion/rtp" + "io" + "net" + "net/url" + "strconv" + "sync" + "time" +) + +type Conn struct { + core.Listener + + // public + + Backchannel bool + SessionName string + + Medias []*core.Media + Session string + UserAgent string + URL *url.URL + + // internal + + auth *tcp.Auth + conn net.Conn + mode core.Mode + state State + stateMu sync.Mutex + reader *bufio.Reader + sequence int + uri string + + receivers []*core.Receiver + senders []*core.Sender + + // stats + + recv int + send int +} + +const ( + ProtoRTSP = "RTSP/1.0" + MethodOptions = "OPTIONS" + MethodSetup = "SETUP" + MethodTeardown = "TEARDOWN" + MethodDescribe = "DESCRIBE" + MethodPlay = "PLAY" + MethodPause = "PAUSE" + MethodAnnounce = "ANNOUNCE" + MethodRecord = "RECORD" +) + +type State byte + +func (s State) String() string { + switch s { + case StateNone: + return "NONE" + case StateConn: + return "CONN" + case StateSetup: + return "SETUP" + case StatePlay: + return "PLAY" + case StateHandle: + return "HANDLE" + } + return strconv.Itoa(int(s)) +} + +const ( + StateNone State = iota + StateConn + StateSetup + StatePlay + StateHandle +) + +func (c *Conn) Handle() (err error) { + c.stateMu.Lock() + + switch c.state { + case StateNone: // Close after PLAY and before Handle is OK (because SETUP after PLAY) + case StatePlay: + c.state = StateHandle + default: + err = fmt.Errorf("RTSP HANDLE from wrong state: %s", c.state) + + c.state = StateNone + _ = c.conn.Close() + } + + ok := c.state == StateHandle + + c.stateMu.Unlock() + + if !ok { + return + } + + var timeout time.Duration + + switch c.mode { + case core.ModeActiveProducer: + // polling frames from remote RTSP Server (ex Camera) + go c.keepalive() + + if len(c.receivers) > 0 { + // if we receiving video/audio from camera + timeout = time.Second * 5 + } else { + // if we only send audio to camera + timeout = time.Second * 30 + } + + case core.ModePassiveProducer: + // polling frames from remote RTSP Client (ex FFmpeg) + timeout = time.Second * 15 + + case core.ModePassiveConsumer: + // pushing frames to remote RTSP Client (ex VLC) + timeout = time.Second * 60 + + default: + return fmt.Errorf("wrong RTSP conn mode: %d", c.mode) + } + + for c.state != StateNone { + if err = c.conn.SetReadDeadline(time.Now().Add(timeout)); err != nil { + return + } + + // we can read: + // 1. RTP interleaved: `$` + 1B channel number + 2B size + // 2. RTSP response: RTSP/1.0 200 OK + // 3. RTSP request: OPTIONS ... + var buf4 []byte // `$` + 1B channel number + 2B size + buf4, err = c.reader.Peek(4) + if err != nil { + return + } + + var channelID byte + var size uint16 + + if buf4[0] != '$' { + switch string(buf4) { + case "RTSP": + var res *tcp.Response + if res, err = tcp.ReadResponse(c.reader); err != nil { + return + } + c.Fire(res) + continue + + case "OPTI", "TEAR", "DESC", "SETU", "PLAY", "PAUS", "RECO", "ANNO", "GET_", "SET_": + var req *tcp.Request + if req, err = tcp.ReadRequest(c.reader); err != nil { + return + } + c.Fire(req) + continue + + default: + for i := 0; ; i++ { + // search next start symbol + if _, err = c.reader.ReadBytes('$'); err != nil { + return err + } + + if channelID, err = c.reader.ReadByte(); err != nil { + return err + } + + // TODO: better check maximum good channel ID + if channelID >= 20 { + continue + } + + buf4 = make([]byte, 2) + if _, err = io.ReadFull(c.reader, buf4); err != nil { + return err + } + + // check if size good for RTP + size = binary.BigEndian.Uint16(buf4) + if size <= 1500 { + break + } + + // 10 tries to find good packet + if i >= 10 { + return fmt.Errorf("RTSP wrong input") + } + } + + c.Fire("RTSP wrong input") + } + } else { + // hope that the odd channels are always RTCP + channelID = buf4[1] + + // get data size + size = binary.BigEndian.Uint16(buf4[2:]) + + // skip 4 bytes from c.reader.Peek + if _, err = c.reader.Discard(4); err != nil { + return + } + } + + // init memory for data + buf := make([]byte, size) + if _, err = io.ReadFull(c.reader, buf); err != nil { + return + } + + c.recv += int(size) + + if channelID&1 == 0 { + packet := &rtp.Packet{} + if err = packet.Unmarshal(buf); err != nil { + return + } + + for _, receiver := range c.receivers { + if receiver.ID == channelID { + receiver.WriteRTP(packet) + break + } + } + } else { + msg := &RTCP{Channel: channelID} + + if err = msg.Header.Unmarshal(buf); err != nil { + continue + } + + msg.Packets, err = rtcp.Unmarshal(buf) + if err != nil { + continue + } + + c.Fire(msg) + } + } + + return +} + +func (c *Conn) keepalive() { + // TODO: rewrite to RTCP + req := &tcp.Request{Method: MethodOptions, URL: c.URL} + for { + time.Sleep(time.Second * 25) + if c.state == StateNone { + return + } + if err := c.Request(req); err != nil { + return + } + } +} diff --git a/pkg/rtsp/consumer.go b/pkg/rtsp/consumer.go index e49db970..b0eaf7ce 100644 --- a/pkg/rtsp/consumer.go +++ b/pkg/rtsp/consumer.go @@ -1,112 +1,101 @@ package rtsp import ( - "encoding/binary" "github.com/AlexxIT/go2rtc/pkg/aac" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h265" "github.com/AlexxIT/go2rtc/pkg/mjpeg" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/pion/rtp" ) -func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { - switch c.mode { - // send our track to RTSP consumer (ex. FFmpeg) - case ModeServerConsumer: - i := len(c.tracks) - channelID := byte(i << 1) +func (c *Conn) GetMedias() []*core.Media { + core.Assert(c.Medias != nil) + return c.Medias +} - codec := track.Codec.Clone() - codec.PayloadType = uint8(96 + i) +func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) (err error) { + core.Assert(media.Direction == core.DirectionSendonly) - if media.MatchAll() { - // fill consumer medias list - c.Medias = append(c.Medias, &streamer.Media{ - Kind: media.Kind, Direction: media.Direction, - Codecs: []*streamer.Codec{codec}, - }) - } else { - // find consumer media and replace codec with right one - for i, m := range c.Medias { - if m == media { - media.Codecs = []*streamer.Codec{codec} - c.Medias[i] = media - break - } - } + for _, sender := range c.senders { + if sender.Codec == codec { + sender.HandleRTP(track) + return } - - track = c.bindTrack(track, channelID, codec.PayloadType) - track.Codec = codec - c.tracks = append(c.tracks, track) - - return track - - // camera with backchannel support - case ModeClientProducer: - consCodec := media.MatchCodec(track.Codec) - consTrack := c.GetTrack(media, consCodec) - if consTrack == nil { - return nil - } - - return track.Bind(func(packet *rtp.Packet) error { - return consTrack.WriteRTP(packet) - }) } - println("WARNING: rtsp: AddTrack to wrong mode") + var channel byte + + switch c.mode { + case core.ModeActiveProducer: // backchannel + if channel, err = c.SetupMedia(media, true); err != nil { + return + } + + case core.ModePassiveConsumer: + channel = byte(len(c.senders)) * 2 + + // for consumer is better to use original track codec + codec = track.Codec.Clone() + // generate new payload type, starting from 96 + codec.PayloadType = byte(96 + len(c.senders)) + + default: + panic(core.Caller()) + } + + // save original codec to sender (can have Codec.Name = ANY) + sender := core.NewSender(media, codec) + sender.Handler = c.packetWriter(codec, channel) + sender.HandleRTP(track) + + c.senders = append(c.senders, sender) return nil } -func (c *Conn) bindTrack( - track *streamer.Track, channel uint8, payloadType uint8, -) *streamer.Track { - push := func(packet *rtp.Packet) error { +func (c *Conn) packetWriter(codec *core.Codec, channel uint8) core.HandlerFunc { + handlerFunc := func(packet *rtp.Packet) { if c.state == StateNone { - return nil + return } - packet.Header.PayloadType = payloadType - size := packet.MarshalSize() + clone := *packet + clone.Header.PayloadType = codec.PayloadType - //log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", track.Codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) + size := clone.MarshalSize() + + //log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) data := make([]byte, 4+size) data[0] = '$' data[1] = channel - binary.BigEndian.PutUint16(data[2:], uint16(size)) + data[2] = byte(size >> 8) + data[3] = byte(size) - if _, err := packet.MarshalTo(data[4:]); err != nil { - return nil + if _, err := clone.MarshalTo(data[4:]); err != nil { + return } - if _, err := c.conn.Write(data); err != nil { - return err + n, err := c.conn.Write(data) + if err != nil { + return } - c.send += size - - return nil + c.send += n } - if !track.Codec.IsRTP() { - switch track.Codec.Name { - case streamer.CodecH264: - wrapper := h264.RTPPay(1500) - push = wrapper(push) - case streamer.CodecH265: - wrapper := h265.RTPPay(1500) - push = wrapper(push) - case streamer.CodecAAC: - wrapper := aac.RTPPay(1500) - push = wrapper(push) - case streamer.CodecJPEG: - wrapper := mjpeg.RTPPay() - push = wrapper(push) + if !codec.IsRTP() { + switch codec.Name { + case core.CodecH264: + handlerFunc = h264.RTPPay(1500, handlerFunc) + case core.CodecH265: + handlerFunc = h265.RTPPay(1500, handlerFunc) + case core.CodecAAC: + handlerFunc = aac.RTPPay(handlerFunc) + case core.CodecJPEG: + handlerFunc = mjpeg.RTPPay(handlerFunc) } } - return track.Bind(push) + return handlerFunc } diff --git a/pkg/rtsp/helpers.go b/pkg/rtsp/helpers.go index c43dcea5..09b0057a 100644 --- a/pkg/rtsp/helpers.go +++ b/pkg/rtsp/helpers.go @@ -2,7 +2,7 @@ package rtsp import ( "bytes" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/rtcp" "github.com/pion/sdp/v3" "net/url" @@ -22,7 +22,7 @@ o=- 0 0 IN IP4 0.0.0.0 s=- t=0 0` -func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) { +func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) { // fix bug from Reolink Doorbell if i := bytes.Index(rawSDP, []byte("a=sendonlym=")); i > 0 { rawSDP = append(rawSDP[:i+11], rawSDP[i+10:]...) @@ -47,25 +47,24 @@ func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) { } } - medias := streamer.UnmarshalMedias(sd.MediaDescriptions) + var medias []*core.Media + + for _, md := range sd.MediaDescriptions { + media := core.UnmarshalMedia(md) - for _, media := range medias { // Check buggy SDP with fmtp for H264 on another track // https://github.com/AlexxIT/WebRTC/issues/419 for _, codec := range media.Codecs { - if codec.Name == streamer.CodecH264 && codec.FmtpLine == "" { + if codec.Name == core.CodecH264 && codec.FmtpLine == "" { codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions) } } - // fix bug in ONVIF spec - // https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf - switch media.Direction { - case streamer.DirectionRecvonly, "": - media.Direction = streamer.DirectionSendonly - case streamer.DirectionSendonly: - media.Direction = streamer.DirectionRecvonly + if media.Direction == "" { + media.Direction = core.DirectionRecvonly } + + medias = append(medias, media) } return medias, nil @@ -74,7 +73,7 @@ func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) { func findFmtpLine(payloadType uint8, descriptions []*sdp.MediaDescription) string { s := strconv.Itoa(int(payloadType)) for _, md := range descriptions { - codec := streamer.UnmarshalCodec(md, s) + codec := core.UnmarshalCodec(md, s) if codec.FmtpLine != "" { return codec.FmtpLine } diff --git a/pkg/rtsp/producer.go b/pkg/rtsp/producer.go index 398d7baf..ea7aa3ea 100644 --- a/pkg/rtsp/producer.go +++ b/pkg/rtsp/producer.go @@ -3,87 +3,74 @@ package rtsp import ( "encoding/json" "fmt" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" ) -func (c *Conn) GetMedias() []*streamer.Media { - if c.Medias != nil { - return c.Medias - } +func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + core.Assert(media.Direction == core.DirectionRecvonly) - return []*streamer.Media{ - { - Kind: streamer.KindVideo, - Direction: streamer.DirectionRecvonly, - Codecs: []*streamer.Codec{ - {Name: streamer.CodecAll}, - }, - }, - { - Kind: streamer.KindAudio, - Direction: streamer.DirectionRecvonly, - Codecs: []*streamer.Codec{ - {Name: streamer.CodecAll}, - }, - }, - } -} - -func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track { - for _, track := range c.tracks { + for _, track := range c.receivers { if track.Codec == codec { - return track + return track, nil } } - // can't setup new tracks from play state - forcing a reconnection feature switch c.state { - case StatePlay, StateHandle: - go c.Close() - return streamer.NewTrack(media, codec) + case StateConn, StateSetup: + default: + return nil, fmt.Errorf("RTSP GetTrack from wrong state: %s", c.state) } - track, err := c.SetupMedia(media, codec, true) + channel, err := c.SetupMedia(media, true) if err != nil { - return nil + return nil, err } - return track + + track := core.NewReceiver(media, codec) + track.ID = byte(channel) + c.receivers = append(c.receivers, track) + + return track, nil } func (c *Conn) Start() error { switch c.mode { - case ModeClientProducer: + case core.ModeActiveProducer: if err := c.Play(); err != nil { return err } - case ModeServerProducer: + case core.ModePassiveProducer: default: return fmt.Errorf("start wrong mode: %d", c.mode) } - return c.Handle() + if err := c.Handle(); c.state != StateNone { + _ = c.conn.Close() + return err + } + + return nil } func (c *Conn) Stop() error { + for _, receiver := range c.receivers { + receiver.Close() + } + for _, sender := range c.senders { + sender.Close() + } return c.Close() } func (c *Conn) MarshalJSON() ([]byte, error) { - info := &streamer.Info{ + info := &core.Info{ + Type: "RTSP " + c.mode.String(), UserAgent: c.UserAgent, Medias: c.Medias, - Tracks: c.tracks, - Recv: uint32(c.receive), - Send: uint32(c.send), - } - - switch c.mode { - case ModeUnknown: - info.Type = "RTSP unknown" - case ModeClientProducer, ModeServerProducer: - info.Type = "RTSP source" - case ModeServerConsumer: - info.Type = "RTSP client" + Receivers: c.receivers, + Senders: c.senders, + Recv: c.recv, + Send: c.send, } if c.URL != nil { @@ -93,14 +80,5 @@ func (c *Conn) MarshalJSON() ([]byte, error) { info.RemoteAddr = c.conn.RemoteAddr().String() } - //for i, track := range c.tracks { - // k := "track:" + strconv.Itoa(i+1) - // if track.MimeType() == streamer.MimeTypeH264 { - // v[k] = h264.Describe(track.Caps()) - // } else { - // v[k] = track.MimeType() - // } - //} - return json.Marshal(info) } diff --git a/pkg/rtsp/rtsp_test.go b/pkg/rtsp/rtsp_test.go index d618fdc6..5306220f 100644 --- a/pkg/rtsp/rtsp_test.go +++ b/pkg/rtsp/rtsp_test.go @@ -1,8 +1,8 @@ package rtsp import ( + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/stretchr/testify/assert" "strings" "testing" @@ -131,7 +131,7 @@ a=appversion:1.0 assert.Nil(t, err) codec := medias[0].Codecs[0] - assert.Equal(t, streamer.CodecH264, codec.Name) + assert.Equal(t, core.CodecH264, codec.Name) sps, _ := h264.GetParameterSet(codec.FmtpLine) assert.Nil(t, sps) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index 8a1875bf..e32009bf 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -4,7 +4,7 @@ import ( "bufio" "errors" "fmt" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/tcp" "net" "net/url" @@ -14,7 +14,6 @@ import ( func NewServer(conn net.Conn) *Conn { c := new(Conn) c.conn = conn - c.mode = ModeServerUnknown c.reader = bufio.NewReader(conn) return c } @@ -24,8 +23,6 @@ func (c *Conn) Auth(username, password string) { c.auth = tcp.NewAuth(info) } -const transport = "RTP/AVP/TCP;unicast;interleaved=" - func (c *Conn) Accept() error { for { req, err := tcp.ReadRequest(c.reader) @@ -76,14 +73,13 @@ func (c *Conn) Accept() error { } // TODO: fix someday... - c.channels = map[byte]*streamer.Track{} for i, media := range c.Medias { - track := streamer.NewTrack(media, nil) - c.tracks = append(c.tracks, track) - c.channels[byte(i<<1)] = track + track := core.NewReceiver(media, media.Codecs[0]) + track.ID = byte(i * 2) + c.receivers = append(c.receivers, track) } - c.mode = ModeServerProducer + c.mode = core.ModePassiveProducer c.Fire(MethodAnnounce) res := &tcp.Response{Request: req} @@ -92,10 +88,10 @@ func (c *Conn) Accept() error { } case MethodDescribe: - c.mode = ModeServerConsumer + c.mode = core.ModePassiveConsumer c.Fire(MethodDescribe) - if c.tracks == nil { + if c.senders == nil { res := &tcp.Response{ Status: "404 Not Found", Request: req, @@ -111,17 +107,17 @@ func (c *Conn) Accept() error { } // convert tracks to real output medias medias - var medias []*streamer.Media - for _, track := range c.tracks { - media := &streamer.Media{ - Kind: streamer.GetKind(track.Codec.Name), - Direction: streamer.DirectionSendonly, - Codecs: []*streamer.Codec{track.Codec}, + var medias []*core.Media + for _, track := range c.senders { + media := &core.Media{ + Kind: core.GetKind(track.Codec.Name), + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{track.Codec}, } medias = append(medias, media) } - res.Body, err = streamer.MarshalSDP(c.SessionName, medias) + res.Body, err = core.MarshalSDP(c.SessionName, medias) if err != nil { return err } @@ -138,6 +134,7 @@ func (c *Conn) Accept() error { Request: req, } + const transport = "RTP/AVP/TCP;unicast;interleaved=" if strings.HasPrefix(tr, transport) { c.Session = "1" // TODO: fixme c.state = StateSetup diff --git a/pkg/srtp/session.go b/pkg/srtp/session.go index 54795560..9382c204 100644 --- a/pkg/srtp/session.go +++ b/pkg/srtp/session.go @@ -1,7 +1,7 @@ package srtp import ( - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/pion/srtp/v2" @@ -16,7 +16,7 @@ type Session struct { remoteCtx *srtp.Context // read context Write func(b []byte) (int, error) - Track *streamer.Track + Track *core.Receiver Recv uint32 lastSequence uint32 @@ -82,7 +82,7 @@ func (s *Session) HandleRTP(data []byte) (err error) { s.lastTimestamp = packet.Timestamp s.lastTime = now - _ = s.Track.WriteRTP(packet) + s.Track.WriteRTP(packet) return } diff --git a/pkg/streamer/helpers.go b/pkg/streamer/helpers.go deleted file mode 100644 index e674f0af..00000000 --- a/pkg/streamer/helpers.go +++ /dev/null @@ -1,104 +0,0 @@ -package streamer - -import ( - "strings" - "time" -) - -type Mode byte - -const ( - ModeActiveProducer Mode = iota + 1 // typical source (client) - ModePassiveConsumer - ModePassiveProducer - ModeActiveConsumer -) - -func (m Mode) String() string { - switch m { - case ModeActiveProducer: - return "active producer" - case ModePassiveConsumer: - return "passive consumer" - case ModePassiveProducer: - return "passive producer" - case ModeActiveConsumer: - return "active consumer" - } - return "unknown" -} - -type Info struct { - Type string `json:"type,omitempty"` - URL string `json:"url,omitempty"` - RemoteAddr string `json:"remote_addr,omitempty"` - UserAgent string `json:"user_agent,omitempty"` - Medias []*Media `json:"medias,omitempty"` - Tracks []*Track `json:"tracks,omitempty"` - Recv uint32 `json:"recv,omitempty"` - Send uint32 `json:"send,omitempty"` -} - -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 -} - -type Probe struct { - deadline time.Time - items map[interface{}]struct{} -} - -func NewProbe(enable bool) *Probe { - if enable { - return &Probe{ - deadline: time.Now().Add(time.Second * 3), - items: map[interface{}]struct{}{}, - } - } else { - return nil - } -} - -// Active return true if probe enabled and not finish -func (p *Probe) Active() bool { - return len(p.items) < 2 && time.Now().Before(p.deadline) -} - -// Append safe to run if Probe is nil -func (p *Probe) Append(v interface{}) { - if p != nil { - p.items[v] = struct{}{} - } -} diff --git a/pkg/streamer/media.go b/pkg/streamer/media.go deleted file mode 100644 index 9a806995..00000000 --- a/pkg/streamer/media.go +++ /dev/null @@ -1,352 +0,0 @@ -package streamer - -import ( - "encoding/json" - "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" - CodecJPEG = "JPEG" // payloadType: 26 - - CodecPCMU = "PCMU" // payloadType: 0 - CodecPCMA = "PCMA" // payloadType: 8 - CodecAAC = "MPEG4-GENERIC" - CodecOpus = "OPUS" // payloadType: 111 - CodecG722 = "G722" - CodecMP3 = "MPA" // payload: 14, aka MPEG-1 Layer III - - CodecELD = "ELD" // AAC-ELD - - CodecAll = "ALL" - CodecAny = "ANY" -) - -const PayloadTypeRAW byte = 255 - -func GetKind(name string) string { - switch name { - case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG: - return KindVideo - case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecELD: - 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? -} - -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) MarshalJSON() ([]byte, error) { - return json.Marshal(m.String()) -} - -func (m *Media) Clone() *Media { - clone := &Media{ - Kind: m.Kind, - Direction: m.Direction, - Codecs: make([]*Codec, len(m.Codecs)), - MID: m.MID, - Control: m.Control, - } - for i, codec := range m.Codecs { - clone.Codecs[i] = codec.Clone() - } - return clone -} - -func (m *Media) AV() bool { - return m.Kind == KindVideo || m.Kind == KindAudio -} - -func (m *Media) MatchCodec(codec *Codec) *Codec { - for _, c := range m.Codecs { - if c.Match(codec) { - return c - } - } - return nil -} - -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 { - for _, remoteCodec := range media.Codecs { - if localCodec.Match(remoteCodec) { - return localCodec - } - } - } - return nil -} - -func (m *Media) MatchAll() bool { - return len(m.Codecs) > 0 && m.Codecs[0].Name == CodecAll -} - -// 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 (c *Codec) String() string { - s := fmt.Sprintf("%d %s", c.PayloadType, c.Name) - if c.ClockRate != 90000 { - s = fmt.Sprintf("%s/%d", s, c.ClockRate) - } - if c.Channels > 0 { - s = fmt.Sprintf("%s/%d", s, c.Channels) - } - return s -} - -func (c *Codec) IsRTP() bool { - return c.PayloadType != PayloadTypeRAW -} - -func (c *Codec) Clone() *Codec { - clone := *c - return &clone -} - -func (c *Codec) Match(codec *Codec) bool { - switch codec.Name { - case CodecAll, CodecAny: - return true - } - - return c.Name == codec.Name && - (c.ClockRate == codec.ClockRate || codec.ClockRate == 0) && - (c.Channels == codec.Channels || codec.Channels == 0) -} - -func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*Media) { - for _, md := range descriptions { - 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 -} - -func MarshalSDP(name string, medias []*Media) ([]byte, error) { - sd := &sdp.SessionDescription{ - Origin: sdp.Origin{ - Username: "-", SessionID: 1, SessionVersion: 1, - NetworkType: "IN", AddressType: "IP4", UnicastAddress: "0.0.0.0", - }, - SessionName: sdp.SessionName(name), - ConnectionInformation: &sdp.ConnectionInformation{ - NetworkType: "IN", AddressType: "IP4", Address: &sdp.Address{ - Address: "0.0.0.0", - }, - }, - TimeDescriptions: []sdp.TimeDescription{ - {Timing: sdp.Timing{}}, - }, - } - - payloadType := uint8(96) - - for _, media := range medias { - if media.Codecs == nil { - continue - } - - codec := media.Codecs[0] - - name := codec.Name - if name == CodecELD { - name = CodecAAC - } - - md := &sdp.MediaDescription{ - MediaName: sdp.MediaName{ - Media: media.Kind, - Protos: []string{"RTP", "AVP"}, - }, - } - md.WithCodec(payloadType, 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 == "" { - // https://en.wikipedia.org/wiki/RTP_payload_formats - switch payloadType { - case "0": - c.Name = CodecPCMU - c.ClockRate = 8000 - case "8": - c.Name = CodecPCMA - c.ClockRate = 8000 - case "14": - c.Name = CodecMP3 - c.ClockRate = 44100 - case "26": - c.Name = CodecJPEG - c.ClockRate = 90000 - default: - c.Name = payloadType - } - } - - return c -} - -func ParseQuery(query map[string][]string) (medias []*Media) { - // set media candidates from query list - for key, values := range query { - switch key { - case KindVideo, KindAudio: - for _, value := range values { - media := &Media{Kind: key, Direction: DirectionRecvonly} - - for _, name := range strings.Split(value, ",") { - name = strings.ToUpper(name) - - // check aliases - switch name { - case "", "COPY": - name = CodecAny - case "MJPEG": - name = CodecJPEG - case "AAC": - name = CodecAAC - case "MP3": - name = CodecMP3 - } - - media.Codecs = append(media.Codecs, &Codec{Name: name}) - } - - medias = append(medias, media) - } - } - } - - return -} - -func atoi(s string) (i int) { - i, _ = strconv.Atoi(s) - return -} diff --git a/pkg/streamer/streamer.go b/pkg/streamer/streamer.go deleted file mode 100644 index aa599d2d..00000000 --- a/pkg/streamer/streamer.go +++ /dev/null @@ -1,67 +0,0 @@ -// Package streamer -// -// 1. Consumer.GetMedias - return list of Media, that Consumer can play/load/consume: -// - Media with DirectionRecvonly for audio/video -// - Media with DirectionSendonly for backchannel -// -// 2. Producer.GetMedias - return list of Media, that Producer can generate/create/produce -// - Media with DirectionSendonly for audio/video -// - Media with DirectionRecvonly for backchannel -// -// 3. Producer.GetTrack - get Media from Producer and Codec from that Media return Track from Producer: -// - Media with DirectionSendonly should Track.WriteRTP after Producer.Start -// - Media with DirectionRecvonly should Track.Bind and wait Track.WriteRTP from Consumer -// -// 4. Consumer.AddTrack - takes Media from Consumer and Track from Producer: -// - Media with DirectionRecvonly should Track.WriteRTP -// - Media with DirectionSendonly should Track.Bind -// -// 5. Producer.Start - run loop with reading rtp.Packet from source -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 -} diff --git a/pkg/streamer/track.go b/pkg/streamer/track.go deleted file mode 100644 index 0eded691..00000000 --- a/pkg/streamer/track.go +++ /dev/null @@ -1,89 +0,0 @@ -package streamer - -import ( - "encoding/json" - "fmt" - "github.com/pion/rtp" - "sync" -) - -type WriterFunc func(packet *rtp.Packet) error -type WrapperFunc func(push WriterFunc) WriterFunc - -type Track struct { - Codec *Codec - Direction string - sink map[*Track]WriterFunc - sinkMu *sync.RWMutex -} - -func NewTrack(media *Media, codec *Codec) *Track { - if codec == nil { - codec = media.Codecs[0] - } - return &Track{Codec: codec, Direction: media.Direction, sinkMu: new(sync.RWMutex)} -} - -func (t *Track) String() string { - s := t.Codec.String() - if t.Codec.FmtpLine != "" { - s += " " + t.Codec.FmtpLine - } - if t.sinkMu.TryRLock() { - s += fmt.Sprintf(", sinks=%d", len(t.sink)) - t.sinkMu.RUnlock() - } else { - s += fmt.Sprintf(", sinks=?") - } - return s -} - -func (t *Track) MarshalJSON() ([]byte, error) { - return json.Marshal(t.String()) -} - -func (t *Track) WriteRTP(p *rtp.Packet) error { - t.sinkMu.RLock() - for _, f := range t.sink { - _ = f(p) - } - t.sinkMu.RUnlock() - return nil -} - -// Bind - attach WriterFunc (Consumer) for receiving rtp.Packet(s) -// and return new Track copy. Later you can run Unbind for new Track -func (t *Track) Bind(w WriterFunc) *Track { - t.sinkMu.Lock() - - if t.sink == nil { - t.sink = map[*Track]WriterFunc{} - } - - clone := *t - t.sink[&clone] = w - - t.sinkMu.Unlock() - - return &clone -} - -// Unbind - detach WriterFunc that related to this Track from -// consuming track data -func (t *Track) Unbind() { - t.sinkMu.Lock() - delete(t.sink, t) - t.sinkMu.Unlock() -} - -func (t *Track) GetSink(from *Track) { - t.sinkMu.Lock() - t.sink = from.sink - t.sinkMu.Unlock() -} - -func (t *Track) HasSink() bool { - t.sinkMu.RLock() - defer t.sinkMu.RUnlock() - return len(t.sink) > 0 -} diff --git a/pkg/tapo/backchannel.go b/pkg/tapo/backchannel.go deleted file mode 100644 index cec8e395..00000000 --- a/pkg/tapo/backchannel.go +++ /dev/null @@ -1,53 +0,0 @@ -package tapo - -import ( - "bytes" - "github.com/AlexxIT/go2rtc/pkg/mpegts" - "github.com/AlexxIT/go2rtc/pkg/streamer" - "github.com/pion/rtp" - "strconv" -) - -func (c *Client) backchannelWriter() streamer.WriterFunc { - w := mpegts.NewWriter() - w.AddPES(68, mpegts.StreamTypePCMATapo) - w.WritePAT() - w.WritePMT() - - return func(packet *rtp.Packet) (err error) { - // don't know why 68 and 192 - w.WritePES(68, 192, packet.Payload) - err = c.WriteBackchannel(w.Bytes()) - w.Reset() - return - } -} - -func (c *Client) SetupBackchannel() (err error) { - // if conn1 is not used - we will use it for backchannel - // or we need to start another conn for session2 - if c.session1 != "" { - if c.conn2, err = c.newConn(); err != nil { - return - } - } else { - c.conn2 = c.conn1 - } - - c.session2, err = c.Request(c.conn2, []byte(`{"params":{"talk":{"mode":"aec"},"method":"get"},"seq":3,"type":"request"}`)) - return -} - -func (c *Client) WriteBackchannel(body []byte) (err error) { - // TODO: fixme (size) - buf := bytes.NewBuffer(nil) - buf.WriteString("----client-stream-boundary--\r\n") - buf.WriteString("Content-Type: audio/mp2t\r\n") - buf.WriteString("X-If-Encrypt: 0\r\n") - buf.WriteString("X-Session-Id: " + c.session2 + "\r\n") - buf.WriteString("Content-Length: " + strconv.Itoa(len(body)) + "\r\n\r\n") - buf.Write(body) - - _, err = buf.WriteTo(c.conn2) - return -} diff --git a/pkg/tapo/client.go b/pkg/tapo/client.go index aa8928ef..29994734 100644 --- a/pkg/tapo/client.go +++ b/pkg/tapo/client.go @@ -8,8 +8,8 @@ import ( "encoding/json" "errors" "fmt" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mpegts" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/tcp" "mime/multipart" "net" @@ -19,12 +19,13 @@ import ( ) type Client struct { - streamer.Element + core.Listener url string - medias []*streamer.Media - tracks map[byte]*streamer.Track + medias []*core.Media + receivers []*core.Receiver + sender *core.Sender conn1 net.Conn conn2 net.Conn @@ -33,6 +34,9 @@ type Client struct { session1 string session2 string + + recv int + send int } // block ciphers using cipher block chaining. @@ -102,7 +106,7 @@ func (c *Client) newDectypter(res *http.Response, username, password string) { // extract nonce from response // cipher="AES_128_CBC" username="admin" padding="PKCS7_16" algorithm="MD5" nonce="***" nonce := res.Header.Get("Key-Exchange") - nonce = streamer.Between(nonce, `nonce="`, `"`) + nonce = core.Between(nonce, `nonce="`, `"`) key := md5.Sum([]byte(nonce + ":" + password)) iv := md5.Sum([]byte(username + ":" + nonce)) @@ -158,6 +162,8 @@ func (c *Client) Handle() error { return err } + c.recv += size + body := make([]byte, size) b := body @@ -178,8 +184,11 @@ func (c *Client) Handle() error { break } - if track := c.tracks[pkt.PayloadType]; track != nil { - _ = track.WriteRTP(pkt) + for _, receiver := range c.receivers { + if receiver.ID == pkt.PayloadType { + receiver.WriteRTP(pkt) + break + } } } } diff --git a/pkg/tapo/consumer.go b/pkg/tapo/consumer.go index f39be437..0a6b7792 100644 --- a/pkg/tapo/consumer.go +++ b/pkg/tapo/consumer.go @@ -1,18 +1,62 @@ package tapo import ( - "github.com/AlexxIT/go2rtc/pkg/streamer" + "bytes" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/mpegts" "github.com/pion/rtp" + "strconv" ) -func (c *Client) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { - consCodec := media.MatchCodec(track.Codec) - consTrack := c.GetTrack(media, consCodec) - if consTrack == nil { - return nil +func (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { + if c.sender == nil { + if err := c.SetupBackchannel(); err != nil { + return nil + } + + w := mpegts.NewWriter() + w.AddPES(68, mpegts.StreamTypePCMATapo) + w.WritePAT() + w.WritePMT() + + c.sender = core.NewSender(media, track.Codec) + c.sender.Handler = func(packet *rtp.Packet) { + // don't know why 68 and 192 + w.WritePES(68, 192, packet.Payload) + _ = c.WriteBackchannel(w.Bytes()) + w.Reset() + } } - return track.Bind(func(packet *rtp.Packet) error { - return consTrack.WriteRTP(packet) - }) + c.sender.HandleRTP(track) + return nil +} + +func (c *Client) SetupBackchannel() (err error) { + // if conn1 is not used - we will use it for backchannel + // or we need to start another conn for session2 + if c.session1 != "" { + if c.conn2, err = c.newConn(); err != nil { + return + } + } else { + c.conn2 = c.conn1 + } + + c.session2, err = c.Request(c.conn2, []byte(`{"params":{"talk":{"mode":"aec"},"method":"get"},"seq":3,"type":"request"}`)) + return +} + +func (c *Client) WriteBackchannel(body []byte) (err error) { + // TODO: fixme (size) + buf := bytes.NewBuffer(nil) + buf.WriteString("----client-stream-boundary--\r\n") + buf.WriteString("Content-Type: audio/mp2t\r\n") + buf.WriteString("X-If-Encrypt: 0\r\n") + buf.WriteString("X-Session-Id: " + c.session2 + "\r\n") + buf.WriteString("Content-Length: " + strconv.Itoa(len(body)) + "\r\n\r\n") + buf.Write(body) + + _, err = buf.WriteTo(c.conn2) + return } diff --git a/pkg/tapo/producer.go b/pkg/tapo/producer.go index e714cc7e..ac213e15 100644 --- a/pkg/tapo/producer.go +++ b/pkg/tapo/producer.go @@ -1,34 +1,34 @@ package tapo import ( + "encoding/json" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mpegts" - "github.com/AlexxIT/go2rtc/pkg/streamer" ) -func (c *Client) GetMedias() []*streamer.Media { - // producer should have persistent medias +func (c *Client) GetMedias() []*core.Media { if c.medias == nil { // don't know if all Tapo has this capabilities... - c.medias = []*streamer.Media{ + c.medias = []*core.Media{ { - Kind: streamer.KindVideo, - Direction: streamer.DirectionSendonly, - Codecs: []*streamer.Codec{ - {Name: streamer.CodecH264, ClockRate: 90000, PayloadType: streamer.PayloadTypeRAW}, + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + {Name: core.CodecH264, ClockRate: 90000, PayloadType: core.PayloadTypeRAW}, }, }, { - Kind: streamer.KindAudio, - Direction: streamer.DirectionSendonly, - Codecs: []*streamer.Codec{ - {Name: streamer.CodecPCMA, ClockRate: 8000, PayloadType: 8}, + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + {Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8}, }, }, { - Kind: streamer.KindAudio, - Direction: streamer.DirectionRecvonly, - Codecs: []*streamer.Codec{ - {Name: streamer.CodecPCMA, ClockRate: 8000, PayloadType: 8}, + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8}, }, }, } @@ -37,44 +37,26 @@ func (c *Client) GetMedias() []*streamer.Media { return c.medias } -func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) (track *streamer.Track) { - for _, track := range c.tracks { +func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + for _, track := range c.receivers { if track.Codec == codec { - return track + return track, nil } } - if c.tracks == nil { - c.tracks = map[byte]*streamer.Track{} + if err := c.SetupStream(); err != nil { + return nil, err } - if media.Direction == streamer.DirectionSendonly { - var payloadType byte - if media.Kind == streamer.KindVideo { - payloadType = mpegts.StreamTypeH264 - } else { - payloadType = mpegts.StreamTypePCMATapo - } - - if err := c.SetupStream(); err != nil { - return nil - } - - track = streamer.NewTrack(media, codec) - c.tracks[payloadType] = track - } else { - if err := c.SetupBackchannel(); err != nil { - return nil - } - - if w := c.backchannelWriter(); w != nil { - track = streamer.NewTrack(media, codec) - track.Bind(w) - c.tracks[0] = track - } + track := core.NewReceiver(media, codec) + switch media.Kind { + case core.KindVideo: + track.ID = mpegts.StreamTypeH264 + case core.KindAudio: + track.ID = mpegts.StreamTypePCMATapo } - - return + c.receivers = append(c.receivers, track) + return track, nil } func (c *Client) Start() error { @@ -82,5 +64,25 @@ func (c *Client) Start() error { } func (c *Client) Stop() error { + for _, receiver := range c.receivers { + receiver.Close() + } + if c.sender != nil { + c.sender.Close() + } return c.Close() } + +func (c *Client) MarshalJSON() ([]byte, error) { + info := &core.Info{ + Type: "Tapo active producer", + Medias: c.medias, + Recv: c.recv, + Receivers: c.receivers, + Send: c.send, + } + if c.sender != nil { + info.Senders = []*core.Sender{c.sender} + } + return json.Marshal(info) +} diff --git a/pkg/tcp/server.go b/pkg/tcp/server.go deleted file mode 100644 index 5a0186d8..00000000 --- a/pkg/tcp/server.go +++ /dev/null @@ -1,36 +0,0 @@ -package tcp - -import ( - "github.com/AlexxIT/go2rtc/pkg/streamer" - "net" -) - -type Server struct { - streamer.Element - - listener net.Listener - closed bool -} - -func NewServer(address string) (srv *Server, err error) { - srv = &Server{} - srv.listener, err = net.Listen("tcp", address) - return -} - -func (s *Server) Serve() { - for { - conn, err := s.listener.Accept() - if err != nil { - return - } - go func() { - s.Fire(conn) - _ = conn.Close() - }() - } -} - -func (s *Server) Close() error { - return s.listener.Close() -} diff --git a/pkg/webrtc/api.go b/pkg/webrtc/api.go index 4d4bec43..8a5c7668 100644 --- a/pkg/webrtc/api.go +++ b/pkg/webrtc/api.go @@ -8,6 +8,9 @@ import ( "strings" ) +// ReceiveMTU = Ethernet MTU (1500) - IP Header (20) - UDP Header (8) +const ReceiveMTU = 1472 + func NewAPI(address string) (*webrtc.API, error) { // for debug logs add to env: `PION_LOG_DEBUG=all` m := &webrtc.MediaEngine{} @@ -41,8 +44,7 @@ func NewAPI(address string) (*webrtc.API, error) { // fix https://github.com/pion/webrtc/pull/2407 s.SetDTLSInsecureSkipHelloVerify(true) - // Ethernet MTU (1500) - IP Header (20) - UDP Header (8) - s.SetReceiveMTU(1472) + s.SetReceiveMTU(ReceiveMTU) if address != "" { address, network, _ := strings.Cut(address, "/") diff --git a/pkg/webrtc/client.go b/pkg/webrtc/client.go index 8a5e2562..1e33fd10 100644 --- a/pkg/webrtc/client.go +++ b/pkg/webrtc/client.go @@ -1,27 +1,27 @@ package webrtc import ( - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/sdp/v3" "github.com/pion/webrtc/v3" ) -func (c *Conn) CreateOffer(medias []*streamer.Media) (string, error) { +func (c *Conn) CreateOffer(medias []*core.Media) (string, error) { // 1. Create transeivers with proper kind and direction for _, media := range medias { var err error switch media.Direction { - case streamer.DirectionRecvonly: + case core.DirectionRecvonly: _, err = c.pc.AddTransceiverFromKind( webrtc.NewRTPCodecType(media.Kind), webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly}, ) - case streamer.DirectionSendonly: + case core.DirectionSendonly: _, err = c.pc.AddTransceiverFromTrack( NewTrack(media.Kind), webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly}, ) - case streamer.DirectionSendRecv: + case core.DirectionSendRecv: // default transceiver is sendrecv _, err = c.pc.AddTransceiverFromTrack(NewTrack(media.Kind)) } @@ -45,7 +45,7 @@ func (c *Conn) CreateOffer(medias []*streamer.Media) (string, error) { return c.pc.LocalDescription().SDP, nil } -func (c *Conn) CreateCompleteOffer(medias []*streamer.Media) (string, error) { +func (c *Conn) CreateCompleteOffer(medias []*core.Media) (string, error) { if _, err := c.CreateOffer(medias); err != nil { return "", err } @@ -68,21 +68,7 @@ func (c *Conn) SetAnswer(answer string) (err error) { return } - medias := streamer.UnmarshalMedias(sd.MediaDescriptions) - - // sort medias, so video will always be before audio - // and ignore application media from Hass default lovelace card - // ignore media without direction (inactive media) - for _, media := range medias { - if media.Kind == streamer.KindVideo && media.Direction != "" { - c.medias = append(c.medias, media) - } - } - for _, media := range medias { - if media.Kind == streamer.KindAudio && media.Direction != "" { - c.medias = append(c.medias, media) - } - } + c.medias = UnmarshalMedias(sd.MediaDescriptions) return nil } diff --git a/pkg/webrtc/client_test.go b/pkg/webrtc/client_test.go index 5659285d..3dce0372 100644 --- a/pkg/webrtc/client_test.go +++ b/pkg/webrtc/client_test.go @@ -1,7 +1,7 @@ package webrtc import ( - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/webrtc/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -17,10 +17,10 @@ func TestClient(t *testing.T) { prod := NewConn(pc) - medias := []*streamer.Media{ - {Kind: streamer.KindVideo, Direction: streamer.DirectionRecvonly}, - {Kind: streamer.KindAudio, Direction: streamer.DirectionRecvonly}, - {Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly}, + medias := []*core.Media{ + {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, + {Kind: core.KindAudio, Direction: core.DirectionRecvonly}, + {Kind: core.KindAudio, Direction: core.DirectionSendonly}, } offer, err := prod.CreateOffer(medias) diff --git a/pkg/webrtc/conn.go b/pkg/webrtc/conn.go index 2b60721f..41a8fc68 100644 --- a/pkg/webrtc/conn.go +++ b/pkg/webrtc/conn.go @@ -2,9 +2,6 @@ package webrtc import ( "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/h264" - "github.com/AlexxIT/go2rtc/pkg/h265" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/pion/webrtc/v3" @@ -12,19 +9,20 @@ import ( ) type Conn struct { - streamer.Element + core.Listener UserAgent string Desc string - Mode streamer.Mode + Mode core.Mode pc *webrtc.PeerConnection - medias []*streamer.Media - tracks []*streamer.Track + medias []*core.Media + receivers []*core.Receiver + senders []*core.Sender - receive int - send int + recv int + send int offer string remote string @@ -56,13 +54,26 @@ func NewConn(pc *webrtc.PeerConnection) *Conn { ) }) - pc.OnTrack(func(remote *webrtc.TrackRemote, _ *webrtc.RTPReceiver) { - track := c.getRecvTrack(remote) - if track == nil { - return // it's OK when we not need, for example, audio from producer + pc.OnTrack(func(remote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { + media, codec := c.getMediaCodec(remote) + if media == nil { + return } - if c.Mode == streamer.ModePassiveProducer && remote.Kind() == webrtc.RTPCodecTypeVideo { + track, err := c.GetTrack(media, codec) + if err != nil { + return + } + + switch c.Mode { + case core.ModePassiveProducer, core.ModeActiveProducer: + // replace the theoretical list of codecs with the actual list of codecs + if len(media.Codecs) > 1 { + media.Codecs = []*core.Codec{codec} + } + } + + if c.Mode == core.ModePassiveProducer && remote.Kind() == webrtc.RTPCodecTypeVideo { go func() { pkts := []rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(remote.SSRC())}} for range time.NewTicker(time.Second * 2).C { @@ -74,15 +85,20 @@ func NewConn(pc *webrtc.PeerConnection) *Conn { } for { - packet, _, err := remote.ReadRTP() + b := make([]byte, ReceiveMTU) + n, _, err := remote.Read(b) if err != nil { return } - if len(packet.Payload) == 0 { - continue + + c.recv += n + + packet := &rtp.Packet{} + if err := packet.Unmarshal(b[:n]); err != nil { + return } - c.receive += len(packet.Payload) - _ = track.WriteRTP(packet) + + track.WriteRTP(packet) } }) @@ -127,106 +143,34 @@ func (c *Conn) getTranseiver(mid string) *webrtc.RTPTransceiver { } return nil } -func (c *Conn) addSendTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { - tr := c.getTranseiver(media.MID) - sender := tr.Sender() - localTrack := sender.Track().(*Track) - codec := track.Codec - - // important to get remote PayloadType - payloadType := media.MatchCodec(codec).PayloadType - - push := func(packet *rtp.Packet) error { - c.send += packet.MarshalSize() - return localTrack.WriteRTP(payloadType, packet) - } - - switch codec.Name { - case streamer.CodecH264: - wrapper := h264.RTPPay(1200) - push = wrapper(push) - - if codec.IsRTP() { - wrapper = h264.RTPDepay(track) - } else { - wrapper = h264.RepairAVC(track) - } - push = wrapper(push) - - case streamer.CodecH265: - // SafariPay because it is the only browser in the world - // that supports WebRTC + H265 - wrapper := h265.SafariPay(1200) - push = wrapper(push) - - wrapper = h265.RTPDepay(track) - push = wrapper(push) - } - - return track.Bind(push) -} - -func (c *Conn) getRecvTrack(remote *webrtc.TrackRemote) *streamer.Track { - payloadType := uint8(remote.PayloadType()) - - switch c.Mode { - case streamer.ModePassiveConsumer: - // Situation: - // - Browser (passive consumer) connects to go2rtc for receiving AV from IP-camera - // - Video and audio tracks marked as local "sendonly" - // - Browser sends microphone remote track to go2rtc, this track marked as local "recvonly" - // - go2rtc should ReadRTP from this remote track and sends it to camera - for _, track := range c.tracks { - if track.Direction == streamer.DirectionRecvonly && track.Codec.PayloadType == payloadType { - return track - } +func (c *Conn) getMediaCodec(remote *webrtc.TrackRemote) (*core.Media, *core.Codec) { + for _, tr := range c.pc.GetTransceivers() { + // search Transeiver for this TrackRemote + if tr.Receiver() == nil || tr.Receiver().Track() != remote { + continue } - case streamer.ModeActiveProducer: - // Situation: - // - go2rtc (active producer) connects to remote server (ex. webtorrent) for receiving AV - // - remote server sends remote tracks, this tracks marked as remote "sendonly" - for _, track := range c.tracks { - if track.Direction == streamer.DirectionSendonly && track.Codec.PayloadType == payloadType { - return track - } - } - - case streamer.ModePassiveProducer: - // Situation: - // - OBS Studio (passive producer) connects to go2rtc for send AV - // - OBS sends remote tracks, this tracks marked as remote "sendonly" - for i, media := range c.medias { - // check only tracks with same kind - if media.Kind != remote.Kind().String() { - continue - } - - // check only incoming tracks (remote media "sendonly") - if media.Direction != streamer.DirectionSendonly { + // search Media for this MID + for _, media := range c.medias { + if media.ID != tr.Mid() || media.Direction != core.DirectionRecvonly { continue } + // search codec for this PayloadType for _, codec := range media.Codecs { - if codec.PayloadType != payloadType { + if codec.PayloadType != uint8(remote.PayloadType()) { continue } - - // leave only one codec in supported media list - if len(media.Codecs) > 1 { - c.medias[i].Codecs = []*streamer.Codec{codec} - } - - // forward request to passive producer GetTrack - // will create NewTrack for sendonly media - return c.GetTrack(media, codec) + return media, codec } } - - default: - panic("not implemented") } - return nil + // fix moment when core.ModePassiveProducer or core.ModeActiveProducer + // sends new codec with new payload type to same media + // check GetTrack + panic(core.Caller()) + + return nil, nil } diff --git a/pkg/webrtc/consumer.go b/pkg/webrtc/consumer.go index 4b74cd97..0a278924 100644 --- a/pkg/webrtc/consumer.go +++ b/pkg/webrtc/consumer.go @@ -2,72 +2,77 @@ package webrtc import ( "encoding/json" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/h264" + "github.com/AlexxIT/go2rtc/pkg/h265" "github.com/pion/rtp" - "github.com/pion/webrtc/v3" ) -func (c *Conn) GetMedias() []*streamer.Media { +func (c *Conn) GetMedias() []*core.Media { return c.medias } -func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { - switch c.Mode { - case streamer.ModePassiveConsumer: - switch track.Direction { - case streamer.DirectionSendonly: - // send our track to WebRTC consumer - return c.addSendTrack(media, track) +func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { + core.Assert(media.Direction == core.DirectionSendonly) - case streamer.DirectionRecvonly: - // receive track from WebRTC consumer (microphone, backchannel, two way audio) - return c.addConsumerRecvTrack(media, track) - } - - case streamer.ModePassiveProducer: - // "Stream to camera" function - consCodec := media.MatchCodec(track.Codec) - consTrack := c.GetTrack(media, consCodec) - if consTrack == nil { + for _, sender := range c.senders { + if sender.Codec == codec { + sender.HandleRTP(track) return nil } - - return track.Bind(func(packet *rtp.Packet) error { - return consTrack.WriteRTP(packet) - }) } - panic("not implemented") -} - -func (c *Conn) addConsumerRecvTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { - params := webrtc.RTPCodecParameters{ - RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: MimeType(track.Codec), - ClockRate: track.Codec.ClockRate, - Channels: track.Codec.Channels, - }, - PayloadType: 0, // don't know if this necessary + switch c.Mode { + case core.ModePassiveConsumer: // video/audio for browser + case core.ModeActiveProducer: // go2rtc as WebRTC client (backchannel) + case core.ModePassiveProducer: // WebRTC/WHIP + default: + panic(core.Caller()) } - tr := c.getTranseiver(media.MID) + localTrack := c.getTranseiver(media.ID).Sender().Track().(*Track) - // set codec for consumer recv track so remote peer should send media with this codec - _ = tr.SetCodecPreferences([]webrtc.RTPCodecParameters{params}) + sender := core.NewSender(media, track.Codec) + sender.Handler = func(packet *rtp.Packet) { + c.send += packet.MarshalSize() + //important to send with remote PayloadType + _ = localTrack.WriteRTP(codec.PayloadType, packet) + } - c.tracks = append(c.tracks, track) - return track + switch codec.Name { + case core.CodecH264: + sender.Handler = h264.RTPPay(1200, sender.Handler) + if track.Codec.IsRTP() { + sender.Handler = h264.RTPDepay(track.Codec, sender.Handler) + } else { + sender.Handler = h264.RepairAVC(track.Codec, sender.Handler) + } + + case core.CodecH265: + // SafariPay because it is the only browser in the world + // that supports WebRTC + H265 + sender.Handler = h265.SafariPay(1200, sender.Handler) + if track.Codec.IsRTP() { + sender.Handler = h265.RTPDepay(track.Codec, sender.Handler) + } + } + + sender.HandleRTP(track) + + c.senders = append(c.senders, sender) + return nil } func (c *Conn) MarshalJSON() ([]byte, error) { - info := &streamer.Info{ + info := &core.Info{ Type: c.Desc + " " + c.Mode.String(), RemoteAddr: c.remote, UserAgent: c.UserAgent, Medias: c.medias, - Tracks: c.tracks, - Recv: uint32(c.receive), - Send: uint32(c.send), + Receivers: c.receivers, + Senders: c.senders, + Recv: c.recv, + Send: c.send, } return json.Marshal(info) } diff --git a/pkg/webrtc/helpers.go b/pkg/webrtc/helpers.go index 14832c5f..b6e36ee6 100644 --- a/pkg/webrtc/helpers.go +++ b/pkg/webrtc/helpers.go @@ -3,8 +3,9 @@ package webrtc import ( "errors" "fmt" - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/ice/v2" + "github.com/pion/sdp/v3" "github.com/pion/stun" "github.com/pion/webrtc/v3" "hash/crc32" @@ -14,6 +15,43 @@ import ( "time" ) +func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*core.Media) { + // 1. Sort medias, so video will always be before audio + // 2. Ignore application media from Hass default lovelace card + // 3. Ignore media without direction (inactive media) + // 4. Inverse media direction (because it is remote peer medias list) + for _, kind := range []string{core.KindVideo, core.KindAudio} { + for _, md := range descriptions { + if md.MediaName.Media != kind { + continue + } + + media := core.UnmarshalMedia(md) + switch media.Direction { + case core.DirectionSendRecv: + media.Direction = core.DirectionRecvonly + medias = append(medias, media) + + media = media.Clone() + media.Direction = core.DirectionSendonly + + case core.DirectionRecvonly: + media.Direction = core.DirectionSendonly + + case core.DirectionSendonly: + media.Direction = core.DirectionRecvonly + + case "": + continue + } + + medias = append(medias, media) + } + } + + return +} + func NewCandidate(network, address string) (string, error) { i := strings.LastIndexByte(address, ':') if i < 0 { @@ -135,25 +173,25 @@ func IsIP(host string) bool { return true } -func MimeType(codec *streamer.Codec) string { +func MimeType(codec *core.Codec) string { switch codec.Name { - case streamer.CodecH264: + case core.CodecH264: return webrtc.MimeTypeH264 - case streamer.CodecH265: + case core.CodecH265: return webrtc.MimeTypeH265 - case streamer.CodecVP8: + case core.CodecVP8: return webrtc.MimeTypeVP8 - case streamer.CodecVP9: + case core.CodecVP9: return webrtc.MimeTypeVP9 - case streamer.CodecAV1: + case core.CodecAV1: return webrtc.MimeTypeAV1 - case streamer.CodecPCMU: + case core.CodecPCMU: return webrtc.MimeTypePCMU - case streamer.CodecPCMA: + case core.CodecPCMA: return webrtc.MimeTypePCMA - case streamer.CodecOpus: + case core.CodecOpus: return webrtc.MimeTypeOpus - case streamer.CodecG722: + case core.CodecG722: return webrtc.MimeTypeG722 } panic("not implemented") diff --git a/pkg/webrtc/producer.go b/pkg/webrtc/producer.go index b2c33048..d4136a5c 100644 --- a/pkg/webrtc/producer.go +++ b/pkg/webrtc/producer.go @@ -1,28 +1,46 @@ package webrtc import ( - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/webrtc/v3" ) -func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track { - if c.Mode != streamer.ModeActiveProducer && c.Mode != streamer.ModePassiveProducer { - panic("not implemented") - } +func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + core.Assert(media.Direction == core.DirectionRecvonly) - for _, track := range c.tracks { + for _, track := range c.receivers { if track.Codec == codec { - return track + return track, nil } } - track := streamer.NewTrack(media, codec) + switch c.Mode { + case core.ModePassiveConsumer: // backchannel from browser + // set codec for consumer recv track so remote peer should send media with this codec + params := webrtc.RTPCodecParameters{ + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: MimeType(codec), + ClockRate: codec.ClockRate, + Channels: codec.Channels, + }, + PayloadType: 0, // don't know if this necessary + } - if media.Direction == streamer.DirectionRecvonly { - track = c.addSendTrack(media, track) + tr := c.getTranseiver(media.ID) + + _ = tr.SetCodecPreferences([]webrtc.RTPCodecParameters{params}) + + case core.ModePassiveProducer, core.ModeActiveProducer: + // Passive producers: OBS Studio via WHIP or Browser + // Active producers: go2rtc as WebRTC client or WebTorrent + + default: + panic(core.Caller()) } - c.tracks = append(c.tracks, track) - return track + track := core.NewReceiver(media, codec) + c.receivers = append(c.receivers, track) + return track, nil } func (c *Conn) Start() error { @@ -31,5 +49,11 @@ func (c *Conn) Start() error { } func (c *Conn) Stop() error { + for _, receiver := range c.receivers { + receiver.Close() + } + for _, sender := range c.senders { + sender.Close() + } return c.pc.Close() } diff --git a/pkg/webrtc/server.go b/pkg/webrtc/server.go index aba95468..57efdc7a 100644 --- a/pkg/webrtc/server.go +++ b/pkg/webrtc/server.go @@ -1,7 +1,7 @@ package webrtc import ( - "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/sdp/v3" "github.com/pion/webrtc/v3" ) @@ -20,14 +20,14 @@ func (c *Conn) SetOffer(offer string) (err error) { var tr *webrtc.RTPTransceiver for _, attr := range md.Attributes { switch attr.Key { - case streamer.DirectionSendRecv: + case core.DirectionSendRecv: tr, _ = c.pc.AddTransceiverFromTrack(NewTrack(md.MediaName.Media)) - case streamer.DirectionSendonly: + case core.DirectionSendonly: tr, _ = c.pc.AddTransceiverFromKind( webrtc.NewRTPCodecType(md.MediaName.Media), webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly}, ) - case streamer.DirectionRecvonly: + case core.DirectionRecvonly: tr, _ = c.pc.AddTransceiverFromTrack( NewTrack(md.MediaName.Media), webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly}, @@ -42,20 +42,7 @@ func (c *Conn) SetOffer(offer string) (err error) { } } - medias := streamer.UnmarshalMedias(sd.MediaDescriptions) - - // sort medias, so video will always be before audio - // and ignore application media from Hass default lovelace card - for _, media := range medias { - if media.Kind == streamer.KindVideo { - c.medias = append(c.medias, media) - } - } - for _, media := range medias { - if media.Kind == streamer.KindAudio { - c.medias = append(c.medias, media) - } - } + c.medias = UnmarshalMedias(sd.MediaDescriptions) return } @@ -67,15 +54,21 @@ func (c *Conn) GetAnswer() (answer string, err error) { return "", err } - // disable transceivers if we don't have track - // make direction=inactive - // don't really necessary, but anyway + // disable transceivers if we don't have track, make direction=inactive +transeivers: for _, tr := range c.pc.GetTransceivers() { - if tr.Direction() == webrtc.RTPTransceiverDirectionSendonly && tr.Sender() == nil { - if err = tr.Stop(); err != nil { - return + for _, sender := range c.senders { + if sender.Media.ID == tr.Mid() { + continue transeivers } } + + switch tr.Direction() { + case webrtc.RTPTransceiverDirectionSendrecv: + _ = tr.Sender().Stop() + case webrtc.RTPTransceiverDirectionSendonly: + _ = tr.Stop() + } } if desc, err = c.pc.CreateAnswer(nil); err != nil { diff --git a/pkg/webrtc/track.go b/pkg/webrtc/track.go index 676da2dd..547fafb1 100644 --- a/pkg/webrtc/track.go +++ b/pkg/webrtc/track.go @@ -1,7 +1,6 @@ package webrtc import ( - "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/rtp" "github.com/pion/webrtc/v3" ) @@ -18,8 +17,8 @@ type Track struct { func NewTrack(kind string) *Track { return &Track{ kind: kind, - id: core.RandString(16), - streamID: core.RandString(16), + id: "go2rtc-" + kind, + streamID: "go2rtc", } } diff --git a/pkg/webtorrent/client.go b/pkg/webtorrent/client.go index fb8a20c6..ac35685e 100644 --- a/pkg/webtorrent/client.go +++ b/pkg/webtorrent/client.go @@ -4,7 +4,6 @@ import ( "encoding/base64" "fmt" "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/webrtc" "github.com/gorilla/websocket" pion "github.com/pion/webrtc/v3" @@ -16,11 +15,11 @@ func NewClient(tracker, share, pwd string, pc *pion.PeerConnection) (*webrtc.Con // 1. Create WebRTC producer prod := webrtc.NewConn(pc) prod.Desc = "WebRTC/WebTorrent sync" - prod.Mode = streamer.ModeActiveProducer + prod.Mode = core.ModeActiveProducer - medias := []*streamer.Media{ - {Kind: streamer.KindVideo, Direction: streamer.DirectionRecvonly}, - {Kind: streamer.KindAudio, Direction: streamer.DirectionRecvonly}, + medias := []*core.Media{ + {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, + {Kind: core.KindAudio, Direction: core.DirectionRecvonly}, } // 2. Create offer diff --git a/pkg/webtorrent/server.go b/pkg/webtorrent/server.go index 144911e0..46e450aa 100644 --- a/pkg/webtorrent/server.go +++ b/pkg/webtorrent/server.go @@ -4,14 +4,13 @@ import ( "encoding/base64" "fmt" "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/gorilla/websocket" "sync" "time" ) type Server struct { - streamer.Element + core.Listener URL string Exchange func(src, offer string) (answer string, err error) diff --git a/www/README.md b/www/README.md index 75ef9d54..5c95bf17 100644 --- a/www/README.md +++ b/www/README.md @@ -72,3 +72,4 @@ https://webrtc.org/getting-started/unified-plan-transition-guide?hl=en - https://chromestatus.com/feature/5100845653819392 - https://developer.apple.com/documentation/webkit/delivering_video_content_for_safari - https://dirask.com/posts/JavaScript-supported-Audio-Video-MIME-Types-by-MediaRecorder-Chrome-and-Firefox-jERn81 +- https://privacycheck.sec.lrz.de/active/fp_cpt/fp_can_play_type.html diff --git a/www/links.html b/www/links.html index 1717a707..bc7d9dc0 100644 --- a/www/links.html +++ b/www/links.html @@ -115,9 +115,9 @@
- + - +