From 340fd81778ce2e2d71c6707b733b76e5e412828b Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 9 Nov 2024 18:17:41 +0300 Subject: [PATCH 01/22] Fix loop request, ex. `camera1: ffmpeg:camera1` --- internal/ffmpeg/ffmpeg.go | 2 ++ internal/rtsp/rtsp.go | 3 +++ internal/streams/add_consumer.go | 6 ++++++ pkg/core/connection.go | 5 +++++ 4 files changed, 16 insertions(+) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 12a9be83..b934be53 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -179,6 +179,7 @@ func parseArgs(s string) *ffmpeg.Args { Version: verAV, } + var source = s var query url.Values if i := strings.IndexByte(s, '#'); i >= 0 { query = streams.ParseQuery(s[i+1:]) @@ -221,6 +222,7 @@ func parseArgs(s string) *ffmpeg.Args { default: s += "?video&audio" } + s += "&source=ffmpeg:" + url.QueryEscape(source) args.Input = inputTemplate("rtsp", s, query) } else if i = strings.Index(s, "?"); i > 0 { switch s[:i] { diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index 230bdece..a4075f6c 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -188,6 +188,9 @@ func tcpHandler(conn *rtsp.Conn) { conn.PacketSize = uint16(core.Atoi(s)) } + // will help to protect looping requests to same source + conn.Connection.Source = query.Get("source") + if err := stream.AddConsumer(conn); err != nil { log.Warn().Err(err).Str("stream", name).Msg("[rtsp]") return diff --git a/internal/streams/add_consumer.go b/internal/streams/add_consumer.go index eb767691..d72e17ee 100644 --- a/internal/streams/add_consumer.go +++ b/internal/streams/add_consumer.go @@ -22,6 +22,12 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) { producers: for prodN, prod := range s.producers { + // check for loop request, ex. `camera1: ffmpeg:camera1` + if info, ok := cons.(core.Info); ok && prod.url == info.GetSource() { + log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN) + continue + } + if prodErrors[prodN] != nil { log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN) continue diff --git a/pkg/core/connection.go b/pkg/core/connection.go index 2c3f2196..cc0f43e4 100644 --- a/pkg/core/connection.go +++ b/pkg/core/connection.go @@ -25,6 +25,7 @@ type Info interface { SetSource(string) SetURL(string) WithRequest(*http.Request) + GetSource() string } // Connection just like webrtc.PeerConnection @@ -123,6 +124,10 @@ func (c *Connection) WithRequest(r *http.Request) { c.UserAgent = r.UserAgent() } +func (c *Connection) GetSource() string { + return c.Source +} + // Create like os.Create, init Consumer with existing Transport func Create(w io.Writer) (*Connection, error) { return &Connection{Transport: w}, nil From e982257271e15135c80be6f102b38d1e9394a9b1 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Sun, 10 Nov 2024 09:00:37 +0900 Subject: [PATCH 02/22] docs: update README.md shapshot -> snapshot --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 70ad4712..c37bf2f6 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ Available modules: - [api](#module-api) - HTTP API (important for WebRTC support) - [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support) - [webrtc](#module-webrtc) - WebRTC Server -- [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot Server +- [mp4](#module-mp4) - MSE, MP4 stream and MP4 snapshot Server - [hls](#module-hls) - HLS TS or fMP4 stream Server - [mjpeg](#module-mjpeg) - MJPEG Server - [ffmpeg](#source-ffmpeg) - FFmpeg integration From 2348d12e9d701b1506db76cddf59af30a1acaf4e Mon Sep 17 00:00:00 2001 From: Jerome Date: Sun, 10 Nov 2024 13:13:31 +0100 Subject: [PATCH 03/22] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c37bf2f6..e87e35a0 100644 --- a/README.md +++ b/README.md @@ -648,10 +648,11 @@ This source type support Roborock vacuums with cameras. Known working models: - Roborock S6 MaxV - only video (the vacuum has no microphone) - Roborock S7 MaxV - video and two way audio +- Roborock Qrevo MaxV - video and two way audio -Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config. +Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock) or the [core integration](https://www.home-assistant.io/integrations/roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config. -If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 678) to the end of the roborock-link. +If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 789) to the end of the roborock-link. #### Source: WebRTC From fde04bd62512cfc4eb998354381a31a7f9ed21cc Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Sun, 10 Nov 2024 19:27:59 +0100 Subject: [PATCH 04/22] Improve codec not matched error by including kind --- internal/streams/add_consumer.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/internal/streams/add_consumer.go b/internal/streams/add_consumer.go index d72e17ee..f1c9aebc 100644 --- a/internal/streams/add_consumer.go +++ b/internal/streams/add_consumer.go @@ -130,12 +130,15 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error // 2. Return "codecs not matched" if prodMedias != nil { - var prod, cons string + var prod, cons map[string]string = make(map[string]string), make(map[string]string) for _, media := range prodMedias { if media.Direction == core.DirectionRecvonly { for _, codec := range media.Codecs { - prod = appendString(prod, codec.PrintName()) + if _, ok := prod[codec.Name]; !ok { + prod[media.Kind] = "" + } + prod[media.Kind] = appendString(prod[media.Kind], codec.PrintName()) } } } @@ -143,18 +146,29 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error for _, media := range consMedias { if media.Direction == core.DirectionSendonly { for _, codec := range media.Codecs { - cons = appendString(cons, codec.PrintName()) + if _, ok := cons[codec.Name]; !ok { + cons[media.Kind] = "" + } + cons[media.Kind] = appendString(cons[media.Kind], codec.PrintName()) } } } - return errors.New("streams: codecs not matched: " + prod + " => " + cons) + return errors.New("streams: codecs not matched: " + mapToString(prod) + " => " + mapToString(cons)) } // 3. Return unknown error return errors.New("streams: unknown error") } +func mapToString(m map[string]string) string { + var s string + for k, v := range m { + s = appendString(s, "("+k+": "+v+")") + } + return s +} + func appendString(s, elem string) string { if strings.Contains(s, elem) { return s From 7640a42bfcdc6e6cc50ffd0fc2dab9864d8d2f4e Mon Sep 17 00:00:00 2001 From: Andrew Marshall Date: Sun, 10 Nov 2024 15:42:40 -0500 Subject: [PATCH 05/22] Read from credential files See https://systemd.io/CREDENTIALS/. This will also work for Docker Secrets by setting `CREDENTIALS_DIRECTORY=/run/secrets`. --- internal/app/README.md | 6 +++--- pkg/shell/shell.go | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/app/README.md b/internal/app/README.md index 2460daa2..9ec3d9fc 100644 --- a/internal/app/README.md +++ b/internal/app/README.md @@ -19,15 +19,15 @@ go2rtc -c log.format=text -c /config/go2rtc.yaml -c rtsp.listen='' -c /usr/local ## Environment variables -Also go2rtc support templates for using environment variables in any part of config: +There is support for loading external variables into the config. First, they will be attempted to be loaded from [credential files](https://systemd.io/CREDENTIALS). If `CREDENTIALS_DIRECTORY` is not set, then the key will be loaded from an environment variable. If no environment variable is set, then the string will be left as-is. ```yaml streams: camera1: rtsp://rtsp:${CAMERA_PASSWORD}@192.168.1.123/av_stream/ch0 rtsp: - username: ${RTSP_USER:admin} # "admin" if env "RTSP_USER" not set - password: ${RTSP_PASS:secret} # "secret" if env "RTSP_PASS" not set + username: ${RTSP_USER:admin} # "admin" if "RTSP_USER" not set + password: ${RTSP_PASS:secret} # "secret" if "RTSP_PASS" not set ``` ## JSON Schema diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go index d538b961..75df671f 100644 --- a/pkg/shell/shell.go +++ b/pkg/shell/shell.go @@ -3,6 +3,7 @@ package shell import ( "os" "os/signal" + "path/filepath" "regexp" "strings" "syscall" @@ -51,6 +52,13 @@ func ReplaceEnvVars(text string) string { dok = true } + if dir, vok := os.LookupEnv("CREDENTIALS_DIRECTORY"); vok { + value, err := os.ReadFile(filepath.Join(dir, key)) + if err == nil { + return strings.TrimSpace(string(value)) + } + } + if value, vok := os.LookupEnv(key); vok { return value } From d372597bdbbb0618093d0b2cec72b2d1180f52ea Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 11 Nov 2024 09:27:21 +0100 Subject: [PATCH 06/22] Lower codec not matched error for ffmpeg to debug --- internal/rtsp/rtsp.go | 9 +++++- internal/streams/add_consumer.go | 48 ++++++++++++++++++++------------ 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index a4075f6c..cc6d5727 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -192,7 +192,14 @@ func tcpHandler(conn *rtsp.Conn) { conn.Connection.Source = query.Get("source") if err := stream.AddConsumer(conn); err != nil { - log.Warn().Err(err).Str("stream", name).Msg("[rtsp]") + logEvent := log.Warn() + + if _, ok := err.(*streams.CodecNotMatchedError); ok && strings.HasPrefix(query.Get("source"), "ffmpeg") { + // lower codec not matched error for ffmpeg to debug + logEvent = log.Debug() + } + + logEvent.Err(err).Str("stream", name).Msg("[rtsp]") return } diff --git a/internal/streams/add_consumer.go b/internal/streams/add_consumer.go index d72e17ee..0b8f6cf0 100644 --- a/internal/streams/add_consumer.go +++ b/internal/streams/add_consumer.go @@ -130,25 +130,10 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error // 2. Return "codecs not matched" if prodMedias != nil { - var prod, cons string - - for _, media := range prodMedias { - if media.Direction == core.DirectionRecvonly { - for _, codec := range media.Codecs { - prod = appendString(prod, codec.PrintName()) - } - } + return &CodecNotMatchedError{ + producerMedias: prodMedias, + consumerMedias: consMedias, } - - for _, media := range consMedias { - if media.Direction == core.DirectionSendonly { - for _, codec := range media.Codecs { - cons = appendString(cons, codec.PrintName()) - } - } - } - - return errors.New("streams: codecs not matched: " + prod + " => " + cons) } // 3. Return unknown error @@ -164,3 +149,30 @@ func appendString(s, elem string) string { } return s + ", " + elem } + +type CodecNotMatchedError struct { + producerMedias []*core.Media + consumerMedias []*core.Media +} + +func (e *CodecNotMatchedError) Error() string { + var prod, cons string + + for _, media := range e.producerMedias { + if media.Direction == core.DirectionRecvonly { + for _, codec := range media.Codecs { + prod = appendString(prod, codec.PrintName()) + } + } + } + + for _, media := range e.consumerMedias { + if media.Direction == core.DirectionSendonly { + for _, codec := range media.Codecs { + cons = appendString(cons, codec.PrintName()) + } + } + } + + return "streams: codecs not matched: " + prod + " => " + cons +} From 831aa03c9f184b0be20b624569bcd5dda8510e50 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 11 Nov 2024 11:16:12 +0100 Subject: [PATCH 07/22] Implement suggestion --- internal/ffmpeg/ffmpeg.go | 2 + internal/rtsp/rtsp.go | 8 ++-- internal/streams/add_consumer.go | 66 ++++++++++++++++++-------------- 3 files changed, 44 insertions(+), 32 deletions(-) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index b934be53..b57dcc70 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -223,6 +223,8 @@ func parseArgs(s string) *ffmpeg.Args { s += "?video&audio" } s += "&source=ffmpeg:" + url.QueryEscape(source) + // change codec not matched error level to debug + s += "&" + string(streams.CodecNotMatchedErrorCode) + "=" + zerolog.DebugLevel.String() args.Input = inputTemplate("rtsp", s, query) } else if i = strings.Index(s, "?"); i > 0 { switch s[:i] { diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index cc6d5727..1bb41d83 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -194,9 +194,11 @@ func tcpHandler(conn *rtsp.Conn) { if err := stream.AddConsumer(conn); err != nil { logEvent := log.Warn() - if _, ok := err.(*streams.CodecNotMatchedError); ok && strings.HasPrefix(query.Get("source"), "ffmpeg") { - // lower codec not matched error for ffmpeg to debug - logEvent = log.Debug() + if err, ok := err.(*streams.ErrorWithErrorCode); ok { + level, parseErr := zerolog.ParseLevel(query.Get(err.Code())) + if parseErr == nil { + logEvent = log.WithLevel(level) + } } logEvent.Err(err).Str("stream", name).Msg("[rtsp]") diff --git a/internal/streams/add_consumer.go b/internal/streams/add_consumer.go index 0b8f6cf0..efe8542b 100644 --- a/internal/streams/add_consumer.go +++ b/internal/streams/add_consumer.go @@ -1,7 +1,6 @@ package streams import ( - "errors" "strings" "github.com/AlexxIT/go2rtc/pkg/core" @@ -125,19 +124,34 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error } if len(text) != 0 { - return errors.New("streams: " + text) + return &ErrorWithErrorCode{MultipleErrorCode, "streams: " + text} } // 2. Return "codecs not matched" if prodMedias != nil { - return &CodecNotMatchedError{ - producerMedias: prodMedias, - consumerMedias: consMedias, + var prod, cons string + + for _, media := range prodMedias { + if media.Direction == core.DirectionRecvonly { + for _, codec := range media.Codecs { + prod = appendString(prod, codec.PrintName()) + } + } } + + for _, media := range consMedias { + if media.Direction == core.DirectionSendonly { + for _, codec := range media.Codecs { + cons = appendString(cons, codec.PrintName()) + } + } + } + + return &ErrorWithErrorCode{CodecNotMatchedErrorCode, "streams: codecs not matched: " + prod + " => " + cons} } // 3. Return unknown error - return errors.New("streams: unknown error") + return &ErrorWithErrorCode{UnknownErrorCode, "streams: unknown error"} } func appendString(s, elem string) string { @@ -150,29 +164,23 @@ func appendString(s, elem string) string { return s + ", " + elem } -type CodecNotMatchedError struct { - producerMedias []*core.Media - consumerMedias []*core.Media +type ErrorCode string + +const ( + CodecNotMatchedErrorCode ErrorCode = "codecNotMatched" + MultipleErrorCode ErrorCode = "multiple" + UnknownErrorCode ErrorCode = "unknown" +) + +type ErrorWithErrorCode struct { + code ErrorCode + message string } -func (e *CodecNotMatchedError) Error() string { - var prod, cons string - - for _, media := range e.producerMedias { - if media.Direction == core.DirectionRecvonly { - for _, codec := range media.Codecs { - prod = appendString(prod, codec.PrintName()) - } - } - } - - for _, media := range e.consumerMedias { - if media.Direction == core.DirectionSendonly { - for _, codec := range media.Codecs { - cons = appendString(cons, codec.PrintName()) - } - } - } - - return "streams: codecs not matched: " + prod + " => " + cons +func (e *ErrorWithErrorCode) Error() string { + return e.message +} + +func (e *ErrorWithErrorCode) Code() string { + return string(e.code) } From 9ee8174d5f12382831f1f2b36c094f416738c153 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 11 Nov 2024 16:36:51 +0300 Subject: [PATCH 08/22] Code refactoring for #1448 --- internal/streams/add_consumer.go | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/internal/streams/add_consumer.go b/internal/streams/add_consumer.go index f1c9aebc..7400ce6e 100644 --- a/internal/streams/add_consumer.go +++ b/internal/streams/add_consumer.go @@ -130,15 +130,12 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error // 2. Return "codecs not matched" if prodMedias != nil { - var prod, cons map[string]string = make(map[string]string), make(map[string]string) + var prod, cons string for _, media := range prodMedias { if media.Direction == core.DirectionRecvonly { for _, codec := range media.Codecs { - if _, ok := prod[codec.Name]; !ok { - prod[media.Kind] = "" - } - prod[media.Kind] = appendString(prod[media.Kind], codec.PrintName()) + prod = appendString(prod, media.Kind+":"+codec.PrintName()) } } } @@ -146,29 +143,18 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error for _, media := range consMedias { if media.Direction == core.DirectionSendonly { for _, codec := range media.Codecs { - if _, ok := cons[codec.Name]; !ok { - cons[media.Kind] = "" - } - cons[media.Kind] = appendString(cons[media.Kind], codec.PrintName()) + cons = appendString(cons, media.Kind+":"+codec.PrintName()) } } } - return errors.New("streams: codecs not matched: " + mapToString(prod) + " => " + mapToString(cons)) + return errors.New("streams: codecs not matched: " + prod + " => " + cons) } // 3. Return unknown error return errors.New("streams: unknown error") } -func mapToString(m map[string]string) string { - var s string - for k, v := range m { - s = appendString(s, "("+k+": "+v+")") - } - return s -} - func appendString(s, elem string) string { if strings.Contains(s, elem) { return s From 570b7d0d97ea222b9db719802c4dce0eea0ef7b9 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 11 Nov 2024 17:45:55 +0300 Subject: [PATCH 09/22] Code refactoring for #1450 --- internal/ffmpeg/ffmpeg.go | 5 +++-- internal/rtsp/rtsp.go | 21 ++++++++++----------- internal/streams/add_consumer.go | 28 ++++------------------------ 3 files changed, 17 insertions(+), 37 deletions(-) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index b57dcc70..25d61e4b 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -223,8 +223,9 @@ func parseArgs(s string) *ffmpeg.Args { s += "?video&audio" } s += "&source=ffmpeg:" + url.QueryEscape(source) - // change codec not matched error level to debug - s += "&" + string(streams.CodecNotMatchedErrorCode) + "=" + zerolog.DebugLevel.String() + for _, v := range query["query"] { + s += "&" + v + } args.Input = inputTemplate("rtsp", s, query) } else if i = strings.Index(s, "?"); i > 0 { switch s[:i] { diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index 1bb41d83..0fe135f8 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -147,6 +147,7 @@ func tcpHandler(conn *rtsp.Conn) { var closer func() trace := log.Trace().Enabled() + level := zerolog.WarnLevel conn.Listen(func(msg any) { if trace { @@ -188,20 +189,18 @@ func tcpHandler(conn *rtsp.Conn) { conn.PacketSize = uint16(core.Atoi(s)) } + // param name like ffmpeg style https://ffmpeg.org/ffmpeg-protocols.html + if s := query.Get("log_level"); s != "" { + if lvl, err := zerolog.ParseLevel(s); err == nil { + level = lvl + } + } + // will help to protect looping requests to same source conn.Connection.Source = query.Get("source") if err := stream.AddConsumer(conn); err != nil { - logEvent := log.Warn() - - if err, ok := err.(*streams.ErrorWithErrorCode); ok { - level, parseErr := zerolog.ParseLevel(query.Get(err.Code())) - if parseErr == nil { - logEvent = log.WithLevel(level) - } - } - - logEvent.Err(err).Str("stream", name).Msg("[rtsp]") + log.WithLevel(level).Err(err).Str("stream", name).Msg("[rtsp]") return } @@ -239,7 +238,7 @@ func tcpHandler(conn *rtsp.Conn) { if err := conn.Accept(); err != nil { if err != io.EOF { - log.Warn().Err(err).Caller().Send() + log.WithLevel(level).Err(err).Caller().Send() } if closer != nil { closer() diff --git a/internal/streams/add_consumer.go b/internal/streams/add_consumer.go index efe8542b..d72e17ee 100644 --- a/internal/streams/add_consumer.go +++ b/internal/streams/add_consumer.go @@ -1,6 +1,7 @@ package streams import ( + "errors" "strings" "github.com/AlexxIT/go2rtc/pkg/core" @@ -124,7 +125,7 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error } if len(text) != 0 { - return &ErrorWithErrorCode{MultipleErrorCode, "streams: " + text} + return errors.New("streams: " + text) } // 2. Return "codecs not matched" @@ -147,11 +148,11 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error } } - return &ErrorWithErrorCode{CodecNotMatchedErrorCode, "streams: codecs not matched: " + prod + " => " + cons} + return errors.New("streams: codecs not matched: " + prod + " => " + cons) } // 3. Return unknown error - return &ErrorWithErrorCode{UnknownErrorCode, "streams: unknown error"} + return errors.New("streams: unknown error") } func appendString(s, elem string) string { @@ -163,24 +164,3 @@ func appendString(s, elem string) string { } return s + ", " + elem } - -type ErrorCode string - -const ( - CodecNotMatchedErrorCode ErrorCode = "codecNotMatched" - MultipleErrorCode ErrorCode = "multiple" - UnknownErrorCode ErrorCode = "unknown" -) - -type ErrorWithErrorCode struct { - code ErrorCode - message string -} - -func (e *ErrorWithErrorCode) Error() string { - return e.message -} - -func (e *ErrorWithErrorCode) Code() string { - return string(e.code) -} From dbe9e4aadeeae306b2e90a4668f077d405448eff Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 11 Nov 2024 20:20:53 +0300 Subject: [PATCH 10/22] Update version to 1.9.7 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index df3468f4..7f94b930 100644 --- a/main.go +++ b/main.go @@ -36,7 +36,7 @@ import ( ) func main() { - app.Version = "1.9.6" + app.Version = "1.9.7" // 1. Core modules: app, api/ws, streams From 25145f72e56560666e6718740442d16a8c5127bb Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 14 Nov 2024 19:39:26 +0300 Subject: [PATCH 11/22] Fix broken incoming sources after v1.9.7 #1458 --- internal/streams/play.go | 2 +- internal/streams/stream.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/streams/play.go b/internal/streams/play.go index 7ada66e6..9bec7258 100644 --- a/internal/streams/play.go +++ b/internal/streams/play.go @@ -103,7 +103,7 @@ func (s *Stream) Play(source string) error { } func (s *Stream) AddInternalProducer(conn core.Producer) { - producer := &Producer{conn: conn, state: stateInternal} + producer := &Producer{conn: conn, state: stateInternal, url: "internal"} s.mu.Lock() s.producers = append(s.producers, producer) s.mu.Unlock() diff --git a/internal/streams/stream.go b/internal/streams/stream.go index e194e0ac..569e63ee 100644 --- a/internal/streams/stream.go +++ b/internal/streams/stream.go @@ -76,7 +76,7 @@ func (s *Stream) RemoveConsumer(cons core.Consumer) { } func (s *Stream) AddProducer(prod core.Producer) { - producer := &Producer{conn: prod, state: stateExternal} + producer := &Producer{conn: prod, state: stateExternal, url: "external"} s.mu.Lock() s.producers = append(s.producers, producer) s.mu.Unlock() From 194d1dae51ef547f368d8c467dbee782527ef11f Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 24 Nov 2024 13:09:13 +0300 Subject: [PATCH 12/22] 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 13/22] 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 14/22] 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 15/22] 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" From 8ecaabfce9b563f3fb6a0025531add7786865338 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 16 Dec 2024 20:24:45 +0300 Subject: [PATCH 16/22] Add support VIGI cameras #1470 --- internal/tapo/tapo.go | 4 ++ pkg/tapo/client.go | 106 +++++++++++++++++++++++++++++------------- pkg/tapo/producer.go | 2 +- 3 files changed, 79 insertions(+), 33 deletions(-) diff --git a/internal/tapo/tapo.go b/internal/tapo/tapo.go index 724c9e86..88eff5c4 100644 --- a/internal/tapo/tapo.go +++ b/internal/tapo/tapo.go @@ -15,4 +15,8 @@ func Init() { streams.HandleFunc("tapo", func(source string) (core.Producer, error) { return tapo.Dial(source) }) + + streams.HandleFunc("vigi", func(source string) (core.Producer, error) { + return tapo.Dial(source) + }) } diff --git a/pkg/tapo/client.go b/pkg/tapo/client.go index 3585011c..6ccafe4e 100644 --- a/pkg/tapo/client.go +++ b/pkg/tapo/client.go @@ -27,7 +27,7 @@ import ( type Client struct { core.Listener - url string + url *url.URL medias []*core.Media receivers []*core.Receiver @@ -52,17 +52,15 @@ type cbcMode interface { SetIV([]byte) } -func Dial(url string) (*Client, error) { - var err error - c := &Client{url: url} - if c.conn1, err = c.newConn(); err != nil { - return nil, err - } - return c, nil -} - -func (c *Client) newConn() (net.Conn, error) { - u, err := url.Parse(c.url) +// Dial support different urls: +// - tapo://{cloud-password}@192.168.1.123 - auth to Tapo cameras +// with cloud password (autodetect hash method) +// - tapo://admin:{hashed-cloud-password}@192.168.1.123 - auth to Tapo cameras +// with pre-hashed cloud password +// - vigi://admin:{password}@192.168.1.123 - auth to Vigi cameras with password +// for admin account (other not supported) +func Dial(rawURL string) (*Client, error) { + u, err := url.Parse(rawURL) if err != nil { return nil, err } @@ -71,21 +69,31 @@ func (c *Client) newConn() (net.Conn, error) { u.Host += ":8800" } - req, err := http.NewRequest("POST", "http://"+u.Host+"/stream", nil) + c := &Client{url: u} + if c.conn1, err = c.newConn(); err != nil { + return nil, err + } + return c, nil +} + +func (c *Client) newConn() (net.Conn, error) { + req, err := http.NewRequest("POST", "http://"+c.url.Host+"/stream", nil) if err != nil { return nil, err } - query := u.Query() + query := c.url.Query() if deviceId := query.Get("deviceId"); deviceId != "" { req.URL.RawQuery = "deviceId=" + deviceId } - req.URL.User = u.User req.Header.Set("Content-Type", "multipart/mixed; boundary=--client-stream-boundary--") - conn, res, err := dial(req) + username := c.url.User.Username() + password, _ := c.url.User.Password() + + conn, res, err := dial(req, c.url.Scheme, username, password) if err != nil { return nil, err } @@ -95,7 +103,7 @@ func (c *Client) newConn() (net.Conn, error) { } if c.decrypt == nil { - c.newDectypter(res) + c.newDectypter(res, c.url.Scheme, username, password) } channel := query.Get("channel") @@ -119,14 +127,18 @@ func (c *Client) newConn() (net.Conn, error) { return conn, nil } -func (c *Client) newDectypter(res *http.Response) { - username := res.Request.URL.User.Username() - password, _ := res.Request.URL.User.Password() +func (c *Client) newDectypter(res *http.Response, brand, username, password string) { + exchange := res.Header.Get("Key-Exchange") + nonce := core.Between(exchange, `nonce="`, `"`) - // extract nonce from response - // cipher="AES_128_CBC" username="admin" padding="PKCS7_16" algorithm="MD5" nonce="***" - nonce := res.Header.Get("Key-Exchange") - nonce = core.Between(nonce, `nonce="`, `"`) + if brand == "tapo" && password == "" { + if strings.Contains(exchange, `encrypt_type="3"`) { + password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username))) + } else { + password = fmt.Sprintf("%16X", md5.Sum([]byte(username))) + } + username = "admin" + } key := md5.Sum([]byte(nonce + ":" + password)) iv := md5.Sum([]byte(username + ":" + nonce)) @@ -263,16 +275,12 @@ func (c *Client) Request(conn net.Conn, body []byte) (string, error) { } } -func dial(req *http.Request) (net.Conn, *http.Response, error) { +func dial(req *http.Request, brand, username, password string) (net.Conn, *http.Response, error) { conn, err := net.DialTimeout("tcp", req.URL.Host, core.ConnDialTimeout) if err != nil { return nil, nil, err } - username := req.URL.User.Username() - password, _ := req.URL.User.Password() - req.URL.User = nil - if err = req.Write(conn); err != nil { return nil, nil, err } @@ -291,7 +299,7 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) { return nil, nil, fmt.Errorf("Expected StatusCode to be %d, received %d", http.StatusUnauthorized, res.StatusCode) } - if password == "" { + if brand == "tapo" && password == "" { // support cloud password in place of username if strings.Contains(auth, `encrypt_type="3"`) { password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username))) @@ -299,6 +307,8 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) { password = fmt.Sprintf("%16X", md5.Sum([]byte(username))) } username = "admin" + } else if brand == "vigi" && username == "admin" { + password = securityEncode(password) } realm := tcp.Between(auth, `realm="`, `"`) @@ -331,7 +341,39 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) { return nil, nil, err } - req.URL.User = url.UserPassword(username, password) - return conn, res, nil } + +const ( + keyShort = "RDpbLfCPsJZ7fiv" + keyLong = "yLwVl0zKqws7LgKPRQ84Mdt708T1qQ3Ha7xv3H7NyU84p21BriUWBU43odz3iP4rBL3cD02KZciXTysVXiV8ngg6vL48rPJyAUw0HurW20xqxv9aYb4M9wK1Ae0wlro510qXeU07kV57fQMc8L6aLgMLwygtc0F10a0Dg70TOoouyFhdysuRMO51yY5ZlOZZLEal1h0t9YQW0Ko7oBwmCAHoic4HYbUyVeU3sfQ1xtXcPcf1aT303wAQhv66qzW" +) + +func securityEncode(s string) string { + size := len(s) + + var n int // max + if size > len(keyShort) { + n = size + } else { + n = len(keyShort) + } + + b := make([]byte, n) + + for i := 0; i < n; i++ { + c1 := 187 + c2 := 187 + if i >= size { + c1 = int(keyShort[i]) + } else if i >= len(keyShort) { + c2 = int(s[i]) + } else { + c1 = int(keyShort[i]) + c2 = int(s[i]) + } + b[i] = keyLong[(c1^c2)%len(keyLong)] + } + + return string(b) +} diff --git a/pkg/tapo/producer.go b/pkg/tapo/producer.go index 7d66d907..87a91ff5 100644 --- a/pkg/tapo/producer.go +++ b/pkg/tapo/producer.go @@ -77,7 +77,7 @@ func (c *Client) Stop() error { func (c *Client) MarshalJSON() ([]byte, error) { info := &core.Connection{ ID: core.ID(c), - FormatName: "tapo", + FormatName: c.url.Scheme, Protocol: "http", Medias: c.medias, Recv: c.recv, From 3a50b3678d132f301fe53de1be7ba8054665c4e4 Mon Sep 17 00:00:00 2001 From: Alex Cortelyou <1689668+acortelyou@users.noreply.github.com> Date: Mon, 23 Dec 2024 23:43:39 -0800 Subject: [PATCH 17/22] Extend onvif server to support Unifi Protect --- internal/onvif/init.go | 13 ++++++ pkg/onvif/server.go | 100 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 103 insertions(+), 10 deletions(-) diff --git a/internal/onvif/init.go b/internal/onvif/init.go index 014c5e18..b8b4fca6 100644 --- a/internal/onvif/init.go +++ b/internal/onvif/init.go @@ -70,6 +70,10 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { // important for Hass: Media section res = onvif.GetCapabilitiesResponse(r.Host) + case onvif.ActionGetServices: + // important for Unifi: Media section + res = onvif.GetServicesResponse(r.Host) + case onvif.ActionGetSystemDateAndTime: // important for Hass res = onvif.GetSystemDateAndTimeResponse() @@ -95,8 +99,13 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { case onvif.ActionGetProfiles: // important for Hass: H264 codec, width, height + // important for Unifi: framerate, bitrate, quality res = onvif.GetProfilesResponse(streams.GetAll()) + case onvif.ActionGetVideoSources: + // important for Unifi: framerate, resolution + res = onvif.GetVideoSourcesResponse(streams.GetAll()) + case onvif.ActionGetStreamUri: host, _, err := net.SplitHostPort(r.Host) if err != nil { @@ -107,6 +116,10 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken") res = onvif.GetStreamUriResponse(uri) + case onvif.ActionGetSnapshotUri: + uri := "http://" + r.Host + "/api/frame.jpeg?src=" + onvif.FindTagValue(b, "ProfileToken") + res = onvif.GetSnapshotUriResponse(uri) + default: http.Error(w, "unsupported action", http.StatusBadRequest) log.Debug().Msgf("[onvif] unsupported request:\n%s", b) diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go index f8f2883c..df53dfab 100644 --- a/pkg/onvif/server.go +++ b/pkg/onvif/server.go @@ -16,6 +16,7 @@ const ( ActionGetServiceCapabilities = "GetServiceCapabilities" ActionGetProfiles = "GetProfiles" ActionGetStreamUri = "GetStreamUri" + ActionGetSnapshotUri = "GetSnapshotUri" ActionSystemReboot = "SystemReboot" ActionGetServices = "GetServices" @@ -65,6 +66,32 @@ func GetCapabilitiesResponse(host string) string { ` } +func GetServicesResponse(host string) string { + return ` + + + + + http://www.onvif.org/ver10/device/wsdl + http://` + host + `/onvif/device_service + + 2 + 5 + + + + http://www.onvif.org/ver10/media/wsdl + http://` + host + `/onvif/media_service + + 2 + 5 + + + + +` +} + func GetSystemDateAndTimeResponse() string { loc := time.Now() utc := loc.UTC() @@ -142,7 +169,7 @@ func GetServiceCapabilitiesResponse() string { - + @@ -171,14 +198,27 @@ func GetProfilesResponse(names []string) string { for i, name := range names { buf.WriteString(` - ` + name + ` - - H264 - - 1920 - 1080 - - + ` + name + ` + + ` + name + ` + H264 + + 1920 + 1080 + + + 29.97003 + 1 + 5000 + + 4 + PT1000S + + + ` + name + ` + ` + strconv.Itoa(i) + ` + + `) } @@ -190,15 +230,55 @@ func GetProfilesResponse(names []string) string { return buf.String() } + +func GetVideoSourcesResponse(names []string) string { + buf := bytes.NewBuffer(nil) + buf.WriteString(` + + + `) + + for i, _ := range names { + buf.WriteString(` + + 29.97003 + + 1920 + 1080 + + `) + } + + buf.WriteString(` + + +`) + + return buf.String() +} + func GetStreamUriResponse(uri string) string { return ` - ` + uri + ` + ` + uri + ` ` } + +func GetSnapshotUriResponse(uri string) string { + return ` + + + + + ` + uri + ` + + + +` +} From 159d9425a732eedef06a9dd797ec6a284339a1b6 Mon Sep 17 00:00:00 2001 From: Alex Cortelyou <1689668+acortelyou@users.noreply.github.com> Date: Tue, 24 Dec 2024 11:08:18 -0800 Subject: [PATCH 18/22] Remove non-essential fields --- pkg/onvif/server.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go index df53dfab..e2d56556 100644 --- a/pkg/onvif/server.go +++ b/pkg/onvif/server.go @@ -208,11 +208,8 @@ func GetProfilesResponse(names []string) string { 29.97003 - 1 5000 - 4 - PT1000S ` + name + ` @@ -241,7 +238,6 @@ func GetVideoSourcesResponse(names []string) string { for i, _ := range names { buf.WriteString(` - 29.97003 1920 1080 From 0d6b8fc6fc207c5c89c3cf0923b0cbd38f04ad1b Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 29 Dec 2024 11:44:56 +0300 Subject: [PATCH 19/22] Fix OPUS/48000/1 for RTSP from some cameras #1506 --- pkg/rtsp/helpers.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/rtsp/helpers.go b/pkg/rtsp/helpers.go index 6b07342d..346ecf73 100644 --- a/pkg/rtsp/helpers.go +++ b/pkg/rtsp/helpers.go @@ -70,8 +70,15 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) { // 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 == core.CodecH264 && codec.FmtpLine == "" { - codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions) + switch codec.Name { + case core.CodecH264: + if codec.FmtpLine == "" { + codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions) + } + case core.CodecOpus: + // fix OPUS for some cameras https://datatracker.ietf.org/doc/html/rfc7587 + codec.ClockRate = 48000 + codec.Channels = 2 } } From a3f084dcde33b9fd341fb3ae00c0d7f6ca7210e9 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 29 Dec 2024 22:37:04 +0300 Subject: [PATCH 20/22] RTMP server enhancement to support OpenIPC cameras --- pkg/flv/producer.go | 42 +++++++++++++++++++++++++++--------------- pkg/rtmp/server.go | 13 +++++++------ 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/pkg/flv/producer.go b/pkg/flv/producer.go index 66755217..7535a8a4 100644 --- a/pkg/flv/producer.go +++ b/pkg/flv/producer.go @@ -140,23 +140,29 @@ func (c *Producer) probe() error { // 1. Empty video/audio flag // 2. MedaData without stereo key for AAC // 3. Audio header after Video keyframe tag - waitType := []byte{TagData} - timeout := time.Now().Add(core.ProbeTimeout) - for len(waitType) != 0 && time.Now().Before(timeout) { + // OpenIPC camera sends: + // 1. Empty video/audio flag + // 2. No MetaData packet + // 3. Sends a video packet in more than 3 seconds + waitVideo := true + waitAudio := true + timeout := time.Now().Add(time.Second * 5) + + for (waitVideo || waitAudio) && time.Now().Before(timeout) { pkt, err := c.readPacket() if err != nil { return err } - if i := bytes.IndexByte(waitType, pkt.PayloadType); i < 0 { - continue - } else { - waitType = append(waitType[:i], waitType[i+1:]...) - } + //log.Printf("%d %0.20s", pkt.PayloadType, pkt.Payload) switch pkt.PayloadType { case TagAudio: + if !waitAudio { + continue + } + _ = pkt.Payload[1] // bounds codecID := pkt.Payload[0] >> 4 // SoundFormat @@ -179,8 +185,13 @@ func (c *Producer) probe() error { Codecs: []*core.Codec{codec}, } c.Medias = append(c.Medias, media) + waitAudio = false case TagVideo: + if !waitVideo { + continue + } + var codec *core.Codec if isExHeader(pkt.Payload) { @@ -213,19 +224,20 @@ func (c *Producer) probe() error { Codecs: []*core.Codec{codec}, } c.Medias = append(c.Medias, media) + waitVideo = false case TagData: if !bytes.Contains(pkt.Payload, []byte("onMetaData")) { - waitType = append(waitType, TagData) + continue } // Dahua cameras doesn't send videocodecid - if bytes.Contains(pkt.Payload, []byte("videocodecid")) || - bytes.Contains(pkt.Payload, []byte("width")) || - bytes.Contains(pkt.Payload, []byte("framerate")) { - waitType = append(waitType, TagVideo) + if !bytes.Contains(pkt.Payload, []byte("videocodecid")) && + !bytes.Contains(pkt.Payload, []byte("width")) && + !bytes.Contains(pkt.Payload, []byte("framerate")) { + waitVideo = false } - if bytes.Contains(pkt.Payload, []byte("audiocodecid")) { - waitType = append(waitType, TagAudio) + if !bytes.Contains(pkt.Payload, []byte("audiocodecid")) { + waitAudio = false } } } diff --git a/pkg/rtmp/server.go b/pkg/rtmp/server.go index ed727b98..3dcd4048 100644 --- a/pkg/rtmp/server.go +++ b/pkg/rtmp/server.go @@ -117,10 +117,6 @@ func (c *Conn) acceptCommand(b []byte) error { } } - if c.App == "" { - return fmt.Errorf("rtmp: read command %x", b) - } - payload := amf.EncodeItems( "_result", tID, map[string]any{"fmsVer": "FMS/3,0,1,123"}, @@ -129,9 +125,16 @@ func (c *Conn) acceptCommand(b []byte) error { return c.writeMessage(3, TypeCommand, 0, payload) case CommandReleaseStream: + // if app is empty - will use key as app + if c.App == "" && len(items) == 4 { + c.App, _ = items[3].(string) + } + payload := amf.EncodeItems("_result", tID, nil) return c.writeMessage(3, TypeCommand, 0, payload) + case CommandFCPublish: // no response + case CommandCreateStream: payload := amf.EncodeItems("_result", tID, nil, 1) return c.writeMessage(3, TypeCommand, 0, payload) @@ -140,8 +143,6 @@ func (c *Conn) acceptCommand(b []byte) error { c.Intent = cmd c.streamID = 1 - case CommandFCPublish: // no response - default: println("rtmp: unknown command: " + cmd) } From b8303b9a22e1727b9a4db8979b6177f5e13dd35c Mon Sep 17 00:00:00 2001 From: Alex Cortelyou <1689668+acortelyou@users.noreply.github.com> Date: Sun, 29 Dec 2024 16:16:49 -0800 Subject: [PATCH 21/22] Remove optional fields, normalize indentation --- pkg/onvif/server.go | 66 ++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go index e2d56556..bc3f8ffe 100644 --- a/pkg/onvif/server.go +++ b/pkg/onvif/server.go @@ -46,23 +46,23 @@ func GetRequestAction(b []byte) string { func GetCapabilitiesResponse(host string) string { return ` - - - - - http://` + host + `/onvif/device_service - - - http://` + host + `/onvif/media_service - - false - false - true - - - - - + + + + + http://` + host + `/onvif/device_service + + + http://` + host + `/onvif/media_service + + false + false + true + + + + + ` } @@ -197,31 +197,29 @@ func GetProfilesResponse(names []string) string { for i, name := range names { buf.WriteString(` - - ` + name + ` - + + ` + name + ` + ` + name + ` - H264 - - 1920 + H264 + + 1920 1080 - - 29.97003 - 5000 + - + ` + name + ` ` + strconv.Itoa(i) + ` - `) + `) } buf.WriteString(` - - + + `) return buf.String() @@ -233,11 +231,11 @@ func GetVideoSourcesResponse(names []string) string { buf.WriteString(` - `) + `) for i, _ := range names { buf.WriteString(` - + 1920 1080 @@ -246,8 +244,8 @@ func GetVideoSourcesResponse(names []string) string { } buf.WriteString(` - - + + `) return buf.String() From cf88bf9c23e7196cec60dc62644f12b2f20d8083 Mon Sep 17 00:00:00 2001 From: Alex Cortelyou <1689668+acortelyou@users.noreply.github.com> Date: Sun, 29 Dec 2024 16:22:49 -0800 Subject: [PATCH 22/22] Remove inaccurate comments --- internal/onvif/init.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/onvif/init.go b/internal/onvif/init.go index b8b4fca6..e5ed9a7c 100644 --- a/internal/onvif/init.go +++ b/internal/onvif/init.go @@ -71,7 +71,6 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { res = onvif.GetCapabilitiesResponse(r.Host) case onvif.ActionGetServices: - // important for Unifi: Media section res = onvif.GetServicesResponse(r.Host) case onvif.ActionGetSystemDateAndTime: @@ -99,11 +98,9 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { case onvif.ActionGetProfiles: // important for Hass: H264 codec, width, height - // important for Unifi: framerate, bitrate, quality res = onvif.GetProfilesResponse(streams.GetAll()) case onvif.ActionGetVideoSources: - // important for Unifi: framerate, resolution res = onvif.GetVideoSourcesResponse(streams.GetAll()) case onvif.ActionGetStreamUri: