From 194d1dae51ef547f368d8c467dbee782527ef11f Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 24 Nov 2024 13:09:13 +0300 Subject: [PATCH 1/4] Add support doorbird source #1060 --- internal/doorbird/doorbird.go | 36 ++++++++++++ internal/http/http.go | 4 ++ main.go | 2 + pkg/doorbird/backchannel.go | 100 ++++++++++++++++++++++++++++++++++ pkg/pcm/producer.go | 55 +++++++++++++++++++ 5 files changed, 197 insertions(+) create mode 100644 internal/doorbird/doorbird.go create mode 100644 pkg/doorbird/backchannel.go create mode 100644 pkg/pcm/producer.go diff --git a/internal/doorbird/doorbird.go b/internal/doorbird/doorbird.go new file mode 100644 index 00000000..c56fc0f9 --- /dev/null +++ b/internal/doorbird/doorbird.go @@ -0,0 +1,36 @@ +package doorbird + +import ( + "net/url" + + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/doorbird" +) + +func Init() { + streams.RedirectFunc("doorbird", func(rawURL string) (string, error) { + u, err := url.Parse(rawURL) + if err != nil { + return "", err + } + + // https://www.doorbird.com/downloads/api_lan.pdf + switch u.Query().Get("media") { + case "video": + u.Path = "/bha-api/video.cgi" + case "audio": + u.Path = "/bha-api/audio-receive.cgi" + default: + return "", nil + } + + u.Scheme = "http" + + return u.String(), nil + }) + + streams.HandleFunc("doorbird", func(source string) (core.Producer, error) { + return doorbird.Dial(source) + }) +} diff --git a/internal/http/http.go b/internal/http/http.go index a35439d5..4b0560c1 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -14,6 +14,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/image" "github.com/AlexxIT/go2rtc/pkg/magic" "github.com/AlexxIT/go2rtc/pkg/mpjpeg" + "github.com/AlexxIT/go2rtc/pkg/pcm" "github.com/AlexxIT/go2rtc/pkg/tcp" ) @@ -87,6 +88,9 @@ func do(req *http.Request) (core.Producer, error) { return image.Open(res) case ct == "multipart/x-mixed-replace": return mpjpeg.Open(res.Body) + //https://www.iana.org/assignments/media-types/audio/basic + case ct == "audio/basic": + return pcm.Open(res.Body) } return magic.Open(res.Body) diff --git a/main.go b/main.go index 7f94b930..d5c59ffc 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/bubble" "github.com/AlexxIT/go2rtc/internal/debug" + "github.com/AlexxIT/go2rtc/internal/doorbird" "github.com/AlexxIT/go2rtc/internal/dvrip" "github.com/AlexxIT/go2rtc/internal/echo" "github.com/AlexxIT/go2rtc/internal/exec" @@ -82,6 +83,7 @@ func main() { bubble.Init() // bubble source expr.Init() // expr source gopro.Init() // gopro source + doorbird.Init() // doorbird source // 6. Helper modules diff --git a/pkg/doorbird/backchannel.go b/pkg/doorbird/backchannel.go new file mode 100644 index 00000000..c6a7dec1 --- /dev/null +++ b/pkg/doorbird/backchannel.go @@ -0,0 +1,100 @@ +package doorbird + +import ( + "fmt" + "net" + "net/http" + "net/url" + "time" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +type Client struct { + core.Connection + conn net.Conn +} + +func Dial(rawURL string) (*Client, error) { + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + user := u.User.Username() + pass, _ := u.User.Password() + + rawURL = fmt.Sprintf("http://%s/bha-api/audio-transmit.cgi?http-user=%s&&http-password=%s", u.Host, user, pass) + + req, err := http.NewRequest("POST", rawURL, nil) + if err != nil { + return nil, err + } + req.Header = http.Header{ + "Content-Type": []string{"audio/basic"}, + "Content-Length": []string{"9999999"}, + "Connection": []string{"Keep-Alive"}, + "Cache-Control": []string{"no-cache"}, + } + + if u.Port() == "" { + u.Host += ":80" + } + + conn, err := net.DialTimeout("tcp", u.Host, core.ConnDialTimeout) + if err != nil { + return nil, err + } + + _ = conn.SetWriteDeadline(time.Now().Add(core.ConnDeadline)) + if err = req.Write(conn); err != nil { + return nil, err + } + + medias := []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecPCMU, ClockRate: 8000}, + }, + }, + } + + return &Client{ + core.Connection{ + ID: core.NewID(), + FormatName: "doorbird", + Protocol: "http", + URL: rawURL, + Medias: medias, + Transport: conn, + }, + conn, + }, nil +} + +func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + return nil, core.ErrCantGetTrack +} + +func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { + sender := core.NewSender(media, track.Codec) + + sender.Handler = func(pkt *rtp.Packet) { + _ = c.conn.SetWriteDeadline(time.Now().Add(core.ConnDeadline)) + if n, err := c.conn.Write(pkt.Payload); err == nil { + c.Send += n + } + } + + sender.HandleRTP(track) + c.Senders = append(c.Senders, sender) + return nil +} + +func (c *Client) Start() (err error) { + _, err = c.conn.Read(nil) + return +} diff --git a/pkg/pcm/producer.go b/pkg/pcm/producer.go new file mode 100644 index 00000000..8a957f6d --- /dev/null +++ b/pkg/pcm/producer.go @@ -0,0 +1,55 @@ +package pcm + +import ( + "io" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +type Producer struct { + core.Connection + rd io.Reader +} + +func Open(rd io.Reader) (*Producer, error) { + medias := []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + {Name: core.CodecPCMU, ClockRate: 8000}, + }, + }, + } + return &Producer{ + core.Connection{ + ID: core.NewID(), + FormatName: "pcm", + Medias: medias, + Transport: rd, + }, + rd, + }, nil +} + +func (c *Producer) Start() error { + for { + payload := make([]byte, 1024) + if _, err := io.ReadFull(c.rd, payload); err != nil { + return err + } + + c.Recv += 1024 + + if len(c.Receivers) == 0 { + continue + } + + pkt := &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: payload, + } + c.Receivers[0].WriteRTP(pkt) + } +} From 5b53ca7cf1df134923a31dca00aaa84c460a55a7 Mon Sep 17 00:00:00 2001 From: oeiber <46045177+oeiber@users.noreply.github.com> Date: Sun, 24 Nov 2024 16:19:58 +0100 Subject: [PATCH 2/4] Removing double additional '&' in rawURL --- pkg/doorbird/backchannel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/doorbird/backchannel.go b/pkg/doorbird/backchannel.go index c6a7dec1..e5f3257c 100644 --- a/pkg/doorbird/backchannel.go +++ b/pkg/doorbird/backchannel.go @@ -25,7 +25,7 @@ func Dial(rawURL string) (*Client, error) { user := u.User.Username() pass, _ := u.User.Password() - rawURL = fmt.Sprintf("http://%s/bha-api/audio-transmit.cgi?http-user=%s&&http-password=%s", u.Host, user, pass) + rawURL = fmt.Sprintf("http://%s/bha-api/audio-transmit.cgi?http-user=%s&http-password=%s", u.Host, user, pass) req, err := http.NewRequest("POST", rawURL, nil) if err != nil { From d8c0f9d1d931fe33947dd25d2af9c2c5a976579d Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 5 Dec 2024 10:54:46 +0300 Subject: [PATCH 3/4] Update support doorbird source #1060 --- pkg/doorbird/backchannel.go | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/pkg/doorbird/backchannel.go b/pkg/doorbird/backchannel.go index e5f3257c..82379383 100644 --- a/pkg/doorbird/backchannel.go +++ b/pkg/doorbird/backchannel.go @@ -3,7 +3,6 @@ package doorbird import ( "fmt" "net" - "net/http" "net/url" "time" @@ -25,19 +24,6 @@ func Dial(rawURL string) (*Client, error) { user := u.User.Username() pass, _ := u.User.Password() - rawURL = fmt.Sprintf("http://%s/bha-api/audio-transmit.cgi?http-user=%s&http-password=%s", u.Host, user, pass) - - req, err := http.NewRequest("POST", rawURL, nil) - if err != nil { - return nil, err - } - req.Header = http.Header{ - "Content-Type": []string{"audio/basic"}, - "Content-Length": []string{"9999999"}, - "Connection": []string{"Keep-Alive"}, - "Cache-Control": []string{"no-cache"}, - } - if u.Port() == "" { u.Host += ":80" } @@ -47,8 +33,15 @@ func Dial(rawURL string) (*Client, error) { return nil, err } + s := fmt.Sprintf("POST /bha-api/audio-transmit.cgi?http-user=%s&http-password=%s HTTP/1.0\r\n", user, pass) + + "Content-Type: audio/basic\r\n" + + "Content-Length: 9999999\r\n" + + "Connection: Keep-Alive\r\n" + + "Cache-Control: no-cache\r\n" + + "\r\n" + _ = conn.SetWriteDeadline(time.Now().Add(core.ConnDeadline)) - if err = req.Write(conn); err != nil { + if _, err = conn.Write([]byte(s)); err != nil { return nil, err } From f1ba5e95ec21f3e03c158a372ca25d53193c6992 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 6 Dec 2024 12:34:31 +0300 Subject: [PATCH 4/4] Fix parsing RTSP Transport header #1235 --- pkg/rtsp/server.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index 7953b0dc..c96125a2 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -149,7 +149,7 @@ func (c *Conn) Accept() error { } const transport = "RTP/AVP/TCP;unicast;interleaved=" - if strings.HasPrefix(tr, transport) { + if tr = core.Between(tr, "interleaved=", ";"); tr != "" { c.session = core.RandString(8, 10) c.state = StateSetup @@ -157,13 +157,13 @@ func (c *Conn) Accept() error { if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { // mark sender as SETUP c.Senders[i].Media.ID = MethodSetup - tr = fmt.Sprintf("RTP/AVP/TCP;unicast;interleaved=%d-%d", i*2, i*2+1) - res.Header.Set("Transport", tr) + tr = fmt.Sprintf("%d-%d", i*2, i*2+1) + res.Header.Set("Transport", transport+tr) } else { res.Status = "400 Bad Request" } } else { - res.Header.Set("Transport", tr[:len(transport)+3]) + res.Header.Set("Transport", transport+tr) } } else { res.Status = "461 Unsupported transport"