From 340fd81778ce2e2d71c6707b733b76e5e412828b Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 9 Nov 2024 18:17:41 +0300 Subject: [PATCH 01/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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