From e080eac204aa77c3cdc55d3b4cfa45c26bc8a635 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Tue, 30 Apr 2024 01:49:01 +0300 Subject: [PATCH 001/130] refactor(mdns): consolidate platform-specific syscall files - Rename `syscall_linux.go` to `syscall.go` with build constraints for non-BSD and non-Windows platforms. - Merge `syscall_darwin.go` into `syscall_bsd.go` and adjust build constraints for BSD platforms (Darwin, FreeBSD, OpenBSD). - Remove redundant `syscall_freebsd.go`. - Add build constraints to `syscall_windows.go` for Windows platform. --- pkg/mdns/{syscall_linux.go => syscall.go} | 2 ++ .../{syscall_darwin.go => syscall_bsd.go} | 2 ++ pkg/mdns/syscall_freebsd.go | 24 ------------------- pkg/mdns/syscall_windows.go | 2 ++ 4 files changed, 6 insertions(+), 24 deletions(-) rename pkg/mdns/{syscall_linux.go => syscall.go} (78%) rename pkg/mdns/{syscall_darwin.go => syscall_bsd.go} (90%) delete mode 100644 pkg/mdns/syscall_freebsd.go diff --git a/pkg/mdns/syscall_linux.go b/pkg/mdns/syscall.go similarity index 78% rename from pkg/mdns/syscall_linux.go rename to pkg/mdns/syscall.go index fc0caeb0..0e50535a 100644 --- a/pkg/mdns/syscall_linux.go +++ b/pkg/mdns/syscall.go @@ -1,3 +1,5 @@ +//go:build !(darwin || ios || freebsd || openbsd || netbsd || dragonfly || windows) + package mdns import ( diff --git a/pkg/mdns/syscall_darwin.go b/pkg/mdns/syscall_bsd.go similarity index 90% rename from pkg/mdns/syscall_darwin.go rename to pkg/mdns/syscall_bsd.go index c1f1225b..6ebb0c93 100644 --- a/pkg/mdns/syscall_darwin.go +++ b/pkg/mdns/syscall_bsd.go @@ -1,3 +1,5 @@ +//go:build darwin || ios || freebsd || openbsd || netbsd || dragonfly + package mdns import ( diff --git a/pkg/mdns/syscall_freebsd.go b/pkg/mdns/syscall_freebsd.go deleted file mode 100644 index c1f1225b..00000000 --- a/pkg/mdns/syscall_freebsd.go +++ /dev/null @@ -1,24 +0,0 @@ -package mdns - -import ( - "syscall" -) - -func SetsockoptInt(fd uintptr, level, opt int, value int) (err error) { - // change SO_REUSEADDR and REUSEPORT flags simultaneously for BSD-like OS - // https://github.com/AlexxIT/go2rtc/issues/626 - // https://stackoverflow.com/questions/14388706/how-do-so-reuseaddr-and-so-reuseport-differ/14388707 - if opt == syscall.SO_REUSEADDR { - if err = syscall.SetsockoptInt(int(fd), level, opt, value); err != nil { - return - } - - opt = syscall.SO_REUSEPORT - } - - return syscall.SetsockoptInt(int(fd), level, opt, value) -} - -func SetsockoptIPMreq(fd uintptr, level, opt int, mreq *syscall.IPMreq) (err error) { - return syscall.SetsockoptIPMreq(int(fd), level, opt, mreq) -} diff --git a/pkg/mdns/syscall_windows.go b/pkg/mdns/syscall_windows.go index be283655..770510cf 100644 --- a/pkg/mdns/syscall_windows.go +++ b/pkg/mdns/syscall_windows.go @@ -1,3 +1,5 @@ +//go:build windows + package mdns import "syscall" From abe617a346ff22ebba063a9cb8befdea14f658ca Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Wed, 1 May 2024 09:04:04 +0300 Subject: [PATCH 002/130] refactor(ffmpeg): generalize device and hardware support for multiple OS - Rename `device_freebsd.go` to `device_bsd.go` and `hardware_freebsd.go` to `hardware_bsd.go` to reflect broader BSD support (FreeBSD, NetBSD, OpenBSD, Dragonfly). - Update build tags in `device_bsd.go` and `hardware_bsd.go` to include FreeBSD, NetBSD, OpenBSD, and Dragonfly. - Rename `device_linux.go` to `device_unix.go` and `hardware_linux.go` to `hardware_unix.go` to generalize Unix support excluding Darwin-based systems and BSDs. - Add specific build tags to `device_darwin.go`, `device_unix.go`, `hardware_darwin.go`, and `hardware_unix.go` to correctly target their respective operating systems. - Ensure Windows-specific files (`device_windows.go` and `hardware_windows.go`) are correctly tagged for building on Windows. --- internal/ffmpeg/device/{device_freebsd.go => device_bsd.go} | 2 ++ internal/ffmpeg/device/device_darwin.go | 2 ++ internal/ffmpeg/device/{device_linux.go => device_unix.go} | 2 ++ internal/ffmpeg/device/device_windows.go | 2 ++ .../ffmpeg/hardware/{hardware_freebsd.go => hardware_bsd.go} | 2 ++ internal/ffmpeg/hardware/hardware_darwin.go | 2 ++ .../ffmpeg/hardware/{hardware_linux.go => hardware_unix.go} | 2 ++ internal/ffmpeg/hardware/hardware_windows.go | 2 ++ 8 files changed, 16 insertions(+) rename internal/ffmpeg/device/{device_freebsd.go => device_bsd.go} (97%) rename internal/ffmpeg/device/{device_linux.go => device_unix.go} (96%) rename internal/ffmpeg/hardware/{hardware_freebsd.go => hardware_bsd.go} (96%) rename internal/ffmpeg/hardware/{hardware_linux.go => hardware_unix.go} (97%) diff --git a/internal/ffmpeg/device/device_freebsd.go b/internal/ffmpeg/device/device_bsd.go similarity index 97% rename from internal/ffmpeg/device/device_freebsd.go rename to internal/ffmpeg/device/device_bsd.go index f3a26a30..27d5b615 100644 --- a/internal/ffmpeg/device/device_freebsd.go +++ b/internal/ffmpeg/device/device_bsd.go @@ -1,3 +1,5 @@ +//go:build freebsd || netbsd || openbsd || dragonfly + package device import ( diff --git a/internal/ffmpeg/device/device_darwin.go b/internal/ffmpeg/device/device_darwin.go index ac9b5e43..ba97c0aa 100644 --- a/internal/ffmpeg/device/device_darwin.go +++ b/internal/ffmpeg/device/device_darwin.go @@ -1,3 +1,5 @@ +//go:build darwin || ios + package device import ( diff --git a/internal/ffmpeg/device/device_linux.go b/internal/ffmpeg/device/device_unix.go similarity index 96% rename from internal/ffmpeg/device/device_linux.go rename to internal/ffmpeg/device/device_unix.go index d1228b15..7b62187f 100644 --- a/internal/ffmpeg/device/device_linux.go +++ b/internal/ffmpeg/device/device_unix.go @@ -1,3 +1,5 @@ +//go:build unix && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly + package device import ( diff --git a/internal/ffmpeg/device/device_windows.go b/internal/ffmpeg/device/device_windows.go index c14630d3..ff328311 100644 --- a/internal/ffmpeg/device/device_windows.go +++ b/internal/ffmpeg/device/device_windows.go @@ -1,3 +1,5 @@ +//go:build windows + package device import ( diff --git a/internal/ffmpeg/hardware/hardware_freebsd.go b/internal/ffmpeg/hardware/hardware_bsd.go similarity index 96% rename from internal/ffmpeg/hardware/hardware_freebsd.go rename to internal/ffmpeg/hardware/hardware_bsd.go index 6ef753ac..de24ac5c 100644 --- a/internal/ffmpeg/hardware/hardware_freebsd.go +++ b/internal/ffmpeg/hardware/hardware_bsd.go @@ -1,3 +1,5 @@ +//go:build freebsd || netbsd || openbsd || dragonfly + package hardware import ( diff --git a/internal/ffmpeg/hardware/hardware_darwin.go b/internal/ffmpeg/hardware/hardware_darwin.go index 8392d2b1..b1505512 100644 --- a/internal/ffmpeg/hardware/hardware_darwin.go +++ b/internal/ffmpeg/hardware/hardware_darwin.go @@ -1,3 +1,5 @@ +//go:build darwin || ios + package hardware import ( diff --git a/internal/ffmpeg/hardware/hardware_linux.go b/internal/ffmpeg/hardware/hardware_unix.go similarity index 97% rename from internal/ffmpeg/hardware/hardware_linux.go rename to internal/ffmpeg/hardware/hardware_unix.go index f0d4858e..4f688ce4 100644 --- a/internal/ffmpeg/hardware/hardware_linux.go +++ b/internal/ffmpeg/hardware/hardware_unix.go @@ -1,3 +1,5 @@ +//go:build unix && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly + package hardware import ( diff --git a/internal/ffmpeg/hardware/hardware_windows.go b/internal/ffmpeg/hardware/hardware_windows.go index c22639f5..cdf0e12c 100644 --- a/internal/ffmpeg/hardware/hardware_windows.go +++ b/internal/ffmpeg/hardware/hardware_windows.go @@ -1,3 +1,5 @@ +//go:build windows + package hardware import "github.com/AlexxIT/go2rtc/internal/api" From 06d8503fd073406dea530c726e65a33dc9f40d2f Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 5 May 2024 12:32:18 +0300 Subject: [PATCH 003/130] Increase timeout for hls client --- pkg/hls/reader.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/hls/reader.go b/pkg/hls/reader.go index a691943b..37554e3c 100644 --- a/pkg/hls/reader.go +++ b/pkg/hls/reader.go @@ -88,7 +88,7 @@ func (r *reader) RoundTrip(_ *http.Request) (*http.Response, error) { } func (r *reader) getSegment() ([]byte, error) { - for i := 0; i < 5; i++ { + for i := 0; i < 10; i++ { if r.playlist == nil { if wait := time.Second - time.Since(r.lastTime); wait > 0 { time.Sleep(wait) From 8ac834bdd4b74b6f6f0c608529a330d6695fa444 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 5 May 2024 12:35:51 +0300 Subject: [PATCH 004/130] Add support AAC MPEG-2 for magic source --- pkg/magic/producer.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/magic/producer.go b/pkg/magic/producer.go index fb48270e..8ed3e57a 100644 --- a/pkg/magic/producer.go +++ b/pkg/magic/producer.go @@ -34,12 +34,12 @@ func Open(r io.Reader) (core.Producer, error) { case bytes.HasPrefix(b, []byte(flv.Signature)): return flv.Open(rd) - case bytes.HasPrefix(b, []byte{0xFF, 0xF1}): - return aac.Open(rd) - case bytes.HasPrefix(b, []byte("--")): return multipart.Open(rd) + case b[0] == 0xFF && b[1]&0xF7 == 0xF1: + return aac.Open(rd) + case b[0] == mpegts.SyncByte: return mpegts.Open(rd) } From 09109e783e0b204f2013a5df66e824ef5aabd20b Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 5 May 2024 12:36:17 +0300 Subject: [PATCH 005/130] Update RTSP handle error message --- internal/rtsp/rtsp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index bb274d20..e4d3e55f 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -239,7 +239,7 @@ func tcpHandler(conn *rtsp.Conn) { if closer != nil { if err := conn.Handle(); err != nil { - log.Debug().Msgf("[rtsp] handle=%s", err) + log.Debug().Err(err).Msg("[rtsp] handle") } closer() From 290e011061057ce198de61a3b65d070ea8d42c1f Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 6 May 2024 07:32:45 +0300 Subject: [PATCH 006/130] Add support allowed_media_types for RTSP server #1054 --- pkg/rtsp/server.go | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index 15b3f84b..8e0d3134 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -146,7 +146,19 @@ func (c *Conn) Accept() error { if strings.HasPrefix(tr, transport) { c.session = core.RandString(8, 10) c.state = StateSetup - res.Header.Set("Transport", tr[:len(transport)+3]) + + if c.mode == core.ModePassiveConsumer { + 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) + } else { + res.Status = "400 Bad Request" + } + } else { + res.Header.Set("Transport", tr[:len(transport)+3]) + } } else { res.Status = "461 Unsupported transport" } @@ -156,6 +168,15 @@ func (c *Conn) Accept() error { } case MethodRecord, MethodPlay: + if c.mode == core.ModePassiveConsumer { + // stop unconfigured senders + for _, track := range c.senders { + if track.Media.ID != MethodSetup { + track.Close() + } + } + } + res := &tcp.Response{Request: req} err = c.WriteResponse(res) c.playOK = true @@ -172,3 +193,18 @@ func (c *Conn) Accept() error { } } } + +func reqTrackID(req *tcp.Request) int { + var s string + if req.URL.RawQuery != "" { + s = req.URL.RawQuery + } else { + s = req.URL.Path + } + if i := strings.LastIndexByte(s, '='); i > 0 { + if i, err := strconv.Atoi(s[i+1:]); err == nil { + return i + } + } + return -1 +} From b9f984dad00fa1308b5a74761a8b0735bbd5aca0 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 6 May 2024 20:34:25 +0300 Subject: [PATCH 007/130] Update dependencies #1072 #1073 #1075 --- go.mod | 12 ++++++------ go.sum | 12 ++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 5704025a..b1ba4b4c 100644 --- a/go.mod +++ b/go.mod @@ -7,20 +7,20 @@ require ( github.com/expr-lang/expr v1.16.5 github.com/gorilla/websocket v1.5.1 github.com/miekg/dns v1.1.59 - github.com/pion/ice/v2 v2.3.19 + github.com/pion/ice/v2 v2.3.24 github.com/pion/interceptor v0.1.29 github.com/pion/rtcp v1.2.14 github.com/pion/rtp v1.8.6 github.com/pion/sdp/v3 v3.0.9 github.com/pion/srtp/v2 v2.0.18 github.com/pion/stun v0.6.1 - github.com/pion/webrtc/v3 v3.2.39 + github.com/pion/webrtc/v3 v3.2.40 github.com/rs/zerolog v1.32.0 github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f github.com/stretchr/testify v1.9.0 github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 - golang.org/x/crypto v0.22.0 + golang.org/x/crypto v0.23.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -32,7 +32,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pion/datachannel v1.5.6 // indirect - github.com/pion/dtls/v2 v2.2.10 // indirect + github.com/pion/dtls/v2 v2.2.11 // indirect github.com/pion/logging v0.2.2 // indirect github.com/pion/mdns v0.0.12 // indirect github.com/pion/randutil v0.1.0 // indirect @@ -41,8 +41,8 @@ require ( github.com/pion/turn/v2 v2.1.6 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.24.0 // indirect + golang.org/x/net v0.25.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.19.0 // indirect + golang.org/x/sys v0.20.0 // indirect golang.org/x/tools v0.20.0 // indirect ) diff --git a/go.sum b/go.sum index a8a80ebc..09b0abb9 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,12 @@ github.com/pion/datachannel v1.5.6/go.mod h1:1eKT6Q85pRnr2mHiWHxJwO50SfZRtWHTsNI github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.10 h1:u2Axk+FyIR1VFTPurktB+1zoEPGIW3bmyj3LEFrXjAA= github.com/pion/dtls/v2 v2.2.10/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/dtls/v2 v2.2.11 h1:9U/dpCYl1ySttROPWJgqWKEylUdT0fXp/xst6JwY5Ks= +github.com/pion/dtls/v2 v2.2.11/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= github.com/pion/ice/v2 v2.3.19 h1:1GoMRTMnB6bCP4aGy2MjxK3w4laDkk+m7svJb/eqybc= github.com/pion/ice/v2 v2.3.19/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw= +github.com/pion/ice/v2 v2.3.24 h1:RYgzhH/u5lH0XO+ABatVKCtRd+4U1GEaCXSMjNr13tI= +github.com/pion/ice/v2 v2.3.24/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw= github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M= github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= @@ -72,6 +76,8 @@ github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= github.com/pion/webrtc/v3 v3.2.39 h1:Lf2SIMGdE3M9VNm48KpoX5pR8SJ6TsMnktzOkc/oB0o= github.com/pion/webrtc/v3 v3.2.39/go.mod h1:AQ8p56OLbm3MjhYovYdgPuyX6oc+JcKx/HFoCGFcYzA= +github.com/pion/webrtc/v3 v3.2.40 h1:Wtfi6AZMQg+624cvCXUuSmrKWepSB7zfgYDOYqsSOVU= +github.com/pion/webrtc/v3 v3.2.40/go.mod h1:M1RAe3TNTD1tzyvqHrbVODfwdPGSXOUo/OgpoGGJqFY= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -107,6 +113,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= @@ -124,6 +132,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -148,6 +158,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= From a9f2b5158c59c9425f9ee343a4982b010e30ee4e Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 6 May 2024 20:35:28 +0300 Subject: [PATCH 008/130] Update version to 1.9.1 --- internal/app/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/app.go b/internal/app/app.go index 8c5ba79c..79354a04 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -15,7 +15,7 @@ import ( "github.com/rs/zerolog/log" ) -var Version = "1.9.0" +var Version = "1.9.1" var UserAgent = "go2rtc/" + Version var ConfigPath string From a0030194cb25206b04965ddb7d59c789dbebe70e Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 8 May 2024 13:04:59 +0300 Subject: [PATCH 009/130] Add gif logo --- README.md | 2 +- assets/logo.gif | Bin 0 -> 356740 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 assets/logo.gif diff --git a/README.md b/README.md index 3bfe3a48..2f573f96 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- ![go2rtc](assets/logo.png) + ![go2rtc](assets/logo.gif)
[![stars](https://img.shields.io/github/stars/AlexxIT/go2rtc?style=flat-square&logo=github)](https://github.com/AlexxIT/go2rtc/stargazers) [![docker pulls](https://img.shields.io/docker/pulls/alexxit/go2rtc?style=flat-square&logo=docker&logoColor=white&label=pulls)](https://hub.docker.com/r/alexxit/go2rtc) diff --git a/assets/logo.gif b/assets/logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..292e3819ee1feae2883fb166f8ae60704c6b59eb GIT binary patch literal 356740 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

R4#ckb+tl`5kDiN$ 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
T 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�wG@g$S$7Rh?LfGI-~v`eNLKb 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%0Ajqllnl873i9 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

J-n2GZn2$?@sawdIm+}-YhY(KG#J-e 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 literal 0 HcmV?d00001 From f3f08afac86330c65e0ee9c22b121a87e7c52538 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Thu, 9 May 2024 05:58:38 +0300 Subject: [PATCH 010/130] ci(build.yml): enable latest tag and onlatest for hardware suffix This commit updates the GitHub Actions workflow to ensure that images built with a hardware suffix are tagged as 'latest'. Additionally, it modifies the README.md to enhance the documentation around the Docker container deployment, including basic and GPU-accelerated deployment instructions. --- .github/workflows/build.yml | 4 ++-- README.md | 29 ++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f0293f3e..188727d6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -170,8 +170,8 @@ jobs: with: images: ${{ github.repository }} flavor: | - suffix=-hardware - latest=false + suffix=-hardware,onlatest=true + latest=auto tags: | type=ref,event=branch type=semver,pattern={{version}},enable=false diff --git a/README.md b/README.md index 2f573f96..7be647ec 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,34 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac. ### go2rtc: Docker -Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [ngrok](#module-ngrok) and [Python](#source-echo). +The Docker container [`alexxit/go2rtc`](https://hub.docker.com/r/alexxit/go2rtc) supports multiple architectures including `amd64`, `386`, `arm64`, and `arm`. This container offers the same functionality as the [Home Assistant Add-on](#go2rtc-home-assistant-add-on) but is designed to operate independently of Home Assistant. It comes preinstalled with [FFmpeg](#source-ffmpeg), [ngrok](#module-ngrok), and [Python](#source-echo). + +#### Basic Deployment + +```bash +docker run -d \ + --name go2rtc \ + --network host \ + --privileged \ + --restart unless-stopped \ + -e TZ=Atlantic/Bermuda \ + -v ~/go2rtc:/config \ + alexxit/go2rtc +``` + +#### Deployment with GPU Acceleration + +```bash +docker run -d \ + --name go2rtc \ + --network host \ + --privileged \ + --restart unless-stopped \ + -e TZ=Atlantic/Bermuda \ + --gpus all \ + -v ~/go2rtc:/config \ + alexxit/go2rtc:latest-hardware +``` ### go2rtc: Home Assistant Add-on From 122a5505991da8504fcb350b2e08615caad44857 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Thu, 9 May 2024 02:53:36 +0300 Subject: [PATCH 011/130] feat(icons): add website icons and update GH Pages workflow to include icon changes --- .github/workflows/gh-pages.yml | 4 +++- website/icons/android-chrome-192x192.png | Bin 0 -> 3039 bytes website/icons/android-chrome-256x256.png | Bin 0 -> 3734 bytes website/icons/apple-touch-icon.png | Bin 0 -> 2816 bytes website/icons/browserconfig.xml | 9 ++++++++ website/icons/favicon-16x16.png | Bin 0 -> 467 bytes website/icons/favicon-32x32.png | Bin 0 -> 789 bytes website/icons/favicon.ico | Bin 0 -> 15086 bytes website/icons/mstile-150x150.png | Bin 0 -> 1924 bytes website/icons/safari-pinned-tab.svg | 27 +++++++++++++++++++++++ website/icons/site.webmanifest | 19 ++++++++++++++++ 11 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 website/icons/android-chrome-192x192.png create mode 100644 website/icons/android-chrome-256x256.png create mode 100644 website/icons/apple-touch-icon.png create mode 100644 website/icons/browserconfig.xml create mode 100644 website/icons/favicon-16x16.png create mode 100644 website/icons/favicon-32x32.png create mode 100644 website/icons/favicon.ico create mode 100644 website/icons/mstile-150x150.png create mode 100644 website/icons/safari-pinned-tab.svg create mode 100644 website/icons/site.webmanifest diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 4d0e2e67..9ce6221e 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -4,7 +4,9 @@ name: Deploy static content to Pages on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: - + push: + paths: + - 'website/icons/**' # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read diff --git a/website/icons/android-chrome-192x192.png b/website/icons/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..02a3cbdd59dcd8fbc8f4ceaee77c5d83563986d2 GIT binary patch literal 3039 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 literal 0 HcmV?d00001 diff --git a/website/icons/apple-touch-icon.png b/website/icons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2ac9797029e7f6c3dd991b4468e585bbbefc1153 GIT binary patch literal 2816 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 SLN%2D0000 + + + + + #da532c + + + diff --git a/website/icons/favicon-16x16.png b/website/icons/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..b72717b9736dfd39ed3786fc8717b8afc221bd6d GIT binary patch literal 467 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 literal 0 HcmV?d00001 diff --git a/website/icons/safari-pinned-tab.svg b/website/icons/safari-pinned-tab.svg new file mode 100644 index 00000000..3b92e61e --- /dev/null +++ b/website/icons/safari-pinned-tab.svg @@ -0,0 +1,27 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + + diff --git a/website/icons/site.webmanifest b/website/icons/site.webmanifest new file mode 100644 index 00000000..de65106f --- /dev/null +++ b/website/icons/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-256x256.png", + "sizes": "256x256", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} From e71ed5e7ebebcfe61703411293147d49ebda8322 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Thu, 9 May 2024 06:20:20 +0300 Subject: [PATCH 012/130] feat(icons): add favicon and apple-touch-icon links across all pages Added favicon, apple-touch-icon, and related meta tags to all HTML pages to ensure consistent branding and improve user experience on various platforms. --- website/icons/site.webmanifest | 4 ++-- www/add.html | 8 ++++++++ www/codecs.html | 8 ++++++++ www/editor.html | 8 ++++++++ www/index.html | 8 ++++++++ www/links.html | 8 ++++++++ www/log.html | 8 ++++++++ www/stream.html | 8 ++++++++ www/webrtc-sync.html | 8 ++++++++ www/webrtc.html | 8 ++++++++ 10 files changed, 74 insertions(+), 2 deletions(-) diff --git a/website/icons/site.webmanifest b/website/icons/site.webmanifest index de65106f..cf1430a0 100644 --- a/website/icons/site.webmanifest +++ b/website/icons/site.webmanifest @@ -13,7 +13,7 @@ "type": "image/png" } ], - "theme_color": "#ffffff", - "background_color": "#ffffff", + "theme_color": "#000000", + "background_color": "#000000", "display": "standalone" } diff --git a/www/add.html b/www/add.html index 3058f8dc..302253e4 100644 --- a/www/add.html +++ b/www/add.html @@ -4,6 +4,14 @@ Add Stream + + + + + + + + + + + + + + + \ No newline at end of file From bc8295baeec2069679bbbf481afe2ee9532fb578 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 19 May 2024 11:56:33 +0300 Subject: [PATCH 056/130] Improve play audio on RTSP backchannel --- internal/ffmpeg/README.md | 7 +++++ internal/streams/play.go | 16 +++++++----- pkg/rtsp/consumer.go | 55 +++++++++++++++++++++++---------------- 3 files changed, 49 insertions(+), 29 deletions(-) diff --git a/internal/ffmpeg/README.md b/internal/ffmpeg/README.md index 88a91d45..f89996e1 100644 --- a/internal/ffmpeg/README.md +++ b/internal/ffmpeg/README.md @@ -45,6 +45,13 @@ [video4linux2,v4l2 @ 0x7f7de7c58bc0] Compressed: mjpeg : Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960 ``` +## TTS + +```yaml +streams: + tts: ffmpeg:#input=-re -f lavfi -i "flite=text='1 2 3 4 5 6 7 8 9 0'"#audio=pcma +``` + ## Useful links - https://superuser.com/questions/564402/explanation-of-x264-tune diff --git a/internal/streams/play.go b/internal/streams/play.go index 748130f4..7ada66e6 100644 --- a/internal/streams/play.go +++ b/internal/streams/play.go @@ -2,6 +2,8 @@ package streams import ( "errors" + "time" + "github.com/AlexxIT/go2rtc/pkg/core" ) @@ -80,18 +82,20 @@ func (s *Stream) Play(source string) error { s.AddInternalProducer(src) s.AddInternalConsumer(cons) - go func() { - _ = src.Start() - _ = dst.Stop() - s.RemoveProducer(src) - }() - go func() { _ = dst.Start() _ = src.Stop() s.RemoveInternalConsumer(cons) }() + go func() { + _ = src.Start() + // little timeout before stop dst, so the buffer can be transferred + time.Sleep(time.Second) + _ = dst.Stop() + s.RemoveProducer(src) + }() + return nil } diff --git a/pkg/rtsp/consumer.go b/pkg/rtsp/consumer.go index 4bddd77b..79e2b348 100644 --- a/pkg/rtsp/consumer.go +++ b/pkg/rtsp/consumer.go @@ -74,19 +74,38 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv return nil } +const ( + startVideoBuf = 32 * 1024 // 32KB + startAudioBuf = 2 * 1024 // 2KB + maxBuf = 1024 * 1024 // 1MB + rtpHdr = 12 // basic RTP header size + intHdr = 4 // interleaved header size +) + func (c *Conn) packetWriter(codec *core.Codec, channel, payloadType uint8) core.HandlerFunc { var buf []byte var n int video := codec.IsVideo() if video { - buf = make([]byte, 32*1024) // 32KB + buf = make([]byte, startVideoBuf) } else { - buf = make([]byte, 2*1024) // 2KB + buf = make([]byte, startAudioBuf) + } + + flushBuf := func() { + if err := c.conn.SetWriteDeadline(time.Now().Add(Timeout)); err != nil { + return + } + //log.Printf("[rtsp] channel:%2d write_size:%6d buffer_size:%6d", channel, n, len(buf)) + if _, err := c.conn.Write(buf[:n]); err == nil { + c.send += n + } + n = 0 } handlerFunc := func(packet *rtp.Packet) { - if c.state == StateNone || !c.playOK { + if c.state == StateNone { return } @@ -106,16 +125,13 @@ func (c *Conn) packetWriter(codec *core.Codec, channel, payloadType uint8) core. packet.Marker = true // better to have marker on all audio packets } - size := 12 + len(packet.Payload) + size := rtpHdr + len(packet.Payload) - if n+4+size > len(buf) { - if len(buf) < 1024*1024 { - buf = append(buf, make([]byte, len(buf))...) + if l := len(buf); n+intHdr+size > l { + if l < maxBuf { + buf = append(buf, make([]byte, l)...) // double buffer size } else { - if _, err := c.conn.Write(buf[:n]); err == nil { - c.send += n - } - n = 0 + flushBuf() } } @@ -134,21 +150,14 @@ func (c *Conn) packetWriter(codec *core.Codec, channel, payloadType uint8) core. n += 4 + size - if !packet.Marker { - return // collect continious video packets to buffer - } - - if err := c.conn.SetWriteDeadline(time.Now().Add(Timeout)); err != nil { + if !packet.Marker || !c.playOK { + // collect continious video packets to buffer + // or wait OK for PLAY command for backchannel + //log.Printf("[rtsp] collecting buffer ok=%t", c.playOK) return } - //log.Printf("[rtsp] channel:%2d write_size:%6d buffer_size:%6d", channel, n, len(buf)) - - if _, err := c.conn.Write(buf[:n]); err == nil { - c.send += n - } - - n = 0 + flushBuf() } if !codec.IsRTP() { From 99cc21aacbd394d2fcd89e643495c901101a7c19 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 20 May 2024 14:24:04 +0300 Subject: [PATCH 057/130] Code refactoring for magic producer --- pkg/magic/producer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/magic/producer.go b/pkg/magic/producer.go index 8ed3e57a..5728da33 100644 --- a/pkg/magic/producer.go +++ b/pkg/magic/producer.go @@ -25,7 +25,7 @@ func Open(r io.Reader) (core.Producer, error) { } switch { - case bytes.HasPrefix(b, []byte(annexb.StartCode)): + case string(b) == annexb.StartCode: return bitstream.Open(rd) case bytes.HasPrefix(b, []byte{0xFF, 0xD8}): @@ -37,7 +37,7 @@ func Open(r io.Reader) (core.Producer, error) { case bytes.HasPrefix(b, []byte("--")): return multipart.Open(rd) - case b[0] == 0xFF && b[1]&0xF7 == 0xF1: + case b[0] == 0xFF && (b[1] == 0xF1 || b[1] == 0xF9): return aac.Open(rd) case b[0] == mpegts.SyncByte: From a518488289c7012cc9bc584f1e99e773f81b8dd6 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 21 May 2024 17:46:43 +0300 Subject: [PATCH 058/130] Add debug logs for run RTSP pipe --- internal/exec/exec.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/exec/exec.go b/internal/exec/exec.go index a160136c..1e11cc68 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -4,7 +4,6 @@ import ( "crypto/md5" "encoding/hex" "errors" - "fmt" "io" "net/url" "os" @@ -90,6 +89,10 @@ func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error return nil, err } + log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe") + + ts := time.Now() + if err = cmd.Start(); err != nil { return nil, err } @@ -99,6 +102,8 @@ func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error _ = r.Close() } + log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run pipe") + return prod, err } @@ -127,7 +132,7 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { waitersMu.Unlock() }() - log.Debug().Str("url", url).Str("cmd", fmt.Sprintf("%s", strings.Join(cmd.Args, " "))).Msg("[exec] run") + log.Debug().Strs("args", cmd.Args).Msg("[exec] run rtsp") ts := time.Now() @@ -150,7 +155,7 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { // limit message size return nil, errors.New("exec: " + stderr.String()) case prod := <-waiter: - log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run") + log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp") return prod, nil } } From 54c8ca0112dc8ecdef587f2e62ec23ccfbf958f7 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 21 May 2024 17:48:22 +0300 Subject: [PATCH 059/130] Add wav format to magic producer --- pkg/magic/producer.go | 4 ++ pkg/wav/wav.go | 127 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 pkg/wav/wav.go diff --git a/pkg/magic/producer.go b/pkg/magic/producer.go index 5728da33..c49fe8bf 100644 --- a/pkg/magic/producer.go +++ b/pkg/magic/producer.go @@ -14,6 +14,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/magic/mjpeg" "github.com/AlexxIT/go2rtc/pkg/mpegts" "github.com/AlexxIT/go2rtc/pkg/multipart" + "github.com/AlexxIT/go2rtc/pkg/wav" ) func Open(r io.Reader) (core.Producer, error) { @@ -28,6 +29,9 @@ func Open(r io.Reader) (core.Producer, error) { case string(b) == annexb.StartCode: return bitstream.Open(rd) + case string(b) == wav.FourCC: + return wav.Open(rd) + case bytes.HasPrefix(b, []byte{0xFF, 0xD8}): return mjpeg.Open(rd) diff --git a/pkg/wav/wav.go b/pkg/wav/wav.go new file mode 100644 index 00000000..5f572bd6 --- /dev/null +++ b/pkg/wav/wav.go @@ -0,0 +1,127 @@ +package wav + +import ( + "bufio" + "encoding/binary" + "errors" + "io" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +const FourCC = "RIFF" + +func Open(r io.Reader) (*Producer, error) { + // https://en.wikipedia.org/wiki/WAV + // https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html + rd := bufio.NewReaderSize(r, core.BufferSize) + + // skip Master RIFF chunk + if _, err := rd.Discard(12); err != nil { + return nil, err + } + + codec := &core.Codec{} + + for { + chunkID, data, err := readChunk(rd) + if err != nil { + return nil, err + } + + if chunkID == "data" { + break + } + + if chunkID == "fmt " { + // https://audiocoding.cc/articles/2008-05-22-wav-file-structure/wav_formats.txt + switch data[0] { + case 1: + codec.Name = core.CodecPCML + case 6: + codec.Name = core.CodecPCMA + case 7: + codec.Name = core.CodecPCMU + } + + codec.Channels = uint16(data[2]) + codec.ClockRate = binary.LittleEndian.Uint32(data[4:]) + } + } + + if codec.Name == "" { + return nil, errors.New("waw: unsupported codec") + } + + prod := &Producer{rd: rd, cl: r.(io.Closer)} + prod.Type = "WAV producer" + prod.Medias = []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{codec}, + }, + } + return prod, nil +} + +type Producer struct { + core.SuperProducer + rd *bufio.Reader + cl io.Closer +} + +func (c *Producer) Start() error { + var seq uint16 + var ts uint32 + + const PacketSize = 0.040 * 8000 // 40ms + + for { + payload := make([]byte, PacketSize) + if _, err := io.ReadFull(c.rd, payload); err != nil { + return err + } + + c.Recv += PacketSize + + if len(c.Receivers) == 0 { + continue + } + + pkt := &rtp.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: true, + SequenceNumber: seq, + Timestamp: ts, + }, + Payload: payload, + } + c.Receivers[0].WriteRTP(pkt) + + seq++ + ts += PacketSize + } +} + +func (c *Producer) Stop() error { + _ = c.SuperProducer.Close() + return c.cl.Close() +} + +func readChunk(r io.Reader) (chunkID string, data []byte, err error) { + b := make([]byte, 8) + if _, err = io.ReadFull(r, b); err != nil { + return + } + + if chunkID = string(b[:4]); chunkID != "data" { + size := binary.LittleEndian.Uint32(b[4:]) + data = make([]byte, size) + _, err = io.ReadFull(r, data) + } + + return +} From c41bddbbea59c115afbff1ed697ace11bd275c21 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 21 May 2024 17:50:15 +0300 Subject: [PATCH 060/130] Add using wav format for ffmpeg transcoding to PCMA/PCMU --- internal/ffmpeg/README.md | 2 +- internal/ffmpeg/ffmpeg.go | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/internal/ffmpeg/README.md b/internal/ffmpeg/README.md index f89996e1..903aab5d 100644 --- a/internal/ffmpeg/README.md +++ b/internal/ffmpeg/README.md @@ -49,7 +49,7 @@ ```yaml streams: - tts: ffmpeg:#input=-re -f lavfi -i "flite=text='1 2 3 4 5 6 7 8 9 0'"#audio=pcma + tts: ffmpeg:#input=-readrate 1 -readrate_initial_burst 0.001 -f lavfi -i "flite=text='1 2 3 4 5 6 7 8 9 0'"#audio=pcma ``` ## Useful links diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 222c683d..63b4a39d 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -49,6 +49,7 @@ var defaults = map[string]string{ // output "output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", "output/mjpeg": "-f mjpeg -", + "output/wav": "-f wav -", // `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1` // `-tune zerolatency` - for minimal latency @@ -315,11 +316,23 @@ func parseArgs(s string) *ffmpeg.Args { args.AddCodec("-an") } - // transcoding to only mjpeg - if (args.Video == 1 && args.Audio == 0 && query.Get("video") == "mjpeg") || - // no transcoding from mjpeg input - (args.Video == 0 && args.Audio == 0 && strings.Contains(args.Input, " mjpeg ")) { - args.Output = defaults["output/mjpeg"] + // change otput from RTSP to some other pipe format + switch { + case args.Video == 0 && args.Audio == 0: + // no transcoding from mjpeg input (ffmpeg device with support output as raw MJPEG) + if strings.Contains(args.Input, " mjpeg ") { + args.Output = defaults["output/mjpeg"] + } + case args.Video == 1 && args.Audio == 0: + if query.Get("video") == "mjpeg" { + args.Output = defaults["output/mjpeg"] + } + case args.Video == 0 && args.Audio == 1: + codec, _, _ := strings.Cut(query.Get("audio"), "/") + switch codec { + case "pcma", "pcmu", "pcml": + args.Output = defaults["output/wav"] + } } return args From af05083a1f665bcb157244283fa7db7201c7d13e Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 22 May 2024 12:58:21 +0300 Subject: [PATCH 061/130] Code refactoring for ffmpeg device and virtual --- internal/ffmpeg/device/devices.go | 19 ++----- internal/ffmpeg/ffmpeg.go | 20 +++---- internal/ffmpeg/virtual/virtual.go | 87 +++++++++++++++--------------- 3 files changed, 59 insertions(+), 67 deletions(-) diff --git a/internal/ffmpeg/device/devices.go b/internal/ffmpeg/device/devices.go index a51abd64..69b13444 100644 --- a/internal/ffmpeg/device/devices.go +++ b/internal/ffmpeg/device/devices.go @@ -1,11 +1,9 @@ package device import ( - "errors" "net/http" "net/url" "strconv" - "strings" "sync" "github.com/AlexxIT/go2rtc/internal/api" @@ -17,24 +15,15 @@ func Init(bin string) { api.HandleFunc("api/ffmpeg/devices", apiDevices) } -func GetInput(src string) (string, error) { - i := strings.IndexByte(src, '?') - if i < 0 { - return "", errors.New("empty query: " + src) - } - - query, err := url.ParseQuery(src[i+1:]) +func GetInput(src string) string { + query, err := url.ParseQuery(src) if err != nil { - return "", err + return "" } runonce.Do(initDevices) - if input := queryToInput(query); input != "" { - return input, nil - } - - return "", errors.New("wrong query: " + src) + return queryToInput(query) } var Bin string diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 63b4a39d..c5587e3c 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -189,21 +189,21 @@ func parseArgs(s string) *ffmpeg.Args { s += "?video&audio" } args.Input = inputTemplate("rtsp", s, query) - } else if strings.HasPrefix(s, "device?") { - var err error - args.Input, err = device.GetInput(s) - if err != nil { - return nil - } - } else if strings.HasPrefix(s, "virtual?") { - var err error - if args.Input, err = virtual.GetInput(s[8:]); err != nil { - return nil + } else if i = strings.Index(s, "?"); i > 0 { + switch s[:i] { + case "device": + args.Input = device.GetInput(s[i+1:]) + case "virtual": + args.Input = virtual.GetInput(s[i+1:]) } } else { args.Input = inputTemplate("file", s, query) } + if args.Input == "" { + return nil + } + if query["async"] != nil { args.Input = "-use_wallclock_as_timestamps 1 -async 1 " + args.Input } diff --git a/internal/ffmpeg/virtual/virtual.go b/internal/ffmpeg/virtual/virtual.go index 2e1dd9bd..f738c41f 100644 --- a/internal/ffmpeg/virtual/virtual.go +++ b/internal/ffmpeg/virtual/virtual.go @@ -4,56 +4,59 @@ import ( "net/url" ) -func GetInput(src string) (string, error) { +func GetInput(src string) string { query, err := url.ParseQuery(src) if err != nil { - return "", err + return "" } - // set defaults (using Add instead of Set) - query.Add("video", "testsrc") - query.Add("size", "1920x1080") - query.Add("decimals", "2") + input := "-re" - // https://ffmpeg.org/ffmpeg-filters.html - video := query.Get("video") - input := "-re -f lavfi -i " + video + for _, video := range query["video"] { + // https://ffmpeg.org/ffmpeg-filters.html + sep := "=" // first separator - sep := "=" // first separator - for key, values := range query { - value := values[0] - - // https://ffmpeg.org/ffmpeg-utils.html#video-size-syntax - switch key { - case "color", "rate", "duration", "sar": - case "size": - switch value { - case "720": - value = "1280x720" // crf=1 -> 12 Mbps - case "1080": - value = "1920x1080" // crf=1 -> 25 Mbps - case "2K": - value = "2560x1440" // crf=1 -> 43 Mbps - case "4K": - value = "3840x2160" // crf=1 -> 103 Mbps - case "8K": - value = "7680x4230" // https://reolink.com/blog/8k-resolution/ - } - case "decimals": - if video != "testsrc" { - continue - } - default: - continue + if video == "" { + video = "testsrc=decimals=2" // default video + sep = ":" } - input += sep + key + "=" + value - sep = ":" // next separator + input += " -f lavfi -i " + video + + // set defaults (using Add instead of Set) + query.Add("size", "1920x1080") + + for key, values := range query { + value := values[0] + + // https://ffmpeg.org/ffmpeg-utils.html#video-size-syntax + switch key { + case "color", "rate", "duration", "sar", "decimals": + case "size": + switch value { + case "720": + value = "1280x720" // crf=1 -> 12 Mbps + case "1080": + value = "1920x1080" // crf=1 -> 25 Mbps + case "2K": + value = "2560x1440" // crf=1 -> 43 Mbps + case "4K": + value = "3840x2160" // crf=1 -> 103 Mbps + case "8K": + value = "7680x4230" // https://reolink.com/blog/8k-resolution/ + } + default: + continue + } + + input += sep + key + "=" + value + sep = ":" // next separator + } + + if s := query.Get("format"); s != "" { + input += ",format=" + s + } } - if s := query.Get("format"); s != "" { - input += ",format=" + s - } - - return input, nil + return input } From 53242ea02f87c0752775a8f1c7131ef4d7e0b305 Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 22 May 2024 13:00:39 +0300 Subject: [PATCH 062/130] Add ffmpeg tts source --- internal/ffmpeg/ffmpeg.go | 2 ++ internal/ffmpeg/virtual/virtual.go | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index c5587e3c..ff66c835 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -195,6 +195,8 @@ func parseArgs(s string) *ffmpeg.Args { args.Input = device.GetInput(s[i+1:]) case "virtual": args.Input = virtual.GetInput(s[i+1:]) + case "tts": + args.Input = virtual.GetInputTTS(s[i+1:]) } } else { args.Input = inputTemplate("file", s, query) diff --git a/internal/ffmpeg/virtual/virtual.go b/internal/ffmpeg/virtual/virtual.go index f738c41f..4dc3b025 100644 --- a/internal/ffmpeg/virtual/virtual.go +++ b/internal/ffmpeg/virtual/virtual.go @@ -60,3 +60,20 @@ func GetInput(src string) string { return input } + +func GetInputTTS(src string) string { + query, err := url.ParseQuery(src) + if err != nil { + return "" + } + + input := `-re -f lavfi -i "flite=text='` + query.Get("text") + `'` + + // ffmpeg -f lavfi -i flite=list_voices=1 + // awb, kal, kal16, rms, slt + if voice := query.Get("voice"); voice != "" { + input += ":voice" + voice + } + + return input + `"` +} From 78a74da8d6caf6ade3fb63a0fce2d9b62bfb60d0 Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 22 May 2024 18:46:30 +0300 Subject: [PATCH 063/130] Fix aac.DecodeConfig sampleRate parsing --- pkg/aac/aac.go | 2 ++ pkg/aac/aac_test.go | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/pkg/aac/aac.go b/pkg/aac/aac.go index c991431d..5ce4e82d 100644 --- a/pkg/aac/aac.go +++ b/pkg/aac/aac.go @@ -69,6 +69,8 @@ func DecodeConfig(b []byte) (objType, sampleFreqIdx, channels byte, sampleRate u sampleFreqIdx = rd.ReadBits8(4) if sampleFreqIdx == 0b1111 { sampleRate = rd.ReadBits(24) + } else { + sampleRate = sampleRates[sampleFreqIdx] } channels = rd.ReadBits8(4) diff --git a/pkg/aac/aac_test.go b/pkg/aac/aac_test.go index 08d9c436..d1af6e52 100644 --- a/pkg/aac/aac_test.go +++ b/pkg/aac/aac_test.go @@ -41,3 +41,12 @@ func TestADTS(t *testing.T) { require.Equal(t, src[:len(dst)], dst) } + +func TestEncodeConfig(t *testing.T) { + conf := EncodeConfig(TypeAACLC, 48000, 1, false) + require.Equal(t, "1188", hex.EncodeToString(conf)) + conf = EncodeConfig(TypeAACLC, 16000, 1, false) + require.Equal(t, "1408", hex.EncodeToString(conf)) + conf = EncodeConfig(TypeAACLC, 8000, 1, false) + require.Equal(t, "1588", hex.EncodeToString(conf)) +} From 82fa803a37645dfc79c0e1d228f17c99fe2f6c0a Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 22 May 2024 18:48:40 +0300 Subject: [PATCH 064/130] Add ffmpeg virtual tests --- internal/ffmpeg/virtual/virtual_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 internal/ffmpeg/virtual/virtual_test.go diff --git a/internal/ffmpeg/virtual/virtual_test.go b/internal/ffmpeg/virtual/virtual_test.go new file mode 100644 index 00000000..b648a9cd --- /dev/null +++ b/internal/ffmpeg/virtual/virtual_test.go @@ -0,0 +1,20 @@ +package virtual + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetInput(t *testing.T) { + s := GetInput("video") + require.Equal(t, "-re -f lavfi -i testsrc=decimals=2:size=1920x1080", s) + + s = GetInput("video=testsrc2&size=4K") + require.Equal(t, "-re -f lavfi -i testsrc2=size=3840x2160", s) +} + +func TestGetInputTTS(t *testing.T) { + s := GetInputTTS("text=hello world&voice=slt") + require.Equal(t, `-re -f lavfi -i "flite=text='hello world':voiceslt"`, s) +} From 8a7712a4c807ad4af1f3dde73618da050c322c8f Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 22 May 2024 18:49:43 +0300 Subject: [PATCH 065/130] Add ffmpeg auto codec selection logic --- internal/ffmpeg/ffmpeg.go | 12 ++-- internal/ffmpeg/producer.go | 112 ++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 internal/ffmpeg/producer.go diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index ff66c835..48172bcb 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -2,6 +2,7 @@ package ffmpeg import ( "net/url" + "slices" "strings" "github.com/AlexxIT/go2rtc/internal/app" @@ -28,9 +29,14 @@ func Init() { streams.RedirectFunc("ffmpeg", func(url string) (string, error) { args := parseArgs(url[7:]) + if slices.Contains(args.Codecs, "auto") { + return "", nil // force call streams.HandleFunc("ffmpeg") + } return "exec:" + args.String(), nil }) + streams.HandleFunc("ffmpeg", NewProducer) + device.Init(defaults["bin"]) hardware.Init(defaults["bin"]) } @@ -85,6 +91,8 @@ var defaults = map[string]string{ "pcml/8000": "-c:a pcm_s16le -ar:a 8000 -ac:a 1", "pcml/44100": "-c:a pcm_s16le -ar:a 44100 -ac:a 1", + "opus/48000/2": "-c:a libopus -application:a lowdelay -min_comp 0 -ar:a 48000 -ac:a 2", + // hardware Intel and AMD on Linux // better not to set `-async_depth:v 1` like for QSV, because framedrops // `-bf 0` - disable B-frames is very important @@ -202,10 +210,6 @@ func parseArgs(s string) *ffmpeg.Args { args.Input = inputTemplate("file", s, query) } - if args.Input == "" { - return nil - } - if query["async"] != nil { args.Input = "-use_wallclock_as_timestamps 1 -async 1 " + args.Input } diff --git a/internal/ffmpeg/producer.go b/internal/ffmpeg/producer.go new file mode 100644 index 00000000..8cb3cb10 --- /dev/null +++ b/internal/ffmpeg/producer.go @@ -0,0 +1,112 @@ +package ffmpeg + +import ( + "encoding/json" + "errors" + "net/url" + "strconv" + "strings" + + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/aac" + "github.com/AlexxIT/go2rtc/pkg/core" +) + +type Producer struct { + core.SuperProducer + url string + query url.Values + ffmpeg core.Producer +} + +// NewProducer - FFmpeg producer with auto selection video/audio codec based on client capabilities +func NewProducer(url string) (core.Producer, error) { + p := &Producer{} + + i := strings.IndexByte(url, '#') + p.url, p.query = url[:i], streams.ParseQuery(url[i+1:]) + + // ffmpeg.NewProducer support only one audio + if len(p.query["video"]) != 0 || len(p.query["audio"]) != 1 { + return nil, errors.New("ffmpeg: unsupported params: " + url[i:]) + } + + p.Type = "FFmpeg producer" + p.Medias = []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + {Name: core.CodecOpus, ClockRate: 48000, Channels: 2}, + {Name: core.CodecAAC, ClockRate: 16000, FmtpLine: aac.FMTP + "1408"}, + {Name: core.CodecPCM, ClockRate: 16000}, + {Name: core.CodecPCM, ClockRate: 8000}, + {Name: core.CodecPCMA, ClockRate: 16000}, + {Name: core.CodecPCMA, ClockRate: 8000}, + {Name: core.CodecPCMU, ClockRate: 16000}, + {Name: core.CodecPCMU, ClockRate: 8000}, + }, + }, + } + return p, nil +} + +func (p *Producer) Start() error { + var err error + if p.ffmpeg, err = streams.GetProducer(p.newURL()); err != nil { + return err + } + + for i, media := range p.ffmpeg.GetMedias() { + track, err := p.ffmpeg.GetTrack(media, media.Codecs[0]) + if err != nil { + return err + } + p.Receivers[i].Replace(track) + } + + return p.ffmpeg.Start() +} + +func (p *Producer) Stop() error { + if p.ffmpeg == nil { + return nil + } + return p.ffmpeg.Stop() +} + +func (p *Producer) MarshalJSON() ([]byte, error) { + if p.ffmpeg == nil { + return json.Marshal(p.SuperProducer) + } + return json.Marshal(p.ffmpeg) +} + +func (p *Producer) newURL() string { + s := p.url + // rewrite codecs in url from auto to known presets from defaults + for _, receiver := range p.Receivers { + codec := receiver.Codec + switch codec.Name { + case core.CodecPCMU, core.CodecPCMA: + s += "#audio=" + strings.ToLower(codec.Name) + if codec.ClockRate != 0 { + s += "/" + strconv.Itoa(int(codec.ClockRate)) + } + case core.CodecAAC: + s += "#audio=aac/16000" + case core.CodecOpus: + s += "#audio=opus/48000/2" + } + } + // add other params + for key, values := range p.query { + if key != "audio" { + for _, value := range values { + s += "#" + key + "=" + value + } + } + } + + return s +} From 6d9c7012b012edf2dcb18c01184bd87bb5ca0510 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 23 May 2024 12:24:41 +0300 Subject: [PATCH 066/130] Add output/aac for ffmpeg source --- internal/ffmpeg/ffmpeg.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 48172bcb..b8b8627a 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -55,6 +55,7 @@ var defaults = map[string]string{ // output "output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", "output/mjpeg": "-f mjpeg -", + "output/aac": "-f adts -", "output/wav": "-f wav -", // `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1` @@ -336,6 +337,8 @@ func parseArgs(s string) *ffmpeg.Args { case args.Video == 0 && args.Audio == 1: codec, _, _ := strings.Cut(query.Get("audio"), "/") switch codec { + case "aac": + args.Output = defaults["output/aac"] case "pcma", "pcmu", "pcml": args.Output = defaults["output/wav"] } From 02af2e28492f8cbd6de0294cda446b9a22ae542b Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 23 May 2024 12:40:29 +0300 Subject: [PATCH 067/130] Code refactoring for FFmpeg producer --- internal/ffmpeg/ffmpeg.go | 2 -- internal/ffmpeg/producer.go | 9 ++++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index b8b8627a..a2447cc1 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -92,8 +92,6 @@ var defaults = map[string]string{ "pcml/8000": "-c:a pcm_s16le -ar:a 8000 -ac:a 1", "pcml/44100": "-c:a pcm_s16le -ar:a 44100 -ac:a 1", - "opus/48000/2": "-c:a libopus -application:a lowdelay -min_comp 0 -ar:a 48000 -ac:a 2", - // hardware Intel and AMD on Linux // better not to set `-async_depth:v 1` like for QSV, because framedrops // `-bf 0` - disable B-frames is very important diff --git a/internal/ffmpeg/producer.go b/internal/ffmpeg/producer.go index 8cb3cb10..867286ff 100644 --- a/internal/ffmpeg/producer.go +++ b/internal/ffmpeg/producer.go @@ -34,16 +34,19 @@ func NewProducer(url string) (core.Producer, error) { p.Type = "FFmpeg producer" p.Medias = []*core.Media{ { + // we can support only audio, because don't know FmtpLine for H264 and PayloadType for MJPEG Kind: core.KindAudio, Direction: core.DirectionRecvonly, + // codecs in order from best to worst Codecs: []*core.Codec{ + // OPUS will always marked as OPUS/48000/2 {Name: core.CodecOpus, ClockRate: 48000, Channels: 2}, {Name: core.CodecAAC, ClockRate: 16000, FmtpLine: aac.FMTP + "1408"}, {Name: core.CodecPCM, ClockRate: 16000}, - {Name: core.CodecPCM, ClockRate: 8000}, {Name: core.CodecPCMA, ClockRate: 16000}, - {Name: core.CodecPCMA, ClockRate: 8000}, {Name: core.CodecPCMU, ClockRate: 16000}, + {Name: core.CodecPCM, ClockRate: 8000}, + {Name: core.CodecPCMA, ClockRate: 8000}, {Name: core.CodecPCMU, ClockRate: 8000}, }, }, @@ -96,7 +99,7 @@ func (p *Producer) newURL() string { case core.CodecAAC: s += "#audio=aac/16000" case core.CodecOpus: - s += "#audio=opus/48000/2" + s += "#audio=opus" } } // add other params From c726651b8bbbe49e0a0e07a5a7c5f7188a4ff9a4 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 23 May 2024 17:31:02 +0300 Subject: [PATCH 068/130] Add ffmpeg version checker --- internal/ffmpeg/ffmpeg.go | 5 ++- internal/ffmpeg/version.go | 68 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 internal/ffmpeg/version.go diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index a2447cc1..aacf66cb 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -28,6 +28,9 @@ func Init() { } streams.RedirectFunc("ffmpeg", func(url string) (string, error) { + if _, err := Version(); err != nil { + return "", err + } args := parseArgs(url[7:]) if slices.Contains(args.Codecs, "auto") { return "", nil // force call streams.HandleFunc("ffmpeg") @@ -46,7 +49,7 @@ var defaults = map[string]string{ "global": "-hide_banner", // inputs - "file": "-re -i {input}", + "file": "-re -readrate_initial_burst 0.001 -i {input}", "http": "-fflags nobuffer -flags low_delay -i {input}", "rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}", diff --git a/internal/ffmpeg/version.go b/internal/ffmpeg/version.go new file mode 100644 index 00000000..74028c54 --- /dev/null +++ b/internal/ffmpeg/version.go @@ -0,0 +1,68 @@ +package ffmpeg + +import ( + "bytes" + "errors" + "os/exec" + "strings" + "sync" + + "github.com/rs/zerolog/log" +) + +var checkMu sync.Mutex +var checkErr error +var checkVer string + +const ( + FFmpeg50 = "59. 16" + FFmpeg51 = "59. 27" + FFmpeg60 = "60. 3" + FFmpeg61 = "60. 16" + FFmpeg70 = "61. 1" +) + +func Version() (string, error) { + checkMu.Lock() + defer checkMu.Unlock() + + if checkVer != "" { + return checkVer, checkErr + } + + cmd := exec.Command(defaults["bin"], "-version") + b, err := cmd.Output() + if err != nil { + checkVer = "-" + checkErr = err + return checkVer, checkErr + } + + if len(b) < 100 { + checkVer = "?" + return checkVer, nil + } + + // ffmpeg version n7.0-30-g8b0fe91754-20240520 Copyright (c) 2000-2024 the FFmpeg developers + b = b[15:] + if i := bytes.IndexByte(b, ' '); i > 0 { + checkVer = string(b[:i]) + } + + // libavformat 60. 16.100 / 60. 16.100 + if i := strings.Index(string(b), "libavformat"); i > 0 { + // better to compare libavformat, because nightly/master builds + libav := string(b[i+15 : i+25]) + if libav < FFmpeg50 { + checkErr = errors.New("ffmpeg: unsupported version: " + checkVer) + return checkVer, checkErr + } + if libav < FFmpeg61 && strings.Contains(defaults["file"], "readrate_initial_burst") { + defaults["file"] = "-re -i {input}" + } + } + + log.Debug().Str("version", checkVer).Msgf("[ffmpeg] bin") + + return checkVer, nil +} From 6fafd10482bbfece4a14323c984ca30b8f80edbe Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 23 May 2024 17:40:27 +0300 Subject: [PATCH 069/130] Add stream source validation for dynamic streams --- internal/streams/streams.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/internal/streams/streams.go b/internal/streams/streams.go index fc8c13c7..c676fe09 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -1,6 +1,7 @@ package streams import ( + "errors" "net/http" "net/url" "regexp" @@ -49,9 +50,16 @@ func Get(name string) *Stream { var sanitize = regexp.MustCompile(`\s`) -func New(name string, source string) *Stream { - // not allow creating dynamic streams with spaces in the source +// Validate - not allow creating dynamic streams with spaces in the source +func Validate(source string) error { if sanitize.MatchString(source) { + return errors.New("streams: invalid dynamic source") + } + return nil +} + +func New(name string, source string) *Stream { + if Validate(source) != nil { return nil } @@ -203,13 +211,17 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) { // with dst - redirect source to dst if dst := query.Get("dst"); dst != "" { if stream := Get(dst); stream != nil { - if err := stream.Play(src); err != nil { + if err := Validate(src); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } else if err = stream.Play(src); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } else { api.ResponseJSON(w, stream) } } else if stream = Get(src); stream != nil { - if err := stream.Publish(dst); err != nil { + if err := Validate(dst); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } else if err = stream.Publish(dst); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } else { From 8f57b1acb67c0a3f4d6e4cce651abd2ce23f11e9 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 24 May 2024 07:48:17 +0300 Subject: [PATCH 070/130] Fix TTS template --- internal/ffmpeg/virtual/virtual.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ffmpeg/virtual/virtual.go b/internal/ffmpeg/virtual/virtual.go index 4dc3b025..836fc37b 100644 --- a/internal/ffmpeg/virtual/virtual.go +++ b/internal/ffmpeg/virtual/virtual.go @@ -67,7 +67,7 @@ func GetInputTTS(src string) string { return "" } - input := `-re -f lavfi -i "flite=text='` + query.Get("text") + `'` + input := `-re -readrate_initial_burst 0.001 -f lavfi -i "flite=text='` + query.Get("text") + `'` // ffmpeg -f lavfi -i flite=list_voices=1 // awb, kal, kal16, rms, slt From d2346a2aed9519c80d76e814a172dd732c545f1d Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 24 May 2024 07:48:44 +0300 Subject: [PATCH 071/130] Fix FFmpeg producer codecs --- internal/ffmpeg/producer.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/ffmpeg/producer.go b/internal/ffmpeg/producer.go index 867286ff..05df69e3 100644 --- a/internal/ffmpeg/producer.go +++ b/internal/ffmpeg/producer.go @@ -41,13 +41,14 @@ func NewProducer(url string) (core.Producer, error) { Codecs: []*core.Codec{ // OPUS will always marked as OPUS/48000/2 {Name: core.CodecOpus, ClockRate: 48000, Channels: 2}, - {Name: core.CodecAAC, ClockRate: 16000, FmtpLine: aac.FMTP + "1408"}, {Name: core.CodecPCM, ClockRate: 16000}, {Name: core.CodecPCMA, ClockRate: 16000}, {Name: core.CodecPCMU, ClockRate: 16000}, {Name: core.CodecPCM, ClockRate: 8000}, {Name: core.CodecPCMA, ClockRate: 8000}, {Name: core.CodecPCMU, ClockRate: 8000}, + // AAC has unknown problems on Dahua two way + {Name: core.CodecAAC, ClockRate: 16000, FmtpLine: aac.FMTP + "1408"}, }, }, } @@ -91,15 +92,16 @@ func (p *Producer) newURL() string { for _, receiver := range p.Receivers { codec := receiver.Codec switch codec.Name { - case core.CodecPCMU, core.CodecPCMA: - s += "#audio=" + strings.ToLower(codec.Name) - if codec.ClockRate != 0 { - s += "/" + strconv.Itoa(int(codec.ClockRate)) - } - case core.CodecAAC: - s += "#audio=aac/16000" case core.CodecOpus: s += "#audio=opus" + case core.CodecAAC: + s += "#audio=aac/16000" + case core.CodecPCM: + s += "#audio=pcm/" + strconv.Itoa(int(codec.ClockRate)) + case core.CodecPCMA: + s += "#audio=pcma/" + strconv.Itoa(int(codec.ClockRate)) + case core.CodecPCMU: + s += "#audio=pcmu/" + strconv.Itoa(int(codec.ClockRate)) } } // add other params From ff39e2e4961e2962f4b6c556363470f9bd786957 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 24 May 2024 11:04:26 +0300 Subject: [PATCH 072/130] Change import log for hass module from debug to trace --- internal/hass/hass.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/hass/hass.go b/internal/hass/hass.go index 61113959..cd95ffe1 100644 --- a/internal/hass/hass.go +++ b/internal/hass/hass.go @@ -57,7 +57,7 @@ func Init() { // load static entries from Hass config if err := importConfig(conf.Mod.Config); err != nil { - log.Debug().Msgf("[hass] can't import config: %s", err) + log.Trace().Msgf("[hass] can't import config: %s", err) api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) { http.Error(w, "no hass config", http.StatusNotFound) From bf3f81ccacd2483d96e47f865ba419b0ceb48d87 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 24 May 2024 11:06:51 +0300 Subject: [PATCH 073/130] Update ffmpeg pkg for reading files and parsing ffmpeg version --- internal/ffmpeg/ffmpeg.go | 9 +++-- internal/ffmpeg/ffmpeg_test.go | 21 ++++++++++ internal/ffmpeg/version.go | 63 ++++++++++-------------------- internal/ffmpeg/virtual/virtual.go | 2 +- pkg/ffmpeg/ffmpeg.go | 30 ++++++++++++++ 5 files changed, 78 insertions(+), 47 deletions(-) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index aacf66cb..db5890d0 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -49,7 +49,7 @@ var defaults = map[string]string{ "global": "-hide_banner", // inputs - "file": "-re -readrate_initial_burst 0.001 -i {input}", + "file": "-re -i {input}", "http": "-fflags nobuffer -flags low_delay -i {input}", "rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}", @@ -151,9 +151,10 @@ func inputTemplate(name, s string, query url.Values) string { func parseArgs(s string) *ffmpeg.Args { // init FFmpeg arguments args := &ffmpeg.Args{ - Bin: defaults["bin"], - Global: defaults["global"], - Output: defaults["output"], + Bin: defaults["bin"], + Global: defaults["global"], + Output: defaults["output"], + Version: verAV, } var query url.Values diff --git a/internal/ffmpeg/ffmpeg_test.go b/internal/ffmpeg/ffmpeg_test.go index d5a39284..3fc5d208 100644 --- a/internal/ffmpeg/ffmpeg_test.go +++ b/internal/ffmpeg/ffmpeg_test.go @@ -3,6 +3,7 @@ package ffmpeg import ( "testing" + "github.com/AlexxIT/go2rtc/pkg/ffmpeg" "github.com/stretchr/testify/require" ) @@ -292,3 +293,23 @@ func TestDrawText(t *testing.T) { }) } } + +func TestVersion(t *testing.T) { + verAV = ffmpeg.Version61 + tests := []struct { + name string + source string + expect string + }{ + { + source: "/media/bbb.mp4", + expect: `ffmpeg -hide_banner -readrate_initial_burst 0.001 -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + args := parseArgs(test.source) + require.Equal(t, test.expect, args.String()) + }) + } +} diff --git a/internal/ffmpeg/version.go b/internal/ffmpeg/version.go index 74028c54..976c92d0 100644 --- a/internal/ffmpeg/version.go +++ b/internal/ffmpeg/version.go @@ -1,68 +1,47 @@ package ffmpeg import ( - "bytes" "errors" "os/exec" - "strings" "sync" + "github.com/AlexxIT/go2rtc/pkg/ffmpeg" "github.com/rs/zerolog/log" ) -var checkMu sync.Mutex -var checkErr error -var checkVer string - -const ( - FFmpeg50 = "59. 16" - FFmpeg51 = "59. 27" - FFmpeg60 = "60. 3" - FFmpeg61 = "60. 16" - FFmpeg70 = "61. 1" -) +var verMu sync.Mutex +var verErr error +var verFF string +var verAV string func Version() (string, error) { - checkMu.Lock() - defer checkMu.Unlock() + verMu.Lock() + defer verMu.Unlock() - if checkVer != "" { - return checkVer, checkErr + if verFF != "" { + return verFF, verErr } cmd := exec.Command(defaults["bin"], "-version") b, err := cmd.Output() if err != nil { - checkVer = "-" - checkErr = err - return checkVer, checkErr + verFF = "-" + verErr = err + return verFF, verErr } - if len(b) < 100 { - checkVer = "?" - return checkVer, nil + verFF, verAV = ffmpeg.ParseVersion(b) + + if verFF == "" { + verFF = "?" } - // ffmpeg version n7.0-30-g8b0fe91754-20240520 Copyright (c) 2000-2024 the FFmpeg developers - b = b[15:] - if i := bytes.IndexByte(b, ' '); i > 0 { - checkVer = string(b[:i]) + // better to compare libavformat, because nightly/master builds + if verAV != "" && verAV < ffmpeg.Version50 { + verErr = errors.New("ffmpeg: unsupported version: " + verFF) } - // libavformat 60. 16.100 / 60. 16.100 - if i := strings.Index(string(b), "libavformat"); i > 0 { - // better to compare libavformat, because nightly/master builds - libav := string(b[i+15 : i+25]) - if libav < FFmpeg50 { - checkErr = errors.New("ffmpeg: unsupported version: " + checkVer) - return checkVer, checkErr - } - if libav < FFmpeg61 && strings.Contains(defaults["file"], "readrate_initial_burst") { - defaults["file"] = "-re -i {input}" - } - } + log.Debug().Str("version", verFF).Str("libavformat", verAV).Msgf("[ffmpeg] bin") - log.Debug().Str("version", checkVer).Msgf("[ffmpeg] bin") - - return checkVer, nil + return verFF, verErr } diff --git a/internal/ffmpeg/virtual/virtual.go b/internal/ffmpeg/virtual/virtual.go index 836fc37b..4dc3b025 100644 --- a/internal/ffmpeg/virtual/virtual.go +++ b/internal/ffmpeg/virtual/virtual.go @@ -67,7 +67,7 @@ func GetInputTTS(src string) string { return "" } - input := `-re -readrate_initial_burst 0.001 -f lavfi -i "flite=text='` + query.Get("text") + `'` + input := `-re -f lavfi -i "flite=text='` + query.Get("text") + `'` // ffmpeg -f lavfi -i flite=list_voices=1 // awb, kal, kal16, rms, slt diff --git a/pkg/ffmpeg/ffmpeg.go b/pkg/ffmpeg/ffmpeg.go index 912e7684..a7ca71c7 100644 --- a/pkg/ffmpeg/ffmpeg.go +++ b/pkg/ffmpeg/ffmpeg.go @@ -6,6 +6,15 @@ import ( "strings" ) +// correlation of libavformat versions with ffmpeg versions +const ( + Version50 = "59. 16" + Version51 = "59. 27" + Version60 = "60. 3" + Version61 = "60. 16" + Version70 = "61. 1" +) + type Args struct { Bin string // ffmpeg Global string // -hide_banner -v error @@ -13,6 +22,7 @@ type Args struct { Codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency Filters []string // scale=1920:1080 Output string // -f rtsp {output} + Version string // libavformat version, it's more reliable than the ffmpeg version Video, Audio int // count of Video and Audio params } @@ -52,6 +62,11 @@ func (a *Args) String() string { } b.WriteByte(' ') + // starting from FFmpeg 6.1 readrate=1 has default initial bust 0.5 sec + // it might make us miss the first couple seconds of the file + if strings.HasPrefix(a.Input, "-re ") && a.Version >= Version61 { + b.WriteString("-readrate_initial_burst 0.001 ") + } b.WriteString(a.Input) multimode := a.Video > 1 || a.Audio > 1 @@ -91,3 +106,18 @@ func (a *Args) String() string { return b.String() } + +func ParseVersion(b []byte) (ffmpeg string, libavformat string) { + if len(b) > 100 { + // ffmpeg version n7.0-30-g8b0fe91754-20240520 Copyright (c) 2000-2024 the FFmpeg developers + if i := bytes.IndexByte(b[15:], ' '); i > 0 { + ffmpeg = string(b[15 : 15+i]) + } + + // libavformat 60. 16.100 / 60. 16.100 + if i := strings.Index(string(b), "libavformat"); i > 0 { + libavformat = string(b[i+15 : i+25]) + } + } + return +} From b3e9ed23ac37b9175559c0556f726ecf368d776f Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 24 May 2024 12:49:37 +0300 Subject: [PATCH 074/130] Add /api/ffmpeg for playing files and tts on cameras with two-way audio --- internal/ffmpeg/api.go | 51 +++++++++++++++++++++++++++++++++++++++ internal/ffmpeg/ffmpeg.go | 3 +++ www/links.html | 13 +++++++--- 3 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 internal/ffmpeg/api.go diff --git a/internal/ffmpeg/api.go b/internal/ffmpeg/api.go new file mode 100644 index 00000000..d802f87c --- /dev/null +++ b/internal/ffmpeg/api.go @@ -0,0 +1,51 @@ +package ffmpeg + +import ( + "net/http" + "strings" + + "github.com/AlexxIT/go2rtc/internal/streams" +) + +func apiFFmpeg(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "", http.StatusMethodNotAllowed) + return + } + + query := r.URL.Query() + dst := query.Get("dst") + stream := streams.Get(dst) + if stream == nil { + http.Error(w, "", http.StatusNotFound) + return + } + + var src string + if s := query.Get("file"); s != "" { + if streams.Validate(s) == nil { + src = "ffmpeg:" + s + "#audio=auto#input=file" + } + } else if s = query.Get("live"); s != "" { + if streams.Validate(s) == nil { + src = "ffmpeg:" + s + "#audio=auto" + } + } else if s = query.Get("text"); s != "" { + if strings.IndexAny(s, `'"&%$`) < 0 { + src = "ffmpeg:tts?text=" + s + if s = query.Get("voice"); s != "" { + src += "&voice=" + s + } + src += "#audio=auto" + } + } + + if src == "" { + http.Error(w, "", http.StatusBadRequest) + return + } + + if err := stream.Play(src); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index db5890d0..26f9880f 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -5,6 +5,7 @@ import ( "slices" "strings" + "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/ffmpeg/device" "github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware" @@ -40,6 +41,8 @@ func Init() { streams.HandleFunc("ffmpeg", NewProducer) + api.HandleFunc("api/ffmpeg", apiFFmpeg) + device.Init(defaults["bin"]) hardware.Init(defaults["bin"]) } diff --git a/www/links.html b/www/links.html index 940de9fd..3b651762 100644 --- a/www/links.html +++ b/www/links.html @@ -85,16 +85,21 @@

From d9d2bdff44ab777ab8eb353196adf49594560d30 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 24 May 2024 16:26:06 +0300 Subject: [PATCH 075/130] Add timeout query param to RTSP incoming source #1118 --- internal/rtsp/rtsp.go | 5 +++++ pkg/rtsp/conn.go | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index c5e21678..230bdece 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -210,6 +210,11 @@ func tcpHandler(conn *rtsp.Conn) { return } + query := conn.URL.Query() + if s := query.Get("timeout"); s != "" { + conn.Timeout = core.Atoi(s) + } + log.Debug().Str("stream", name).Msg("[rtsp] new producer") stream.AddProducer(conn) diff --git a/pkg/rtsp/conn.go b/pkg/rtsp/conn.go index e801b7e4..91465f2c 100644 --- a/pkg/rtsp/conn.go +++ b/pkg/rtsp/conn.go @@ -124,7 +124,11 @@ func (c *Conn) Handle() (err error) { case core.ModePassiveProducer: // polling frames from remote RTSP Client (ex FFmpeg) - timeout = time.Second * 15 + if c.Timeout == 0 { + timeout = time.Second * 15 + } else { + timeout = time.Second * time.Duration(c.Timeout) + } case core.ModePassiveConsumer: // pushing frames to remote RTSP Client (ex VLC) From 8749562c96c08c3907e1ab19dfeff2507ff47915 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 10 May 2024 21:55:47 +0300 Subject: [PATCH 076/130] Fix detection webrtc without audio #1106 --- www/video-rtc.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/www/video-rtc.js b/www/video-rtc.js index 56893aa9..52fb5dda 100644 --- a/www/video-rtc.js +++ b/www/video-rtc.js @@ -492,7 +492,9 @@ export class VideoRTC extends HTMLElement { pc.addEventListener('connectionstatechange', () => { if (pc.connectionState === 'connected') { - const tracks = pc.getReceivers().map(receiver => receiver.track); + const tracks = pc.getTransceivers() + .filter(tr => tr.currentDirection === 'recvonly') // skip inactive + .map(tr => tr.receiver.track); /** @type {HTMLVideoElement} */ const video2 = document.createElement('video'); video2.addEventListener('loadeddata', () => this.onpcvideo(video2), {once: true}); From f8bc25d0ae5f383f9b2fcf7cb48c18aeee6038e2 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 25 May 2024 08:22:38 +0300 Subject: [PATCH 077/130] Add support rawvideo format --- internal/ffmpeg/ffmpeg.go | 16 ++- pkg/core/core.go | 1 + pkg/core/helpers.go | 7 ++ pkg/magic/producer.go | 4 + pkg/mjpeg/consumer.go | 3 + pkg/mjpeg/helpers.go | 21 ++++ pkg/y4m/y4m.go | 199 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 pkg/y4m/y4m.go diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 26f9880f..2b24c3ce 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -12,6 +12,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/ffmpeg/virtual" "github.com/AlexxIT/go2rtc/internal/rtsp" "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/ffmpeg" ) @@ -61,6 +62,7 @@ var defaults = map[string]string{ // output "output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", "output/mjpeg": "-f mjpeg -", + "output/raw": "-f yuv4mpegpipe -", "output/aac": "-f adts -", "output/wav": "-f wav -", @@ -73,6 +75,12 @@ var defaults = map[string]string{ "mjpeg": "-c:v mjpeg", //"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p", + "raw": "-c:v rawvideo", + "raw/gray8": "-c:v rawvideo -pix_fmt:v gray8", + "raw/yuv420p": "-c:v rawvideo -pix_fmt:v yuv420p", + "raw/yuv422p": "-c:v rawvideo -pix_fmt:v yuv422p", + "raw/yuv444p": "-c:v rawvideo -pix_fmt:v yuv444p", + // https://ffmpeg.org/ffmpeg-codecs.html#libopus-1 // https://github.com/pion/webrtc/issues/1514 // https://ffmpeg.org/ffmpeg-resampler.html @@ -336,12 +344,14 @@ func parseArgs(s string) *ffmpeg.Args { args.Output = defaults["output/mjpeg"] } case args.Video == 1 && args.Audio == 0: - if query.Get("video") == "mjpeg" { + switch core.Before(query.Get("video"), "/") { + case "mjpeg": args.Output = defaults["output/mjpeg"] + case "raw": + args.Output = defaults["output/raw"] } case args.Video == 0 && args.Audio == 1: - codec, _, _ := strings.Cut(query.Get("audio"), "/") - switch codec { + switch core.Before(query.Get("audio"), "/") { case "aac": args.Output = defaults["output/aac"] case "pcma", "pcmu", "pcml": diff --git a/pkg/core/core.go b/pkg/core/core.go index 146533e3..bc855ccc 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -18,6 +18,7 @@ const ( CodecVP9 = "VP9" CodecAV1 = "AV1" CodecJPEG = "JPEG" // payloadType: 26 + CodecRAW = "RAW" CodecPCMU = "PCMU" // payloadType: 0 CodecPCMA = "PCMA" // payloadType: 8 diff --git a/pkg/core/helpers.go b/pkg/core/helpers.go index 0c367e2c..72afe897 100644 --- a/pkg/core/helpers.go +++ b/pkg/core/helpers.go @@ -38,6 +38,13 @@ func RandString(size, base byte) string { return string(b) } +func Before(s, sep string) string { + if i := strings.Index(s, sep); i > 0 { + return s[:i] + } + return s +} + func Between(s, sub1, sub2 string) string { i := strings.Index(s, sub1) if i < 0 { diff --git a/pkg/magic/producer.go b/pkg/magic/producer.go index c49fe8bf..9bde508d 100644 --- a/pkg/magic/producer.go +++ b/pkg/magic/producer.go @@ -15,6 +15,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/mpegts" "github.com/AlexxIT/go2rtc/pkg/multipart" "github.com/AlexxIT/go2rtc/pkg/wav" + "github.com/AlexxIT/go2rtc/pkg/y4m" ) func Open(r io.Reader) (core.Producer, error) { @@ -32,6 +33,9 @@ func Open(r io.Reader) (core.Producer, error) { case string(b) == wav.FourCC: return wav.Open(rd) + case string(b) == y4m.FourCC: + return y4m.Open(rd) + case bytes.HasPrefix(b, []byte{0xFF, 0xD8}): return mjpeg.Open(rd) diff --git a/pkg/mjpeg/consumer.go b/pkg/mjpeg/consumer.go index 444cbdcc..d5fb0d51 100644 --- a/pkg/mjpeg/consumer.go +++ b/pkg/mjpeg/consumer.go @@ -22,6 +22,7 @@ func NewConsumer() *Consumer { Direction: core.DirectionSendonly, Codecs: []*core.Codec{ {Name: core.CodecJPEG}, + {Name: core.CodecRAW}, }, }, }, @@ -40,6 +41,8 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv if track.Codec.IsRTP() { sender.Handler = RTPDepay(sender.Handler) + } else if track.Codec.Name == core.CodecRAW { + sender.Handler = Encoder(track.Codec, sender.Handler) } sender.HandleRTP(track) diff --git a/pkg/mjpeg/helpers.go b/pkg/mjpeg/helpers.go index 21000b9b..08b4408b 100644 --- a/pkg/mjpeg/helpers.go +++ b/pkg/mjpeg/helpers.go @@ -3,6 +3,10 @@ package mjpeg import ( "bytes" "image/jpeg" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/y4m" + "github.com/pion/rtp" ) // FixJPEG - reencode JPEG if it has wrong header @@ -33,3 +37,20 @@ func FixJPEG(b []byte) []byte { } return buf.Bytes() } + +func Encoder(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { + newImage := y4m.NewImage(codec.FmtpLine) + + return func(packet *rtp.Packet) { + img := newImage(packet.Payload) + + buf := bytes.NewBuffer(nil) + if err := jpeg.Encode(buf, img, nil); err != nil { + return + } + + clone := *packet + clone.Payload = buf.Bytes() + handler(&clone) + } +} diff --git a/pkg/y4m/y4m.go b/pkg/y4m/y4m.go new file mode 100644 index 00000000..6caef1a0 --- /dev/null +++ b/pkg/y4m/y4m.go @@ -0,0 +1,199 @@ +package y4m + +import ( + "bufio" + "bytes" + "errors" + "image" + "io" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +const FourCC = "YUV4" + +func Open(r io.Reader) (*Producer, error) { + rd := bufio.NewReaderSize(r, core.BufferSize) + b, err := rd.ReadBytes('\n') + if err != nil { + return nil, err + } + + b = b[:len(b)-1] // remove \n + + sdp := string(b) + var fmtp string + + for b != nil { + // YUV4MPEG2 W1280 H720 F24:1 Ip A1:1 C420mpeg2 XYSCSS=420MPEG2 + // https://manned.org/yuv4mpeg.5 + // https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/yuv4mpegenc.c + key := b[0] + var value string + if i := bytes.IndexByte(b, ' '); i > 0 { + value = string(b[1:i]) + b = b[i+1:] + } else { + value = string(b[1:]) + b = nil + } + + switch key { + case 'W': + fmtp = "width=" + value + case 'H': + fmtp += ";height=" + value + case 'C': + fmtp += ";colorspace=" + value + } + } + + if GetSize(fmtp) == 0 { + return nil, errors.New("y4m: unsupported format: " + sdp) + } + + prod := &Producer{rd: rd, cl: r.(io.Closer)} + prod.Type = "YUV4MPEG2 producer" + prod.SDP = sdp + prod.Medias = []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: core.CodecRAW, + ClockRate: 90000, + FmtpLine: fmtp, + PayloadType: core.PayloadTypeRAW, + }, + }, + }, + } + + return prod, nil +} + +type Producer struct { + core.SuperProducer + rd *bufio.Reader + cl io.Closer +} + +func (c *Producer) Start() error { + size := GetSize(c.Medias[0].Codecs[0].FmtpLine) + + for { + // FRAME\n + if _, err := c.rd.Discard(6); err != nil { + return err + } + + frame := make([]byte, size) + if _, err := io.ReadFull(c.rd, frame); err != nil { + return err + } + + c.Recv += size + + if len(c.Receivers) == 0 { + continue + } + + pkt := &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: frame, + } + c.Receivers[0].WriteRTP(pkt) + } +} + +func (c *Producer) Stop() error { + _ = c.SuperProducer.Close() + return c.cl.Close() +} + +func GetSize(fmtp string) int { + w := core.Atoi(core.Between(fmtp, "width=", ";")) + h := core.Atoi(core.Between(fmtp, "height=", ";")) + + switch core.Between(fmtp, "colorspace=", ";") { + case "mono": + return w * h + case "420mpeg2", "420jpeg": + return w * h * 3 / 2 + case "422": + return w * h * 2 + case "444": + return w * h * 3 + } + + return 0 +} + +func NewImage(fmtp string) func(frame []byte) image.Image { + w := core.Atoi(core.Between(fmtp, "width=", ";")) + h := core.Atoi(core.Between(fmtp, "height=", ";")) + rect := image.Rect(0, 0, w, h) + + switch core.Between(fmtp, "colorspace=", ";") { + case "mono": + return func(frame []byte) image.Image { + return &image.Gray{ + Pix: frame, + Stride: w, + Rect: rect, + } + } + case "420mpeg2", "420jpeg": + i1 := w * h + i2 := i1 + i1/4 + i3 := i2 + i1/4 + + return func(frame []byte) image.Image { + return &image.YCbCr{ + Y: frame[:i1], + Cb: frame[i1:i2], + Cr: frame[i2:i3], + YStride: w, + CStride: w / 2, + SubsampleRatio: image.YCbCrSubsampleRatio420, + Rect: rect, + } + } + case "422": + i1 := w * h + i2 := i1 + i1/2 + i3 := i2 + i1/2 + + return func(frame []byte) image.Image { + return &image.YCbCr{ + Y: frame[:i1], + Cb: frame[i1:i2], + Cr: frame[i2:i3], + YStride: w, + CStride: w / 2, + SubsampleRatio: image.YCbCrSubsampleRatio422, + Rect: rect, + } + } + case "444": + i1 := w * h + i2 := i1 + i1 + i3 := i2 + i1 + + return func(frame []byte) image.Image { + return &image.YCbCr{ + Y: frame[:i1], + Cb: frame[i1:i2], + Cr: frame[i2:i3], + YStride: w, + CStride: w, + SubsampleRatio: image.YCbCrSubsampleRatio444, + Rect: rect, + } + } + } + + return nil +} From 6f34cf0c950ce24c0dd2126274c3dda189ef22b7 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 25 May 2024 11:49:56 +0300 Subject: [PATCH 078/130] Add streaming to rawvideo format --- internal/mjpeg/init.go | 24 +++++++++ pkg/y4m/README.md | 5 ++ pkg/y4m/consumer.go | 67 +++++++++++++++++++++++++ pkg/y4m/producer.go | 110 +++++++++++++++++++++++++++++++++++++++++ pkg/y4m/y4m.go | 105 +-------------------------------------- 5 files changed, 207 insertions(+), 104 deletions(-) create mode 100644 pkg/y4m/README.md create mode 100644 pkg/y4m/consumer.go create mode 100644 pkg/y4m/producer.go diff --git a/internal/mjpeg/init.go b/internal/mjpeg/init.go index 7a4403f9..ea65e2d7 100644 --- a/internal/mjpeg/init.go +++ b/internal/mjpeg/init.go @@ -17,6 +17,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/magic" "github.com/AlexxIT/go2rtc/pkg/mjpeg" "github.com/AlexxIT/go2rtc/pkg/tcp" + "github.com/AlexxIT/go2rtc/pkg/y4m" "github.com/rs/zerolog/log" ) @@ -24,6 +25,7 @@ func Init() { api.HandleFunc("api/frame.jpeg", handlerKeyframe) api.HandleFunc("api/stream.mjpeg", handlerStream) api.HandleFunc("api/stream.ascii", handlerStream) + api.HandleFunc("api/stream.y4m", apiStreamY4M) ws.HandleFunc("mjpeg", handlerWS) } @@ -166,3 +168,25 @@ func handlerWS(tr *ws.Transport, _ *ws.Message) error { return nil } + +func apiStreamY4M(w http.ResponseWriter, r *http.Request) { + src := r.URL.Query().Get("src") + stream := streams.Get(src) + if stream == nil { + http.Error(w, api.StreamNotFound, http.StatusNotFound) + return + } + + cons := y4m.NewConsumer() + cons.RemoteAddr = tcp.RemoteAddr(r) + cons.UserAgent = r.UserAgent() + + if err := stream.AddConsumer(cons); err != nil { + log.Error().Err(err).Caller().Send() + return + } + + _, _ = cons.WriteTo(w) + + stream.RemoveConsumer(cons) +} diff --git a/pkg/y4m/README.md b/pkg/y4m/README.md new file mode 100644 index 00000000..6f4d863e --- /dev/null +++ b/pkg/y4m/README.md @@ -0,0 +1,5 @@ +## Useful links + +- https://learn.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering +- https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_concepts +- https://fourcc.org/yuv.php#YV12 diff --git a/pkg/y4m/consumer.go b/pkg/y4m/consumer.go new file mode 100644 index 00000000..01bece31 --- /dev/null +++ b/pkg/y4m/consumer.go @@ -0,0 +1,67 @@ +package y4m + +import ( + "fmt" + "io" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +type Consumer struct { + core.SuperConsumer + wr *core.WriteBuffer +} + +func NewConsumer() *Consumer { + return &Consumer{ + core.SuperConsumer{ + Type: "YUV4MPEG2 passive consumer", + Medias: []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecRAW}, + }, + }, + }, + }, + core.NewWriteBuffer(nil), + } +} + +func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { + sender := core.NewSender(media, track.Codec) + sender.Handler = func(packet *rtp.Packet) { + if n, err := c.wr.Write([]byte(frameHdr)); err == nil { + c.Send += n + } + if n, err := c.wr.Write(packet.Payload); err == nil { + c.Send += n + } + } + + hdr := fmt.Sprintf( + "YUV4MPEG2 W%s H%s C%s\n", + core.Between(track.Codec.FmtpLine, "width=", ";"), + core.Between(track.Codec.FmtpLine, "height=", ";"), + core.Between(track.Codec.FmtpLine, "colorspace=", ";"), + ) + if _, err := c.wr.Write([]byte(hdr)); err != nil { + return err + } + + sender.HandleRTP(track) + c.Senders = append(c.Senders, sender) + return nil +} + +func (c *Consumer) WriteTo(wr io.Writer) (int64, error) { + return c.wr.WriteTo(wr) +} + +func (c *Consumer) Stop() error { + _ = c.SuperConsumer.Close() + return c.wr.Close() +} diff --git a/pkg/y4m/producer.go b/pkg/y4m/producer.go new file mode 100644 index 00000000..05f98a6f --- /dev/null +++ b/pkg/y4m/producer.go @@ -0,0 +1,110 @@ +package y4m + +import ( + "bufio" + "bytes" + "errors" + "io" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +func Open(r io.Reader) (*Producer, error) { + rd := bufio.NewReaderSize(r, core.BufferSize) + b, err := rd.ReadBytes('\n') + if err != nil { + return nil, err + } + + b = b[:len(b)-1] // remove \n + + sdp := string(b) + var fmtp string + + for b != nil { + // YUV4MPEG2 W1280 H720 F24:1 Ip A1:1 C420mpeg2 XYSCSS=420MPEG2 + // https://manned.org/yuv4mpeg.5 + // https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/yuv4mpegenc.c + key := b[0] + var value string + if i := bytes.IndexByte(b, ' '); i > 0 { + value = string(b[1:i]) + b = b[i+1:] + } else { + value = string(b[1:]) + b = nil + } + + switch key { + case 'W': + fmtp = "width=" + value + case 'H': + fmtp += ";height=" + value + case 'C': + fmtp += ";colorspace=" + value + } + } + + if GetSize(fmtp) == 0 { + return nil, errors.New("y4m: unsupported format: " + sdp) + } + + prod := &Producer{rd: rd, cl: r.(io.Closer)} + prod.Type = "YUV4MPEG2 producer" + prod.SDP = sdp + prod.Medias = []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: core.CodecRAW, + ClockRate: 90000, + FmtpLine: fmtp, + PayloadType: core.PayloadTypeRAW, + }, + }, + }, + } + + return prod, nil +} + +type Producer struct { + core.SuperProducer + rd *bufio.Reader + cl io.Closer +} + +func (c *Producer) Start() error { + size := GetSize(c.Medias[0].Codecs[0].FmtpLine) + + for { + if _, err := c.rd.Discard(len(frameHdr)); err != nil { + return err + } + + frame := make([]byte, size) + if _, err := io.ReadFull(c.rd, frame); err != nil { + return err + } + + c.Recv += size + + if len(c.Receivers) == 0 { + continue + } + + pkt := &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: frame, + } + c.Receivers[0].WriteRTP(pkt) + } +} + +func (c *Producer) Stop() error { + _ = c.SuperProducer.Close() + return c.cl.Close() +} diff --git a/pkg/y4m/y4m.go b/pkg/y4m/y4m.go index 6caef1a0..8184ea97 100644 --- a/pkg/y4m/y4m.go +++ b/pkg/y4m/y4m.go @@ -1,117 +1,14 @@ package y4m import ( - "bufio" - "bytes" - "errors" "image" - "io" "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/pion/rtp" ) const FourCC = "YUV4" -func Open(r io.Reader) (*Producer, error) { - rd := bufio.NewReaderSize(r, core.BufferSize) - b, err := rd.ReadBytes('\n') - if err != nil { - return nil, err - } - - b = b[:len(b)-1] // remove \n - - sdp := string(b) - var fmtp string - - for b != nil { - // YUV4MPEG2 W1280 H720 F24:1 Ip A1:1 C420mpeg2 XYSCSS=420MPEG2 - // https://manned.org/yuv4mpeg.5 - // https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/yuv4mpegenc.c - key := b[0] - var value string - if i := bytes.IndexByte(b, ' '); i > 0 { - value = string(b[1:i]) - b = b[i+1:] - } else { - value = string(b[1:]) - b = nil - } - - switch key { - case 'W': - fmtp = "width=" + value - case 'H': - fmtp += ";height=" + value - case 'C': - fmtp += ";colorspace=" + value - } - } - - if GetSize(fmtp) == 0 { - return nil, errors.New("y4m: unsupported format: " + sdp) - } - - prod := &Producer{rd: rd, cl: r.(io.Closer)} - prod.Type = "YUV4MPEG2 producer" - prod.SDP = sdp - prod.Medias = []*core.Media{ - { - Kind: core.KindVideo, - Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{ - { - Name: core.CodecRAW, - ClockRate: 90000, - FmtpLine: fmtp, - PayloadType: core.PayloadTypeRAW, - }, - }, - }, - } - - return prod, nil -} - -type Producer struct { - core.SuperProducer - rd *bufio.Reader - cl io.Closer -} - -func (c *Producer) Start() error { - size := GetSize(c.Medias[0].Codecs[0].FmtpLine) - - for { - // FRAME\n - if _, err := c.rd.Discard(6); err != nil { - return err - } - - frame := make([]byte, size) - if _, err := io.ReadFull(c.rd, frame); err != nil { - return err - } - - c.Recv += size - - if len(c.Receivers) == 0 { - continue - } - - pkt := &rtp.Packet{ - Header: rtp.Header{Timestamp: core.Now90000()}, - Payload: frame, - } - c.Receivers[0].WriteRTP(pkt) - } -} - -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.cl.Close() -} +const frameHdr = "FRAME\n" func GetSize(fmtp string) int { w := core.Atoi(core.Between(fmtp, "width=", ";")) From 0bd2fcde542614b4b8fb88e1870160c03aee33eb Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 25 May 2024 13:52:55 +0300 Subject: [PATCH 079/130] Update color index func for ascii stream --- pkg/ascii/ascii.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/ascii/ascii.go b/pkg/ascii/ascii.go index c58eca92..6636e278 100644 --- a/pkg/ascii/ascii.go +++ b/pkg/ascii/ascii.go @@ -156,7 +156,7 @@ const x256b = "\x00\x00\x00\x00\x80\x80\x80\xc0\x80\x00\x00\x00\xff\xff\xff\xff\ func xterm256color(r, g, b uint8, n int) (index uint8) { best := uint16(0xFFFF) for i := 0; i < n; i++ { - diff := uint16(r-x256r[i]) + uint16(g-x256g[i]) + uint16(b-x256b[i]) + diff := sqDiff(r, x256r[i]) + sqDiff(g, x256g[i]) + sqDiff(b, x256b[i]) if diff < best { best = diff index = uint8(i) @@ -164,3 +164,10 @@ func xterm256color(r, g, b uint8, n int) (index uint8) { } return } + +// sqDiff - just like from image/color/color.go +func sqDiff(x, y uint8) uint16 { + d := uint16(x - y) + //return d + return (d * d) >> 2 +} From 268629f551e43387a0ebaa8a1e6c63cbc00efa1e Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 25 May 2024 16:47:57 +0300 Subject: [PATCH 080/130] Fix pix_fmt for publishing to RTMP servers --- README.md | 19 ++++--- pkg/bits/reader.go | 4 ++ pkg/flv/muxer.go | 2 + pkg/h264/sps.go | 135 ++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 151 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0ec1f0d0..c31ed748 100644 --- a/README.md +++ b/README.md @@ -779,7 +779,7 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important: - Supported codecs: H264 for video and AAC for audio -- Pixel format should be `yuv420p`, for cameras with `yuvj420p` format you SHOULD use [transcoding](#source-ffmpeg) +- AAC audio is required for YouTube, videos without audio will not work - You don't need to enable [RTMP module](#module-rtmp) listening for this task You can use API: @@ -792,16 +792,19 @@ Or config file: ```yaml publish: - # publish stream "tplink_tapo" to Telegram - tplink_tapo: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx - # publish stream "other_camera" to Telegram and YouTube - other_camera: + # publish stream "video_audio_transcode" to Telegram + video_audio_transcode: - rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx - - rtmps://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx + # publish stream "audio_transcode" to Telegram and YouTube + audio_transcode: + - rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx + - rtmp://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx streams: - # for TP-Link cameras it's important to use transcoding because of wrong pixel format - tplink_tapo: ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware#audio=aac + video_audio_transcode: + - ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware#audio=aac + audio_transcode: + - ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=copy#audio=aac ``` - **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming. diff --git a/pkg/bits/reader.go b/pkg/bits/reader.go index 10ea2253..31ea9ef8 100644 --- a/pkg/bits/reader.go +++ b/pkg/bits/reader.go @@ -131,3 +131,7 @@ func (r *Reader) ReadSEGolomb() int32 { func (r *Reader) Left() []byte { return r.buf[r.pos:] } + +func (r *Reader) Pos() (int, byte) { + return r.pos - 1, r.bits +} diff --git a/pkg/flv/muxer.go b/pkg/flv/muxer.go index 499f3aa0..98794265 100644 --- a/pkg/flv/muxer.go +++ b/pkg/flv/muxer.go @@ -54,6 +54,8 @@ func (m *Muxer) GetInit() []byte { sps, pps := h264.GetParameterSet(codec.FmtpLine) if len(sps) == 0 { sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2} + } else { + h264.FixPixFmt(sps) } if len(pps) == 0 { pps = []byte{0x68, 0xce, 0x38, 0x80} diff --git a/pkg/h264/sps.go b/pkg/h264/sps.go index 71ab5b45..6bcca669 100644 --- a/pkg/h264/sps.go +++ b/pkg/h264/sps.go @@ -1,6 +1,10 @@ package h264 -import "github.com/AlexxIT/go2rtc/pkg/bits" +import ( + "fmt" + + "github.com/AlexxIT/go2rtc/pkg/bits" +) // http://www.itu.int/rec/T-REC-H.264 // https://webrtc.googlesource.com/src/+/refs/heads/main/common_video/h264/sps_parser.cc @@ -229,3 +233,132 @@ func (s *SPS) scaling_list(r *bits.Reader, sizeOfScalingList int) { } } } + +func (s *SPS) Profile() string { + switch s.profile_idc { + case 0x42: + return "Baseline" + case 0x4D: + return "Main" + case 0x58: + return "Extended" + case 0x64: + return "High" + } + return fmt.Sprintf("0x%02X", s.profile_idc) +} + +func (s *SPS) PixFmt() string { + if s.bit_depth_luma_minus8 == 0 { + switch s.chroma_format_idc { + case 1: + if s.video_full_range_flag == 1 { + return "yuvj420p" + } + return "yuv420p" + case 2: + return "yuv422p" + case 3: + return "yuv444p" + } + } + return "" +} + +func (s *SPS) String() string { + return fmt.Sprintf( + "%s %d.%d, %s, %dx%d", + s.Profile(), s.level_idc/10, s.level_idc%10, s.PixFmt(), s.Width(), s.Height(), + ) +} + +// FixPixFmt - change yuvj420p to yuv420p in SPS +// same as "-c:v copy -bsf:v h264_metadata=video_full_range_flag=0" +func FixPixFmt(sps []byte) { + r := bits.NewReader(sps) + + _ = r.ReadByte() + + profile := r.ReadByte() + _ = r.ReadByte() + _ = r.ReadByte() + _ = r.ReadUEGolomb() + + switch profile { + case 100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135: + n := byte(8) + + if r.ReadUEGolomb() == 3 { + _ = r.ReadBit() + n = 12 + } + + _ = r.ReadUEGolomb() + _ = r.ReadUEGolomb() + _ = r.ReadBit() + + if r.ReadBit() != 0 { + for i := byte(0); i < n; i++ { + if r.ReadBit() != 0 { + return // skip + } + } + } + } + + _ = r.ReadUEGolomb() + + switch r.ReadUEGolomb() { + case 0: + _ = r.ReadUEGolomb() + case 1: + _ = r.ReadBit() + _ = r.ReadSEGolomb() + _ = r.ReadSEGolomb() + + n := r.ReadUEGolomb() + for i := uint32(0); i < n; i++ { + _ = r.ReadSEGolomb() + } + } + + _ = r.ReadUEGolomb() + _ = r.ReadBit() + + _ = r.ReadUEGolomb() + _ = r.ReadUEGolomb() + + if r.ReadBit() == 0 { + _ = r.ReadBit() + } + + _ = r.ReadBit() + + if r.ReadBit() != 0 { + _ = r.ReadUEGolomb() + _ = r.ReadUEGolomb() + _ = r.ReadUEGolomb() + _ = r.ReadUEGolomb() + } + + if r.ReadBit() != 0 { + if r.ReadBit() != 0 { + if r.ReadByte() == 255 { + _ = r.ReadUint16() + _ = r.ReadUint16() + } + } + + if r.ReadBit() != 0 { + _ = r.ReadBit() + } + + if r.ReadBit() != 0 { + _ = r.ReadBits8(3) + if r.ReadBit() == 1 { + pos, bit := r.Pos() + sps[pos] &= ^byte(1 << bit) + } + } + } +} From 8bae4631d2af771a16897d720c8dc71d06d0373e Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 26 May 2024 00:17:31 +0300 Subject: [PATCH 081/130] Fix support some RTSP servers --- pkg/flv/amf/amf_test.go | 64 +++++++++++++++++++++++++++++++++++++++++ pkg/rtmp/conn.go | 8 ++++-- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/pkg/flv/amf/amf_test.go b/pkg/flv/amf/amf_test.go index f22308e6..81e506d8 100644 --- a/pkg/flv/amf/amf_test.go +++ b/pkg/flv/amf/amf_test.go @@ -137,6 +137,70 @@ func TestNewReader(t *testing.T) { }, }, }, + { + name: "mediamtx", + actual: "0200075f726573756c74003ff0000000000000030006666d7356657202000d4c4e5820392c302c3132342c32000c6361706162696c697469657300403f0000000000000000090300056c6576656c0200067374617475730004636f646502001d4e6574436f6e6e656374696f6e2e436f6e6e6563742e53756363657373000b6465736372697074696f6e020015436f6e6e656374696f6e207375636365656465642e000e6f626a656374456e636f64696e67000000000000000000000009", + expect: []any{ + "_result", float64(1), map[string]any{ + "capabilities": float64(31), + "fmsVer": "LNX 9,0,124,2", + }, map[string]any{ + "code": "NetConnection.Connect.Success", + "description": "Connection succeeded.", + "level": "status", + "objectEncoding": float64(0), + }, + }, + }, + { + name: "mediamtx", + actual: "0200075f726573756c7400401000000000000005003ff0000000000000", + expect: []any{"_result", float64(4), any(nil), float64(1)}, + }, + { + name: "mediamtx", + actual: "0200086f6e537461747573004014000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e506c61792e5265736574000b6465736372697074696f6e02000a706c6179207265736574000009", + expect: []any{ + "onStatus", float64(5), any(nil), map[string]any{ + "code": "NetStream.Play.Reset", + "description": "play reset", + "level": "status", + }, + }, + }, + { + name: "mediamtx", + actual: "0200086f6e537461747573004014000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e506c61792e5374617274000b6465736372697074696f6e02000a706c6179207374617274000009", + expect: []any{ + "onStatus", float64(5), any(nil), map[string]any{ + "code": "NetStream.Play.Start", + "description": "play start", + "level": "status", + }, + }, + }, + { + name: "mediamtx", + actual: "0200086f6e537461747573004014000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e446174612e5374617274000b6465736372697074696f6e02000a64617461207374617274000009", + expect: []any{ + "onStatus", float64(5), any(nil), map[string]any{ + "code": "NetStream.Data.Start", + "description": "data start", + "level": "status", + }, + }, + }, + { + name: "mediamtx", + actual: "0200086f6e537461747573004014000000000000050300056c6576656c0200067374617475730004636f646502001c4e657453747265616d2e506c61792e5075626c6973684e6f74696679000b6465736372697074696f6e02000e7075626c697368206e6f74696679000009", + expect: []any{ + "onStatus", float64(5), any(nil), map[string]any{ + "code": "NetStream.Play.PublishNotify", + "description": "publish notify", + "level": "status", + }, + }, + }, { name: "obs-connect", actual: "020007636f6e6e656374003ff000000000000003000361707002000c617070312f73747265616d3100047479706502000a6e6f6e70726976617465000e737570706f727473476f4177617901010008666c61736856657202001f464d4c452f332e302028636f6d70617469626c653b20464d53632f312e3029000673776655726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d310005746355726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d31000009", diff --git a/pkg/rtmp/conn.go b/pkg/rtmp/conn.go index f81b1dec..2c7c3dda 100644 --- a/pkg/rtmp/conn.go +++ b/pkg/rtmp/conn.go @@ -52,13 +52,14 @@ func (c *Conn) readResponse(transID float64) ([]any, error) { if err != nil { return nil, err } + //log.Printf("[rtmp] type=%d data=%s", msgType, b) switch msgType { case TypeSetPacketSize: c.rdPacketSize = binary.BigEndian.Uint32(b) case TypeCommand: items, _ := amf.NewReader(b).ReadItems() - if len(items) >= 3 && items[1] == transID { + if len(items) >= 3 && (items[1] == transID || items[1] == float64(0)) { return items, nil } } @@ -288,7 +289,7 @@ func (c *Conn) writePublish() error { return err } - v, err := c.readResponse(0) + v, err := c.readResponse(5) if err != nil { return nil } @@ -307,7 +308,8 @@ func (c *Conn) writePlay() error { return err } - v, err := c.readResponse(0) + // Reolink response with ID=0, other software respose with ID=5 + v, err := c.readResponse(5) if err != nil { return nil } From 0ccfcb0ec027824746e93b430949faaa12d2b258 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 26 May 2024 00:18:56 +0300 Subject: [PATCH 082/130] Fix timestamps for RTMP client --- pkg/rtmp/README.md | 3 +- pkg/rtmp/client.go | 2 +- pkg/rtmp/conn.go | 130 +++++++++++++++++++++++++-------------------- pkg/rtmp/server.go | 2 +- 4 files changed, 75 insertions(+), 62 deletions(-) diff --git a/pkg/rtmp/README.md b/pkg/rtmp/README.md index 11382210..4196d570 100644 --- a/pkg/rtmp/README.md +++ b/pkg/rtmp/README.md @@ -16,4 +16,5 @@ response []interface {}{"onStatus", 0, interface {}(nil), map[string]interface { - https://en.wikipedia.org/wiki/Flash_Video - https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol -- https://rtmp.veriskope.com/pdf/rtmp_specification_1.0.pdf \ No newline at end of file +- https://rtmp.veriskope.com/pdf/rtmp_specification_1.0.pdf +- https://rtmp.veriskope.com/docs/spec/ diff --git a/pkg/rtmp/client.go b/pkg/rtmp/client.go index 00544d5b..aff8e23c 100644 --- a/pkg/rtmp/client.go +++ b/pkg/rtmp/client.go @@ -65,7 +65,7 @@ func NewClient(conn net.Conn, u *url.URL) (*Conn, error) { rd: bufio.NewReaderSize(conn, core.BufferSize), wr: conn, - chunks: map[uint8]*header{}, + chunks: map[uint8]*chunk{}, rdPacketSize: 128, wrPacketSize: 4096, // OBS - 4096, Reolink - 4096 diff --git a/pkg/rtmp/conn.go b/pkg/rtmp/conn.go index 2c7c3dda..2083a148 100644 --- a/pkg/rtmp/conn.go +++ b/pkg/rtmp/conn.go @@ -29,7 +29,7 @@ type Conn struct { rdPacketSize uint32 wrPacketSize uint32 - chunks map[byte]*header + chunks map[byte]*chunk streamID byte url string @@ -66,11 +66,59 @@ func (c *Conn) readResponse(transID float64) ([]any, error) { } } -type header struct { - timeMS uint32 +type chunk struct { + conn *Conn + rawTime uint32 dataSize uint32 tagType byte streamID uint32 + timeMS uint32 +} + +func (c *chunk) readHeader(typ byte) error { + switch typ { + case 0: // 12 byte header (full header) + b, err := c.conn.readSize(11) + if err != nil { + return err + } + c.rawTime = Uint24(b) + c.dataSize = Uint24(b[3:]) + c.tagType = b[6] + c.streamID = binary.LittleEndian.Uint32(b[7:]) + c.timeMS = c.readExtendedTime() + + case 1: // 8 bytes - like type b00, not including message ID (4 last bytes) + b, err := c.conn.readSize(7) + if err != nil { + return err + } + c.rawTime = Uint24(b) + c.dataSize = Uint24(b[3:]) // msgdatalen + c.tagType = b[6] // msgtypeid + c.timeMS += c.readExtendedTime() + + case 2: // 4 bytes - Basic Header and timestamp (3 bytes) are included + b, err := c.conn.readSize(3) + if err != nil { + return err + } + c.rawTime = Uint24(b) // timestamp + c.timeMS += c.readExtendedTime() + + case 3: // 1 byte - only the Basic Header is included + // use here hdr from previous msg with same session ID (sid) + } + return nil +} + +func (c *chunk) readExtendedTime() uint32 { + if c.rawTime == 0xFFFFFF { + if b, err := c.conn.readSize(4); err == nil { + return binary.BigEndian.Uint32(b) + } + } + return c.rawTime } //var ErrNotImplemented = errors.New("rtmp: not implemented") @@ -85,93 +133,57 @@ func (c *Conn) readMessage() (byte, uint32, []byte, error) { chunkID := b[0] & 0b111111 // storing header information for support header type 3 - hdr, ok := c.chunks[chunkID] + ch, ok := c.chunks[chunkID] if !ok { - hdr = &header{} - c.chunks[chunkID] = hdr + ch = &chunk{conn: c} + c.chunks[chunkID] = ch } - switch hdrType { - case 0: // 12 byte header (full header) - if b, err = c.readSize(11); err != nil { - return 0, 0, nil, err - } - _ = b[7] - hdr.timeMS = Uint24(b) - hdr.dataSize = Uint24(b[3:]) - hdr.tagType = b[6] - hdr.streamID = binary.LittleEndian.Uint32(b[7:]) - - case 1: // 8 bytes - like type b00, not including message ID (4 last bytes) - if b, err = c.readSize(7); err != nil { - return 0, 0, nil, err - } - _ = b[6] - hdr.timeMS = Uint24(b) // timestamp - hdr.dataSize = Uint24(b[3:]) // msgdatalen - hdr.tagType = b[6] // msgtypeid - - case 2: // 4 bytes - Basic Header and timestamp (3 bytes) are included - if b, err = c.readSize(3); err != nil { - return 0, 0, nil, err - } - hdr.timeMS = Uint24(b) // timestamp - - case 3: // 1 byte - only the Basic Header is included - // use here hdr from previous msg with same session ID (sid) + if err = ch.readHeader(hdrType); err != nil { + return 0, 0, nil, err } - timeMS := hdr.timeMS - if timeMS == 0xFFFFFF { - if b, err = c.readSize(4); err != nil { - return 0, 0, nil, err - } - timeMS = binary.BigEndian.Uint32(b) - } - - //log.Printf("[rtmp] hdr=%d chunkID=%d timeMS=%d size=%d tagType=%d streamID=%d", hdrType, chunkID, hdr.timeMS, hdr.dataSize, hdr.tagType, hdr.streamID) + //log.Printf("[rtmp] hdr=%d chunkID=%d timeMS=%d size=%d tagType=%d streamID=%d", hdrType, chunkID, ch.timeMS, ch.dataSize, ch.tagType, ch.streamID) // 1. Response zero size - if hdr.dataSize == 0 { - return hdr.tagType, timeMS, nil, nil + if ch.dataSize == 0 { + return ch.tagType, ch.timeMS, nil, nil } - b = make([]byte, hdr.dataSize) + data := make([]byte, ch.dataSize) // 2. Response small packet - if hdr.dataSize <= c.rdPacketSize { - if _, err = io.ReadFull(c.rd, b); err != nil { + if ch.dataSize <= c.rdPacketSize { + if _, err = io.ReadFull(c.rd, data); err != nil { return 0, 0, nil, err } - return hdr.tagType, timeMS, b, nil + return ch.tagType, ch.timeMS, data, nil } // 3. Response big packet var i0 uint32 - for i1 := c.rdPacketSize; i1 < hdr.dataSize; i1 += c.rdPacketSize { - if _, err = io.ReadFull(c.rd, b[i0:i1]); err != nil { + for i1 := c.rdPacketSize; i1 < ch.dataSize; i1 += c.rdPacketSize { + if _, err = io.ReadFull(c.rd, data[i0:i1]); err != nil { return 0, 0, nil, err } + // hopefully this will be hdrType=3 with same chunkID if _, err = c.readSize(1); err != nil { return 0, 0, nil, err } - if hdr.timeMS == 0xFFFFFF { - if _, err = c.readSize(4); err != nil { - return 0, 0, nil, err - } - } + _ = ch.readExtendedTime() i0 = i1 } - if _, err = io.ReadFull(c.rd, b[i0:]); err != nil { + if _, err = io.ReadFull(c.rd, data[i0:]); err != nil { return 0, 0, nil, err } - return hdr.tagType, timeMS, b, nil + return ch.tagType, ch.timeMS, data, nil } + func (c *Conn) writeMessage(chunkID, tagType byte, timeMS uint32, payload []byte) error { c.mu.Lock() c.resetBuffer() @@ -324,7 +336,7 @@ func (c *Conn) writePlay() error { func (c *Conn) readSize(n uint32) ([]byte, error) { b := make([]byte, n) - if _, err := io.ReadAtLeast(c.rd, b, int(n)); err != nil { + if _, err := io.ReadFull(c.rd, b); err != nil { return nil, err } return b, nil diff --git a/pkg/rtmp/server.go b/pkg/rtmp/server.go index f5fc96f8..ed727b98 100644 --- a/pkg/rtmp/server.go +++ b/pkg/rtmp/server.go @@ -17,7 +17,7 @@ func NewServer(conn net.Conn) (*Conn, error) { rd: bufio.NewReaderSize(conn, core.BufferSize), wr: conn, - chunks: map[uint8]*header{}, + chunks: map[uint8]*chunk{}, rdPacketSize: 128, wrPacketSize: 4096, From 8e571a66e3b86b0c6a52b0d0f4eba3278b491b6f Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 26 May 2024 00:19:26 +0300 Subject: [PATCH 083/130] Code refactoring for debug packet logger --- pkg/debug/debug.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/debug/debug.go b/pkg/debug/debug.go index 918cddfb..ff6ccce2 100644 --- a/pkg/debug/debug.go +++ b/pkg/debug/debug.go @@ -24,7 +24,7 @@ func Logger(include func(packet *rtp.Packet) bool) func(packet *rtp.Packet) { now := time.Now() fmt.Printf( - "%s: size:%6d, ts:%10d, type:%2d, ssrc:%d, seq:%5d, mark:%t, dts:%4d, dtime:%3d\n", + "%s: size=%6d ts=%10d type=%2d ssrc=%d seq=%5d mark=%t dts=%4d dtime=%3dms\n", now.Format("15:04:05.000"), len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker, packet.Timestamp-lastTS, now.Sub(lastTime).Milliseconds(), @@ -41,7 +41,7 @@ func Logger(include func(packet *rtp.Packet) bool) func(packet *rtp.Packet) { if dt := now.Sub(secTime); dt > time.Second { fmt.Printf( - "%s: size:%6d, cnt:%d, dts: %d, dtime:%d\n", + "%s: size=%6d cnt=%d dts=%d dtime=%3dms\n", now.Format("15:04:05.000"), secSize, secCnt, lastTS-secTS, dt.Milliseconds(), ) From 4534b4d8ca8288a66996a5ed4c24ee0305f282ba Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 26 May 2024 21:28:34 +0300 Subject: [PATCH 084/130] Add more log customization options --- internal/app/app.go | 10 +++++++- internal/app/log.go | 59 +++++++++++++++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 090c023a..3ccb5f44 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -13,6 +13,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/shell" "github.com/AlexxIT/go2rtc/pkg/yaml" + "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) @@ -117,9 +118,16 @@ func Init() { Mod map[string]string `yaml:"log"` } + cfg.Mod = map[string]string{ + "format": "color", + "level": zerolog.LevelInfoValue, + "output": "stdout", // TODO: change to stderr someday + "time": zerolog.TimeFormatUnixMs, + } + LoadConfig(&cfg) - log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"]) + log.Logger = NewLogger(cfg.Mod) modules = cfg.Mod diff --git a/internal/app/log.go b/internal/app/log.go index e8d4bc88..c32de685 100644 --- a/internal/app/log.go +++ b/internal/app/log.go @@ -8,29 +8,58 @@ import ( "github.com/rs/zerolog/log" ) -var MemoryLog *circularBuffer +var MemoryLog = newBuffer(16) -func NewLogger(format string, level string) zerolog.Logger { - var writer io.Writer = os.Stdout +func NewLogger(config map[string]string) zerolog.Logger { + var writer io.Writer - if format != "json" { - writer = zerolog.ConsoleWriter{ - Out: writer, TimeFormat: "15:04:05.000", NoColor: format == "text", + // support output only to memory + switch config["output"] { + case "stderr": + writer = os.Stderr + case "stdout": + writer = os.Stdout + } + + timeFormat := config["time"] + + if writer != nil { + switch format := config["format"]; format { + case "color", "text": + if timeFormat != "" { + writer = &zerolog.ConsoleWriter{ + Out: writer, + NoColor: format == "text", + TimeFormat: "15:04:05.000", + } + } else { + writer = &zerolog.ConsoleWriter{ + Out: writer, + NoColor: format == "text", + PartsOrder: []string{ + zerolog.LevelFieldName, + zerolog.CallerFieldName, + zerolog.MessageFieldName, + }, + } + } + case "json": // none } + + writer = zerolog.MultiLevelWriter(writer, MemoryLog) + } else { + writer = MemoryLog } - MemoryLog = newBuffer(16) + logger := zerolog.New(writer) - writer = zerolog.MultiLevelWriter(writer, MemoryLog) - - zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs - - lvl, err := zerolog.ParseLevel(level) - if err != nil || lvl == zerolog.NoLevel { - lvl = zerolog.InfoLevel + if timeFormat != "" { + zerolog.TimeFieldFormat = timeFormat + logger = logger.With().Timestamp().Logger() } - return zerolog.New(writer).With().Timestamp().Logger().Level(lvl) + lvl, _ := zerolog.ParseLevel(config["level"]) + return logger.Level(lvl) } func GetLogger(module string) zerolog.Logger { From 3932dbaa84afaf1ba8dd3ade5dcd5240c67c6db5 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 27 May 2024 20:23:55 +0300 Subject: [PATCH 085/130] Add print exec stderr to logs for debug level --- internal/exec/exec.go | 59 ++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/internal/exec/exec.go b/internal/exec/exec.go index 1e11cc68..454c54a4 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -4,7 +4,7 @@ import ( "crypto/md5" "encoding/hex" "errors" - "io" + "fmt" "net/url" "os" "os/exec" @@ -68,8 +68,9 @@ func execHandle(rawURL string) (core.Producer, error) { args := shell.QuoteSplit(rawURL[5:]) // remove `exec:` cmd := exec.Command(args[0], args[1:]...) - if log.Debug().Enabled() { - cmd.Stderr = os.Stderr + cmd.Stderr = &logWriter{ + buf: make([]byte, 512), + debug: log.Debug().Enabled(), } if path == "" { @@ -104,18 +105,10 @@ func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run pipe") - return prod, err + return prod, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr) } func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { - stderr := limitBuffer{buf: make([]byte, 512)} - - if cmd.Stderr != nil { - cmd.Stderr = io.MultiWriter(cmd.Stderr, &stderr) - } else { - cmd.Stderr = &stderr - } - if log.Trace().Enabled() { cmd.Stdout = os.Stdout } @@ -150,10 +143,10 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { case <-time.After(time.Second * 60): _ = cmd.Process.Kill() log.Error().Str("url", url).Msg("[exec] timeout") - return nil, errors.New("timeout") + return nil, errors.New("exec: timeout") case <-done: // limit message size - return nil, errors.New("exec: " + stderr.String()) + return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr) case prod := <-waiter: log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp") return prod, nil @@ -168,21 +161,47 @@ var ( waitersMu sync.Mutex ) -type limitBuffer struct { - buf []byte - n int +type logWriter struct { + buf []byte + debug bool + n int } -func (l *limitBuffer) String() string { +func (l *logWriter) String() string { if l.n == len(l.buf) { return string(l.buf) + "..." } return string(l.buf[:l.n]) } -func (l *limitBuffer) Write(p []byte) (int, error) { +func (l *logWriter) Write(p []byte) (n int, err error) { if l.n < cap(l.buf) { l.n += copy(l.buf[l.n:], p) } - return len(p), nil + n = len(p) + if l.debug { + if p = trimSpace(p); p != nil { + log.Debug().Msgf("[exec] %s", p) + } + } + return +} + +func trimSpace(b []byte) []byte { + start := 0 + stop := len(b) + for ; start < stop; start++ { + if b[start] >= ' ' { + break // trim all ASCII before 0x20 + } + } + for ; ; stop-- { + if stop == start { + return nil // skip empty output + } + if b[stop-1] > ' ' { + break // trim all ASCII before 0x21 + } + } + return b[start:stop] } From 8cb513cb89b4739ccf86bde5bff79264d71a539f Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 27 May 2024 20:24:24 +0300 Subject: [PATCH 086/130] Add log level for ffmpeg module --- internal/app/app.go | 2 +- internal/ffmpeg/ffmpeg.go | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 3ccb5f44..add11dd7 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -120,7 +120,7 @@ func Init() { cfg.Mod = map[string]string{ "format": "color", - "level": zerolog.LevelInfoValue, + "level": "info", "output": "stdout", // TODO: change to stderr someday "time": zerolog.TimeFormatUnixMs, } diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 2b24c3ce..aeba85fb 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -19,15 +19,22 @@ import ( func Init() { var cfg struct { Mod map[string]string `yaml:"ffmpeg"` + Log struct { + Level string `yaml:"ffmpeg"` + } `yaml:"log"` } cfg.Mod = defaults // will be overriden from yaml + cfg.Log.Level = "error" app.LoadConfig(&cfg) - if app.GetLogger("exec").GetLevel() >= 0 { - defaults["global"] += " -v error" + // zerolog levels: trace debug info warn error fatal panic disabled + // FFmpeg levels: trace debug verbose info warning error fatal panic quiet + if cfg.Log.Level == "warn" { + cfg.Log.Level = "warning" } + defaults["global"] += " -v " + cfg.Log.Level streams.RedirectFunc("ffmpeg", func(url string) (string, error) { if _, err := Version(); err != nil { From 649de0131cbaf64bd3d3f11cf575e0d630f785b3 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 27 May 2024 20:25:09 +0300 Subject: [PATCH 087/130] Change logs timestamp format in WebUI --- www/log.html | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/www/log.html b/www/log.html index e3aff5d5..84ec0675 100644 --- a/www/log.html +++ b/www/log.html @@ -56,7 +56,7 @@ - + @@ -98,11 +98,16 @@ lines = lines.reverse(); } return lines.map(line => { - const ts = new Date(line['time']); + const ts = new Date(line['time']).toLocaleString(undefined, { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + fractionalSecondDigits: 3 + }); const msg = Object.keys(line).reduce((msg, key) => { return KEYS.indexOf(key) < 0 ? `${msg} ${key}=${line[key]}` : msg; }, line['message']); - return ``; + return ``; }).join(''); } From 50ad3b20c4c7c3258d7b1c56db912e3afbd3c5d8 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 28 May 2024 09:08:57 +0300 Subject: [PATCH 088/130] Add config schema.json --- website/schema.json | 486 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 486 insertions(+) create mode 100644 website/schema.json diff --git a/website/schema.json b/website/schema.json new file mode 100644 index 00000000..d5e19436 --- /dev/null +++ b/website/schema.json @@ -0,0 +1,486 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "go2rtc", + "type": "object", + "additionalProperties": false, + "definitions": { + "listen": { + "type": "string", + "anyOf": [ + { + "type": "string", + "pattern": ":[0-9]{1,5}$" + }, + { + "type": "string", + "const": "" + } + ] + }, + "log_level": { + "type": "string", + "enum": [ + "trace", + "debug", + "info", + "warn", + "error" + ] + } + }, + "properties": { + "api": { + "type": "object", + "properties": { + "listen": { + "default": ":1984", + "examples": [ + "127.0.0.1:8080" + ], + "$ref": "#/definitions/listen" + }, + "username": { + "type": "string", + "examples": [ + "admin" + ] + }, + "password": { + "type": "string" + }, + "base_path": { + "type": "string", + "examples": [ + "/go2rtc" + ] + }, + "static_dir": { + "type": "string", + "examples": [ + "/var/www" + ] + }, + "origin": { + "type": "string", + "const": "*" + }, + "tls_listen": { + "$ref": "#/definitions/listen" + }, + "tls_cert": { + "type": "string", + "examples": [ + "-----BEGIN CERTIFICATE-----", + "/ssl/fullchain.pem" + ] + }, + "tls_key": { + "type": "string", + "examples": [ + "-----BEGIN PRIVATE KEY-----", + "/ssl/privkey.pem" + ] + }, + "unix_listen": { + "type": "string", + "examples": [ + "/tmp/go2rtc.sock" + ] + } + } + }, + "ffmpeg": { + "type": "object", + "properties": { + "bin": { + "type": "string", + "default": "ffmpeg" + } + }, + "additionalProperties": { + "description": "FFmpeg template", + "type": "string" + } + }, + "hass": { + "type": "object", + "properties": { + "config": { + "description": "Home Assistant config directory path", + "type": "string", + "examples": [ + "/config" + ] + } + } + }, + "homekit": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "pin": { + "type": "string", + "default": "19550224", + "pattern": "^[0-9]{8}$" + }, + "name": { + "type": "string" + }, + "device_id": { + "type": "string" + }, + "device_private": { + "type": "string" + }, + "pairings": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "log": { + "type": "object", + "properties": { + "format": { + "type": "string", + "default": "color", + "enum": [ + "color", + "json", + "text" + ] + }, + "level": { + "description": "Defaul log level", + "default": "info", + "$ref": "#/definitions/log_level" + }, + "output": { + "type": "string", + "default": "stdout", + "enum": [ + "", + "stdout", + "stderr" + ] + }, + "time": { + "type": "string", + "default": "UNIXMS", + "anyOf": [ + { + "type": "string", + "enum": [ + "", + "UNIXMS", + "UNIXMICRO", + "UNIXNANO", + "2006-01-02T15:04:05Z07:00", + "2006-01-02T15:04:05.999999999Z07:00" + ] + }, + { + "type": "string" + } + ] + }, + "api": { + "$ref": "#/definitions/log_level" + }, + "echo": { + "$ref": "#/definitions/log_level" + }, + "exec": { + "description": "Value `exec: debug` will print stderr", + "$ref": "#/definitions/log_level" + }, + "expr": { + "$ref": "#/definitions/log_level" + }, + "ffmpeg": { + "description": "Will only be displayed with `exec: debug` setting", + "default": "error", + "$ref": "#/definitions/log_level" + }, + "hass": { + "$ref": "#/definitions/log_level" + }, + "hls": { + "$ref": "#/definitions/log_level" + }, + "homekit": { + "$ref": "#/definitions/log_level" + }, + "mp4": { + "$ref": "#/definitions/log_level" + }, + "ngrok": { + "$ref": "#/definitions/log_level" + }, + "onvif": { + "$ref": "#/definitions/log_level" + }, + "rtmp": { + "$ref": "#/definitions/log_level" + }, + "rtsp": { + "$ref": "#/definitions/log_level" + }, + "streams": { + "$ref": "#/definitions/log_level" + }, + "webrtc": { + "$ref": "#/definitions/log_level" + }, + "webtorrent": { + "$ref": "#/definitions/log_level" + } + } + }, + "ngrok": { + "type": "object", + "properties": { + "command": { + "type": "string", + "examples": [ + "ngrok tcp 8555 --authtoken xxx", + "ngrok start --all --config ngrok.yaml" + ] + } + } + }, + "publish": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "examples": [ + "rtmp://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx", + "rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx" + ] + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "rtmp": { + "type": "object", + "properties": { + "listen": { + "examples": [ + ":1935" + ], + "$ref": "#/definitions/listen" + } + } + }, + "rtsp": { + "type": "object", + "properties": { + "listen": { + "default": ":8554", + "$ref": "#/definitions/listen" + }, + "username": { + "type": "string", + "examples": [ + "admin" + ] + }, + "password": { + "type": "string" + }, + "default_query": { + "type": "string", + "default": "video&audio" + }, + "pkt_size": { + "type": "integer" + } + } + }, + "srtp": { + "description": "SRTP server for HomeKit", + "type": "object", + "properties": { + "listen": { + "default": ":8443", + "$ref": "#/definitions/listen" + } + } + }, + "streams": { + "type": "object", + "additionalProperties": { + "title": "Stream", + "anyOf": [ + { + "description": "Source", + "type": "string", + "examples": [ + "rtsp://username:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif", + "rtsp://username:password@192.168.1.123/stream1", + "rtsp://username:password@192.168.1.123/h264Preview_01_main", + "rtmp://192.168.1.123/bcs/channel0_main.bcs?channel=0&stream=0&user=username&password=password", + "http://192.168.1.123/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password", + "http://username:password@192.168.1.123/cgi-bin/snapshot.cgi?channel=1", + "ffmpeg:media.mp4#video=h264#hadware#width=1920#height=1080#rotate=180#audio=copy", + "ffmpeg:virtual?video=testsrc&size=4K#video=h264#hardware#bitrate=50M", + "bubble://username:password@192.168.1.123:34567/bubble/live?ch=0&stream=0", + "dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0", + "exec:ffmpeg -re -i media.mp4 -c copy -rtsp_transport tcp -f rtsp {output}", + "isapi://username:password@192.168.1.123:80/", + "kasa://username:password@192.168.1.123:19443/https/stream/mixed", + "onvif://username:password@192.168.1.123:80?subtype=0", + "tapo://password@192.168.1.123:8800?channel=0&subtype=0", + "webtorrent:?share=xxx&pwd=xxx" + ] + }, + { + "type": "array", + "items": { + "description": "Source", + "type": "string" + } + } + ] + } + }, + "webrtc": { + "type": "object", + "properties": { + "listen": { + "default": ":8555/tcp", + "type": "string", + "anyOf": [ + { + "type": "string", + "pattern": ":[0-9]{1,5}(/tcp|/udp)?$" + }, + { + "type": "string", + "const": "" + } + ] + }, + "candidates": { + "type": "array", + "items": { + "$ref": "#/definitions/listen/anyOf/0" + }, + "examples": [ + "216.58.210.174:8555", + "stun:8555", + "home.duckdns.org:8555" + ] + }, + "ice_servers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "urls": { + "type": "array", + "items": { + "type": "string", + "examples": [ + "stun:stun.l.google.com:19302", + "turn:123.123.123.123:3478" + ] + } + }, + "username": { + "type": "string" + }, + "credential": { + "type": "string" + } + } + } + }, + "filters": { + "type": "object", + "properties": { + "candidates": { + "description": "Keep only these candidates", + "type": "array", + "items": { + "type": "string" + } + }, + "interfaces": { + "description": "Keep only these interfaces", + "type": "array", + "items": { + "type": "string" + } + }, + "ips": { + "description": "Keep only these IP-addresses", + "type": "array", + "items": { + "type": "string" + } + }, + "networks": { + "description": "Use only these network types", + "type": "array", + "items": { + "enum": [ + "tcp4", + "tcp6", + "udp4", + "udp6" + ], + "type": "string" + } + }, + "udp_ports": { + "description": "Use only these UDP ports range [min, max]", + "type": "array", + "items": { + "type": "integer" + }, + "maxItems": 2, + "minItems": 2 + } + } + } + } + }, + "webtorrent": { + "type": "object", + "properties": { + "trackers": { + "type": "array", + "items": { + "type": "string" + } + }, + "shares": { + "additionalProperties": { + "type": "object", + "properties": { + "pwd": { + "type": "string" + }, + "src": { + "type": "string" + } + } + } + } + } + } + } +} \ No newline at end of file From a79061c7c285d0d93b385c2b57c3adb3250a10dd Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Tue, 28 May 2024 09:10:32 +0300 Subject: [PATCH 089/130] feat(logging): add interactive shell detection for console output --- internal/app/log.go | 3 ++- pkg/shell/tty.go | 7 +++++++ pkg/shell/tty_unix.go | 10 ++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 pkg/shell/tty.go create mode 100644 pkg/shell/tty_unix.go diff --git a/internal/app/log.go b/internal/app/log.go index e8d4bc88..e656737c 100644 --- a/internal/app/log.go +++ b/internal/app/log.go @@ -4,6 +4,7 @@ import ( "io" "os" + "github.com/AlexxIT/go2rtc/pkg/shell" "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) @@ -15,7 +16,7 @@ func NewLogger(format string, level string) zerolog.Logger { if format != "json" { writer = zerolog.ConsoleWriter{ - Out: writer, TimeFormat: "15:04:05.000", NoColor: format == "text", + Out: writer, TimeFormat: "15:04:05.000", NoColor: (format == "text" || !shell.IsInteractive(os.Stdout.Fd())), } } diff --git a/pkg/shell/tty.go b/pkg/shell/tty.go new file mode 100644 index 00000000..d433369f --- /dev/null +++ b/pkg/shell/tty.go @@ -0,0 +1,7 @@ +//go:build !unix + +package shell + +func IsInteractive(fd uintptr) bool { + return false +} diff --git a/pkg/shell/tty_unix.go b/pkg/shell/tty_unix.go new file mode 100644 index 00000000..07b68a60 --- /dev/null +++ b/pkg/shell/tty_unix.go @@ -0,0 +1,10 @@ +//go:build unix + +package shell + +import "golang.org/x/sys/unix" + +func IsInteractive(fd uintptr) bool { + _, err := unix.IoctlGetTermios(int(fd), unix.TIOCGETA) + return err == nil +} From cc74504ed812df848e00fad26c6660ca3df1978c Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Tue, 28 May 2024 10:16:48 +0300 Subject: [PATCH 090/130] feat(shell): add Windows support for TTY detection --- pkg/shell/tty.go | 2 +- pkg/shell/tty_windows.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 pkg/shell/tty_windows.go diff --git a/pkg/shell/tty.go b/pkg/shell/tty.go index d433369f..f320130c 100644 --- a/pkg/shell/tty.go +++ b/pkg/shell/tty.go @@ -1,4 +1,4 @@ -//go:build !unix +//go:build !unix && !windows package shell diff --git a/pkg/shell/tty_windows.go b/pkg/shell/tty_windows.go new file mode 100644 index 00000000..6216e742 --- /dev/null +++ b/pkg/shell/tty_windows.go @@ -0,0 +1,19 @@ +//go:build windows + +package shell + +import ( + "syscall" + "unsafe" +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + procGetConsoleMode = kernel32.NewProc("GetConsoleMode") +) + +func IsInteractive(fd uintptr) bool { + var st uint32 + r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0) + return r != 0 && e == 0 +} From a6b9b4993f42fc3b61927d3723dfc934885095c1 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 28 May 2024 13:21:33 +0300 Subject: [PATCH 091/130] Code refactoring after #1141 --- internal/app/app.go | 2 +- internal/app/log.go | 45 ++++++++++++++++++++++++---------------- pkg/shell/tty.go | 7 ------- pkg/shell/tty_unix.go | 10 --------- pkg/shell/tty_windows.go | 19 ----------------- 5 files changed, 28 insertions(+), 55 deletions(-) delete mode 100644 pkg/shell/tty.go delete mode 100644 pkg/shell/tty_unix.go delete mode 100644 pkg/shell/tty_windows.go diff --git a/internal/app/app.go b/internal/app/app.go index add11dd7..9dec2848 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -119,7 +119,7 @@ func Init() { } cfg.Mod = map[string]string{ - "format": "color", + "format": "", // useless, but anyway "level": "info", "output": "stdout", // TODO: change to stderr someday "time": zerolog.TimeFormatUnixMs, diff --git a/internal/app/log.go b/internal/app/log.go index 65be3161..222f6f2b 100644 --- a/internal/app/log.go +++ b/internal/app/log.go @@ -4,17 +4,21 @@ import ( "io" "os" - "github.com/AlexxIT/go2rtc/pkg/shell" + "github.com/mattn/go-isatty" "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) var MemoryLog = newBuffer(16) +// NewLogger support: +// - output: empty (only to memory), stderr, stdout +// - format: empty (autodetect color support), color, json, text +// - time: empty (disable timestamp), UNIXMS, UNIXMICRO, UNIXNANO +// - level: disabled, trace, debug, info, warn, error... func NewLogger(config map[string]string) zerolog.Logger { var writer io.Writer - // support output only to memory switch config["output"] { case "stderr": writer = os.Stderr @@ -25,26 +29,31 @@ func NewLogger(config map[string]string) zerolog.Logger { timeFormat := config["time"] if writer != nil { - switch format := config["format"]; format { - case "color", "text": + if format := config["format"]; format != "json" { + console := &zerolog.ConsoleWriter{Out: writer} + + switch format { + case "text": + console.NoColor = true + case "color": + console.NoColor = false // useless, but anyway + default: + // autodetection if output support color + // go-isatty - dependency for go-colorable - dependency for ConsoleWriter + console.NoColor = !isatty.IsTerminal(writer.(*os.File).Fd()) + } + if timeFormat != "" { - writer = &zerolog.ConsoleWriter{ - Out: writer, - NoColor: format == "text" || !shell.IsInteractive(os.Stdout.Fd()), - TimeFormat: "15:04:05.000", - } + console.TimeFormat = "15:04:05.000" } else { - writer = &zerolog.ConsoleWriter{ - Out: writer, - NoColor: format == "text" || !shell.IsInteractive(os.Stdout.Fd()), - PartsOrder: []string{ - zerolog.LevelFieldName, - zerolog.CallerFieldName, - zerolog.MessageFieldName, - }, + console.PartsOrder = []string{ + zerolog.LevelFieldName, + zerolog.CallerFieldName, + zerolog.MessageFieldName, } } - case "json": // none + + writer = console } writer = zerolog.MultiLevelWriter(writer, MemoryLog) diff --git a/pkg/shell/tty.go b/pkg/shell/tty.go deleted file mode 100644 index f320130c..00000000 --- a/pkg/shell/tty.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !unix && !windows - -package shell - -func IsInteractive(fd uintptr) bool { - return false -} diff --git a/pkg/shell/tty_unix.go b/pkg/shell/tty_unix.go deleted file mode 100644 index 07b68a60..00000000 --- a/pkg/shell/tty_unix.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build unix - -package shell - -import "golang.org/x/sys/unix" - -func IsInteractive(fd uintptr) bool { - _, err := unix.IoctlGetTermios(int(fd), unix.TIOCGETA) - return err == nil -} diff --git a/pkg/shell/tty_windows.go b/pkg/shell/tty_windows.go deleted file mode 100644 index 6216e742..00000000 --- a/pkg/shell/tty_windows.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build windows - -package shell - -import ( - "syscall" - "unsafe" -) - -var ( - kernel32 = syscall.NewLazyDLL("kernel32.dll") - procGetConsoleMode = kernel32.NewProc("GetConsoleMode") -) - -func IsInteractive(fd uintptr) bool { - var st uint32 - r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0) - return r != 0 && e == 0 -} From ea17b420d68d470395ee3e5d98f3e189b098d9d2 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 28 May 2024 21:36:12 +0300 Subject: [PATCH 092/130] Fix two-way audio for webrtc client --- pkg/webrtc/consumer.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/webrtc/consumer.go b/pkg/webrtc/consumer.go index 9d96ef59..3bcaf49a 100644 --- a/pkg/webrtc/consumer.go +++ b/pkg/webrtc/consumer.go @@ -77,7 +77,13 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv sender.Handler = pcm.RepackG711(false, sender.Handler) } - sender.Bind(track) + // TODO: rewrite this dirty logic + // maybe not best solution, but ActiveProducer connected before AddTrack + if c.Mode != core.ModeActiveProducer { + sender.Bind(track) + } else { + sender.HandleRTP(track) + } c.senders = append(c.senders, sender) return nil From a9e7a73cc8e0a25d21226f00f606703feb83b932 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 28 May 2024 14:01:42 +0300 Subject: [PATCH 093/130] Add video bitrate setting for HomeKit source --- internal/homekit/homekit.go | 32 ++++++++++++++++++++++++++++++-- pkg/hap/camera/stream.go | 13 ++++++++++--- pkg/homekit/client.go | 4 +++- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/internal/homekit/homekit.go b/internal/homekit/homekit.go index bfe3e971..743aeab9 100644 --- a/internal/homekit/homekit.go +++ b/internal/homekit/homekit.go @@ -133,12 +133,19 @@ func Init() { var log zerolog.Logger var servers map[string]*server -func streamHandler(url string) (core.Producer, error) { +func streamHandler(rawURL string) (core.Producer, error) { if srtp.Server == nil { return nil, errors.New("homekit: can't work without SRTP server") } - return homekit.Dial(url, srtp.Server) + rawURL, rawQuery, _ := strings.Cut(rawURL, "#") + client, err := homekit.Dial(rawURL, srtp.Server) + if client != nil && rawQuery != "" { + query := streams.ParseQuery(rawQuery) + client.Bitrate = parseBitrate(query.Get("bitrate")) + } + + return client, err } func hapPairSetup(w http.ResponseWriter, r *http.Request) { @@ -199,3 +206,24 @@ func findHomeKitURL(stream *streams.Stream) string { return "" } + +func parseBitrate(s string) int { + n := len(s) + if n == 0 { + return 0 + } + + var k int + switch n--; s[n] { + case 'K': + k = 1024 + s = s[:n] + case 'M': + k = 1024 * 1024 + s = s[:n] + default: + k = 1 + } + + return k * core.Atoi(s) +} diff --git a/pkg/hap/camera/stream.go b/pkg/hap/camera/stream.go index b2ef0d9f..23d53c39 100644 --- a/pkg/hap/camera/stream.go +++ b/pkg/hap/camera/stream.go @@ -15,7 +15,8 @@ type Stream struct { } func NewStream( - client *hap.Client, videoCodec *VideoCodec, audioCodec *AudioCodec, videoSession, audioSession *srtp.Session, + client *hap.Client, videoCodec *VideoCodec, audioCodec *AudioCodec, + videoSession, audioSession *srtp.Session, bitrate int, ) (*Stream, error) { stream := &Stream{ id: core.RandString(16, 0), @@ -30,11 +31,17 @@ func NewStream( return nil, err } + if bitrate != 0 { + bitrate /= 1024 // convert bps to kbps + } else { + bitrate = 4096 // default kbps for general FullHD camera + } + videoCodec.RTPParams = []RTPParams{ { PayloadType: 99, SSRC: videoSession.Local.SSRC, - MaxBitrate: 299, + MaxBitrate: uint16(bitrate), // iPhone query 299Kbps, iPad/AppleTV query 802Kbps RTCPInterval: 0.5, MaxMTU: []uint16{1378}, }, @@ -43,7 +50,7 @@ func NewStream( { PayloadType: 110, SSRC: audioSession.Local.SSRC, - MaxBitrate: 24, + MaxBitrate: 24, // any iDevice query 24Kbps (this is OK for 16KHz and 1 channel) RTCPInterval: 5, ComfortNoisePayloadType: []uint8{13}, diff --git a/pkg/homekit/client.go b/pkg/homekit/client.go index c61acea6..133499d3 100644 --- a/pkg/homekit/client.go +++ b/pkg/homekit/client.go @@ -28,6 +28,8 @@ type Client struct { audioSession *srtp.Session stream *camera.Stream + + Bitrate int // in bits/s } func Dial(rawURL string, server *srtp.Server) (*Client, error) { @@ -132,7 +134,7 @@ func (c *Client) Start() error { c.audioSession = &srtp.Session{Local: c.srtpEndpoint()} var err error - c.stream, err = camera.NewStream(c.hap, videoCodec, audioCodec, c.videoSession, c.audioSession) + c.stream, err = camera.NewStream(c.hap, videoCodec, audioCodec, c.videoSession, c.audioSession, c.Bitrate) if err != nil { return err } From 2ab1d9d774265c050383382fbc6187d6b2789ad6 Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 29 May 2024 17:32:11 +0300 Subject: [PATCH 094/130] Add handling if mp4 client drops connection --- internal/mp4/mp4.go | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/internal/mp4/mp4.go b/internal/mp4/mp4.go index 78708a35..2f59ba04 100644 --- a/internal/mp4/mp4.go +++ b/internal/mp4/mp4.go @@ -1,6 +1,7 @@ package mp4 import ( + "context" "net/http" "strconv" "strings" @@ -127,20 +128,20 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { header.Set("Content-Disposition", `attachment; filename="`+filename+`"`) } - var duration *time.Timer - if s := query.Get("duration"); s != "" { - if i, _ := strconv.Atoi(s); i > 0 { - duration = time.AfterFunc(time.Second*time.Duration(i), func() { - _ = cons.Stop() - }) - } + ctx := r.Context() // handle when the client drops the connection + + if i := core.Atoi(query.Get("duration")); i > 0 { + timeout := time.Second * time.Duration(i) + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() } + go func() { + <-ctx.Done() + _ = cons.Stop() + stream.RemoveConsumer(cons) + }() + _, _ = cons.WriteTo(w) - - stream.RemoveConsumer(cons) - - if duration != nil { - duration.Stop() - } } From aa86c1ec25c63af9f7e7250635b3f73e2d9bcb64 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Thu, 30 May 2024 11:27:29 +0300 Subject: [PATCH 095/130] ci(workflow): add GitHub Container Registry login and update image paths --- .github/workflows/build.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 188727d6..cec5f4a3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -123,7 +123,9 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: ${{ github.repository }} + images: | + ${{ github.repository }} + ghcr.io/${{ github.repository }} tags: | type=ref,event=branch type=semver,pattern={{version}},enable=false @@ -142,6 +144,13 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push uses: docker/build-push-action@v5 with: @@ -168,7 +177,9 @@ jobs: id: meta-hw uses: docker/metadata-action@v5 with: - images: ${{ github.repository }} + images: | + ${{ github.repository }} + ghcr.io/${{ github.repository }} flavor: | suffix=-hardware,onlatest=true latest=auto @@ -189,6 +200,14 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 From 79245eeff4ed093d2ed734af7a14b3f8f450ad41 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Thu, 30 May 2024 11:48:15 +0300 Subject: [PATCH 096/130] fix(ci): skip GitHub Container Registry login on pull requests --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cec5f4a3..f32c2333 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -145,6 +145,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry + if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ghcr.io From df1d44d24eb335c4b6190feca6fc073e634a16ca Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Thu, 30 May 2024 17:12:56 +0300 Subject: [PATCH 097/130] chore(deps): update Go version to 1.22 across project files --- .github/workflows/build.yml | 2 +- .github/workflows/test.yml | 2 +- Dockerfile | 2 +- go.mod | 2 +- hardware.Dockerfile | 2 +- pkg/homekit/consumer.go | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 188727d6..4811b59d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 - with: { go-version: '1.21' } + with: { go-version: '1.22' } - name: Build go2rtc_win64 env: { GOOS: windows, GOARCH: amd64 } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b23faf53..dc47bdb5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.21' + go-version: '1.22' - name: Build Go binary run: go build -ldflags "-s -w" -trimpath -o ./go2rtc diff --git a/Dockerfile b/Dockerfile index 46b85d30..b3888820 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # 0. Prepare images ARG PYTHON_VERSION="3.11" -ARG GO_VERSION="1.21" +ARG GO_VERSION="1.22" ARG NGROK_VERSION="3" FROM python:${PYTHON_VERSION}-alpine AS base diff --git a/go.mod b/go.mod index b1ba4b4c..0d5d67c9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/AlexxIT/go2rtc -go 1.21 +go 1.22 require ( github.com/asticode/go-astits v1.13.0 diff --git a/hardware.Dockerfile b/hardware.Dockerfile index 0aa85374..2254f9be 100644 --- a/hardware.Dockerfile +++ b/hardware.Dockerfile @@ -4,7 +4,7 @@ # only debian 13 (trixie) has latest ffmpeg # https://packages.debian.org/trixie/ffmpeg ARG DEBIAN_VERSION="trixie-slim" -ARG GO_VERSION="1.21-bookworm" +ARG GO_VERSION="1.22-bookworm" ARG NGROK_VERSION="3" FROM debian:${DEBIAN_VERSION} AS base diff --git a/pkg/homekit/consumer.go b/pkg/homekit/consumer.go index 1e04fedf..05ea2427 100644 --- a/pkg/homekit/consumer.go +++ b/pkg/homekit/consumer.go @@ -3,7 +3,7 @@ package homekit import ( "fmt" "io" - "math/rand" + "math/rand/v2" "net" "time" From 756be9801e362379752799a1fda9171547d38f20 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 1 Jun 2024 19:18:26 +0300 Subject: [PATCH 098/130] Code refactoring for app module --- internal/app/app.go | 155 ++++----------------------- internal/app/config.go | 109 +++++++++++++++++++ internal/app/log.go | 58 ++++++---- internal/app/migrate.go | 35 ------ internal/dvrip/dvrip.go | 6 +- internal/ffmpeg/ffmpeg.go | 5 + internal/ffmpeg/hardware/hardware.go | 3 - internal/ffmpeg/version.go | 1 - internal/mjpeg/init.go | 7 +- internal/mpegts/aac.go | 2 - internal/mpegts/mpegts.go | 2 - main.go | 2 + pkg/opus/{opus.go => .opus.go} | 1 - pkg/roborock/client.go | 5 +- pkg/roborock/iot/client.go | 8 +- 15 files changed, 182 insertions(+), 217 deletions(-) create mode 100644 internal/app/config.go delete mode 100644 internal/app/migrate.go rename pkg/opus/{opus.go => .opus.go} (97%) diff --git a/internal/app/app.go b/internal/app/app.go index 9dec2848..9331f041 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1,29 +1,20 @@ package app import ( - "errors" "flag" "fmt" "os" "os/exec" - "path/filepath" "runtime" "runtime/debug" - "strings" - - "github.com/AlexxIT/go2rtc/pkg/shell" - "github.com/AlexxIT/go2rtc/pkg/yaml" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" ) -var Version = "1.9.2" -var UserAgent = "go2rtc/" + Version - -var ConfigPath string -var Info = map[string]any{ - "version": Version, -} +var ( + Version string + UserAgent string + ConfigPath string + Info = make(map[string]any) +) const usage = `Usage of go2rtc: @@ -33,12 +24,12 @@ const usage = `Usage of go2rtc: ` func Init() { - var confs Config + var config flagConfig var daemon bool var version bool - flag.Var(&confs, "config", "") - flag.Var(&confs, "c", "") + flag.Var(&config, "config", "") + flag.Var(&config, "c", "") flag.BoolVar(&daemon, "daemon", false, "") flag.BoolVar(&daemon, "d", false, "") flag.BoolVar(&version, "version", false, "") @@ -69,118 +60,30 @@ func Init() { // Re-run the program in background and exit cmd := exec.Command(os.Args[0], args...) if err := cmd.Start(); err != nil { - log.Fatal().Err(err).Send() + fmt.Println(err) + os.Exit(1) } fmt.Println("Running in daemon mode with PID:", cmd.Process.Pid) os.Exit(0) } - if confs == nil { - confs = []string{"go2rtc.yaml"} - } - - for _, conf := range confs { - if len(conf) == 0 { - continue - } - if conf[0] == '{' { - // config as raw YAML or JSON - configs = append(configs, []byte(conf)) - } else if data := parseConfString(conf); data != nil { - configs = append(configs, data) - } else { - // config as file - if ConfigPath == "" { - ConfigPath = conf - } - - if data, _ = os.ReadFile(conf); data == nil { - continue - } - - data = []byte(shell.ReplaceEnvVars(string(data))) - configs = append(configs, data) - } - } - - if ConfigPath != "" { - if !filepath.IsAbs(ConfigPath) { - if cwd, err := os.Getwd(); err == nil { - ConfigPath = filepath.Join(cwd, ConfigPath) - } - } - Info["config_path"] = ConfigPath - } + UserAgent = "go2rtc/" + Version + Info["version"] = Version Info["revision"] = revision - var cfg struct { - Mod map[string]string `yaml:"log"` - } - - cfg.Mod = map[string]string{ - "format": "", // useless, but anyway - "level": "info", - "output": "stdout", // TODO: change to stderr someday - "time": zerolog.TimeFormatUnixMs, - } - - LoadConfig(&cfg) - - log.Logger = NewLogger(cfg.Mod) - - modules = cfg.Mod + initConfig(config) + initLogger() platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) - log.Info().Str("version", Version).Str("platform", platform).Str("revision", revision).Msg("go2rtc") - log.Debug().Str("version", runtime.Version()).Str("vcs.time", vcsTime).Msg("build") + Logger.Info().Str("version", Version).Str("platform", platform).Str("revision", revision).Msg("go2rtc") + Logger.Debug().Str("version", runtime.Version()).Str("vcs.time", vcsTime).Msg("build") if ConfigPath != "" { - log.Info().Str("path", ConfigPath).Msg("config") - } - - migrateStore() -} - -func LoadConfig(v any) { - for _, data := range configs { - if err := yaml.Unmarshal(data, v); err != nil { - log.Warn().Err(err).Msg("[app] read config") - } + Logger.Info().Str("path", ConfigPath).Msg("config") } } -func PatchConfig(key string, value any, path ...string) error { - if ConfigPath == "" { - return errors.New("config file disabled") - } - - // empty config is OK - b, _ := os.ReadFile(ConfigPath) - - b, err := yaml.Patch(b, key, value, path...) - if err != nil { - return err - } - - return os.WriteFile(ConfigPath, b, 0644) -} - -// internal - -type Config []string - -func (c *Config) String() string { - return strings.Join(*c, " ") -} - -func (c *Config) Set(value string) error { - *c = append(*c, value) - return nil -} - -var configs [][]byte - func readRevisionTime() (revision, vcsTime string) { if info, ok := debug.ReadBuildInfo(); ok { for _, setting := range info.Settings { @@ -202,25 +105,3 @@ func readRevisionTime() (revision, vcsTime string) { } return } - -func parseConfString(s string) []byte { - i := strings.IndexByte(s, '=') - if i < 0 { - return nil - } - - items := strings.Split(s[:i], ".") - if len(items) < 2 { - return nil - } - - // `log.level=trace` => `{log: {level: trace}}` - var pre string - var suf = s[i+1:] - for _, item := range items { - pre += "{" + item + ": " - suf += "}" - } - - return []byte(pre + suf) -} diff --git a/internal/app/config.go b/internal/app/config.go new file mode 100644 index 00000000..8ae6d460 --- /dev/null +++ b/internal/app/config.go @@ -0,0 +1,109 @@ +package app + +import ( + "errors" + "os" + "path/filepath" + "strings" + + "github.com/AlexxIT/go2rtc/pkg/shell" + "github.com/AlexxIT/go2rtc/pkg/yaml" +) + +func LoadConfig(v any) { + for _, data := range configs { + if err := yaml.Unmarshal(data, v); err != nil { + Logger.Warn().Err(err).Send() + } + } +} + +func PatchConfig(key string, value any, path ...string) error { + if ConfigPath == "" { + return errors.New("config file disabled") + } + + // empty config is OK + b, _ := os.ReadFile(ConfigPath) + + b, err := yaml.Patch(b, key, value, path...) + if err != nil { + return err + } + + return os.WriteFile(ConfigPath, b, 0644) +} + +type flagConfig []string + +func (c *flagConfig) String() string { + return strings.Join(*c, " ") +} + +func (c *flagConfig) Set(value string) error { + *c = append(*c, value) + return nil +} + +var configs [][]byte + +func initConfig(confs flagConfig) { + if confs == nil { + confs = []string{"go2rtc.yaml"} + } + + for _, conf := range confs { + if len(conf) == 0 { + continue + } + if conf[0] == '{' { + // config as raw YAML or JSON + configs = append(configs, []byte(conf)) + } else if data := parseConfString(conf); data != nil { + configs = append(configs, data) + } else { + // config as file + if ConfigPath == "" { + ConfigPath = conf + } + + if data, _ = os.ReadFile(conf); data == nil { + continue + } + + data = []byte(shell.ReplaceEnvVars(string(data))) + configs = append(configs, data) + } + } + + if ConfigPath != "" { + if !filepath.IsAbs(ConfigPath) { + if cwd, err := os.Getwd(); err == nil { + ConfigPath = filepath.Join(cwd, ConfigPath) + } + } + Info["config_path"] = ConfigPath + } +} + +func parseConfString(s string) []byte { + i := strings.IndexByte(s, '=') + if i < 0 { + return nil + } + + items := strings.Split(s[:i], ".") + if len(items) < 2 { + return nil + } + + // `log.level=trace` => `{log: {level: trace}}` + var pre string + var suf = s[i+1:] + for _, item := range items { + pre += "{" + item + ": " + suf += "}" + } + + return []byte(pre + suf) +} diff --git a/internal/app/log.go b/internal/app/log.go index 222f6f2b..094dfbbf 100644 --- a/internal/app/log.go +++ b/internal/app/log.go @@ -6,30 +6,49 @@ import ( "github.com/mattn/go-isatty" "github.com/rs/zerolog" - "github.com/rs/zerolog/log" ) var MemoryLog = newBuffer(16) -// NewLogger support: +func GetLogger(module string) zerolog.Logger { + if s, ok := modules[module]; ok { + lvl, err := zerolog.ParseLevel(s) + if err == nil { + return Logger.Level(lvl) + } + Logger.Warn().Err(err).Caller().Send() + } + + return Logger +} + +// initLogger support: // - output: empty (only to memory), stderr, stdout // - format: empty (autodetect color support), color, json, text // - time: empty (disable timestamp), UNIXMS, UNIXMICRO, UNIXNANO // - level: disabled, trace, debug, info, warn, error... -func NewLogger(config map[string]string) zerolog.Logger { +func initLogger() { + var cfg struct { + Mod map[string]string `yaml:"log"` + } + + cfg.Mod = modules // defaults + + LoadConfig(&cfg) + var writer io.Writer - switch config["output"] { + switch modules["output"] { case "stderr": writer = os.Stderr case "stdout": writer = os.Stdout } - timeFormat := config["time"] + timeFormat := modules["time"] if writer != nil { - if format := config["format"]; format != "json" { + if format := modules["format"]; format != "json" { console := &zerolog.ConsoleWriter{Out: writer} switch format { @@ -61,31 +80,24 @@ func NewLogger(config map[string]string) zerolog.Logger { writer = MemoryLog } - logger := zerolog.New(writer) + lvl, _ := zerolog.ParseLevel(modules["level"]) + Logger = zerolog.New(writer).Level(lvl) if timeFormat != "" { zerolog.TimeFieldFormat = timeFormat - logger = logger.With().Timestamp().Logger() + Logger = Logger.With().Timestamp().Logger() } - - lvl, _ := zerolog.ParseLevel(config["level"]) - return logger.Level(lvl) } -func GetLogger(module string) zerolog.Logger { - if s, ok := modules[module]; ok { - lvl, err := zerolog.ParseLevel(s) - if err == nil { - return log.Level(lvl) - } - log.Warn().Err(err).Caller().Send() - } - - return log.Logger -} +var Logger zerolog.Logger // modules log levels -var modules map[string]string +var modules = map[string]string{ + "format": "", // useless, but anyway + "level": "info", + "output": "stdout", // TODO: change to stderr someday + "time": zerolog.TimeFormatUnixMs, +} const chunkSize = 1 << 16 diff --git a/internal/app/migrate.go b/internal/app/migrate.go deleted file mode 100644 index 95c51c51..00000000 --- a/internal/app/migrate.go +++ /dev/null @@ -1,35 +0,0 @@ -package app - -import ( - "encoding/json" - "os" - - "github.com/rs/zerolog/log" -) - -func migrateStore() { - const name = "go2rtc.json" - - data, _ := os.ReadFile(name) - if data == nil { - return - } - - var store struct { - Streams map[string]string `json:"streams"` - } - - if err := json.Unmarshal(data, &store); err != nil { - log.Warn().Err(err).Caller().Send() - return - } - - for id, url := range store.Streams { - if err := PatchConfig(id, url, "streams"); err != nil { - log.Warn().Err(err).Caller().Send() - return - } - } - - _ = os.Remove(name) -} diff --git a/internal/dvrip/dvrip.go b/internal/dvrip/dvrip.go index 470e8afd..095372d2 100644 --- a/internal/dvrip/dvrip.go +++ b/internal/dvrip/dvrip.go @@ -12,7 +12,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/dvrip" - "github.com/rs/zerolog/log" ) func Init() { @@ -92,10 +91,7 @@ func sendBroadcasts(conn *net.UDPConn) { for i := 0; i < 3; i++ { time.Sleep(100 * time.Millisecond) - - if _, err = conn.WriteToUDP(data, addr); err != nil { - log.Err(err).Caller().Send() - } + _, _ = conn.WriteToUDP(data, addr) } } diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index aeba85fb..062e5aaf 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -14,6 +14,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/ffmpeg" + "github.com/rs/zerolog" ) func Init() { @@ -29,6 +30,8 @@ func Init() { app.LoadConfig(&cfg) + log = app.GetLogger("ffmpeg") + // zerolog levels: trace debug info warn error fatal panic disabled // FFmpeg levels: trace debug verbose info warning error fatal panic quiet if cfg.Log.Level == "warn" { @@ -145,6 +148,8 @@ var defaults = map[string]string{ "h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v main -level:v 5.1", } +var log zerolog.Logger + // configTemplate - return template from config (defaults) if exist or return raw template func configTemplate(template string) string { if s := defaults[template]; s != "" { diff --git a/internal/ffmpeg/hardware/hardware.go b/internal/ffmpeg/hardware/hardware.go index ebbdc4fa..39ce3323 100644 --- a/internal/ffmpeg/hardware/hardware.go +++ b/internal/ffmpeg/hardware/hardware.go @@ -7,8 +7,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/pkg/ffmpeg" - - "github.com/rs/zerolog/log" ) const ( @@ -152,7 +150,6 @@ var cache = map[string]string{} func run(bin string, args string) bool { err := exec.Command(bin, strings.Split(args, " ")...).Run() - log.Printf("%v %v", args, err) return err == nil } diff --git a/internal/ffmpeg/version.go b/internal/ffmpeg/version.go index 976c92d0..717e08a4 100644 --- a/internal/ffmpeg/version.go +++ b/internal/ffmpeg/version.go @@ -6,7 +6,6 @@ import ( "sync" "github.com/AlexxIT/go2rtc/pkg/ffmpeg" - "github.com/rs/zerolog/log" ) var verMu sync.Mutex diff --git a/internal/mjpeg/init.go b/internal/mjpeg/init.go index ea65e2d7..0bed95c6 100644 --- a/internal/mjpeg/init.go +++ b/internal/mjpeg/init.go @@ -10,6 +10,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/api/ws" + "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/ffmpeg" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/ascii" @@ -18,7 +19,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/mjpeg" "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/AlexxIT/go2rtc/pkg/y4m" - "github.com/rs/zerolog/log" + "github.com/rs/zerolog" ) func Init() { @@ -28,8 +29,12 @@ func Init() { api.HandleFunc("api/stream.y4m", apiStreamY4M) ws.HandleFunc("mjpeg", handlerWS) + + log = app.GetLogger("mjpeg") } +var log zerolog.Logger + func handlerKeyframe(w http.ResponseWriter, r *http.Request) { src := r.URL.Query().Get("src") stream := streams.Get(src) diff --git a/internal/mpegts/aac.go b/internal/mpegts/aac.go index 3008a658..867dc971 100644 --- a/internal/mpegts/aac.go +++ b/internal/mpegts/aac.go @@ -7,7 +7,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/aac" "github.com/AlexxIT/go2rtc/pkg/tcp" - "github.com/rs/zerolog/log" ) func apiStreamAAC(w http.ResponseWriter, r *http.Request) { @@ -23,7 +22,6 @@ func apiStreamAAC(w http.ResponseWriter, r *http.Request) { cons.UserAgent = r.UserAgent() if err := stream.AddConsumer(cons); err != nil { - log.Error().Err(err).Caller().Send() http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/internal/mpegts/mpegts.go b/internal/mpegts/mpegts.go index 6f4f6ab2..6ef00ba1 100644 --- a/internal/mpegts/mpegts.go +++ b/internal/mpegts/mpegts.go @@ -7,7 +7,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/mpegts" "github.com/AlexxIT/go2rtc/pkg/tcp" - "github.com/rs/zerolog/log" ) func Init() { @@ -36,7 +35,6 @@ func outputMpegTS(w http.ResponseWriter, r *http.Request) { cons.UserAgent = r.UserAgent() if err := stream.AddConsumer(cons); err != nil { - log.Error().Err(err).Caller().Send() http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/main.go b/main.go index 91bc9938..27490f6a 100644 --- a/main.go +++ b/main.go @@ -36,6 +36,8 @@ import ( ) func main() { + app.Version = "1.9.2" + // 1. Core modules: app, api/ws, streams app.Init() // init config and logs diff --git a/pkg/opus/opus.go b/pkg/opus/.opus.go similarity index 97% rename from pkg/opus/opus.go rename to pkg/opus/.opus.go index 9fe1d8b6..42043977 100644 --- a/pkg/opus/opus.go +++ b/pkg/opus/.opus.go @@ -5,7 +5,6 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/rtp" - "github.com/rs/zerolog/log" ) func Log(handler core.HandlerFunc) core.HandlerFunc { diff --git a/pkg/roborock/client.go b/pkg/roborock/client.go index 39caab88..6a3bf0e0 100644 --- a/pkg/roborock/client.go +++ b/pkg/roborock/client.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "log" "net/rpc" "net/url" "strconv" @@ -138,7 +137,7 @@ func (c *Client) Connect() error { } offer := pc.LocalDescription() - log.Printf("[roborock] offer\n%s", offer.SDP) + //log.Printf("[roborock] offer\n%s", offer.SDP) if err = c.SendSDPtoRobot(offer); err != nil { return err } @@ -151,7 +150,7 @@ func (c *Client) Connect() error { time.Sleep(time.Second) if desc, _ := c.GetDeviceSDP(); desc != nil { - log.Printf("[roborock] answer\n%s", desc.SDP) + //log.Printf("[roborock] answer\n%s", desc.SDP) if err = c.conn.SetAnswer(desc.SDP); err != nil { return err } diff --git a/pkg/roborock/iot/client.go b/pkg/roborock/iot/client.go index 8773455d..c3b2d97f 100644 --- a/pkg/roborock/iot/client.go +++ b/pkg/roborock/iot/client.go @@ -6,12 +6,12 @@ import ( "encoding/hex" "encoding/json" "fmt" - "github.com/AlexxIT/go2rtc/pkg/mqtt" - "github.com/rs/zerolog/log" "net" "net/rpc" "net/url" "time" + + "github.com/AlexxIT/go2rtc/pkg/mqtt" ) type Codec struct { @@ -56,7 +56,7 @@ func (c *Codec) WriteRequest(r *rpc.Request, v any) error { return err } - log.Printf("[roborock] send: %s", payload) + //log.Printf("[roborock] send: %s", payload) payload = c.Encrypt(payload, ts, ts, ts) @@ -86,7 +86,7 @@ func (c *Codec) ReadResponseHeader(r *rpc.Response) error { continue } - log.Printf("[roborock] recv %s", payload) + //log.Printf("[roborock] recv %s", payload) // get content from response payload: // {"t":1676871268,"dps":{"102":"{\"id\":315003,\"result\":[\"ok\"]}"}} From 9bb36ebb6c6af157a274e95de9e93c8430bce69e Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 5 Jun 2024 19:59:22 +0300 Subject: [PATCH 099/130] Fix ghost exec/ffmpeg process --- internal/exec/exec.go | 8 ++++++-- internal/streams/producer.go | 6 +++++- pkg/rtsp/client.go | 3 +++ pkg/rtsp/conn.go | 1 + 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/internal/exec/exec.go b/internal/exec/exec.go index 454c54a4..d30b0dbe 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -113,7 +113,7 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { cmd.Stdout = os.Stdout } - waiter := make(chan core.Producer) + waiter := make(chan *pkg.Conn, 1) waitersMu.Lock() waiters[path] = waiter @@ -149,6 +149,10 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr) case prod := <-waiter: log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp") + prod.OnClose = func() error { + log.Debug().Msgf("[exec] kill rtsp") + return errors.Join(cmd.Process.Kill(), cmd.Wait()) + } return prod, nil } } @@ -157,7 +161,7 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { var ( log zerolog.Logger - waiters = map[string]chan core.Producer{} + waiters = make(map[string]chan *pkg.Conn) waitersMu sync.Mutex ) diff --git a/internal/streams/producer.go b/internal/streams/producer.go index 5a25dba5..daca7edf 100644 --- a/internal/streams/producer.go +++ b/internal/streams/producer.go @@ -207,7 +207,7 @@ func (p *Producer) reconnect(workerID, retry int) { for _, media := range conn.GetMedias() { switch media.Direction { case core.DirectionRecvonly: - for _, receiver := range p.receivers { + for i, receiver := range p.receivers { codec := media.MatchCodec(receiver.Codec) if codec == nil { continue @@ -219,6 +219,7 @@ func (p *Producer) reconnect(workerID, retry int) { } receiver.Replace(track) + p.receivers[i] = track break } @@ -234,6 +235,9 @@ func (p *Producer) reconnect(workerID, retry int) { } } + // stop previous connection after moving tracks (fix ghost exec/ffmpeg) + _ = p.conn.Stop() + // swap connections p.conn = conn go p.worker(conn, workerID) diff --git a/pkg/rtsp/client.go b/pkg/rtsp/client.go index ca32ce32..59f96e94 100644 --- a/pkg/rtsp/client.go +++ b/pkg/rtsp/client.go @@ -304,5 +304,8 @@ func (c *Conn) Close() error { if c.mode == core.ModeActiveProducer { _ = c.Teardown() } + if c.OnClose != nil { + _ = c.OnClose() + } return c.conn.Close() } diff --git a/pkg/rtsp/conn.go b/pkg/rtsp/conn.go index 91465f2c..1d9edf06 100644 --- a/pkg/rtsp/conn.go +++ b/pkg/rtsp/conn.go @@ -24,6 +24,7 @@ type Conn struct { Backchannel bool Media string + OnClose func() error PacketSize uint16 SessionName string Timeout int From e0b1a503561b8def00eaec5ec914ba8b04cfa604 Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 5 Jun 2024 20:00:41 +0300 Subject: [PATCH 100/130] Add rtsp_client for testing ghost exec process --- examples/rtsp_client/main.go | 39 ++++++++++++++++++++++++++++++++++++ internal/streams/README.md | 8 ++++++++ pkg/rtsp/client.go | 14 +++++++++++-- 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 examples/rtsp_client/main.go create mode 100644 internal/streams/README.md diff --git a/examples/rtsp_client/main.go b/examples/rtsp_client/main.go new file mode 100644 index 00000000..9c2112d1 --- /dev/null +++ b/examples/rtsp_client/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "log" + "os" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/rtsp" + "github.com/AlexxIT/go2rtc/pkg/shell" +) + +func main() { + client := rtsp.NewClient(os.Args[1]) + if err := client.Dial(); err != nil { + log.Panic(err) + } + + client.Medias = []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + {Name: core.CodecPCMU, ClockRate: 8000}, + }, + ID: "streamid=0", + }, + } + if err := client.Announce(); err != nil { + log.Panic(err) + } + if _, err := client.SetupMedia(client.Medias[0]); err != nil { + log.Panic(err) + } + if err := client.Record(); err != nil { + log.Panic(err) + } + + shell.RunUntilSignal() +} diff --git a/internal/streams/README.md b/internal/streams/README.md new file mode 100644 index 00000000..6bbc268a --- /dev/null +++ b/internal/streams/README.md @@ -0,0 +1,8 @@ +## Testing notes + +```yaml +streams: + test1-basic: ffmpeg:virtual?video#video=h264 + test2-reconnect: ffmpeg:virtual?video&duration=10#video=h264 + test3-execkill: exec:./examples/rtsp_client/rtsp_client/rtsp_client {output} +``` diff --git a/pkg/rtsp/client.go b/pkg/rtsp/client.go index 59f96e94..9002d0a1 100644 --- a/pkg/rtsp/client.go +++ b/pkg/rtsp/client.go @@ -186,10 +186,20 @@ func (c *Conn) Announce() (err error) { return err } - res, err := c.Do(req) + _, err = c.Do(req) + return +} - _ = res +func (c *Conn) Record() (err error) { + req := &tcp.Request{ + Method: MethodRecord, + URL: c.URL, + Header: map[string][]string{ + "Range": {"npt=0.000-"}, + }, + } + _, err = c.Do(req) return } From 31e4ba27222d6f106abc1348f134a583a370e93b Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 5 Jun 2024 20:01:47 +0300 Subject: [PATCH 101/130] Rewrite Receiver/Sender classes --- pkg/core/codec.go | 5 + pkg/core/core_test.go | 120 ++++++++++++++++++ pkg/core/node.go | 87 +++++++++++++ pkg/core/track.go | 282 +++++++++++++++++------------------------- 4 files changed, 327 insertions(+), 167 deletions(-) create mode 100644 pkg/core/core_test.go create mode 100644 pkg/core/node.go diff --git a/pkg/core/codec.go b/pkg/core/codec.go index f38d7965..fe813de3 100644 --- a/pkg/core/codec.go +++ b/pkg/core/codec.go @@ -2,6 +2,7 @@ package core import ( "encoding/base64" + "encoding/json" "fmt" "strconv" "strings" @@ -18,6 +19,10 @@ type Codec struct { PayloadType uint8 } +func (c *Codec) MarshalJSON() ([]byte, error) { + return json.Marshal(c.String()) +} + func (c *Codec) String() string { s := fmt.Sprintf("%d %s", c.PayloadType, c.Name) if c.ClockRate != 0 && c.ClockRate != 90000 { diff --git a/pkg/core/core_test.go b/pkg/core/core_test.go new file mode 100644 index 00000000..4a05380a --- /dev/null +++ b/pkg/core/core_test.go @@ -0,0 +1,120 @@ +package core + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +type producer struct { + Medias []*Media + Receivers []*Receiver + + id byte +} + +func (p *producer) GetMedias() []*Media { + return p.Medias +} + +func (p *producer) GetTrack(_ *Media, codec *Codec) (*Receiver, error) { + for _, receiver := range p.Receivers { + if receiver.Codec == codec { + return receiver, nil + } + } + receiver := NewReceiver(nil, codec) + p.Receivers = append(p.Receivers, receiver) + return receiver, nil +} + +func (p *producer) Start() error { + pkt := &Packet{Payload: []byte{p.id}} + p.Receivers[0].Input(pkt) + return nil +} + +func (p *producer) Stop() error { + for _, receiver := range p.Receivers { + receiver.Close() + } + return nil +} + +type consumer struct { + Medias []*Media + Senders []*Sender + + cache chan byte +} + +func (c *consumer) GetMedias() []*Media { + return c.Medias +} + +func (c *consumer) AddTrack(_ *Media, _ *Codec, track *Receiver) error { + c.cache = make(chan byte, 1) + sender := NewSender(nil, track.Codec) + sender.Output = func(packet *Packet) { + c.cache <- packet.Payload[0] + } + sender.HandleRTP(track) + c.Senders = append(c.Senders, sender) + return nil +} + +func (c *consumer) Stop() error { + for _, sender := range c.Senders { + sender.Close() + } + return nil +} + +func (c *consumer) read() byte { + return <-c.cache +} + +func TestName(t *testing.T) { + GetProducer := func(b byte) Producer { + return &producer{ + Medias: []*Media{ + { + Kind: KindVideo, + Direction: DirectionRecvonly, + Codecs: []*Codec{ + {Name: CodecH264}, + }, + }, + }, + id: b, + } + } + + // stage1 + prod1 := GetProducer(1) + cons2 := &consumer{} + + media1 := prod1.GetMedias()[0] + track1, _ := prod1.GetTrack(media1, media1.Codecs[0]) + + _ = cons2.AddTrack(nil, nil, track1) + + _ = prod1.Start() + require.Equal(t, byte(1), cons2.read()) + + // stage2 + prod2 := GetProducer(2) + media2 := prod2.GetMedias()[0] + require.NotEqual(t, fmt.Sprintf("%p", media1), fmt.Sprintf("%p", media2)) + track2, _ := prod2.GetTrack(media2, media2.Codecs[0]) + track1.Replace(track2) + + _ = prod1.Stop() + + _ = prod2.Start() + require.Equal(t, byte(2), cons2.read()) + + // stage3 + _ = prod2.Stop() +} diff --git a/pkg/core/node.go b/pkg/core/node.go new file mode 100644 index 00000000..fd58f2d7 --- /dev/null +++ b/pkg/core/node.go @@ -0,0 +1,87 @@ +package core + +import ( + "sync" + + "github.com/pion/rtp" +) + +//type Packet struct { +// Payload []byte +// Timestamp uint32 // PTS if DTS == 0 else DTS +// Composition uint32 // CTS = PTS-DTS (for support B-frames) +// Sequence uint16 +//} + +type Packet = rtp.Packet + +// HandlerFunc - process input packets (just like http.HandlerFunc) +type HandlerFunc func(packet *Packet) + +// Filter - a decorator for any HandlerFunc +type Filter func(handler HandlerFunc) HandlerFunc + +// Node - Receiver or Sender or Filter (transform) +type Node struct { + Codec *Codec `json:"codec"` + Input HandlerFunc `json:"-"` + Output HandlerFunc `json:"-"` + + childs []*Node + parent *Node + + mu sync.Mutex +} + +func (n *Node) WithParent(parent *Node) *Node { + parent.AppendChild(n) + return n +} + +func (n *Node) AppendChild(child *Node) { + n.mu.Lock() + n.childs = append(n.childs, child) + n.mu.Unlock() + + child.parent = n +} + +func (n *Node) RemoveChild(child *Node) { + n.mu.Lock() + for i, ch := range n.childs { + if ch == child { + n.childs = append(n.childs[:i], n.childs[i+1:]...) + break + } + } + n.mu.Unlock() +} + +func (n *Node) Close() { + if parent := n.parent; parent != nil { + parent.RemoveChild(n) + + if len(parent.childs) == 0 { + parent.Close() + } + } else { + for _, childs := range n.childs { + childs.Close() + } + } +} + +func MoveNode(dst, src *Node) { + src.mu.Lock() + childs := src.childs + src.childs = nil + src.mu.Unlock() + + dst.mu.Lock() + dst.childs = childs + dst.mu.Unlock() + + for _, child := range childs { + child.parent = dst + } +} diff --git a/pkg/core/track.go b/pkg/core/track.go index 72e47074..83c39e01 100644 --- a/pkg/core/track.go +++ b/pkg/core/track.go @@ -1,225 +1,173 @@ package core import ( - "encoding/json" "errors" - "fmt" - "strconv" - "sync" "github.com/pion/rtp" ) -type Packet struct { - PayloadType uint8 - Sequence uint16 - Timestamp uint32 // PTS if DTS == 0 else DTS - Composition uint32 // CTS = PTS-DTS (for support B-frames) - Payload []byte -} - var ErrCantGetTrack = errors.New("can't get track") type Receiver struct { - Codec *Codec - Media *Media + Node - ID byte // Channel for RTSP, PayloadType for MPEG-TS + // Deprecated: should be removed + Media *Media `json:"-"` + // Deprecated: should be removed + ID byte `json:"-"` // Channel for RTSP, PayloadType for MPEG-TS - senders map[*Sender]chan *rtp.Packet - mu sync.RWMutex - bytes int + Bytes int `json:"bytes,omitempty"` + Packets int `json:"packets,omitempty"` } 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 { - select { - case buffer <- packet: - default: - sender.overflow++ + r := &Receiver{ + Node: Node{Codec: codec}, + Media: media, + } + r.Input = func(packet *Packet) { + r.Bytes += len(packet.Payload) + r.Packets++ + for _, child := range r.childs { + child.Input(packet) } } - t.mu.Unlock() + return r } -func (t *Receiver) Senders() (senders []*Sender) { - t.mu.RLock() - for sender := range t.senders { - senders = append(senders, sender) +// Deprecated: should be removed +func (r *Receiver) WriteRTP(packet *rtp.Packet) { + r.Input(packet) +} + +// Deprecated: should be removed +func (r *Receiver) Senders() []*Sender { + if len(r.childs) > 0 { + return []*Sender{{}} + } else { + return nil } - t.mu.RUnlock() - 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() +// Deprecated: should be removed +func (r *Receiver) Replace(target *Receiver) { + MoveNode(&target.Node, &r.Node) } -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) - t.mu.RLock() - s += fmt.Sprintf(", senders=%d", len(t.senders)) - t.mu.RUnlock() - return s -} - -func (t *Receiver) MarshalJSON() ([]byte, error) { - return json.Marshal(t.String()) +func (r *Receiver) Close() { + r.Node.Close() } type Sender struct { - Codec *Codec - Media *Media + Node - Handler HandlerFunc + // Deprecated: + Media *Media `json:"-"` + // Deprecated: + Handler HandlerFunc `json:"-"` - receivers []*Receiver - mu sync.RWMutex - bytes int + Bytes int `json:"bytes,omitempty"` + Packets int `json:"packets,omitempty"` + Drops int `json:"drops,omitempty"` - overflow int + buf chan *Packet + done chan struct{} } func NewSender(media *Media, codec *Codec) *Sender { - return &Sender{Codec: codec, Media: media} -} + var bufSize uint16 -// HandlerFunc like http.HandlerFunc -type HandlerFunc func(packet *rtp.Packet) - -func (s *Sender) HandleRTP(track *Receiver) { - s.Bind(track) - go s.worker(track) -} - -func (s *Sender) Bind(track *Receiver) { - var bufferSize uint16 - - if GetKind(track.Codec.Name) == KindVideo { - if track.Codec.IsRTP() { + if GetKind(codec.Name) == KindVideo { + if codec.IsRTP() { // in my tests 40Mbit/s 4K-video can generate up to 1500 items // for the h264.RTPDepay => RTPPay queue - bufferSize = 5000 + bufSize = 4096 } else { - bufferSize = 50 + bufSize = 64 } } else { - bufferSize = 100 + bufSize = 128 } - buffer := make(chan *rtp.Packet, bufferSize) - - track.mu.Lock() - if track.senders == nil { - track.senders = map[*Sender]chan *rtp.Packet{} + buf := make(chan *Packet, bufSize) + s := &Sender{ + Node: Node{Codec: codec}, + Media: media, + buf: buf, } - track.senders[s] = buffer - track.mu.Unlock() - - s.mu.Lock() - s.receivers = append(s.receivers, track) - s.mu.Unlock() + s.Input = func(packet *Packet) { + // writing to nil chan - OK, writing to closed chan - panic + s.mu.Lock() + select { + case s.buf <- packet: + s.Bytes += len(packet.Payload) + s.Packets++ + default: + s.Drops++ + } + s.mu.Unlock() + } + s.Output = func(packet *Packet) { + s.Handler(packet) + } + return s } -func (s *Sender) worker(track *Receiver) { - track.mu.Lock() - buffer := track.senders[s] - track.mu.Unlock() +// Deprecated: should be removed +func (s *Sender) HandleRTP(parent *Receiver) { + s.WithParent(parent) + s.Start() +} - // read packets from buffer channel until it will be closed - if buffer != nil { - for packet := range buffer { - s.bytes += len(packet.Payload) - s.Handler(packet) - } - } +// Deprecated: should be removed +func (s *Sender) Bind(parent *Receiver) { + s.WithParent(parent) +} - // 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) WithParent(parent *Receiver) *Sender { + s.Node.WithParent(&parent.Node) + return s } func (s *Sender) Start() { s.mu.Lock() - for _, track := range s.receivers { - go s.worker(track) + defer s.mu.Unlock() + + if s.buf == nil || s.done != nil { + return } - s.mu.Unlock() + s.done = make(chan struct{}) + + go func() { + for packet := range s.buf { + s.Output(packet) + } + close(s.done) + }() +} + +func (s *Sender) Wait() { + if done := s.done; s.done != nil { + <-done + } +} + +func (s *Sender) State() string { + if s.buf == nil { + return "closed" + } + if s.done == nil { + return "new" + } + return "connected" } 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() + // close buffer if exists + if buf := s.buf; buf != nil { + s.buf = nil + defer close(buf) } - s.receivers = nil - s.mu.Unlock() -} -func (s *Sender) String() string { - info := s.Codec.String() + ", bytes=" + strconv.Itoa(s.bytes) - s.mu.RLock() - info += ", receivers=" + strconv.Itoa(len(s.receivers)) - s.mu.RUnlock() - if s.overflow > 0 { - info += ", overflow=" + strconv.Itoa(s.overflow) - } - return info -} - -func (s *Sender) MarshalJSON() ([]byte, error) { - return json.Marshal(s.String()) -} - -// VA - helper, for extract video and audio receivers from list -func VA(receivers []*Receiver) (video, audio *Receiver) { - for _, receiver := range receivers { - switch GetKind(receiver.Codec.Name) { - case KindVideo: - video = receiver - case KindAudio: - audio = receiver - } - } - return + s.Node.Close() } From ec33796bd34bb6c2bbd03d4d17cf900188946558 Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 5 Jun 2024 20:02:10 +0300 Subject: [PATCH 102/130] Add goweight to useful commands --- scripts/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/README.md b/scripts/README.md index b893b312..efcef154 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -14,6 +14,7 @@ go get -u go mod tidy go mod why github.com/pion/rtcp go list -deps .\cmd\go2rtc_rtsp\ +./goweight ``` ## Dependencies From 8377ad1d0547a50aecf7bb64a3bb894613b69adf Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 6 Jun 2024 13:16:12 +0300 Subject: [PATCH 103/130] Update codec section in stream info --- pkg/core/codec.go | 88 +++++++++++++++++++++++++++++++---------------- pkg/core/media.go | 2 +- 2 files changed, 60 insertions(+), 30 deletions(-) diff --git a/pkg/core/codec.go b/pkg/core/codec.go index fe813de3..91f6fddc 100644 --- a/pkg/core/codec.go +++ b/pkg/core/codec.go @@ -4,7 +4,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "strconv" "strings" "unicode" @@ -19,38 +18,70 @@ type Codec struct { PayloadType uint8 } +// MarshalJSON - return FFprobe compatible output func (c *Codec) MarshalJSON() ([]byte, error) { - return json.Marshal(c.String()) -} - -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) + info := map[string]any{} + if name := FFmpegCodecName(c.Name); name != "" { + info["codec_name"] = name + info["codec_type"] = c.Kind() } - 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 + if c.Name == CodecH264 { + profile, level := DecodeH264(c.FmtpLine) + if profile != "" { + info["profile"] = profile + info["level"] = level } - return c.Name } - - s := c.Name if c.ClockRate != 0 && c.ClockRate != 90000 { - s += "/" + strconv.Itoa(int(c.ClockRate)) + info["sample_rate"] = c.ClockRate } if c.Channels > 0 { - s += "/" + strconv.Itoa(int(c.Channels)) + info["channels"] = c.Channels } - return s + return json.Marshal(info) +} + +func FFmpegCodecName(name string) string { + switch name { + case CodecH264: + return "h264" + case CodecH265: + return "h265" + case CodecJPEG: + return "mjpeg" + case CodecRAW: + return "rawvideo" + case CodecPCMA: + return "pcm_alaw" + case CodecPCMU: + return "pcm_mulaw" + case CodecPCM: + return "pcm_s16be" + case CodecPCML: + return "pcm_s16le" + case CodecAAC: + return "aac" + case CodecOpus: + return "opus" + case CodecVP8: + return "vp8" + case CodecVP9: + return "vp9" + case CodecAV1: + return "av1" + } + return "" +} + +func (c *Codec) String() (s string) { + s = c.Name + if c.ClockRate != 0 && c.ClockRate != 90000 { + s += fmt.Sprintf("/%d", c.ClockRate) + } + if c.Channels > 0 { + s += fmt.Sprintf("/%d", c.Channels) + } + return } func (c *Codec) IsRTP() bool { @@ -186,10 +217,9 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec { return c } -func DecodeH264(fmtp string) string { +func DecodeH264(fmtp string) (profile string, level byte) { 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" @@ -203,8 +233,8 @@ func DecodeH264(fmtp string) string { profile = fmt.Sprintf("0x%02X", sps[1]) } - return fmt.Sprintf("%s %d.%d", profile, sps[3]/10, sps[3]%10) + level = sps[3] } } - return "" + return } diff --git a/pkg/core/media.go b/pkg/core/media.go index fe58cfd6..ef9ef74b 100644 --- a/pkg/core/media.go +++ b/pkg/core/media.go @@ -22,7 +22,7 @@ type Media struct { func (m *Media) String() string { s := fmt.Sprintf("%s, %s", m.Kind, m.Direction) for _, codec := range m.Codecs { - name := codec.Text() + name := codec.String() if strings.Contains(s, name) { continue From 2bab0a014d91a782ca5117a5019d8513874ca139 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 6 Jun 2024 14:34:16 +0300 Subject: [PATCH 104/130] Update dependencies --- go.mod | 16 ++++++++-------- go.sum | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 0d5d67c9..d3cb791f 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,9 @@ go 1.22 require ( github.com/asticode/go-astits v1.13.0 - github.com/expr-lang/expr v1.16.5 + github.com/expr-lang/expr v1.16.9 github.com/gorilla/websocket v1.5.1 + github.com/mattn/go-isatty v0.0.20 github.com/miekg/dns v1.1.59 github.com/pion/ice/v2 v2.3.24 github.com/pion/interceptor v0.1.29 @@ -15,12 +16,12 @@ require ( github.com/pion/srtp/v2 v2.0.18 github.com/pion/stun v0.6.1 github.com/pion/webrtc/v3 v3.2.40 - github.com/rs/zerolog v1.32.0 + github.com/rs/zerolog v1.33.0 github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f github.com/stretchr/testify v1.9.0 github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 - golang.org/x/crypto v0.23.0 + golang.org/x/crypto v0.24.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -30,7 +31,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/kr/pretty v0.2.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/pion/datachannel v1.5.6 // indirect github.com/pion/dtls/v2 v2.2.11 // indirect github.com/pion/logging v0.2.2 // indirect @@ -40,9 +40,9 @@ require ( github.com/pion/transport/v2 v2.2.5 // indirect github.com/pion/turn/v2 v2.1.6 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.25.0 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/net v0.26.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/tools v0.20.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/tools v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index 09b0abb9..727787ac 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/expr-lang/expr v1.16.5 h1:m2hvtguFeVaVNTHj8L7BoAyt7O0PAIBaSVbjdHgRXMs= github.com/expr-lang/expr v1.16.5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ= +github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI= +github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -85,6 +87,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I= github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA= @@ -115,10 +119,14 @@ golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -134,6 +142,8 @@ golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -160,6 +170,8 @@ golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -184,6 +196,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= From e3188a0a6dcd872f8c011a119aeb5c565551d146 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 6 Jun 2024 15:18:55 +0300 Subject: [PATCH 105/130] Update docs about config --- internal/app/README.md | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/internal/app/README.md b/internal/app/README.md index e3d0571f..2460daa2 100644 --- a/internal/app/README.md +++ b/internal/app/README.md @@ -1,6 +1,10 @@ - By default go2rtc will search config file `go2rtc.yaml` in current work directory -- go2rtc support multiple config files -- go2rtc support inline config as `YAML`, `JSON` or `key=value` format from command line +- go2rtc support multiple config files: + - `go2rtc -c config1.yaml -c config2.yaml -c config3.yaml` +- go2rtc support inline config as multiple formats from command line: + - **YAML**: `go2rtc -c '{log: {format: text}}'` + - **JSON**: `go2rtc -c '{"log":{"format":"text"}}'` + - **key=value**: `go2rtc -c log.format=text` - Every next config will overwrite previous (but only defined params) ``` @@ -21,15 +25,24 @@ Also go2rtc support templates for using environment variables in any part of con streams: camera1: rtsp://rtsp:${CAMERA_PASSWORD}@192.168.1.123/av_stream/ch0 - ${LOGS:} # empty default value - rtsp: username: ${RTSP_USER:admin} # "admin" if env "RTSP_USER" not set password: ${RTSP_PASS:secret} # "secret" if env "RTSP_PASS" not set ``` +## JSON Schema + +Editors like [GoLand](https://www.jetbrains.com/go/) and [VS Code](https://code.visualstudio.com/) supports autocomplete and syntax validation. + +```yaml +# yaml-language-server: $schema=https://raw.githubusercontent.com/AlexxIT/go2rtc/master/website/schema.json +``` + ## Defaults +- Default values may change in updates +- FFmpeg module has many presets, they are not listed here because they may also change in updates + ```yaml api: listen: ":1984" @@ -38,7 +51,10 @@ ffmpeg: bin: "ffmpeg" log: + format: "color" level: "info" + output: "stdout" + time: "UNIXMS" rtsp: listen: ":8554" @@ -51,4 +67,4 @@ webrtc: listen: ":8555/tcp" ice_servers: - urls: [ "stun:stun.l.google.com:19302" ] -``` \ No newline at end of file +``` From cd777ba2b4c08f3d5aae2cb83704612a5937e85d Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 6 Jun 2024 16:01:01 +0300 Subject: [PATCH 106/130] Update version to 1.9.3 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 27490f6a..d40851cc 100644 --- a/main.go +++ b/main.go @@ -36,7 +36,7 @@ import ( ) func main() { - app.Version = "1.9.2" + app.Version = "1.9.3" // 1. Core modules: app, api/ws, streams From bf303ed471fb117433332f42ebd7a8cece518d69 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 6 Jun 2024 17:58:31 +0300 Subject: [PATCH 107/130] Fix -d flag --- internal/app/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/app.go b/internal/app/app.go index 9331f041..1d63a2c8 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -53,7 +53,7 @@ func Init() { args := os.Args[1:] for i, arg := range args { - if arg == "-daemon" { + if arg == "-daemon" || arg == "-d" { args[i] = "" } } From b389d0eb9c390bd218198f5ddf11941765bc621d Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Thu, 6 Jun 2024 18:54:40 +0300 Subject: [PATCH 108/130] fix(app): handle daemon process correctly on Unix systems --- internal/app/app.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 1d63a2c8..ab4b6c94 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -7,6 +7,7 @@ import ( "os/exec" "runtime" "runtime/debug" + "syscall" ) var ( @@ -45,20 +46,23 @@ func Init() { os.Exit(0) } + if os.Getppid() == 1 || syscall.Getppid() == 1 { + daemon = false + } else { + parent, err := os.FindProcess(os.Getppid()) + if err != nil || parent.Pid < 1 { + daemon = false + } + } + if daemon { if runtime.GOOS == "windows" { fmt.Println("Daemon not supported on Windows") os.Exit(1) } - args := os.Args[1:] - for i, arg := range args { - if arg == "-daemon" || arg == "-d" { - args[i] = "" - } - } // Re-run the program in background and exit - cmd := exec.Command(os.Args[0], args...) + cmd := exec.Command(os.Args[0], os.Args[1:]...) if err := cmd.Start(); err != nil { fmt.Println(err) os.Exit(1) From aca0781c4b38f5ba400fa1be635014f0fd4e5426 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 7 Jun 2024 12:25:58 +0300 Subject: [PATCH 109/130] Code refactoring for api/streams --- internal/streams/api.go | 105 ++++++++++++++++++++++++++++++++++++ internal/streams/streams.go | 100 +--------------------------------- 2 files changed, 106 insertions(+), 99 deletions(-) create mode 100644 internal/streams/api.go diff --git a/internal/streams/api.go b/internal/streams/api.go new file mode 100644 index 00000000..72099425 --- /dev/null +++ b/internal/streams/api.go @@ -0,0 +1,105 @@ +package streams + +import ( + "net/http" + + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/pkg/probe" + "github.com/AlexxIT/go2rtc/pkg/tcp" +) + +func apiStreams(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + src := query.Get("src") + + // without source - return all streams list + if src == "" && r.Method != "POST" { + api.ResponseJSON(w, streams) + return + } + + // Not sure about all this API. Should be rewrited... + switch r.Method { + case "GET": + stream := Get(src) + if stream == nil { + http.Error(w, "", http.StatusNotFound) + return + } + + cons := probe.NewProbe(query) + if len(cons.Medias) != 0 { + cons.RemoteAddr = tcp.RemoteAddr(r) + cons.UserAgent = r.UserAgent() + if err := stream.AddConsumer(cons); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + api.ResponsePrettyJSON(w, stream) + + stream.RemoveConsumer(cons) + } else { + api.ResponsePrettyJSON(w, streams[src]) + } + + case "PUT": + name := query.Get("name") + if name == "" { + name = src + } + + if New(name, src) == nil { + http.Error(w, "", http.StatusBadRequest) + return + } + + if err := app.PatchConfig(name, src, "streams"); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + case "PATCH": + name := query.Get("name") + if name == "" { + http.Error(w, "", http.StatusBadRequest) + return + } + + // support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass + if Patch(name, src) == nil { + http.Error(w, "", http.StatusBadRequest) + } + + case "POST": + // with dst - redirect source to dst + if dst := query.Get("dst"); dst != "" { + if stream := Get(dst); stream != nil { + if err := Validate(src); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } else if err = stream.Play(src); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } else { + api.ResponseJSON(w, stream) + } + } else if stream = Get(src); stream != nil { + if err := Validate(dst); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } else if err = stream.Publish(dst); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } else { + http.Error(w, "", http.StatusNotFound) + } + } else { + http.Error(w, "", http.StatusBadRequest) + } + + case "DELETE": + delete(streams, src) + + if err := app.PatchConfig(src, nil, "streams"); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } +} diff --git a/internal/streams/streams.go b/internal/streams/streams.go index c676fe09..6d6fa773 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -2,7 +2,6 @@ package streams import ( "errors" - "net/http" "net/url" "regexp" "sync" @@ -10,8 +9,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/app" - "github.com/AlexxIT/go2rtc/pkg/probe" - "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog" ) @@ -29,7 +26,7 @@ func Init() { streams[name] = NewStream(item) } - api.HandleFunc("api/streams", streamsHandler) + api.HandleFunc("api/streams", apiStreams) if cfg.Publish == nil { return @@ -145,101 +142,6 @@ func Delete(id string) { delete(streams, id) } -func streamsHandler(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - src := query.Get("src") - - // without source - return all streams list - if src == "" && r.Method != "POST" { - api.ResponseJSON(w, streams) - return - } - - // Not sure about all this API. Should be rewrited... - switch r.Method { - case "GET": - stream := Get(src) - if stream == nil { - http.Error(w, "", http.StatusNotFound) - return - } - - cons := probe.NewProbe(query) - if len(cons.Medias) != 0 { - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() - if err := stream.AddConsumer(cons); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - api.ResponsePrettyJSON(w, stream) - - stream.RemoveConsumer(cons) - } else { - api.ResponsePrettyJSON(w, streams[src]) - } - - case "PUT": - name := query.Get("name") - if name == "" { - name = src - } - - if New(name, src) == nil { - http.Error(w, "", http.StatusBadRequest) - return - } - - if err := app.PatchConfig(name, src, "streams"); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - } - - case "PATCH": - name := query.Get("name") - if name == "" { - http.Error(w, "", http.StatusBadRequest) - return - } - - // support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass - if Patch(name, src) == nil { - http.Error(w, "", http.StatusBadRequest) - } - - case "POST": - // with dst - redirect source to dst - if dst := query.Get("dst"); dst != "" { - if stream := Get(dst); stream != nil { - if err := Validate(src); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - } else if err = stream.Play(src); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } else { - api.ResponseJSON(w, stream) - } - } else if stream = Get(src); stream != nil { - if err := Validate(dst); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - } else if err = stream.Publish(dst); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } else { - http.Error(w, "", http.StatusNotFound) - } - } else { - http.Error(w, "", http.StatusBadRequest) - } - - case "DELETE": - delete(streams, src) - - if err := app.PatchConfig(src, nil, "streams"); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - } - } -} - var log zerolog.Logger var streams = map[string]*Stream{} var streamsMu sync.Mutex From 0667683e4d6b25dd17d5e182f255b71989e4e133 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 7 Jun 2024 17:57:36 +0300 Subject: [PATCH 110/130] Restore support old cipher suites after go1.22 #1172 --- pkg/tcp/request.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pkg/tcp/request.go b/pkg/tcp/request.go index 88da83b6..13463cd7 100644 --- a/pkg/tcp/request.go +++ b/pkg/tcp/request.go @@ -19,11 +19,11 @@ func Do(req *http.Request) (*http.Response, error) { switch req.URL.Scheme { case "httpx": - secure = &tls.Config{InsecureSkipVerify: true} + secure = insecureConfig req.URL.Scheme = "https" case "https": if hostname := req.URL.Hostname(); IsIP(hostname) { - secure = &tls.Config{InsecureSkipVerify: true} + secure = insecureConfig } } @@ -144,6 +144,22 @@ type key string var connKey = key("conn") var secureKey = key("secure") +var insecureConfig = &tls.Config{ + InsecureSkipVerify: true, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + + // this cipher suites disabled starting from https://tip.golang.org/doc/go1.22 + // but cameras can't work without them https://github.com/AlexxIT/go2rtc/issues/1172 + tls.TLS_RSA_WITH_AES_128_GCM_SHA256, // insecure + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, // insecure + }, +} + func WithConn() (context.Context, *net.Conn) { pconn := new(net.Conn) return context.WithValue(context.Background(), connKey, pconn), pconn From 03956968667bba2febb6a513d159dc9a00b3e4c8 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 7 Jun 2024 17:59:21 +0300 Subject: [PATCH 111/130] Fix exec pipe output --- internal/exec/exec.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/exec/exec.go b/internal/exec/exec.go index d30b0dbe..6c41fe9e 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -101,11 +101,12 @@ func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error prod, err := magic.Open(r) if err != nil { _ = r.Close() + return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr) } log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run pipe") - return prod, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr) + return prod, nil } func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { From 72d7e8aaaa5bcbaf9e788522c0bd22fb47d44cdb Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sat, 8 Jun 2024 15:05:26 +0300 Subject: [PATCH 112/130] refactor(app): remove syscall import and improve error messages --- internal/app/app.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index ab4b6c94..cdbb870b 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -7,7 +7,6 @@ import ( "os/exec" "runtime" "runtime/debug" - "syscall" ) var ( @@ -46,10 +45,11 @@ func Init() { os.Exit(0) } - if os.Getppid() == 1 || syscall.Getppid() == 1 { + ppid := os.Getppid() + if ppid == 1 { daemon = false } else { - parent, err := os.FindProcess(os.Getppid()) + parent, err := os.FindProcess(ppid) if err != nil || parent.Pid < 1 { daemon = false } @@ -57,14 +57,14 @@ func Init() { if daemon { if runtime.GOOS == "windows" { - fmt.Println("Daemon not supported on Windows") + fmt.Println("Daemon mode is not supported on Windows") os.Exit(1) } // Re-run the program in background and exit cmd := exec.Command(os.Args[0], os.Args[1:]...) if err := cmd.Start(); err != nil { - fmt.Println(err) + fmt.Println("Failed to start daemon:", err) os.Exit(1) } fmt.Println("Running in daemon mode with PID:", cmd.Process.Pid) From 1ac9d54dab4911776d22ecadb281f52ca193dcef Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 10 Jun 2024 16:42:34 +0300 Subject: [PATCH 113/130] Code refactoring for stream MarshalJSON --- internal/streams/producer.go | 7 +++---- internal/streams/stream.go | 15 ++++----------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/internal/streams/producer.go b/internal/streams/producer.go index daca7edf..09e2dcc5 100644 --- a/internal/streams/producer.go +++ b/internal/streams/producer.go @@ -132,11 +132,10 @@ func (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re } func (p *Producer) MarshalJSON() ([]byte, error) { - if p.conn != nil { - return json.Marshal(p.conn) + if conn := p.conn; conn != nil { + return json.Marshal(conn) } - - info := core.Info{URL: p.url} + info := map[string]string{"url": p.url} return json.Marshal(info) } diff --git a/internal/streams/stream.go b/internal/streams/stream.go index 5dacf991..bb832694 100644 --- a/internal/streams/stream.go +++ b/internal/streams/stream.go @@ -112,19 +112,12 @@ producers: } func (s *Stream) MarshalJSON() ([]byte, error) { - if !s.mu.TryLock() { - log.Warn().Msgf("[streams] json locked") - return json.Marshal(nil) - } - - var info struct { + var info = struct { Producers []*Producer `json:"producers"` Consumers []core.Consumer `json:"consumers"` + }{ + Producers: s.producers, + Consumers: s.consumers, } - info.Producers = s.producers - info.Consumers = s.consumers - - s.mu.Unlock() - return json.Marshal(info) } From ecfe802065fdcdef770a0ed49aea24339a807212 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 14 Jun 2024 12:48:29 +0300 Subject: [PATCH 114/130] Code refactoring for streams HandleFunc --- internal/bubble/bubble.go | 12 +++--------- internal/debug/debug.go | 8 -------- internal/dvrip/dvrip.go | 11 +---------- internal/gopro/gopro.go | 8 +++----- internal/hass/hass.go | 9 ++------- internal/isapi/init.go | 15 +++------------ internal/ivideon/ivideon.go | 10 ++-------- internal/nest/init.go | 12 +++--------- internal/roborock/roborock.go | 15 +++------------ internal/tapo/tapo.go | 8 ++++---- pkg/bubble/client.go | 8 ++++++-- pkg/isapi/client.go | 8 ++++++-- pkg/ivideon/client.go | 9 +++++++-- pkg/nest/client.go | 2 +- pkg/roborock/client.go | 11 +++++++++-- 15 files changed, 53 insertions(+), 93 deletions(-) diff --git a/internal/bubble/bubble.go b/internal/bubble/bubble.go index 65d0237e..6c526fc5 100644 --- a/internal/bubble/bubble.go +++ b/internal/bubble/bubble.go @@ -7,13 +7,7 @@ import ( ) func Init() { - streams.HandleFunc("bubble", handle) -} - -func handle(url string) (core.Producer, error) { - conn := bubble.NewClient(url) - if err := conn.Dial(); err != nil { - return nil, err - } - return conn, nil + streams.HandleFunc("bubble", func(source string) (core.Producer, error) { + return bubble.Dial(source) + }) } diff --git a/internal/debug/debug.go b/internal/debug/debug.go index 3d40d1f1..fc7d2453 100644 --- a/internal/debug/debug.go +++ b/internal/debug/debug.go @@ -2,16 +2,8 @@ package debug import ( "github.com/AlexxIT/go2rtc/internal/api" - "github.com/AlexxIT/go2rtc/internal/streams" - "github.com/AlexxIT/go2rtc/pkg/core" ) func Init() { api.HandleFunc("api/stack", stackHandler) - - streams.HandleFunc("null", nullHandler) -} - -func nullHandler(string) (core.Producer, error) { - return nil, nil } diff --git a/internal/dvrip/dvrip.go b/internal/dvrip/dvrip.go index 095372d2..db1c60db 100644 --- a/internal/dvrip/dvrip.go +++ b/internal/dvrip/dvrip.go @@ -10,25 +10,16 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/streams" - "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/dvrip" ) func Init() { - streams.HandleFunc("dvrip", handle) + streams.HandleFunc("dvrip", dvrip.Dial) // DVRIP client autodiscovery api.HandleFunc("api/dvrip", apiDvrip) } -func handle(url string) (core.Producer, error) { - client, err := dvrip.Dial(url) - if err != nil { - return nil, err - } - return client, nil -} - const Port = 34569 // UDP port number for dvrip discovery func apiDvrip(w http.ResponseWriter, r *http.Request) { diff --git a/internal/gopro/gopro.go b/internal/gopro/gopro.go index 55d2641b..ee578049 100644 --- a/internal/gopro/gopro.go +++ b/internal/gopro/gopro.go @@ -10,15 +10,13 @@ import ( ) func Init() { - streams.HandleFunc("gopro", handleGoPro) + streams.HandleFunc("gopro", func(source string) (core.Producer, error) { + return gopro.Dial(source) + }) api.HandleFunc("api/gopro", apiGoPro) } -func handleGoPro(rawURL string) (core.Producer, error) { - return gopro.Dial(rawURL) -} - func apiGoPro(w http.ResponseWriter, r *http.Request) { var items []*api.Source diff --git a/internal/hass/hass.go b/internal/hass/hass.go index cd95ffe1..ea172b02 100644 --- a/internal/hass/hass.go +++ b/internal/hass/hass.go @@ -45,14 +45,9 @@ func Init() { return "", nil }) - streams.HandleFunc("hass", func(url string) (core.Producer, error) { + streams.HandleFunc("hass", func(source string) (core.Producer, error) { // support hass://supervisor?entity_id=camera.driveway_doorbell - client, err := hass.NewClient(url) - if err != nil { - return nil, err - } - - return client, nil + return hass.NewClient(source) }) // load static entries from Hass config diff --git a/internal/isapi/init.go b/internal/isapi/init.go index a37afa23..887a6748 100644 --- a/internal/isapi/init.go +++ b/internal/isapi/init.go @@ -7,16 +7,7 @@ import ( ) func Init() { - streams.HandleFunc("isapi", handle) -} - -func handle(url string) (core.Producer, error) { - conn, err := isapi.NewClient(url) - if err != nil { - return nil, err - } - if err = conn.Dial(); err != nil { - return nil, err - } - return conn, nil + streams.HandleFunc("isapi", func(source string) (core.Producer, error) { + return isapi.Dial(source) + }) } diff --git a/internal/ivideon/ivideon.go b/internal/ivideon/ivideon.go index 0ae5dc9f..03feb742 100644 --- a/internal/ivideon/ivideon.go +++ b/internal/ivideon/ivideon.go @@ -4,16 +4,10 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/ivideon" - "strings" ) func Init() { - 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 { - return nil, err - } - return prod, nil + streams.HandleFunc("ivideon", func(source string) (core.Producer, error) { + return ivideon.Dial(source) }) } diff --git a/internal/nest/init.go b/internal/nest/init.go index 1281ccdc..01682414 100644 --- a/internal/nest/init.go +++ b/internal/nest/init.go @@ -10,19 +10,13 @@ import ( ) func Init() { - streams.HandleFunc("nest", streamNest) + streams.HandleFunc("nest", func(source string) (core.Producer, error) { + return nest.Dial(source) + }) api.HandleFunc("api/nest", apiNest) } -func streamNest(url string) (core.Producer, error) { - client, err := nest.NewClient(url) - if err != nil { - return nil, err - } - return client, nil -} - func apiNest(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() cliendID := query.Get("client_id") diff --git a/internal/roborock/roborock.go b/internal/roborock/roborock.go index 27e29bb5..32a436d8 100644 --- a/internal/roborock/roborock.go +++ b/internal/roborock/roborock.go @@ -11,22 +11,13 @@ import ( ) func Init() { - streams.HandleFunc("roborock", handle) + streams.HandleFunc("roborock", func(source string) (core.Producer, error) { + return roborock.Dial(source) + }) api.HandleFunc("api/roborock", apiHandle) } -func handle(url string) (core.Producer, error) { - conn := roborock.NewClient(url) - if err := conn.Dial(); err != nil { - return nil, err - } - if err := conn.Connect(); err != nil { - return nil, err - } - return conn, nil -} - var Auth struct { UserData *roborock.UserInfo `json:"user_data"` BaseURL string `json:"base_url"` diff --git a/internal/tapo/tapo.go b/internal/tapo/tapo.go index a54c8c5e..724c9e86 100644 --- a/internal/tapo/tapo.go +++ b/internal/tapo/tapo.go @@ -8,11 +8,11 @@ import ( ) func Init() { - streams.HandleFunc("kasa", func(url string) (core.Producer, error) { - return kasa.Dial(url) + streams.HandleFunc("kasa", func(source string) (core.Producer, error) { + return kasa.Dial(source) }) - streams.HandleFunc("tapo", func(url string) (core.Producer, error) { - return tapo.Dial(url) + streams.HandleFunc("tapo", func(source string) (core.Producer, error) { + return tapo.Dial(source) }) } diff --git a/pkg/bubble/client.go b/pkg/bubble/client.go index b8b77ae9..c0a79701 100644 --- a/pkg/bubble/client.go +++ b/pkg/bubble/client.go @@ -43,8 +43,12 @@ type Client struct { recv int } -func NewClient(url string) *Client { - return &Client{url: url} +func Dial(rawURL string) (*Client, error) { + client := &Client{url: rawURL} + if err := client.Dial(); err != nil { + return nil, err + } + return client, nil } const ( diff --git a/pkg/isapi/client.go b/pkg/isapi/client.go index e5dfafd4..83dd9026 100644 --- a/pkg/isapi/client.go +++ b/pkg/isapi/client.go @@ -23,7 +23,7 @@ type Client struct { send int } -func NewClient(rawURL string) (*Client, error) { +func Dial(rawURL string) (*Client, error) { // check if url is valid url u, err := url.Parse(rawURL) if err != nil { @@ -33,7 +33,11 @@ func NewClient(rawURL string) (*Client, error) { u.Scheme = "http" u.Path = "" - return &Client{url: u.String()}, nil + client := &Client{url: u.String()} + if err = client.Dial(); err != nil { + return nil, err + } + return client, err } func (c *Client) Dial() (err error) { diff --git a/pkg/ivideon/client.go b/pkg/ivideon/client.go index c1b055b8..7cbf0b38 100644 --- a/pkg/ivideon/client.go +++ b/pkg/ivideon/client.go @@ -46,8 +46,13 @@ type Client struct { recv int } -func NewClient(id string) *Client { - return &Client{ID: id} +func Dial(source string) (*Client, error) { + id := strings.Replace(source[8:], "/", ":", 1) + client := &Client{ID: id} + if err := client.Dial(); err != nil { + return nil, err + } + return client, nil } func (c *Client) Dial() (err error) { diff --git a/pkg/nest/client.go b/pkg/nest/client.go index cb73cc98..2169773b 100644 --- a/pkg/nest/client.go +++ b/pkg/nest/client.go @@ -14,7 +14,7 @@ type Client struct { api *API } -func NewClient(rawURL string) (*Client, error) { +func Dial(rawURL string) (*Client, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err diff --git a/pkg/roborock/client.go b/pkg/roborock/client.go index 6a3bf0e0..522b0e13 100644 --- a/pkg/roborock/client.go +++ b/pkg/roborock/client.go @@ -34,8 +34,15 @@ type Client struct { backchannel bool } -func NewClient(url string) *Client { - return &Client{url: url} +func Dial(rawURL string) (*Client, error) { + client := &Client{url: rawURL} + if err := client.Dial(); err != nil { + return nil, err + } + if err := client.Connect(); err != nil { + return nil, err + } + return client, nil } func (c *Client) Dial() error { From 96504e2fb0c89870a6cd18e08d27af8e1cd1b0e8 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 15 Jun 2024 16:46:03 +0300 Subject: [PATCH 115/130] BIG rewrite stream info --- internal/exec/exec.go | 27 ++++- internal/ffmpeg/producer.go | 7 +- internal/hass/api.go | 2 +- internal/hls/hls.go | 11 +- internal/hls/ws.go | 6 +- internal/http/http.go | 29 +++-- internal/mjpeg/init.go | 27 ++--- internal/mp4/mp4.go | 7 +- internal/mp4/ws.go | 10 +- internal/mpegts/aac.go | 4 +- internal/mpegts/mpegts.go | 4 +- internal/rtmp/rtmp.go | 11 +- internal/streams/api.go | 4 +- internal/streams/handlers.go | 2 +- internal/webrtc/client.go | 8 +- internal/webrtc/kinesis.go | 8 +- internal/webrtc/milestone.go | 4 +- internal/webrtc/openipc.go | 4 +- internal/webrtc/server.go | 8 +- internal/webrtc/webrtc.go | 5 +- internal/webtorrent/init.go | 2 +- pkg/README.md | 82 +++++++++++++ pkg/aac/consumer.go | 23 ++-- pkg/aac/producer.go | 26 ++-- pkg/bubble/client.go | 1 + pkg/bubble/producer.go | 15 ++- pkg/core/codec.go | 2 +- pkg/core/connection.go | 139 ++++++++++++++++++++++ pkg/core/core.go | 89 +------------- pkg/core/media.go | 2 +- pkg/core/node.go | 7 +- pkg/core/track.go | 45 ++++++- pkg/dvrip/{consumer.go => backchannel.go} | 15 +-- pkg/dvrip/dvrip.go | 17 ++- pkg/dvrip/producer.go | 6 +- pkg/flv/consumer.go | 25 ++-- pkg/flv/producer.go | 19 +-- pkg/gopro/{gopro.go => producer.go} | 13 +- pkg/hass/client.go | 4 +- pkg/hls/producer.go | 11 +- pkg/homekit/consumer.go | 48 ++++---- pkg/homekit/{client.go => producer.go} | 17 +-- pkg/image/producer.go | 92 ++++++++++++++ pkg/isapi/{consumer.go => backchannel.go} | 14 ++- pkg/isapi/client.go | 1 + pkg/ivideon/client.go | 1 + pkg/ivideon/producer.go | 16 ++- pkg/kasa/producer.go | 24 ++-- pkg/magic/bitstream/producer.go | 24 ++-- pkg/magic/keyframe.go | 39 +++--- pkg/magic/mjpeg/producer.go | 21 ++-- pkg/magic/producer.go | 34 +++--- pkg/mjpeg/client.go | 75 ------------ pkg/mjpeg/consumer.go | 37 +++--- pkg/mjpeg/producer.go | 61 ---------- pkg/mp4/consumer.go | 20 ++-- pkg/mp4/keyframe.go | 16 +-- pkg/mpegts/consumer.go | 37 +++--- pkg/mpegts/producer.go | 20 ++-- pkg/{multipart => mpjpeg}/multipart.go | 2 +- pkg/mpjpeg/producer.go | 65 ++++++++++ pkg/multipart/producer.go | 68 ----------- pkg/nest/client.go | 4 +- pkg/probe/{probe.go => producer.go} | 20 ++-- pkg/roborock/client.go | 5 +- pkg/rtmp/client.go | 9 +- pkg/rtmp/flv.go | 15 ++- pkg/rtsp/client.go | 16 ++- pkg/rtsp/conn.go | 20 +--- pkg/rtsp/consumer.go | 17 +-- pkg/rtsp/producer.go | 32 ++--- pkg/rtsp/server.go | 30 +++-- pkg/stdin/{consumer.go => backchannel.go} | 10 +- pkg/stdin/client.go | 1 + pkg/tapo/{consumer.go => backchannel.go} | 0 pkg/tapo/client.go | 1 + pkg/tapo/producer.go | 18 ++- pkg/tcp/helpers.go | 12 -- pkg/wav/{wav.go => producer.go} | 22 ++-- pkg/webrtc/client.go | 2 +- pkg/webrtc/conn.go | 51 +++++--- pkg/webrtc/consumer.go | 23 +--- pkg/webrtc/producer.go | 14 +-- pkg/webrtc/server.go | 4 +- pkg/webtorrent/client.go | 8 +- pkg/y4m/consumer.go | 16 ++- pkg/y4m/producer.go | 55 +++------ pkg/y4m/y4m.go | 29 +++++ 88 files changed, 1043 insertions(+), 854 deletions(-) create mode 100644 pkg/core/connection.go rename pkg/dvrip/{consumer.go => backchannel.go} (78%) rename pkg/gopro/{gopro.go => producer.go} (90%) rename pkg/homekit/{client.go => producer.go} (95%) create mode 100644 pkg/image/producer.go rename pkg/isapi/{consumer.go => backchannel.go} (83%) delete mode 100644 pkg/mjpeg/client.go delete mode 100644 pkg/mjpeg/producer.go rename pkg/{multipart => mpjpeg}/multipart.go (98%) create mode 100644 pkg/mpjpeg/producer.go delete mode 100644 pkg/multipart/producer.go rename pkg/probe/{probe.go => producer.go} (72%) rename pkg/stdin/{consumer.go => backchannel.go} (88%) rename pkg/tapo/{consumer.go => backchannel.go} (100%) delete mode 100644 pkg/tcp/helpers.go rename pkg/wav/{wav.go => producer.go} (89%) diff --git a/internal/exec/exec.go b/internal/exec/exec.go index 6c41fe9e..ac1691d3 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "os/exec" + "slices" "strings" "sync" "time" @@ -80,7 +81,7 @@ func execHandle(rawURL string) (core.Producer, error) { return handleRTSP(rawURL, cmd, path) } -func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error) { +func handlePipe(source string, cmd *exec.Cmd, query url.Values) (core.Producer, error) { if query.Get("backchannel") == "1" { return stdin.NewClient(cmd) } @@ -104,12 +105,17 @@ func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr) } + if info, ok := prod.(core.Info); ok { + info.SetProtocol("pipe") + setRemoteInfo(info, source, cmd.Args) + } + log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run pipe") return prod, nil } -func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { +func handleRTSP(source string, cmd *exec.Cmd, path string) (core.Producer, error) { if log.Trace().Enabled() { cmd.Stdout = os.Stdout } @@ -131,7 +137,7 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { ts := time.Now() if err := cmd.Start(); err != nil { - log.Error().Err(err).Str("url", url).Msg("[exec]") + log.Error().Err(err).Str("source", source).Msg("[exec]") return nil, err } @@ -143,13 +149,14 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { select { case <-time.After(time.Second * 60): _ = cmd.Process.Kill() - log.Error().Str("url", url).Msg("[exec] timeout") + log.Error().Str("source", source).Msg("[exec] timeout") return nil, errors.New("exec: timeout") case <-done: // limit message size return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr) case prod := <-waiter: log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp") + setRemoteInfo(prod, source, cmd.Args) prod.OnClose = func() error { log.Debug().Msgf("[exec] kill rtsp") return errors.Join(cmd.Process.Kill(), cmd.Wait()) @@ -210,3 +217,15 @@ func trimSpace(b []byte) []byte { } return b[start:stop] } + +func setRemoteInfo(info core.Info, source string, args []string) { + info.SetSource(source) + + if i := slices.Index(args, "-i"); i > 0 && i < len(args)-1 { + rawURL := args[i+1] + if u, err := url.Parse(rawURL); err == nil && u.Host != "" { + info.SetRemoteAddr(u.Host) + info.SetURL(rawURL) + } + } +} diff --git a/internal/ffmpeg/producer.go b/internal/ffmpeg/producer.go index 05df69e3..d132d253 100644 --- a/internal/ffmpeg/producer.go +++ b/internal/ffmpeg/producer.go @@ -13,7 +13,7 @@ import ( ) type Producer struct { - core.SuperProducer + core.Connection url string query url.Values ffmpeg core.Producer @@ -31,7 +31,8 @@ func NewProducer(url string) (core.Producer, error) { return nil, errors.New("ffmpeg: unsupported params: " + url[i:]) } - p.Type = "FFmpeg producer" + p.ID = core.NewID() + p.FormatName = "ffmpeg" p.Medias = []*core.Media{ { // we can support only audio, because don't know FmtpLine for H264 and PayloadType for MJPEG @@ -81,7 +82,7 @@ func (p *Producer) Stop() error { func (p *Producer) MarshalJSON() ([]byte, error) { if p.ffmpeg == nil { - return json.Marshal(p.SuperProducer) + return json.Marshal(p.Connection) } return json.Marshal(p.ffmpeg) } diff --git a/internal/hass/api.go b/internal/hass/api.go index 4628cc11..e3de23b3 100644 --- a/internal/hass/api.go +++ b/internal/hass/api.go @@ -63,7 +63,7 @@ func apiStream(w http.ResponseWriter, r *http.Request) { return } - s, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent()) + s, err = webrtc.ExchangeSDP(stream, string(offer), "hass/webrtc", r.UserAgent()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/internal/hls/hls.go b/internal/hls/hls.go index 5d3cd918..5c136450 100644 --- a/internal/hls/hls.go +++ b/internal/hls/hls.go @@ -12,7 +12,6 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mp4" "github.com/AlexxIT/go2rtc/pkg/mpegts" - "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog" ) @@ -63,15 +62,13 @@ func handlerStream(w http.ResponseWriter, r *http.Request) { medias := mp4.ParseQuery(r.URL.Query()) if medias != nil { c := mp4.NewConsumer(medias) - c.Type = "HLS/fMP4 consumer" - c.RemoteAddr = tcp.RemoteAddr(r) - c.UserAgent = r.UserAgent() + c.FormatName = "hls/fmp4" + c.WithRequest(r) cons = c } else { c := mpegts.NewConsumer() - c.Type = "HLS/TS consumer" - c.RemoteAddr = tcp.RemoteAddr(r) - c.UserAgent = r.UserAgent() + c.FormatName = "hls/mpegts" + c.WithRequest(r) cons = c } diff --git a/internal/hls/ws.go b/internal/hls/ws.go index ea1f5a3a..608f515f 100644 --- a/internal/hls/ws.go +++ b/internal/hls/ws.go @@ -8,7 +8,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/api/ws" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/mp4" - "github.com/AlexxIT/go2rtc/pkg/tcp" ) func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error { @@ -20,9 +19,8 @@ func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error { codecs := msg.String() medias := mp4.ParseCodecs(codecs, true) cons := mp4.NewConsumer(medias) - cons.Type = "HLS/fMP4 consumer" - cons.RemoteAddr = tcp.RemoteAddr(tr.Request) - cons.UserAgent = tr.Request.UserAgent() + cons.FormatName = "hls/fmp4" + cons.WithRequest(tr.Request) log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs) diff --git a/internal/http/http.go b/internal/http/http.go index 8b1903f3..a35439d5 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -11,9 +11,9 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/hls" + "github.com/AlexxIT/go2rtc/pkg/image" "github.com/AlexxIT/go2rtc/pkg/magic" - "github.com/AlexxIT/go2rtc/pkg/mjpeg" - "github.com/AlexxIT/go2rtc/pkg/multipart" + "github.com/AlexxIT/go2rtc/pkg/mpjpeg" "github.com/AlexxIT/go2rtc/pkg/tcp" ) @@ -45,6 +45,21 @@ func handleHTTP(rawURL string) (core.Producer, error) { } } + prod, err := do(req) + if err != nil { + return nil, err + } + + if info, ok := prod.(core.Info); ok { + info.SetProtocol("http") + info.SetRemoteAddr(req.URL.Host) // TODO: rewrite to net.Conn + info.SetURL(rawURL) + } + + return prod, nil +} + +func do(req *http.Request) (core.Producer, error) { res, err := tcp.Do(req) if err != nil { return nil, err @@ -66,14 +81,12 @@ func handleHTTP(rawURL string) (core.Producer, error) { } switch { - case ct == "image/jpeg": - return mjpeg.NewClient(res), nil - - case ct == "multipart/x-mixed-replace": - return multipart.Open(res.Body) - case ct == "application/vnd.apple.mpegurl" || ext == "m3u8": return hls.OpenURL(req.URL, res.Body) + case ct == "image/jpeg": + return image.Open(res) + case ct == "multipart/x-mixed-replace": + return mpjpeg.Open(res.Body) } return magic.Open(res.Body) diff --git a/internal/mjpeg/init.go b/internal/mjpeg/init.go index 0bed95c6..2bb7093a 100644 --- a/internal/mjpeg/init.go +++ b/internal/mjpeg/init.go @@ -17,7 +17,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/magic" "github.com/AlexxIT/go2rtc/pkg/mjpeg" - "github.com/AlexxIT/go2rtc/pkg/tcp" + "github.com/AlexxIT/go2rtc/pkg/mpjpeg" "github.com/AlexxIT/go2rtc/pkg/y4m" "github.com/rs/zerolog" ) @@ -44,8 +44,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) { } cons := magic.NewKeyframe() - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() + cons.WithRequest(r) if err := stream.AddConsumer(cons); err != nil { log.Error().Err(err).Caller().Send() @@ -100,8 +99,7 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) { } cons := mjpeg.NewConsumer() - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() + cons.WithRequest(r) if err := stream.AddConsumer(cons); err != nil { log.Error().Err(err).Msg("[api.mjpeg] add consumer") @@ -117,7 +115,7 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) { wr := mjpeg.NewWriter(w) _, _ = cons.WriteTo(wr) } else { - cons.Type = "ASCII passive consumer " + cons.FormatName = "ascii" query := r.URL.Query() wr := ascii.NewWriter(w, query.Get("color"), query.Get("back"), query.Get("text")) @@ -135,17 +133,16 @@ func inputMjpeg(w http.ResponseWriter, r *http.Request) { return } - res := &http.Response{Body: r.Body, Header: r.Header, Request: r} - res.Header.Set("Content-Type", "multipart/mixed;boundary=") + prod, _ := mpjpeg.Open(r.Body) + prod.WithRequest(r) - client := mjpeg.NewClient(res) - stream.AddProducer(client) + stream.AddProducer(prod) - if err := client.Start(); err != nil && err != io.EOF { + if err := prod.Start(); err != nil && err != io.EOF { log.Warn().Err(err).Caller().Send() } - stream.RemoveProducer(client) + stream.RemoveProducer(prod) } func handlerWS(tr *ws.Transport, _ *ws.Message) error { @@ -155,8 +152,7 @@ func handlerWS(tr *ws.Transport, _ *ws.Message) error { } cons := mjpeg.NewConsumer() - cons.RemoteAddr = tcp.RemoteAddr(tr.Request) - cons.UserAgent = tr.Request.UserAgent() + cons.WithRequest(tr.Request) if err := stream.AddConsumer(cons); err != nil { log.Debug().Err(err).Msg("[mjpeg] add consumer") @@ -183,8 +179,7 @@ func apiStreamY4M(w http.ResponseWriter, r *http.Request) { } cons := y4m.NewConsumer() - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() + cons.WithRequest(r) if err := stream.AddConsumer(cons); err != nil { log.Error().Err(err).Caller().Send() diff --git a/internal/mp4/mp4.go b/internal/mp4/mp4.go index 2f59ba04..cca5220c 100644 --- a/internal/mp4/mp4.go +++ b/internal/mp4/mp4.go @@ -13,7 +13,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mp4" - "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog" ) @@ -100,9 +99,9 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { medias := mp4.ParseQuery(r.URL.Query()) cons := mp4.NewConsumer(medias) - cons.Type = "MP4/HTTP active consumer" - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() + cons.FormatName = "mp4" + cons.Protocol = "http" + cons.WithRequest(r) if err := stream.AddConsumer(cons); err != nil { log.Error().Err(err).Caller().Send() diff --git a/internal/mp4/ws.go b/internal/mp4/ws.go index 060ff5f6..c880fb58 100644 --- a/internal/mp4/ws.go +++ b/internal/mp4/ws.go @@ -8,7 +8,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mp4" - "github.com/AlexxIT/go2rtc/pkg/tcp" ) func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error { @@ -24,9 +23,8 @@ func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error { } cons := mp4.NewConsumer(medias) - cons.Type = "MSE/WebSocket active consumer" - cons.RemoteAddr = tcp.RemoteAddr(tr.Request) - cons.UserAgent = tr.Request.UserAgent() + cons.FormatName = "mse/fmp4" + cons.WithRequest(tr.Request) if err := stream.AddConsumer(cons); err != nil { log.Debug().Err(err).Msg("[mp4] add consumer") @@ -57,9 +55,7 @@ func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error { } cons := mp4.NewKeyframe(medias) - cons.Type = "MP4/WebSocket active consumer" - cons.RemoteAddr = tcp.RemoteAddr(tr.Request) - cons.UserAgent = tr.Request.UserAgent() + cons.WithRequest(tr.Request) if err := stream.AddConsumer(cons); err != nil { log.Error().Err(err).Caller().Send() diff --git a/internal/mpegts/aac.go b/internal/mpegts/aac.go index 867dc971..3b1522fe 100644 --- a/internal/mpegts/aac.go +++ b/internal/mpegts/aac.go @@ -6,7 +6,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/aac" - "github.com/AlexxIT/go2rtc/pkg/tcp" ) func apiStreamAAC(w http.ResponseWriter, r *http.Request) { @@ -18,8 +17,7 @@ func apiStreamAAC(w http.ResponseWriter, r *http.Request) { } cons := aac.NewConsumer() - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() + cons.WithRequest(r) if err := stream.AddConsumer(cons); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/internal/mpegts/mpegts.go b/internal/mpegts/mpegts.go index 6ef00ba1..d5f7752b 100644 --- a/internal/mpegts/mpegts.go +++ b/internal/mpegts/mpegts.go @@ -6,7 +6,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/mpegts" - "github.com/AlexxIT/go2rtc/pkg/tcp" ) func Init() { @@ -31,8 +30,7 @@ func outputMpegTS(w http.ResponseWriter, r *http.Request) { } cons := mpegts.NewConsumer() - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() + cons.WithRequest(r) if err := stream.AddConsumer(cons); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/internal/rtmp/rtmp.go b/internal/rtmp/rtmp.go index 07aa5f71..afc363a9 100644 --- a/internal/rtmp/rtmp.go +++ b/internal/rtmp/rtmp.go @@ -12,7 +12,6 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/flv" "github.com/AlexxIT/go2rtc/pkg/rtmp" - "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog" ) @@ -128,11 +127,7 @@ func tcpHandle(netConn net.Conn) error { var log zerolog.Logger func streamsHandle(url string) (core.Producer, error) { - client, err := rtmp.DialPlay(url) - if err != nil { - return nil, err - } - return client, nil + return rtmp.DialPlay(url) } func streamsConsumerHandle(url string) (core.Consumer, func(), error) { @@ -165,9 +160,7 @@ func outputFLV(w http.ResponseWriter, r *http.Request) { } cons := flv.NewConsumer() - cons.Type = "HTTP-FLV consumer" - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() + cons.WithRequest(r) if err := stream.AddConsumer(cons); err != nil { log.Error().Err(err).Caller().Send() diff --git a/internal/streams/api.go b/internal/streams/api.go index 72099425..69d2276a 100644 --- a/internal/streams/api.go +++ b/internal/streams/api.go @@ -6,7 +6,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/pkg/probe" - "github.com/AlexxIT/go2rtc/pkg/tcp" ) func apiStreams(w http.ResponseWriter, r *http.Request) { @@ -30,8 +29,7 @@ func apiStreams(w http.ResponseWriter, r *http.Request) { cons := probe.NewProbe(query) if len(cons.Medias) != 0 { - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() + cons.WithRequest(r) if err := stream.AddConsumer(cons); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/internal/streams/handlers.go b/internal/streams/handlers.go index 3009dd66..3240abb5 100644 --- a/internal/streams/handlers.go +++ b/internal/streams/handlers.go @@ -7,7 +7,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" ) -type Handler func(url string) (core.Producer, error) +type Handler func(source string) (core.Producer, error) var handlers = map[string]Handler{} diff --git a/internal/webrtc/client.go b/internal/webrtc/client.go index ae1a455b..4b8b1b9a 100644 --- a/internal/webrtc/client.go +++ b/internal/webrtc/client.go @@ -41,7 +41,7 @@ func streamsHandler(rawURL string) (core.Producer, error) { // https://aws.amazon.com/kinesis/video-streams/ // https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html // https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc - return kinesisClient(rawURL, query, "WebRTC/Kinesis") + return kinesisClient(rawURL, query, "webrtc/kinesis") } else if format == "openipc" { return openIPCClient(rawURL, query) } else { @@ -86,8 +86,9 @@ func go2rtcClient(url string) (core.Producer, error) { var connMu sync.Mutex prod := webrtc.NewConn(pc) - prod.Desc = "WebRTC/WebSocket async" prod.Mode = core.ModeActiveProducer + prod.Protocol = "ws" + prod.URL = url prod.Listen(func(msg any) { switch msg := msg.(type) { case *pion.ICECandidate: @@ -180,8 +181,9 @@ func whepClient(url string) (core.Producer, error) { } prod := webrtc.NewConn(pc) - prod.Desc = "WebRTC/WHEP sync" prod.Mode = core.ModeActiveProducer + prod.Protocol = "http" + prod.URL = url medias := []*core.Media{ {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, diff --git a/internal/webrtc/kinesis.go b/internal/webrtc/kinesis.go index 7ef9d9bb..2ea1cf7a 100644 --- a/internal/webrtc/kinesis.go +++ b/internal/webrtc/kinesis.go @@ -34,7 +34,7 @@ func (k kinesisResponse) String() string { return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload) } -func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer, error) { +func kinesisClient(rawURL string, query url.Values, format string) (core.Producer, error) { // 1. Connect to signalign server conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil) if err != nil { @@ -79,8 +79,10 @@ func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer, } prod := webrtc.NewConn(pc) - prod.Desc = desc + prod.FormatName = format prod.Mode = core.ModeActiveProducer + prod.Protocol = "ws" + prod.URL = rawURL prod.Listen(func(msg any) { switch msg := msg.(type) { case *pion.ICECandidate: @@ -216,5 +218,5 @@ func wyzeClient(rawURL string) (core.Producer, error) { "ice_servers": []string{string(kvs.Servers)}, } - return kinesisClient(kvs.URL, query, "WebRTC/Wyze") + return kinesisClient(kvs.URL, query, "webrtc/wyze") } diff --git a/internal/webrtc/milestone.go b/internal/webrtc/milestone.go index b4e695c9..6a696cb0 100644 --- a/internal/webrtc/milestone.go +++ b/internal/webrtc/milestone.go @@ -193,8 +193,10 @@ func milestoneClient(rawURL string, query url.Values) (core.Producer, error) { } prod := webrtc.NewConn(pc) - prod.Desc = "WebRTC/Milestone" + prod.FormatName = "webrtc/milestone" prod.Mode = core.ModeActiveProducer + prod.Protocol = "http" + prod.URL = rawURL offer, err := mc.GetOffer() if err != nil { diff --git a/internal/webrtc/openipc.go b/internal/webrtc/openipc.go index 8055ea91..8a951d04 100644 --- a/internal/webrtc/openipc.go +++ b/internal/webrtc/openipc.go @@ -53,8 +53,10 @@ func openIPCClient(rawURL string, query url.Values) (core.Producer, error) { var connState core.Waiter prod := webrtc.NewConn(pc) - prod.Desc = "WebRTC/OpenIPC" + prod.FormatName = "webrtc/openipc" prod.Mode = core.ModeActiveProducer + prod.Protocol = "ws" + prod.URL = rawURL prod.Listen(func(msg any) { switch msg := msg.(type) { case *pion.ICECandidate: diff --git a/internal/webrtc/server.go b/internal/webrtc/server.go index fcb72b85..91a237db 100644 --- a/internal/webrtc/server.go +++ b/internal/webrtc/server.go @@ -100,11 +100,11 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) { switch mediaType { case "application/json": - desc = "WebRTC/JSON sync" + desc = "webrtc/json" case MimeSDP: - desc = "WebRTC/WHEP sync" + desc = "webrtc/whep" default: - desc = "WebRTC/HTTP sync" + desc = "webrtc/post" } answer, err := ExchangeSDP(stream, offer, desc, r.UserAgent()) @@ -168,8 +168,8 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) { // create new webrtc instance prod := webrtc.NewConn(pc) - prod.Desc = "WebRTC/WHIP sync" prod.Mode = core.ModePassiveProducer + prod.Protocol = "http" prod.UserAgent = r.UserAgent() if err = prod.SetOffer(string(offer)); err != nil { diff --git a/internal/webrtc/webrtc.go b/internal/webrtc/webrtc.go index cabd88b7..8b4943c3 100644 --- a/internal/webrtc/webrtc.go +++ b/internal/webrtc/webrtc.go @@ -117,8 +117,8 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error { defer sendAnswer.Done(nil) conn := webrtc.NewConn(pc) - conn.Desc = "WebRTC/WebSocket async" conn.Mode = mode + conn.Protocol = "ws" conn.UserAgent = tr.Request.UserAgent() conn.Listen(func(msg any) { switch msg := msg.(type) { @@ -207,8 +207,9 @@ func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer // create new webrtc instance conn := webrtc.NewConn(pc) - conn.Desc = desc + conn.FormatName = desc conn.UserAgent = userAgent + conn.Protocol = "http" conn.Listen(func(msg any) { switch msg := msg.(type) { case pion.PeerConnectionState: diff --git a/internal/webtorrent/init.go b/internal/webtorrent/init.go index 25b7ef9b..b1c25c76 100644 --- a/internal/webtorrent/init.go +++ b/internal/webtorrent/init.go @@ -47,7 +47,7 @@ func Init() { if stream == nil { return "", errors.New(api.StreamNotFound) } - return webrtc.ExchangeSDP(stream, offer, "WebRTC/WebTorrent sync", "") + return webrtc.ExchangeSDP(stream, offer, "webtorrent", "") }, } diff --git a/pkg/README.md b/pkg/README.md index c875dc35..b12f0a70 100644 --- a/pkg/README.md +++ b/pkg/README.md @@ -1,3 +1,85 @@ +# Notes + +go2rtc tries to name formats, protocols and codecs the same way they are named in FFmpeg. +Some formats and protocols go2rtc supports exclusively. They have no equivalent in FFmpeg. + +## Producers (input) + +- The initiator of the connection can be go2rtc - **Source protocols** +- The initiator of the connection can be an external program - **Ingress protocols** +- Codecs can be incoming - **Recevers codecs** +- Codecs can be outgoing (two way audio) - **Senders codecs** + +| Format | Source protocols | Ingress protocols | Recevers codecs | Senders codecs | Example | +|--------------|------------------|-------------------|------------------------------|--------------------|---------------| +| adts | http,tcp,pipe | http | aac | | `http:` | +| bubble | http | | h264,hevc,pcm_alaw | | `bubble:` | +| dvrip | tcp | | h264,hevc,pcm_alaw,pcm_mulaw | pcm_alaw | `dvrip:` | +| flv | http,tcp,pipe | http | h264,aac | | `http:` | +| gopro | http+udp | | TODO | | `gopro:` | +| hass/webrtc | ws+udp,tcp | | TODO | | `hass:` | +| hls/mpegts | http | | h264,h265,aac,opus | | `http:` | +| homekit | homekit+udp | | h264,eld* | | `homekit:` | +| isapi | http | | | pcm_alaw,pcm_mulaw | `isapi:` | +| ivideon | ws | | h264 | | `ivideon:` | +| kasa | http | | h264,pcm_mulaw | | `kasa:` | +| h264 | http,tcp,pipe | http | h264 | | `http:` | +| hevc | http,tcp,pipe | http | hevc | | `http:` | +| mjpeg | http,tcp,pipe | http | mjpeg | | `http:` | +| mpjpeg | http,tcp,pipe | http | mjpeg | | `http:` | +| mpegts | http,tcp,pipe | http | h264,hevc,aac,opus | | `http:` | +| nest/webrtc | http+udp | | TODO | | `nest:` | +| roborock | mqtt+udp | | h264,opus | opus | `roborock:` | +| rtmp | rtmp | rtmp | h264,aac | | `rtmp:` | +| rtsp | rtsp+tcp,ws | rtsp+tcp | h264,hevc,aac,pcm*,opus | pcm*,opus | `rtsp:` | +| stdin | pipe | | | pcm_alaw,pcm_mulaw | `stdin:` | +| tapo | http | | h264,pcma | pcm_alaw | `tapo:` | +| wav | http,tcp,pipe | http | pcm_alaw,pcm_mulaw | | `http:` | +| webrtc* | TODO | TODO | h264,pcm_alaw,pcm_mulaw,opus | pcm_alaw,pcm_mulaw | `webrtc:` | +| webtorrent | TODO | TODO | TODO | TODO | `webtorrent:` | +| yuv4mpegpipe | http,tcp,pipe | http | rawvideo | | `http:` | + +- **eld** - rare variant of aac codec +- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le +- **webrtc** - webrtc/kinesis, webrtc/openipc, webrtc/milestone, webrtc/wyze, webrtc/whep + +## Consumers (output) + +| Format | Protocol | Send codecs | Recv codecs | Example | +|--------------|-------------|------------------------------|-------------------------|---------------------------------------| +| adts | http | aac | | `GET /api/stream.adts` | +| ascii | http | mjpeg | | `GET /api/stream.ascii` | +| flv | http | h264,aac | | `GET /api/stream.flv` | +| hls/mpegts | http | h264,hevc,aac | | `GET /api/stream.m3u8` | +| hls/fmp4 | http | h264,hevc,aac,pcm*,opus | | `GET /api/stream.m3u8?mp4` | +| homekit | homekit+udp | h264,opus | | Apple HomeKit app | +| mjpeg | ws | mjpeg | | `{"type":"mjpeg"}` -> `/api/ws` | +| mpjpeg | http | mjpeg | | `GET /api/stream.mjpeg` | +| mp4 | http | h264,hevc,aac,pcm*,opus | | `GET /api/stream.mp4` | +| mse/fmp4 | ws | h264,hevc,aac,pcm*,opus | | `{"type":"mse"}` -> `/api/ws` | +| mpegts | http | h264,hevc,aac | | `GET /api/stream.ts` | +| rtmp | rtmp | h264,aac | | `rtmp://localhost:1935/{stream_name}` | +| rtsp | rtsp+tcp | h264,hevc,aac,pcm*,opus | | `rtsp://localhost:8554/{stream_name}` | +| webrtc | TODO | h264,pcm_alaw,pcm_mulaw,opus | pcm_alaw,pcm_mulaw,opus | `{"type":"webrtc"}` -> `/api/ws` | +| yuv4mpegpipe | http | rawvideo | | `GET /api/stream.y4m` | + +- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le + +## Snapshots + +| Format | Protocol | Send codecs | Example | +|--------|----------|-------------|-----------------------| +| jpeg | http | mjpeg | `GET /api/frame.jpeg` | +| mp4 | http | h264,hevc | `GET /api/frame.mp4` | + +## Developers + +File naming: + +- `pkg/{format}/producer.go` - producer for this format (also if support backchannel) +- `pkg/{format}/consumer.go` - consumer for this format +- `pkg/{format}/backchanel.go` - producer with only backchannel func + ## Useful links - https://www.wowza.com/blog/streaming-protocols diff --git a/pkg/aac/consumer.go b/pkg/aac/consumer.go index e785adc5..fc67d2a4 100644 --- a/pkg/aac/consumer.go +++ b/pkg/aac/consumer.go @@ -8,15 +8,12 @@ import ( ) type Consumer struct { - core.SuperConsumer + core.Connection wr *core.WriteBuffer } func NewConsumer() *Consumer { - cons := &Consumer{ - wr: core.NewWriteBuffer(nil), - } - cons.Medias = []*core.Media{ + medias := []*core.Media{ { Kind: core.KindAudio, Direction: core.DirectionSendonly, @@ -25,7 +22,16 @@ func NewConsumer() *Consumer { }, }, } - return cons + wr := core.NewWriteBuffer(nil) + return &Consumer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "adts", + Medias: medias, + Transport: wr, + }, + wr: wr, + } } func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { @@ -51,8 +57,3 @@ func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re func (c *Consumer) WriteTo(wr io.Writer) (int64, error) { return c.wr.WriteTo(wr) } - -func (c *Consumer) Stop() error { - _ = c.SuperConsumer.Close() - return c.wr.Close() -} diff --git a/pkg/aac/producer.go b/pkg/aac/producer.go index e9be71fd..efd2d175 100644 --- a/pkg/aac/producer.go +++ b/pkg/aac/producer.go @@ -10,9 +10,8 @@ import ( ) type Producer struct { - core.SuperProducer + core.Connection rd *bufio.Reader - cl io.Closer } func Open(r io.Reader) (*Producer, error) { @@ -23,18 +22,22 @@ func Open(r io.Reader) (*Producer, error) { return nil, err } - codec := ADTSToCodec(b) - - prod := &Producer{rd: rd, cl: r.(io.Closer)} - prod.Type = "ADTS producer" - prod.Medias = []*core.Media{ + medias := []*core.Media{ { Kind: core.KindAudio, Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{codec}, + Codecs: []*core.Codec{ADTSToCodec(b)}, }, } - return prod, nil + return &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "adts", + Medias: medias, + Transport: r, + }, + rd: rd, + }, nil } func (c *Producer) Start() error { @@ -66,8 +69,3 @@ func (c *Producer) Start() error { c.Receivers[0].WriteRTP(pkt) } } - -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.cl.Close() -} diff --git a/pkg/bubble/client.go b/pkg/bubble/client.go index c0a79701..5afba779 100644 --- a/pkg/bubble/client.go +++ b/pkg/bubble/client.go @@ -22,6 +22,7 @@ import ( "github.com/pion/rtp" ) +// Deprecated: should be rewritten to core.Connection type Client struct { core.Listener diff --git a/pkg/bubble/producer.go b/pkg/bubble/producer.go index a7aaa56e..9fa18f25 100644 --- a/pkg/bubble/producer.go +++ b/pkg/bubble/producer.go @@ -65,11 +65,16 @@ func (c *Client) Stop() error { } func (c *Client) MarshalJSON() ([]byte, error) { - info := &core.Info{ - Type: "Bubble active producer", - Medias: c.medias, - Recv: c.recv, - Receivers: c.receivers, + info := &core.Connection{ + ID: core.ID(c), + FormatName: "bubble", + Protocol: "http", + Medias: c.medias, + Recv: c.recv, + Receivers: c.receivers, + } + if c.conn != nil { + info.RemoteAddr = c.conn.RemoteAddr().String() } return json.Marshal(info) } diff --git a/pkg/core/codec.go b/pkg/core/codec.go index 91f6fddc..d07b8b74 100644 --- a/pkg/core/codec.go +++ b/pkg/core/codec.go @@ -46,7 +46,7 @@ func FFmpegCodecName(name string) string { case CodecH264: return "h264" case CodecH265: - return "h265" + return "hevc" case CodecJPEG: return "mjpeg" case CodecRAW: diff --git a/pkg/core/connection.go b/pkg/core/connection.go new file mode 100644 index 00000000..1055c381 --- /dev/null +++ b/pkg/core/connection.go @@ -0,0 +1,139 @@ +package core + +import ( + "io" + "net/http" + "reflect" + "sync/atomic" +) + +func NewID() uint32 { + return id.Add(1) +} + +// Deprecated: use NewID instead +func ID(v any) uint32 { + p := uintptr(reflect.ValueOf(v).UnsafePointer()) + return 0x8000_0000 | uint32(p) +} + +var id atomic.Uint32 + +type Info interface { + SetProtocol(string) + SetRemoteAddr(string) + SetSource(string) + SetURL(string) + WithRequest(*http.Request) +} + +// Connection just like webrtc.PeerConnection +// - ID and RemoteAddr used for building Connection(s) graph +// - FormatName, Protocol, RemoteAddr, Source, URL, SDP, UserAgent used for info about Connection +// - FormatName and Protocol has FFmpeg compatible names +// - Transport used for auto closing on Stop +type Connection struct { + ID uint32 `json:"id,omitempty"` + FormatName string `json:"format_name,omitempty"` // rtsp, webrtc, mp4, mjpeg, mpjpeg... + Protocol string `json:"protocol,omitempty"` // tcp, udp, http, ws, pipe... + RemoteAddr string `json:"remote_addr,omitempty"` // host:port other info + Source string `json:"source,omitempty"` + URL string `json:"url,omitempty"` + SDP string `json:"sdp,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:"bytes_recv,omitempty"` + Send int `json:"bytes_send,omitempty"` + + Transport any `json:"-"` +} + +func (c *Connection) GetMedias() []*Media { + return c.Medias +} + +func (c *Connection) GetTrack(media *Media, codec *Codec) (*Receiver, error) { + for _, receiver := range c.Receivers { + if receiver.Codec == codec { + return receiver, nil + } + } + receiver := NewReceiver(media, codec) + c.Receivers = append(c.Receivers, receiver) + return receiver, nil +} + +func (c *Connection) Stop() error { + for _, receiver := range c.Receivers { + receiver.Close() + } + for _, sender := range c.Senders { + sender.Close() + } + if closer, ok := c.Transport.(io.Closer); ok { + return closer.Close() + } + return nil +} + +// Deprecated: +func (c *Connection) Codecs() []*Codec { + codecs := make([]*Codec, len(c.Senders)) + for i, sender := range c.Senders { + codecs[i] = sender.Codec + } + return codecs +} + +func (c *Connection) SetProtocol(s string) { + c.Protocol = s +} + +func (c *Connection) SetRemoteAddr(s string) { + if c.RemoteAddr == "" { + c.RemoteAddr = s + } else { + c.RemoteAddr += " forward " + c.RemoteAddr + } +} + +func (c *Connection) SetSource(s string) { + c.Source = s +} + +func (c *Connection) SetURL(s string) { + c.URL = s +} + +func (c *Connection) WithRequest(r *http.Request) { + if r.Header.Get("Upgrade") == "websocket" { + c.Protocol = "ws" + } else { + c.Protocol = "http" + } + + c.RemoteAddr = r.RemoteAddr + if remote := r.Header.Get("X-Forwarded-For"); remote != "" { + c.RemoteAddr += " forwarded " + remote + } + + c.UserAgent = r.UserAgent() +} + +// Create like os.Create, init Consumer with existing Transport +func Create(w io.Writer) (*Connection, error) { + return &Connection{Transport: w}, nil +} + +// Open like os.Open, init Producer from existing Transport +func Open(r io.Reader) (*Connection, error) { + return &Connection{Transport: r}, nil +} + +// Dial like net.Dial, init Producer via Dialing +func Dial(rawURL string) (*Connection, error) { + return &Connection{}, nil +} diff --git a/pkg/core/core.go b/pkg/core/core.go index bc855ccc..9555ecfa 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -1,5 +1,7 @@ package core +import "encoding/json" + const ( DirectionRecvonly = "recvonly" DirectionSendonly = "sendonly" @@ -90,89 +92,6 @@ func (m Mode) String() string { 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"` - SDP string `json:"sdp,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" -) - -type SuperProducer struct { - Type string `json:"type,omitempty"` - URL string `json:"url,omitempty"` - SDP string `json:"sdp,omitempty"` - Medias []*Media `json:"medias,omitempty"` - Receivers []*Receiver `json:"receivers,omitempty"` - Recv int `json:"recv,omitempty"` -} - -func (s *SuperProducer) GetMedias() []*Media { - return s.Medias -} - -func (s *SuperProducer) GetTrack(media *Media, codec *Codec) (*Receiver, error) { - for _, receiver := range s.Receivers { - if receiver.Codec == codec { - return receiver, nil - } - } - receiver := NewReceiver(media, codec) - s.Receivers = append(s.Receivers, receiver) - return receiver, nil -} - -func (s *SuperProducer) Close() error { - for _, receiver := range s.Receivers { - receiver.Close() - } - return nil -} - -type SuperConsumer struct { - Type string `json:"type,omitempty"` - URL string `json:"url,omitempty"` - RemoteAddr string `json:"remote_addr,omitempty"` - UserAgent string `json:"user_agent,omitempty"` - SDP string `json:"sdp,omitempty"` - Medias []*Media `json:"medias,omitempty"` - Senders []*Sender `json:"senders,omitempty"` - Send int `json:"send,omitempty"` -} - -func (s *SuperConsumer) GetMedias() []*Media { - return s.Medias -} - -func (s *SuperConsumer) AddTrack(media *Media, codec *Codec, track *Receiver) error { - return nil -} - -//func (b *SuperConsumer) WriteTo(w io.Writer) (n int64, err error) { -// return 0, nil -//} - -func (s *SuperConsumer) Close() error { - for _, sender := range s.Senders { - sender.Close() - } - return nil -} - -func (s *SuperConsumer) Codecs() []*Codec { - codecs := make([]*Codec, len(s.Senders)) - for i, sender := range s.Senders { - codecs[i] = sender.Codec - } - return codecs +func (m Mode) MarshalJSON() ([]byte, error) { + return json.Marshal(m.String()) } diff --git a/pkg/core/media.go b/pkg/core/media.go index ef9ef74b..2284d0cd 100644 --- a/pkg/core/media.go +++ b/pkg/core/media.go @@ -92,7 +92,7 @@ func (m *Media) Equal(media *Media) bool { func GetKind(name string) string { switch name { - case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG: + case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG, CodecRAW: return KindVideo case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecPCML, CodecELD, CodecFLAC: return KindAudio diff --git a/pkg/core/node.go b/pkg/core/node.go index fd58f2d7..a9959c3d 100644 --- a/pkg/core/node.go +++ b/pkg/core/node.go @@ -23,10 +23,11 @@ type Filter func(handler HandlerFunc) HandlerFunc // Node - Receiver or Sender or Filter (transform) type Node struct { - Codec *Codec `json:"codec"` - Input HandlerFunc `json:"-"` - Output HandlerFunc `json:"-"` + Codec *Codec + Input HandlerFunc + Output HandlerFunc + id uint32 childs []*Node parent *Node diff --git a/pkg/core/track.go b/pkg/core/track.go index 83c39e01..8bc65374 100644 --- a/pkg/core/track.go +++ b/pkg/core/track.go @@ -1,6 +1,7 @@ package core import ( + "encoding/json" "errors" "github.com/pion/rtp" @@ -22,7 +23,7 @@ type Receiver struct { func NewReceiver(media *Media, codec *Codec) *Receiver { r := &Receiver{ - Node: Node{Codec: codec}, + Node: Node{id: NewID(), Codec: codec}, Media: media, } r.Input = func(packet *Packet) { @@ -91,7 +92,7 @@ func NewSender(media *Media, codec *Codec) *Sender { buf := make(chan *Packet, bufSize) s := &Sender{ - Node: Node{Codec: codec}, + Node: Node{id: NewID(), Codec: codec}, Media: media, buf: buf, } @@ -171,3 +172,43 @@ func (s *Sender) Close() { s.Node.Close() } + +func (r *Receiver) MarshalJSON() ([]byte, error) { + v := struct { + ID uint32 `json:"id"` + Codec *Codec `json:"codec"` + Childs []uint32 `json:"childs,omitempty"` + Bytes int `json:"bytes,omitempty"` + Packets int `json:"packets,omitempty"` + }{ + ID: r.Node.id, + Codec: r.Node.Codec, + Bytes: r.Bytes, + Packets: r.Packets, + } + for _, child := range r.childs { + v.Childs = append(v.Childs, child.id) + } + return json.Marshal(v) +} + +func (s *Sender) MarshalJSON() ([]byte, error) { + v := struct { + ID uint32 `json:"id"` + Codec *Codec `json:"codec"` + Parent uint32 `json:"parent,omitempty"` + Bytes int `json:"bytes,omitempty"` + Packets int `json:"packets,omitempty"` + Drops int `json:"drops,omitempty"` + }{ + ID: s.Node.id, + Codec: s.Node.Codec, + Bytes: s.Bytes, + Packets: s.Packets, + Drops: s.Drops, + } + if s.parent != nil { + v.Parent = s.parent.id + } + return json.Marshal(v) +} diff --git a/pkg/dvrip/consumer.go b/pkg/dvrip/backchannel.go similarity index 78% rename from pkg/dvrip/consumer.go rename to pkg/dvrip/backchannel.go index 7652c079..0424e965 100644 --- a/pkg/dvrip/consumer.go +++ b/pkg/dvrip/backchannel.go @@ -8,16 +8,16 @@ import ( "github.com/pion/rtp" ) -type Consumer struct { - core.SuperConsumer +type Backchannel struct { + core.Connection client *Client } -func (c *Consumer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { +func (c *Backchannel) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { return nil, core.ErrCantGetTrack } -func (c *Consumer) Start() error { +func (c *Backchannel) Start() error { if err := c.client.conn.SetReadDeadline(time.Time{}); err != nil { return err } @@ -30,12 +30,7 @@ func (c *Consumer) Start() error { } } -func (c *Consumer) Stop() error { - _ = c.SuperConsumer.Close() - return c.client.Close() -} - -func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { +func (c *Backchannel) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { if err := c.client.Talk(); err != nil { return err } diff --git a/pkg/dvrip/dvrip.go b/pkg/dvrip/dvrip.go index 0f914640..c4980a80 100644 --- a/pkg/dvrip/dvrip.go +++ b/pkg/dvrip/dvrip.go @@ -8,17 +8,22 @@ func Dial(url string) (core.Producer, error) { return nil, err } + conn := core.Connection{ + ID: core.NewID(), + FormatName: "dvrip", + Protocol: "tcp", + RemoteAddr: client.conn.RemoteAddr().String(), + Transport: client.conn, + } + if client.stream != "" { - prod := &Producer{client: client} - prod.Type = "DVRIP active producer" + prod := &Producer{Connection: conn, client: client} if err := prod.probe(); err != nil { return nil, err } return prod, nil } else { - cons := &Consumer{client: client} - cons.Type = "DVRIP active consumer" - cons.Medias = []*core.Media{ + conn.Medias = []*core.Media{ { Kind: core.KindAudio, Direction: core.DirectionSendonly, @@ -29,6 +34,6 @@ func Dial(url string) (core.Producer, error) { }, }, } - return cons, nil + return &Backchannel{Connection: conn, client: client}, nil } } diff --git a/pkg/dvrip/producer.go b/pkg/dvrip/producer.go index 412dd0a3..c87017b4 100644 --- a/pkg/dvrip/producer.go +++ b/pkg/dvrip/producer.go @@ -15,7 +15,7 @@ import ( ) type Producer struct { - core.SuperProducer + core.Connection client *Client @@ -92,10 +92,6 @@ func (c *Producer) Start() error { } } -func (c *Producer) Stop() error { - return c.client.Close() -} - func (c *Producer) probe() error { if err := c.client.Play(); err != nil { return err diff --git a/pkg/flv/consumer.go b/pkg/flv/consumer.go index 59e65d9c..fe966bfc 100644 --- a/pkg/flv/consumer.go +++ b/pkg/flv/consumer.go @@ -10,17 +10,13 @@ import ( ) type Consumer struct { - core.SuperConsumer + core.Connection wr *core.WriteBuffer muxer *Muxer } func NewConsumer() *Consumer { - c := &Consumer{ - wr: core.NewWriteBuffer(nil), - muxer: &Muxer{}, - } - c.Medias = []*core.Media{ + medias := []*core.Media{ { Kind: core.KindVideo, Direction: core.DirectionSendonly, @@ -36,7 +32,17 @@ func NewConsumer() *Consumer { }, }, } - return c + wr := core.NewWriteBuffer(nil) + return &Consumer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "flv", + Medias: medias, + Transport: wr, + }, + wr: wr, + muxer: &Muxer{}, + } } func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { @@ -86,8 +92,3 @@ func (c *Consumer) WriteTo(wr io.Writer) (int64, error) { } return c.wr.WriteTo(wr) } - -func (c *Consumer) Stop() error { - _ = c.SuperConsumer.Close() - return c.wr.Close() -} diff --git a/pkg/flv/producer.go b/pkg/flv/producer.go index 3972e666..66755217 100644 --- a/pkg/flv/producer.go +++ b/pkg/flv/producer.go @@ -15,18 +15,24 @@ import ( ) type Producer struct { - core.SuperProducer + core.Connection rd *core.ReadBuffer video, audio *core.Receiver } func Open(rd io.Reader) (*Producer, error) { - prod := &Producer{rd: core.NewReadBuffer(rd)} + prod := &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "flv", + Transport: rd, + }, + rd: core.NewReadBuffer(rd), + } if err := prod.probe(); err != nil { return nil, err } - prod.Type = "FLV producer" return prod, nil } @@ -57,7 +63,7 @@ const ( ) func (c *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { - receiver, _ := c.SuperProducer.GetTrack(media, codec) + receiver, _ := c.Connection.GetTrack(media, codec) if media.Kind == core.KindVideo { c.video = receiver } else { @@ -117,11 +123,6 @@ func (c *Producer) Start() error { } } -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.rd.Close() -} - func (c *Producer) probe() error { if err := c.readHeader(); err != nil { return err diff --git a/pkg/gopro/gopro.go b/pkg/gopro/producer.go similarity index 90% rename from pkg/gopro/gopro.go rename to pkg/gopro/producer.go index 2d6a098b..1873159f 100644 --- a/pkg/gopro/gopro.go +++ b/pkg/gopro/producer.go @@ -8,11 +8,10 @@ import ( "net/url" "time" - "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mpegts" ) -func Dial(rawURL string) (core.Producer, error) { +func Dial(rawURL string) (*mpegts.Producer, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err @@ -32,7 +31,15 @@ func Dial(rawURL string) (core.Producer, error) { return nil, err } - return mpegts.Open(r) + prod, err := mpegts.Open(r) + if err != nil { + return nil, err + } + + prod.FormatName = "gopro" + prod.RemoteAddr = u.Host + + return prod, nil } type listener struct { diff --git a/pkg/hass/client.go b/pkg/hass/client.go index c1ed5b4b..5b236051 100644 --- a/pkg/hass/client.go +++ b/pkg/hass/client.go @@ -61,8 +61,10 @@ func NewClient(rawURL string) (*Client, error) { } conn := webrtc.NewConn(pc) - conn.Desc = "Hass" + conn.FormatName = "hass/webrtc" conn.Mode = core.ModeActiveProducer + conn.Protocol = "ws" + conn.URL = rawURL // https://developers.google.com/nest/device-access/traits/device/camera-live-stream#generatewebrtcstream-request-fields medias := []*core.Media{ diff --git a/pkg/hls/producer.go b/pkg/hls/producer.go index 410e771a..e1c3ed43 100644 --- a/pkg/hls/producer.go +++ b/pkg/hls/producer.go @@ -4,14 +4,19 @@ import ( "io" "net/url" - "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mpegts" ) -func OpenURL(u *url.URL, body io.ReadCloser) (core.Producer, error) { +func OpenURL(u *url.URL, body io.ReadCloser) (*mpegts.Producer, error) { rd, err := NewReader(u, body) if err != nil { return nil, err } - return mpegts.Open(rd) + prod, err := mpegts.Open(rd) + if err != nil { + return nil, err + } + prod.FormatName = "hls/mpegts" + prod.RemoteAddr = u.Host + return prod, nil } diff --git a/pkg/homekit/consumer.go b/pkg/homekit/consumer.go index 05ea2427..1c665233 100644 --- a/pkg/homekit/consumer.go +++ b/pkg/homekit/consumer.go @@ -16,7 +16,7 @@ import ( ) type Consumer struct { - core.SuperConsumer + core.Connection conn net.Conn srtp *srtp.Server @@ -29,28 +29,31 @@ type Consumer struct { } func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer { - return &Consumer{ - SuperConsumer: core.SuperConsumer{ - Type: "HomeKit passive consumer", - RemoteAddr: conn.RemoteAddr().String(), - Medias: []*core.Media{ - { - Kind: core.KindVideo, - Direction: core.DirectionSendonly, - Codecs: []*core.Codec{ - {Name: core.CodecH264}, - }, - }, - { - Kind: core.KindAudio, - Direction: core.DirectionSendonly, - Codecs: []*core.Codec{ - {Name: core.CodecOpus}, - }, - }, + medias := []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecH264}, }, }, - + { + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecOpus}, + }, + }, + } + return &Consumer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "homekit", + Protocol: "udp", + RemoteAddr: conn.RemoteAddr().String(), + Medias: medias, + Transport: conn, + }, conn: conn, srtp: server, } @@ -175,11 +178,10 @@ func (c *Consumer) WriteTo(io.Writer) (int64, error) { } func (c *Consumer) Stop() error { - _ = c.SuperConsumer.Close() if c.deadline != nil { c.deadline.Reset(0) } - return c.SuperConsumer.Close() + return c.Connection.Stop() } func (c *Consumer) srtpEndpoint() *srtp.Endpoint { diff --git a/pkg/homekit/client.go b/pkg/homekit/producer.go similarity index 95% rename from pkg/homekit/client.go rename to pkg/homekit/producer.go index 133499d3..c2781e27 100644 --- a/pkg/homekit/client.go +++ b/pkg/homekit/producer.go @@ -15,8 +15,9 @@ import ( "github.com/pion/rtp" ) +// Deprecated: rename to Producer type Client struct { - core.SuperProducer + core.Connection hap *hap.Client srtp *srtp.Server @@ -52,9 +53,12 @@ func Dial(rawURL string, server *srtp.Server) (*Client, error) { } client := &Client{ - SuperProducer: core.SuperProducer{ - Type: "HomeKit active producer", - URL: conn.URL(), + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "homekit", + Protocol: "udp", + Source: conn.URL(), + Transport: conn, }, hap: conn, srtp: server, @@ -93,7 +97,6 @@ func (c *Client) GetMedias() []*core.Media { return nil } - c.URL = c.hap.URL() c.SDP = fmt.Sprintf("%+v\n%+v", c.videoConfig, c.audioConfig) c.Medias = []*core.Media{ @@ -175,8 +178,6 @@ func (c *Client) Start() error { } func (c *Client) Stop() error { - _ = c.SuperProducer.Close() - if c.videoSession != nil && c.videoSession.Remote != nil { c.srtp.DelSession(c.videoSession) } @@ -184,7 +185,7 @@ func (c *Client) Stop() error { c.srtp.DelSession(c.audioSession) } - return c.hap.Close() + return c.Connection.Stop() } func (c *Client) trackByKind(kind string) *core.Receiver { diff --git a/pkg/image/producer.go b/pkg/image/producer.go new file mode 100644 index 00000000..2081c048 --- /dev/null +++ b/pkg/image/producer.go @@ -0,0 +1,92 @@ +package image + +import ( + "errors" + "io" + "net/http" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/tcp" + "github.com/pion/rtp" +) + +type Producer struct { + core.Connection + + closed bool + res *http.Response +} + +func Open(res *http.Response) (*Producer, error) { + return &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "image", + Protocol: "http", + RemoteAddr: res.Request.URL.Host, + Transport: res.Body, + Medias: []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: core.CodecJPEG, + ClockRate: 90000, + PayloadType: core.PayloadTypeRAW, + }, + }, + }, + }, + }, + res: res, + }, nil +} + +func (c *Producer) Start() error { + body, err := io.ReadAll(c.res.Body) + if err != nil { + return err + } + + pkt := &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: body, + } + c.Receivers[0].WriteRTP(pkt) + + c.Recv += len(body) + + req := c.res.Request + + for !c.closed { + res, err := tcp.Do(req) + if err != nil { + return err + } + + if res.StatusCode != http.StatusOK { + return errors.New("wrong status: " + res.Status) + } + + body, err = io.ReadAll(res.Body) + if err != nil { + return err + } + + c.Recv += len(body) + + pkt = &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: body, + } + c.Receivers[0].WriteRTP(pkt) + } + + return nil +} + +func (c *Producer) Stop() error { + c.closed = true + return c.Connection.Stop() +} diff --git a/pkg/isapi/consumer.go b/pkg/isapi/backchannel.go similarity index 83% rename from pkg/isapi/consumer.go rename to pkg/isapi/backchannel.go index c7b51c9d..ade16255 100644 --- a/pkg/isapi/consumer.go +++ b/pkg/isapi/backchannel.go @@ -2,6 +2,7 @@ package isapi import ( "encoding/json" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/rtp" ) @@ -51,10 +52,15 @@ func (c *Client) Stop() (err error) { } func (c *Client) MarshalJSON() ([]byte, error) { - info := &core.Info{ - Type: "ISAPI active consumer", - Medias: c.medias, - Send: c.send, + info := &core.Connection{ + ID: core.ID(c), + FormatName: "isapi", + Protocol: "http", + Medias: c.medias, + Send: c.send, + } + if c.conn != nil { + info.RemoteAddr = c.conn.RemoteAddr().String() } if c.sender != nil { info.Senders = []*core.Sender{c.sender} diff --git a/pkg/isapi/client.go b/pkg/isapi/client.go index 83dd9026..ba3e6887 100644 --- a/pkg/isapi/client.go +++ b/pkg/isapi/client.go @@ -11,6 +11,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/tcp" ) +// Deprecated: should be rewritten to core.Connection type Client struct { core.Listener diff --git a/pkg/ivideon/client.go b/pkg/ivideon/client.go index 7cbf0b38..ef79010e 100644 --- a/pkg/ivideon/client.go +++ b/pkg/ivideon/client.go @@ -26,6 +26,7 @@ const ( StateHandle ) +// Deprecated: should be rewritten to core.Connection type Client struct { core.Listener diff --git a/pkg/ivideon/producer.go b/pkg/ivideon/producer.go index d0a8fcba..78084123 100644 --- a/pkg/ivideon/producer.go +++ b/pkg/ivideon/producer.go @@ -2,6 +2,7 @@ package ivideon import ( "encoding/json" + "github.com/AlexxIT/go2rtc/pkg/core" ) @@ -32,11 +33,16 @@ func (c *Client) Stop() error { } func (c *Client) MarshalJSON() ([]byte, error) { - info := &core.Info{ - Type: "Ivideon active producer", - URL: c.ID, - Medias: c.medias, - Recv: c.recv, + info := &core.Connection{ + ID: core.ID(c), + FormatName: "ivideon", + Protocol: "ws", + URL: c.ID, + Medias: c.medias, + Recv: c.recv, + } + if c.conn != nil { + info.RemoteAddr = c.conn.RemoteAddr().String() } if c.receiver != nil { info.Receivers = []*core.Receiver{c.receiver} diff --git a/pkg/kasa/producer.go b/pkg/kasa/producer.go index d138cb68..22d10216 100644 --- a/pkg/kasa/producer.go +++ b/pkg/kasa/producer.go @@ -12,13 +12,13 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h264/annexb" - "github.com/AlexxIT/go2rtc/pkg/multipart" + "github.com/AlexxIT/go2rtc/pkg/mpjpeg" "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/pion/rtp" ) type Producer struct { - core.SuperProducer + core.Connection rd *core.ReadBuffer reader *bufio.Reader @@ -65,11 +65,18 @@ func Dial(url string) (*Producer, error) { rd.Reader = httputil.NewChunkedReader(buf) } - prod := &Producer{rd: core.NewReadBuffer(rd)} + prod := &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "kasa", + Protocol: "http", + Transport: rd, + }, + rd: core.NewReadBuffer(rd), + } if err = prod.probe(); err != nil { return nil, err } - prod.Type = "Kasa producer" return prod, nil } @@ -90,7 +97,7 @@ func (c *Producer) Start() error { } for { - header, body, err := multipart.Next(c.reader) + header, body, err := mpjpeg.Next(c.reader) if err != nil { return err } @@ -128,11 +135,6 @@ func (c *Producer) Start() error { } } -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.rd.Close() -} - const ( MimeVideo = "video/x-h264" MimeG711U = "audio/g711u" @@ -151,7 +153,7 @@ func (c *Producer) probe() error { timeout := time.Now().Add(core.ProbeTimeout) for (waitVideo || waitAudio) && time.Now().Before(timeout) { - header, body, err := multipart.Next(c.reader) + header, body, err := mpjpeg.Next(c.reader) if err != nil { return err } diff --git a/pkg/magic/bitstream/producer.go b/pkg/magic/bitstream/producer.go index 2ffa964e..b84f049b 100644 --- a/pkg/magic/bitstream/producer.go +++ b/pkg/magic/bitstream/producer.go @@ -13,7 +13,7 @@ import ( ) type Producer struct { - core.SuperProducer + core.Connection rd *core.ReadBuffer } @@ -28,26 +28,35 @@ func Open(r io.Reader) (*Producer, error) { buf = annexb.EncodeToAVCC(buf, false) // won't break original buffer var codec *core.Codec + var format string switch { case h264.NALUType(buf) == h264.NALUTypeSPS: codec = h264.AVCCToCodec(buf) + format = "h264" case h265.NALUType(buf) == h265.NALUTypeVPS: codec = h265.AVCCToCodec(buf) + format = "hevc" default: return nil, errors.New("bitstream: unsupported header: " + hex.EncodeToString(buf[:8])) } - prod := &Producer{rd: rd} - prod.Type = "Bitstream producer" - prod.Medias = []*core.Media{ + medias := []*core.Media{ { Kind: core.KindVideo, Direction: core.DirectionRecvonly, Codecs: []*core.Codec{codec}, }, } - return prod, nil + return &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: format, + Medias: medias, + Transport: r, + }, + rd: rd, + }, nil } func (c *Producer) Start() error { @@ -84,8 +93,3 @@ func (c *Producer) Start() error { } } } - -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.rd.Close() -} diff --git a/pkg/magic/keyframe.go b/pkg/magic/keyframe.go index d2ae80bd..8f70eec6 100644 --- a/pkg/magic/keyframe.go +++ b/pkg/magic/keyframe.go @@ -12,26 +12,32 @@ import ( ) type Keyframe struct { - core.SuperConsumer + core.Connection wr *core.WriteBuffer } +// Deprecated: should be rewritten func NewKeyframe() *Keyframe { - return &Keyframe{ - core.SuperConsumer{ - Medias: []*core.Media{ - { - Kind: core.KindVideo, - Direction: core.DirectionSendonly, - Codecs: []*core.Codec{ - {Name: core.CodecJPEG}, - {Name: core.CodecH264}, - {Name: core.CodecH265}, - }, - }, + medias := []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecJPEG}, + {Name: core.CodecH264}, + {Name: core.CodecH265}, }, }, - core.NewWriteBuffer(nil), + } + wr := core.NewWriteBuffer(nil) + return &Keyframe{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "keyframe", + Medias: medias, + Transport: wr, + }, + wr: wr, } } @@ -98,8 +104,3 @@ func (k *Keyframe) CodecName() string { func (k *Keyframe) WriteTo(wr io.Writer) (int64, error) { return k.wr.WriteTo(wr) } - -func (k *Keyframe) Stop() error { - _ = k.SuperConsumer.Close() - return k.wr.Close() -} diff --git a/pkg/magic/mjpeg/producer.go b/pkg/magic/mjpeg/producer.go index e5627fd7..e47c168d 100644 --- a/pkg/magic/mjpeg/producer.go +++ b/pkg/magic/mjpeg/producer.go @@ -9,14 +9,12 @@ import ( ) type Producer struct { - core.SuperProducer + core.Connection rd *core.ReadBuffer } func Open(rd io.Reader) (*Producer, error) { - prod := &Producer{rd: core.NewReadBuffer(rd)} - prod.Type = "MJPEG producer" - prod.Medias = []*core.Media{ + medias := []*core.Media{ { Kind: core.KindVideo, Direction: core.DirectionRecvonly, @@ -29,7 +27,15 @@ func Open(rd io.Reader) (*Producer, error) { }, }, } - return prod, nil + return &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "mjpeg", + Medias: medias, + Transport: rd, + }, + rd: core.NewReadBuffer(rd), + }, nil } func (c *Producer) Start() error { @@ -70,8 +76,3 @@ func (c *Producer) Start() error { buf = buf[i:] } } - -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.rd.Close() -} diff --git a/pkg/magic/producer.go b/pkg/magic/producer.go index 9bde508d..3742ccf9 100644 --- a/pkg/magic/producer.go +++ b/pkg/magic/producer.go @@ -13,7 +13,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/magic/bitstream" "github.com/AlexxIT/go2rtc/pkg/magic/mjpeg" "github.com/AlexxIT/go2rtc/pkg/mpegts" - "github.com/AlexxIT/go2rtc/pkg/multipart" + "github.com/AlexxIT/go2rtc/pkg/mpjpeg" "github.com/AlexxIT/go2rtc/pkg/wav" "github.com/AlexxIT/go2rtc/pkg/y4m" ) @@ -26,29 +26,31 @@ func Open(r io.Reader) (core.Producer, error) { return nil, err } - switch { - case string(b) == annexb.StartCode: + switch string(b) { + case annexb.StartCode: return bitstream.Open(rd) - - case string(b) == wav.FourCC: + case wav.FourCC: return wav.Open(rd) - - case string(b) == y4m.FourCC: + case y4m.FourCC: return y4m.Open(rd) + } - case bytes.HasPrefix(b, []byte{0xFF, 0xD8}): - return mjpeg.Open(rd) - - case bytes.HasPrefix(b, []byte(flv.Signature)): + switch string(b[:3]) { + case flv.Signature: return flv.Open(rd) + } - case bytes.HasPrefix(b, []byte("--")): - return multipart.Open(rd) - - case b[0] == 0xFF && (b[1] == 0xF1 || b[1] == 0xF9): + switch string(b[:2]) { + case "\xFF\xD8": + return mjpeg.Open(rd) + case "\xFF\xF1", "\xFF\xF9": return aac.Open(rd) + case "--": + return mpjpeg.Open(rd) + } - case b[0] == mpegts.SyncByte: + switch b[0] { + case mpegts.SyncByte: return mpegts.Open(rd) } diff --git a/pkg/mjpeg/client.go b/pkg/mjpeg/client.go deleted file mode 100644 index f16c42cd..00000000 --- a/pkg/mjpeg/client.go +++ /dev/null @@ -1,75 +0,0 @@ -package mjpeg - -import ( - "errors" - "io" - "net/http" - - "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/tcp" - "github.com/pion/rtp" -) - -type Client struct { - core.Listener - - UserAgent string - RemoteAddr string - - closed bool - res *http.Response - - medias []*core.Media - receiver *core.Receiver - - recv int -} - -func NewClient(res *http.Response) *Client { - return &Client{res: res} -} - -func (c *Client) Handle() error { - body, err := io.ReadAll(c.res.Body) - if err != nil { - return err - } - - pkt := &rtp.Packet{ - Header: rtp.Header{Timestamp: core.Now90000()}, - Payload: body, - } - c.receiver.WriteRTP(pkt) - - c.recv += len(body) - - req := c.res.Request - - for !c.closed { - res, err := tcp.Do(req) - if err != nil { - return err - } - - if res.StatusCode != http.StatusOK { - return errors.New("wrong status: " + res.Status) - } - - body, err = io.ReadAll(res.Body) - if err != nil { - return err - } - - c.recv += len(body) - - if c.receiver != nil { - pkt = &rtp.Packet{ - Header: rtp.Header{Timestamp: core.Now90000()}, - Payload: body, - } - c.receiver.WriteRTP(pkt) - } - } - - return nil -} diff --git a/pkg/mjpeg/consumer.go b/pkg/mjpeg/consumer.go index d5fb0d51..16edc895 100644 --- a/pkg/mjpeg/consumer.go +++ b/pkg/mjpeg/consumer.go @@ -8,26 +8,30 @@ import ( ) type Consumer struct { - core.SuperConsumer + core.Connection wr *core.WriteBuffer } func NewConsumer() *Consumer { - return &Consumer{ - core.SuperConsumer{ - Type: "MJPEG passive consumer", - Medias: []*core.Media{ - { - Kind: core.KindVideo, - Direction: core.DirectionSendonly, - Codecs: []*core.Codec{ - {Name: core.CodecJPEG}, - {Name: core.CodecRAW}, - }, - }, + medias := []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecJPEG}, + {Name: core.CodecRAW}, }, }, - core.NewWriteBuffer(nil), + } + wr := core.NewWriteBuffer(nil) + return &Consumer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "mjpeg", + Medias: medias, + Transport: wr, + }, + wr: wr, } } @@ -53,8 +57,3 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv func (c *Consumer) WriteTo(wr io.Writer) (int64, error) { return c.wr.WriteTo(wr) } - -func (c *Consumer) Stop() error { - _ = c.SuperConsumer.Close() - return c.wr.Close() -} diff --git a/pkg/mjpeg/producer.go b/pkg/mjpeg/producer.go deleted file mode 100644 index 5b352252..00000000 --- a/pkg/mjpeg/producer.go +++ /dev/null @@ -1,61 +0,0 @@ -package mjpeg - -import ( - "encoding/json" - - "github.com/AlexxIT/go2rtc/pkg/core" -) - -func (c *Client) GetMedias() []*core.Media { - if c.medias == nil { - c.medias = []*core.Media{{ - Kind: core.KindVideo, - Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{ - { - Name: core.CodecJPEG, - ClockRate: 90000, - PayloadType: core.PayloadTypeRAW, - }, - }, - }} - } - return c.medias -} - -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.receiver, nil -} - -func (c *Client) Start() error { - // https://github.com/AlexxIT/go2rtc/issues/278 - return c.Handle() -} - -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 - return nil -} - -func (c *Client) MarshalJSON() ([]byte, error) { - info := &core.Info{ - Type: "JPEG active producer", - URL: c.res.Request.URL.String(), - RemoteAddr: c.RemoteAddr, - UserAgent: c.UserAgent, - Medias: c.medias, - Recv: c.recv, - } - if c.receiver != nil { - info.Receivers = []*core.Receiver{c.receiver} - } - return json.Marshal(info) -} diff --git a/pkg/mp4/consumer.go b/pkg/mp4/consumer.go index 83b2d2e3..34849863 100644 --- a/pkg/mp4/consumer.go +++ b/pkg/mp4/consumer.go @@ -14,7 +14,7 @@ import ( ) type Consumer struct { - core.SuperConsumer + core.Connection wr *core.WriteBuffer muxer *Muxer mu sync.Mutex @@ -47,12 +47,17 @@ func NewConsumer(medias []*core.Media) *Consumer { } } - cons := &Consumer{ + wr := core.NewWriteBuffer(nil) + return &Consumer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "mp4", + Medias: medias, + Transport: wr, + }, muxer: &Muxer{}, - wr: core.NewWriteBuffer(nil), + wr: wr, } - cons.Medias = medias - return cons } func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { @@ -182,8 +187,3 @@ func (c *Consumer) WriteTo(wr io.Writer) (int64, error) { return c.wr.WriteTo(wr) } - -func (c *Consumer) Stop() error { - _ = c.SuperConsumer.Close() - return c.wr.Close() -} diff --git a/pkg/mp4/keyframe.go b/pkg/mp4/keyframe.go index 25a6983d..399f95e7 100644 --- a/pkg/mp4/keyframe.go +++ b/pkg/mp4/keyframe.go @@ -10,11 +10,12 @@ import ( ) type Keyframe struct { - core.SuperConsumer + core.Connection wr *core.WriteBuffer muxer *Muxer } +// Deprecated: should be rewritten func NewKeyframe(medias []*core.Media) *Keyframe { if medias == nil { medias = []*core.Media{ @@ -29,9 +30,15 @@ func NewKeyframe(medias []*core.Media) *Keyframe { } } + wr := core.NewWriteBuffer(nil) cons := &Keyframe{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "mp4", + Transport: wr, + }, muxer: &Muxer{}, - wr: core.NewWriteBuffer(nil), + wr: wr, } cons.Medias = medias return cons @@ -95,8 +102,3 @@ func (c *Keyframe) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv func (c *Keyframe) WriteTo(wr io.Writer) (int64, error) { return c.wr.WriteTo(wr) } - -func (c *Keyframe) Stop() error { - _ = c.SuperConsumer.Close() - return c.wr.Close() -} diff --git a/pkg/mpegts/consumer.go b/pkg/mpegts/consumer.go index eb0902fc..fcb57c74 100644 --- a/pkg/mpegts/consumer.go +++ b/pkg/mpegts/consumer.go @@ -11,17 +11,13 @@ import ( ) type Consumer struct { - core.SuperConsumer + core.Connection muxer *Muxer wr *core.WriteBuffer } func NewConsumer() *Consumer { - c := &Consumer{ - muxer: NewMuxer(), - wr: core.NewWriteBuffer(nil), - } - c.Medias = []*core.Media{ + medias := []*core.Media{ { Kind: core.KindVideo, Direction: core.DirectionSendonly, @@ -38,7 +34,17 @@ func NewConsumer() *Consumer { }, }, } - return c + wr := core.NewWriteBuffer(nil) + return &Consumer{ + core.Connection{ + ID: core.NewID(), + FormatName: "mpegts", + Medias: medias, + Transport: wr, + }, + NewMuxer(), + wr, + } } func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { @@ -110,14 +116,9 @@ func (c *Consumer) WriteTo(wr io.Writer) (int64, error) { return c.wr.WriteTo(wr) } -func (c *Consumer) Stop() error { - _ = c.SuperConsumer.Close() - return c.wr.Close() -} - -func TimestampFromRTP(rtp *rtp.Packet, codec *core.Codec) { - if codec.ClockRate == ClockRate { - return - } - rtp.Timestamp = uint32(float64(rtp.Timestamp) / float64(codec.ClockRate) * ClockRate) -} +//func TimestampFromRTP(rtp *rtp.Packet, codec *core.Codec) { +// if codec.ClockRate == ClockRate { +// return +// } +// rtp.Timestamp = uint32(float64(rtp.Timestamp) / float64(codec.ClockRate) * ClockRate) +//} diff --git a/pkg/mpegts/producer.go b/pkg/mpegts/producer.go index 78f320a2..2c72d8aa 100644 --- a/pkg/mpegts/producer.go +++ b/pkg/mpegts/producer.go @@ -13,12 +13,19 @@ import ( ) type Producer struct { - core.SuperProducer + core.Connection rd *core.ReadBuffer } func Open(rd io.Reader) (*Producer, error) { - prod := &Producer{rd: core.NewReadBuffer(rd)} + prod := &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "mpegts", + Transport: rd, + }, + rd: core.NewReadBuffer(rd), + } if err := prod.probe(); err != nil { return nil, err } @@ -26,7 +33,7 @@ func Open(rd io.Reader) (*Producer, error) { } func (c *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { - receiver, _ := c.SuperProducer.GetTrack(media, codec) + receiver, _ := c.Connection.GetTrack(media, codec) receiver.ID = StreamType(codec) return receiver, nil } @@ -40,6 +47,8 @@ func (c *Producer) Start() error { return err } + c.Recv += len(pkt.Payload) + //log.Printf("[mpegts] size: %6d, muxer: %10d, pt: %2d", len(pkt.Payload), pkt.Timestamp, pkt.PayloadType) for _, receiver := range c.Receivers { @@ -52,11 +61,6 @@ func (c *Producer) Start() error { } } -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.rd.Close() -} - func (c *Producer) probe() error { c.rd.BufferSize = core.ProbeSize defer c.rd.Reset() diff --git a/pkg/multipart/multipart.go b/pkg/mpjpeg/multipart.go similarity index 98% rename from pkg/multipart/multipart.go rename to pkg/mpjpeg/multipart.go index aea1b828..abceea43 100644 --- a/pkg/multipart/multipart.go +++ b/pkg/mpjpeg/multipart.go @@ -1,4 +1,4 @@ -package multipart +package mpjpeg import ( "bufio" diff --git a/pkg/mpjpeg/producer.go b/pkg/mpjpeg/producer.go new file mode 100644 index 00000000..a8d5e16a --- /dev/null +++ b/pkg/mpjpeg/producer.go @@ -0,0 +1,65 @@ +package mpjpeg + +import ( + "bufio" + "errors" + "io" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +type Producer struct { + core.Connection + rd *bufio.Reader +} + +func Open(rd io.Reader) (*Producer, error) { + return &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "mpjpeg", // Multipart JPEG + Transport: rd, + Medias: []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: core.CodecJPEG, + ClockRate: 90000, + PayloadType: core.PayloadTypeRAW, + }, + }, + }, + }, + }, + }, nil +} + +func (c *Producer) Start() error { + if len(c.Receivers) != 1 { + return errors.New("mjpeg: no receivers") + } + + rd := bufio.NewReader(c.Transport.(io.Reader)) + + mjpeg := c.Receivers[0] + + for { + _, body, err := Next(rd) + if err != nil { + return err + } + + c.Recv += len(body) + + if mjpeg != nil { + packet := &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: body, + } + mjpeg.WriteRTP(packet) + } + } +} diff --git a/pkg/multipart/producer.go b/pkg/multipart/producer.go deleted file mode 100644 index 70a2c547..00000000 --- a/pkg/multipart/producer.go +++ /dev/null @@ -1,68 +0,0 @@ -package multipart - -import ( - "bufio" - "errors" - "io" - - "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/pion/rtp" -) - -type Producer struct { - core.SuperProducer - closer io.Closer - reader *bufio.Reader -} - -func Open(rd io.Reader) (*Producer, error) { - prod := &Producer{ - closer: rd.(io.Closer), - reader: bufio.NewReader(rd), - } - prod.Medias = []*core.Media{ - { - Kind: core.KindVideo, - Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{ - { - Name: core.CodecJPEG, - ClockRate: 90000, - PayloadType: core.PayloadTypeRAW, - }, - }, - }, - } - prod.Type = "Multipart producer" - return prod, nil -} - -func (c *Producer) Start() error { - if len(c.Receivers) != 1 { - return errors.New("mjpeg: no receivers") - } - - mjpeg := c.Receivers[0] - - for { - _, body, err := Next(c.reader) - if err != nil { - return err - } - - c.Recv += len(body) - - if mjpeg != nil { - packet := &rtp.Packet{ - Header: rtp.Header{Timestamp: core.Now90000()}, - Payload: body, - } - mjpeg.WriteRTP(packet) - } - } -} - -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.closer.Close() -} diff --git a/pkg/nest/client.go b/pkg/nest/client.go index 2169773b..0b243384 100644 --- a/pkg/nest/client.go +++ b/pkg/nest/client.go @@ -48,8 +48,10 @@ func Dial(rawURL string) (*Client, error) { } conn := webrtc.NewConn(pc) - conn.Desc = "Nest" + conn.FormatName = "nest/webrtc" conn.Mode = core.ModeActiveProducer + conn.Protocol = "http" + conn.URL = rawURL // https://developers.google.com/nest/device-access/traits/device/camera-live-stream#generatewebrtcstream-request-fields medias := []*core.Media{ diff --git a/pkg/probe/probe.go b/pkg/probe/producer.go similarity index 72% rename from pkg/probe/probe.go rename to pkg/probe/producer.go index 61a2b361..1fbd3efb 100644 --- a/pkg/probe/probe.go +++ b/pkg/probe/producer.go @@ -8,17 +8,11 @@ import ( ) type Probe struct { - Type string `json:"type,omitempty"` - RemoteAddr string `json:"remote_addr,omitempty"` - UserAgent string `json:"user_agent,omitempty"` - Medias []*core.Media `json:"medias,omitempty"` - Receivers []*core.Receiver `json:"receivers,omitempty"` - Senders []*core.Sender `json:"senders,omitempty"` + core.Connection } func NewProbe(query url.Values) *Probe { - c := &Probe{Type: "probe"} - c.Medias = core.ParseQuery(query) + medias := core.ParseQuery(query) for _, value := range query["microphone"] { media := &core.Media{Kind: core.KindAudio, Direction: core.DirectionRecvonly} @@ -32,10 +26,16 @@ func NewProbe(query url.Values) *Probe { media.Codecs = append(media.Codecs, &core.Codec{Name: name}) } - c.Medias = append(c.Medias, media) + medias = append(medias, media) } - return c + return &Probe{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "probe", + Medias: medias, + }, + } } func (p *Probe) GetMedias() []*core.Media { diff --git a/pkg/roborock/client.go b/pkg/roborock/client.go index 522b0e13..ef221e65 100644 --- a/pkg/roborock/client.go +++ b/pkg/roborock/client.go @@ -18,6 +18,7 @@ import ( pion "github.com/pion/webrtc/v3" ) +// Deprecated: should be rewritten to core.Connection type Client struct { core.Listener @@ -110,8 +111,10 @@ func (c *Client) Connect() error { var sendOffer sync.WaitGroup c.conn = webrtc.NewConn(pc) - c.conn.Desc = "Roborock" + c.conn.FormatName = "roborock" c.conn.Mode = core.ModeActiveProducer + c.conn.Protocol = "mqtt" + c.conn.URL = c.url c.conn.Listen(func(msg any) { switch msg := msg.(type) { case *pion.ICECandidate: diff --git a/pkg/rtmp/client.go b/pkg/rtmp/client.go index aff8e23c..138d727d 100644 --- a/pkg/rtmp/client.go +++ b/pkg/rtmp/client.go @@ -8,10 +8,11 @@ import ( "strings" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/flv" "github.com/AlexxIT/go2rtc/pkg/tcp" ) -func DialPlay(rawURL string) (core.Producer, error) { +func DialPlay(rawURL string) (*flv.Producer, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err @@ -22,16 +23,16 @@ func DialPlay(rawURL string) (core.Producer, error) { return nil, err } - rtmpConn, err := NewClient(conn, u) + client, err := NewClient(conn, u) if err != nil { return nil, err } - if err = rtmpConn.play(); err != nil { + if err = client.play(); err != nil { return nil, err } - return rtmpConn.Producer() + return client.Producer() } func DialPublish(rawURL string) (io.Writer, error) { diff --git a/pkg/rtmp/flv.go b/pkg/rtmp/flv.go index 87bef0a8..350f4c3c 100644 --- a/pkg/rtmp/flv.go +++ b/pkg/rtmp/flv.go @@ -1,11 +1,10 @@ package rtmp import ( - "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/flv" ) -func (c *Conn) Producer() (core.Producer, error) { +func (c *Conn) Producer() (*flv.Producer, error) { c.rdBuf = []byte{ 'F', 'L', 'V', // signature 1, // version @@ -13,7 +12,17 @@ func (c *Conn) Producer() (core.Producer, error) { 0, 0, 0, 9, // header size } - return flv.Open(c) + prod, err := flv.Open(c) + if err != nil { + return nil, err + } + + prod.FormatName = "rtmp" + prod.Protocol = "rtmp" + prod.RemoteAddr = c.conn.RemoteAddr().String() + prod.URL = c.url + + return prod, nil } // Read - convert RTMP to FLV format diff --git a/pkg/rtsp/client.go b/pkg/rtsp/client.go index 9002d0a1..352c00a1 100644 --- a/pkg/rtsp/client.go +++ b/pkg/rtsp/client.go @@ -20,7 +20,13 @@ import ( var Timeout = time.Second * 5 func NewClient(uri string) *Conn { - return &Conn{uri: uri} + return &Conn{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "rtsp", + }, + uri: uri, + } } func (c *Conn) Dial() (err error) { @@ -36,8 +42,10 @@ func (c *Conn) Dial() (err error) { timeout = time.Second * time.Duration(c.Timeout) } conn, err = tcp.Dial(c.URL, timeout) + c.Protocol = "rtsp+tcp" } else { conn, err = websocket.Dial(c.Transport) + c.Protocol = "ws" } if err != nil { return @@ -53,6 +61,10 @@ func (c *Conn) Dial() (err error) { c.sequence = 0 c.state = StateConn + c.Connection.RemoteAddr = conn.RemoteAddr().String() + c.Connection.Transport = conn + c.Connection.URL = c.uri + return nil } @@ -143,7 +155,7 @@ func (c *Conn) Describe() error { } } - c.sdp = string(res.Body) // for info + c.SDP = string(res.Body) // for info medias, err := UnmarshalSDP(res.Body) if err != nil { diff --git a/pkg/rtsp/conn.go b/pkg/rtsp/conn.go index 1d9edf06..0c2009d7 100644 --- a/pkg/rtsp/conn.go +++ b/pkg/rtsp/conn.go @@ -18,6 +18,7 @@ import ( ) type Conn struct { + core.Connection core.Listener // public @@ -30,9 +31,7 @@ type Conn struct { Timeout int Transport string // custom transport support, ex. RTSP over WebSocket - Medias []*core.Media - UserAgent string - URL *url.URL + URL *url.URL // internal @@ -44,19 +43,10 @@ type Conn struct { reader *bufio.Reader sequence int session string - sdp string uri string state State stateMu sync.Mutex - - receivers []*core.Receiver - senders []*core.Sender - - // stats - - recv int - send int } const ( @@ -114,7 +104,7 @@ func (c *Conn) Handle() (err error) { // polling frames from remote RTSP Server (ex Camera) timeout = time.Second * 5 - if len(c.receivers) == 0 { + if len(c.Receivers) == 0 { // if we only send audio to camera // https://github.com/AlexxIT/go2rtc/issues/659 timeout += keepaliveDT @@ -239,7 +229,7 @@ func (c *Conn) Handle() (err error) { return } - c.recv += int(size) + c.Recv += int(size) if channelID&1 == 0 { packet := &rtp.Packet{} @@ -247,7 +237,7 @@ func (c *Conn) Handle() (err error) { return } - for _, receiver := range c.receivers { + for _, receiver := range c.Receivers { if receiver.ID == channelID { receiver.WriteRTP(packet) break diff --git a/pkg/rtsp/consumer.go b/pkg/rtsp/consumer.go index 79e2b348..b6df188f 100644 --- a/pkg/rtsp/consumer.go +++ b/pkg/rtsp/consumer.go @@ -18,15 +18,6 @@ func (c *Conn) GetMedias() []*core.Media { } func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) (err error) { - core.Assert(media.Direction == core.DirectionSendonly) - - for _, sender := range c.senders { - if sender.Codec == codec { - sender.HandleRTP(track) - return - } - } - var channel byte switch c.mode { @@ -47,12 +38,12 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv c.state = StateSetup case core.ModePassiveConsumer: - channel = byte(len(c.senders)) * 2 + 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)) + codec.PayloadType = byte(96 + len(c.Senders)) default: panic(core.Caller()) @@ -70,7 +61,7 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv sender.HandleRTP(track) - c.senders = append(c.senders, sender) + c.Senders = append(c.Senders, sender) return nil } @@ -99,7 +90,7 @@ func (c *Conn) packetWriter(codec *core.Codec, channel, payloadType uint8) core. } //log.Printf("[rtsp] channel:%2d write_size:%6d buffer_size:%6d", channel, n, len(buf)) if _, err := c.conn.Write(buf[:n]); err == nil { - c.send += n + c.Send += n } n = 0 } diff --git a/pkg/rtsp/producer.go b/pkg/rtsp/producer.go index d0f36a1c..de115808 100644 --- a/pkg/rtsp/producer.go +++ b/pkg/rtsp/producer.go @@ -10,7 +10,7 @@ import ( func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { core.Assert(media.Direction == core.DirectionRecvonly) - for _, track := range c.receivers { + for _, track := range c.Receivers { if track.Codec == codec { return track, nil } @@ -34,7 +34,7 @@ func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, e track := core.NewReceiver(media, codec) track.ID = channel - c.receivers = append(c.receivers, track) + c.Receivers = append(c.Receivers, track) return track, nil } @@ -81,10 +81,10 @@ func (c *Conn) Start() (err error) { } func (c *Conn) Stop() (err error) { - for _, receiver := range c.receivers { + for _, receiver := range c.Receivers { receiver.Close() } - for _, sender := range c.senders { + for _, sender := range c.Senders { sender.Close() } @@ -99,25 +99,7 @@ func (c *Conn) Stop() (err error) { } func (c *Conn) MarshalJSON() ([]byte, error) { - info := &core.Info{ - Type: "RTSP " + c.mode.String(), - SDP: c.sdp, - UserAgent: c.UserAgent, - Medias: c.Medias, - Receivers: c.receivers, - Senders: c.senders, - Recv: c.recv, - Send: c.send, - } - - if c.URL != nil { - info.URL = c.URL.String() - } - if c.conn != nil { - info.RemoteAddr = c.conn.RemoteAddr().String() - } - - return json.Marshal(info) + return json.Marshal(c.Connection) } func (c *Conn) Reconnect() error { @@ -135,12 +117,12 @@ func (c *Conn) Reconnect() error { } // restore previous medias - for _, receiver := range c.receivers { + for _, receiver := range c.Receivers { if _, err := c.SetupMedia(receiver.Media); err != nil { return err } } - for _, sender := range c.senders { + for _, sender := range c.Senders { if _, err := c.SetupMedia(sender.Media); err != nil { return err } diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index 8e0d3134..7953b0dc 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -14,10 +14,16 @@ import ( ) func NewServer(conn net.Conn) *Conn { - c := new(Conn) - c.conn = conn - c.reader = bufio.NewReader(conn) - return c + return &Conn{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "rtsp", + Protocol: "rtsp+tcp", + RemoteAddr: conn.RemoteAddr().String(), + }, + conn: conn, + reader: bufio.NewReader(conn), + } } func (c *Conn) Auth(username, password string) { @@ -70,7 +76,7 @@ func (c *Conn) Accept() error { return errors.New("wrong content type") } - c.sdp = string(req.Body) // for info + c.SDP = string(req.Body) // for info c.Medias, err = UnmarshalSDP(req.Body) if err != nil { @@ -81,7 +87,7 @@ func (c *Conn) Accept() error { for i, media := range c.Medias { track := core.NewReceiver(media, media.Codecs[0]) track.ID = byte(i * 2) - c.receivers = append(c.receivers, track) + c.Receivers = append(c.Receivers, track) } c.mode = core.ModePassiveProducer @@ -96,7 +102,7 @@ func (c *Conn) Accept() error { c.mode = core.ModePassiveConsumer c.Fire(MethodDescribe) - if c.senders == nil { + if c.Senders == nil { res := &tcp.Response{ Status: "404 Not Found", Request: req, @@ -113,7 +119,7 @@ func (c *Conn) Accept() error { // convert tracks to real output medias medias var medias []*core.Media - for i, track := range c.senders { + for i, track := range c.Senders { media := &core.Media{ Kind: core.GetKind(track.Codec.Name), Direction: core.DirectionRecvonly, @@ -128,7 +134,7 @@ func (c *Conn) Accept() error { return err } - c.sdp = string(res.Body) // for info + c.SDP = string(res.Body) // for info if err = c.WriteResponse(res); err != nil { return err @@ -148,9 +154,9 @@ func (c *Conn) Accept() error { c.state = StateSetup if c.mode == core.ModePassiveConsumer { - if i := reqTrackID(req); i >= 0 && i < len(c.senders) { + if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { // mark sender as SETUP - c.senders[i].Media.ID = MethodSetup + 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) } else { @@ -170,7 +176,7 @@ func (c *Conn) Accept() error { case MethodRecord, MethodPlay: if c.mode == core.ModePassiveConsumer { // stop unconfigured senders - for _, track := range c.senders { + for _, track := range c.Senders { if track.Media.ID != MethodSetup { track.Close() } diff --git a/pkg/stdin/consumer.go b/pkg/stdin/backchannel.go similarity index 88% rename from pkg/stdin/consumer.go rename to pkg/stdin/backchannel.go index a1284948..b9a4a6d4 100644 --- a/pkg/stdin/consumer.go +++ b/pkg/stdin/backchannel.go @@ -49,10 +49,12 @@ func (c *Client) Stop() (err error) { } func (c *Client) MarshalJSON() ([]byte, error) { - info := &core.Info{ - Type: "Exec active consumer", - Medias: c.medias, - Send: c.send, + info := &core.Connection{ + ID: core.ID(c), + FormatName: "exec", + Protocol: "pipe", + Medias: c.medias, + Send: c.send, } if c.sender != nil { info.Senders = []*core.Sender{c.sender} diff --git a/pkg/stdin/client.go b/pkg/stdin/client.go index 51db30ee..09e525ad 100644 --- a/pkg/stdin/client.go +++ b/pkg/stdin/client.go @@ -6,6 +6,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" ) +// Deprecated: should be rewritten to core.Connection type Client struct { cmd *exec.Cmd diff --git a/pkg/tapo/consumer.go b/pkg/tapo/backchannel.go similarity index 100% rename from pkg/tapo/consumer.go rename to pkg/tapo/backchannel.go diff --git a/pkg/tapo/client.go b/pkg/tapo/client.go index ed79e500..3585011c 100644 --- a/pkg/tapo/client.go +++ b/pkg/tapo/client.go @@ -23,6 +23,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/tcp" ) +// Deprecated: should be rewritten to core.Connection type Client struct { core.Listener diff --git a/pkg/tapo/producer.go b/pkg/tapo/producer.go index ac213e15..7d66d907 100644 --- a/pkg/tapo/producer.go +++ b/pkg/tapo/producer.go @@ -2,6 +2,7 @@ package tapo import ( "encoding/json" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mpegts" ) @@ -74,15 +75,20 @@ func (c *Client) Stop() error { } 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, + info := &core.Connection{ + ID: core.ID(c), + FormatName: "tapo", + Protocol: "http", + Medias: c.medias, + Recv: c.recv, + Receivers: c.receivers, + Send: c.send, } if c.sender != nil { info.Senders = []*core.Sender{c.sender} } + if c.conn1 != nil { + info.RemoteAddr = c.conn1.RemoteAddr().String() + } return json.Marshal(info) } diff --git a/pkg/tcp/helpers.go b/pkg/tcp/helpers.go deleted file mode 100644 index 9db42a89..00000000 --- a/pkg/tcp/helpers.go +++ /dev/null @@ -1,12 +0,0 @@ -package tcp - -import ( - "net/http" -) - -func RemoteAddr(r *http.Request) string { - if remote := r.Header.Get("X-Forwarded-For"); remote != "" { - return remote + ", " + r.RemoteAddr - } - return r.RemoteAddr -} diff --git a/pkg/wav/wav.go b/pkg/wav/producer.go similarity index 89% rename from pkg/wav/wav.go rename to pkg/wav/producer.go index 5f572bd6..63f6d01a 100644 --- a/pkg/wav/wav.go +++ b/pkg/wav/producer.go @@ -54,22 +54,27 @@ func Open(r io.Reader) (*Producer, error) { return nil, errors.New("waw: unsupported codec") } - prod := &Producer{rd: rd, cl: r.(io.Closer)} - prod.Type = "WAV producer" - prod.Medias = []*core.Media{ + medias := []*core.Media{ { Kind: core.KindAudio, Direction: core.DirectionRecvonly, Codecs: []*core.Codec{codec}, }, } - return prod, nil + return &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "wav", + Medias: medias, + Transport: r, + }, + rd: rd, + }, nil } type Producer struct { - core.SuperProducer + core.Connection rd *bufio.Reader - cl io.Closer } func (c *Producer) Start() error { @@ -106,11 +111,6 @@ func (c *Producer) Start() error { } } -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.cl.Close() -} - func readChunk(r io.Reader) (chunkID string, data []byte, err error) { b := make([]byte, 8) if _, err = io.ReadFull(r, b); err != nil { diff --git a/pkg/webrtc/client.go b/pkg/webrtc/client.go index 50c7773d..9a7a7b2f 100644 --- a/pkg/webrtc/client.go +++ b/pkg/webrtc/client.go @@ -71,7 +71,7 @@ func (c *Conn) SetAnswer(answer string) (err error) { return } - c.medias = UnmarshalMedias(sd.MediaDescriptions) + c.Medias = UnmarshalMedias(sd.MediaDescriptions) return nil } diff --git a/pkg/webrtc/conn.go b/pkg/webrtc/conn.go index 0e10874e..3e3ecc4f 100644 --- a/pkg/webrtc/conn.go +++ b/pkg/webrtc/conn.go @@ -1,6 +1,9 @@ package webrtc import ( + "encoding/json" + "fmt" + "strings" "time" "github.com/AlexxIT/go2rtc/pkg/core" @@ -10,28 +13,25 @@ import ( ) type Conn struct { + core.Connection core.Listener - UserAgent string - Desc string - Mode core.Mode + Mode core.Mode `json:"mode"` pc *webrtc.PeerConnection - medias []*core.Media - receivers []*core.Receiver - senders []*core.Sender - - recv int - send int - offer string - remote string closed core.Waiter } func NewConn(pc *webrtc.PeerConnection) *Conn { - c := &Conn{pc: pc} + c := &Conn{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "webrtc", + }, + pc: pc, + } pc.OnICECandidate(func(candidate *webrtc.ICECandidate) { // last candidate will be empty @@ -50,7 +50,15 @@ func NewConn(pc *webrtc.PeerConnection) *Conn { } pc.SCTP().Transport().ICETransport().OnSelectedCandidatePairChange( func(pair *webrtc.ICECandidatePair) { - c.remote = pair.Remote.String() + c.Protocol += "+" + pair.Remote.Protocol.String() + c.RemoteAddr = fmt.Sprintf( + "%s:%d %s", sanitizeIP6(pair.Remote.Address), pair.Remote.Port, pair.Remote.Typ, + ) + if pair.Remote.RelatedAddress != "" { + c.RemoteAddr += fmt.Sprintf( + " %s:%d", sanitizeIP6(pair.Remote.RelatedAddress), pair.Remote.RelatedPort, + ) + } }, ) }) @@ -92,7 +100,7 @@ func NewConn(pc *webrtc.PeerConnection) *Conn { return } - c.recv += n + c.Recv += n packet := &rtp.Packet{} if err := packet.Unmarshal(b[:n]); err != nil { @@ -121,7 +129,7 @@ func NewConn(pc *webrtc.PeerConnection) *Conn { switch state { case webrtc.PeerConnectionStateConnected: - for _, sender := range c.senders { + for _, sender := range c.Senders { sender.Start() } case webrtc.PeerConnectionStateDisconnected, webrtc.PeerConnectionStateFailed, webrtc.PeerConnectionStateClosed: @@ -134,6 +142,10 @@ func NewConn(pc *webrtc.PeerConnection) *Conn { return c } +func (c *Conn) MarshalJSON() ([]byte, error) { + return json.Marshal(c.Connection) +} + func (c *Conn) Close() error { c.closed.Done(nil) return c.pc.Close() @@ -172,7 +184,7 @@ func (c *Conn) getMediaCodec(remote *webrtc.TrackRemote) (*core.Media, *core.Cod } // search Media for this MID - for _, media := range c.medias { + for _, media := range c.Medias { if media.ID != tr.Mid() || media.Direction != core.DirectionRecvonly { continue } @@ -194,3 +206,10 @@ func (c *Conn) getMediaCodec(remote *webrtc.TrackRemote) (*core.Media, *core.Cod return nil, nil } + +func sanitizeIP6(host string) string { + if strings.IndexByte(host, ':') > 0 { + return "[" + host + "]" + } + return host +} diff --git a/pkg/webrtc/consumer.go b/pkg/webrtc/consumer.go index 3bcaf49a..2dcab436 100644 --- a/pkg/webrtc/consumer.go +++ b/pkg/webrtc/consumer.go @@ -1,7 +1,6 @@ package webrtc import ( - "encoding/json" "errors" "github.com/AlexxIT/go2rtc/pkg/core" @@ -12,13 +11,13 @@ import ( ) func (c *Conn) GetMedias() []*core.Media { - return WithResampling(c.medias) + return WithResampling(c.Medias) } func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { core.Assert(media.Direction == core.DirectionSendonly) - for _, sender := range c.senders { + for _, sender := range c.Senders { if sender.Codec == codec { sender.Bind(track) return nil @@ -42,7 +41,7 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv sender := core.NewSender(media, codec) sender.Handler = func(packet *rtp.Packet) { - c.send += packet.MarshalSize() + c.Send += packet.MarshalSize() //important to send with remote PayloadType _ = localTrack.WriteRTP(payloadType, packet) } @@ -85,20 +84,6 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv sender.HandleRTP(track) } - c.senders = append(c.senders, sender) + c.Senders = append(c.Senders, sender) return nil } - -func (c *Conn) MarshalJSON() ([]byte, error) { - info := &core.Info{ - Type: c.Desc + " " + c.Mode.String(), - RemoteAddr: c.remote, - UserAgent: c.UserAgent, - Medias: c.medias, - Receivers: c.receivers, - Senders: c.senders, - Recv: c.recv, - Send: c.send, - } - return json.Marshal(info) -} diff --git a/pkg/webrtc/producer.go b/pkg/webrtc/producer.go index d4136a5c..a0910c39 100644 --- a/pkg/webrtc/producer.go +++ b/pkg/webrtc/producer.go @@ -8,7 +8,7 @@ import ( func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { core.Assert(media.Direction == core.DirectionRecvonly) - for _, track := range c.receivers { + for _, track := range c.Receivers { if track.Codec == codec { return track, nil } @@ -39,7 +39,7 @@ func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, e } track := core.NewReceiver(media, codec) - c.receivers = append(c.receivers, track) + c.Receivers = append(c.Receivers, track) return track, nil } @@ -47,13 +47,3 @@ func (c *Conn) Start() error { c.closed.Wait() return nil } - -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 ce462e45..9cc89778 100644 --- a/pkg/webrtc/server.go +++ b/pkg/webrtc/server.go @@ -42,7 +42,7 @@ func (c *Conn) SetOffer(offer string) (err error) { } } - c.medias = UnmarshalMedias(sd.MediaDescriptions) + c.Medias = UnmarshalMedias(sd.MediaDescriptions) return } @@ -57,7 +57,7 @@ func (c *Conn) GetAnswer() (answer string, err error) { // disable transceivers if we don't have track, make direction=inactive transeivers: for _, tr := range c.pc.GetTransceivers() { - for _, sender := range c.senders { + for _, sender := range c.Senders { if sender.Media.ID == tr.Mid() { continue transeivers } diff --git a/pkg/webtorrent/client.go b/pkg/webtorrent/client.go index de6b21c7..3594679d 100644 --- a/pkg/webtorrent/client.go +++ b/pkg/webtorrent/client.go @@ -3,19 +3,21 @@ package webtorrent import ( "encoding/base64" "fmt" + "strconv" + "time" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/webrtc" "github.com/gorilla/websocket" pion "github.com/pion/webrtc/v3" - "strconv" - "time" ) func NewClient(tracker, share, pwd string, pc *pion.PeerConnection) (*webrtc.Conn, error) { // 1. Create WebRTC producer prod := webrtc.NewConn(pc) - prod.Desc = "WebRTC/WebTorrent sync" + prod.FormatName = "webtorrent" prod.Mode = core.ModeActiveProducer + prod.Protocol = "ws" medias := []*core.Media{ {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, diff --git a/pkg/y4m/consumer.go b/pkg/y4m/consumer.go index 01bece31..dd9b46e9 100644 --- a/pkg/y4m/consumer.go +++ b/pkg/y4m/consumer.go @@ -9,14 +9,17 @@ import ( ) type Consumer struct { - core.SuperConsumer + core.Connection wr *core.WriteBuffer } func NewConsumer() *Consumer { + wr := core.NewWriteBuffer(nil) return &Consumer{ - core.SuperConsumer{ - Type: "YUV4MPEG2 passive consumer", + core.Connection{ + ID: core.NewID(), + Transport: wr, + FormatName: "yuv4mpegpipe", Medias: []*core.Media{ { Kind: core.KindVideo, @@ -27,7 +30,7 @@ func NewConsumer() *Consumer { }, }, }, - core.NewWriteBuffer(nil), + wr, } } @@ -60,8 +63,3 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv func (c *Consumer) WriteTo(wr io.Writer) (int64, error) { return c.wr.WriteTo(wr) } - -func (c *Consumer) Stop() error { - _ = c.SuperConsumer.Close() - return c.wr.Close() -} diff --git a/pkg/y4m/producer.go b/pkg/y4m/producer.go index 05f98a6f..ee2dd731 100644 --- a/pkg/y4m/producer.go +++ b/pkg/y4m/producer.go @@ -2,7 +2,6 @@ package y4m import ( "bufio" - "bytes" "errors" "io" @@ -19,41 +18,13 @@ func Open(r io.Reader) (*Producer, error) { b = b[:len(b)-1] // remove \n - sdp := string(b) - var fmtp string - - for b != nil { - // YUV4MPEG2 W1280 H720 F24:1 Ip A1:1 C420mpeg2 XYSCSS=420MPEG2 - // https://manned.org/yuv4mpeg.5 - // https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/yuv4mpegenc.c - key := b[0] - var value string - if i := bytes.IndexByte(b, ' '); i > 0 { - value = string(b[1:i]) - b = b[i+1:] - } else { - value = string(b[1:]) - b = nil - } - - switch key { - case 'W': - fmtp = "width=" + value - case 'H': - fmtp += ";height=" + value - case 'C': - fmtp += ";colorspace=" + value - } - } + fmtp := ParseHeader(b) if GetSize(fmtp) == 0 { - return nil, errors.New("y4m: unsupported format: " + sdp) + return nil, errors.New("y4m: unsupported format: " + string(b)) } - prod := &Producer{rd: rd, cl: r.(io.Closer)} - prod.Type = "YUV4MPEG2 producer" - prod.SDP = sdp - prod.Medias = []*core.Media{ + medias := []*core.Media{ { Kind: core.KindVideo, Direction: core.DirectionRecvonly, @@ -67,14 +38,21 @@ func Open(r io.Reader) (*Producer, error) { }, }, } - - return prod, nil + return &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "yuv4mpegpipe", + Medias: medias, + SDP: string(b), + Transport: r, + }, + rd: rd, + }, nil } type Producer struct { - core.SuperProducer + core.Connection rd *bufio.Reader - cl io.Closer } func (c *Producer) Start() error { @@ -103,8 +81,3 @@ func (c *Producer) Start() error { c.Receivers[0].WriteRTP(pkt) } } - -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.cl.Close() -} diff --git a/pkg/y4m/y4m.go b/pkg/y4m/y4m.go index 8184ea97..4ac54da6 100644 --- a/pkg/y4m/y4m.go +++ b/pkg/y4m/y4m.go @@ -1,6 +1,7 @@ package y4m import ( + "bytes" "image" "github.com/AlexxIT/go2rtc/pkg/core" @@ -10,6 +11,34 @@ const FourCC = "YUV4" const frameHdr = "FRAME\n" +func ParseHeader(b []byte) (fmtp string) { + for b != nil { + // YUV4MPEG2 W1280 H720 F24:1 Ip A1:1 C420mpeg2 XYSCSS=420MPEG2 + // https://manned.org/yuv4mpeg.5 + // https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/yuv4mpegenc.c + key := b[0] + + var value string + if i := bytes.IndexByte(b, ' '); i > 0 { + value = string(b[1:i]) + b = b[i+1:] + } else { + value = string(b[1:]) + b = nil + } + + switch key { + case 'W': + fmtp = "width=" + value + case 'H': + fmtp += ";height=" + value + case 'C': + fmtp += ";colorspace=" + value + } + } + return +} + func GetSize(fmtp string) int { w := core.Atoi(core.Between(fmtp, "width=", ";")) h := core.Atoi(core.Between(fmtp, "height=", ";")) From 734393d6385b96bf2e4184ae68f9d9d075ff3630 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Jun 2024 06:36:24 +0300 Subject: [PATCH 116/130] Add streaming network visualisation --- internal/streams/api.go | 21 +++++ internal/streams/dot.go | 164 ++++++++++++++++++++++++++++++++++++ internal/streams/streams.go | 1 + www/index.html | 2 +- www/main.js | 1 + www/network.html | 44 ++++++++++ 6 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 internal/streams/dot.go create mode 100644 www/network.html diff --git a/internal/streams/api.go b/internal/streams/api.go index 69d2276a..d64c4846 100644 --- a/internal/streams/api.go +++ b/internal/streams/api.go @@ -101,3 +101,24 @@ func apiStreams(w http.ResponseWriter, r *http.Request) { } } } + +func apiStreamsDOT(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + + dot := make([]byte, 0, 1024) + dot = append(dot, "digraph {\n"...) + if query.Has("src") { + for _, name := range query["src"] { + if stream := streams[name]; stream != nil { + dot = AppendDOT(dot, stream) + } + } + } else { + for _, stream := range streams { + dot = AppendDOT(dot, stream) + } + } + dot = append(dot, '}') + + api.Response(w, dot, "text/vnd.graphviz") +} diff --git a/internal/streams/dot.go b/internal/streams/dot.go new file mode 100644 index 00000000..aa008c40 --- /dev/null +++ b/internal/streams/dot.go @@ -0,0 +1,164 @@ +package streams + +import ( + "encoding/json" + "fmt" + "strings" +) + +func AppendDOT(dot []byte, stream *Stream) []byte { + for _, prod := range stream.producers { + if prod.conn == nil { + continue + } + c, err := marshalConn(prod.conn) + if err != nil { + continue + } + dot = c.appendDOT(dot, "producer") + } + for _, cons := range stream.consumers { + c, err := marshalConn(cons) + if err != nil { + continue + } + dot = c.appendDOT(dot, "consumer") + } + return dot +} + +func marshalConn(v any) (*conn, error) { + b, err := json.Marshal(v) + if err != nil { + return nil, err + } + var c conn + if err = json.Unmarshal(b, &c); err != nil { + return nil, err + } + return &c, nil +} + +const bytesK = "KMGTP" + +func humanBytes(i int) string { + if i < 1000 { + return fmt.Sprintf("%d B", i) + } + + f := float64(i) / 1000 + var n uint8 + for f >= 1000 && n < 5 { + f /= 1000 + n++ + } + return fmt.Sprintf("%.2f %cB", f, bytesK[n]) +} + +type node struct { + ID uint32 `json:"id"` + Codec map[string]any `json:"codec"` + Parent uint32 `json:"parent"` + Childs []uint32 `json:"childs"` + Bytes int `json:"bytes"` + //Packets uint32 `json:"packets"` + //Drops uint32 `json:"drops"` +} + +var codecKeys = []string{"codec_name", "sample_rate", "channels", "profile", "level"} + +func (n *node) codec() []byte { + b := make([]byte, 0, 128) + for _, k := range codecKeys { + if v := n.Codec[k]; v != nil { + b = fmt.Appendf(b, "%s=%v\n", k, v) + } + } + return b[:len(b)-1] +} + +func (n *node) appendDOT(dot []byte, group string) []byte { + dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", n.ID, group, n.Codec["codec_name"], n.codec()) + //for _, sink := range n.Childs { + // dot = fmt.Appendf(dot, "%d -> %d;\n", n.ID, sink) + //} + return dot +} + +type conn struct { + ID uint32 `json:"id"` + FormatName string `json:"format_name"` + Protocol string `json:"protocol"` + RemoteAddr string `json:"remote_addr"` + Source string `json:"source"` + URL string `json:"url"` + UserAgent string `json:"user_agent"` + Receivers []node `json:"receivers"` + Senders []node `json:"senders"` + BytesRecv int `json:"bytes_recv"` + BytesSend int `json:"bytes_send"` +} + +func (c *conn) appendDOT(dot []byte, group string) []byte { + host := c.host() + dot = fmt.Appendf(dot, "%s [group=host];\n", host) + dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", c.ID, group, c.FormatName, c.label()) + if group == "producer" { + dot = fmt.Appendf(dot, "%s -> %d [label=%q];\n", host, c.ID, humanBytes(c.BytesRecv)) + } else { + dot = fmt.Appendf(dot, "%d -> %s [label=%q];\n", c.ID, host, humanBytes(c.BytesSend)) + } + + for _, recv := range c.Receivers { + dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", c.ID, recv.ID, humanBytes(recv.Bytes)) + dot = recv.appendDOT(dot, "node") + } + for _, send := range c.Senders { + dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.Parent, c.ID, humanBytes(send.Bytes)) + //dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.ID, c.ID, humanBytes(send.Bytes)) + //dot = send.appendDOT(dot, "node") + } + return dot +} + +func (c *conn) host() (s string) { + if c.Protocol == "pipe" { + return "127.0.0.1" + } + + if s = c.RemoteAddr; s == "" { + return "unknown" + } + + if i := strings.Index(s, "forwarded"); i > 0 { + s = s[i+10:] + } + + if s[0] == '[' { + if i := strings.Index(s, "]"); i > 0 { + return s[1:i] + } + } + + if i := strings.IndexAny(s, " ,:"); i > 0 { + return s[:i] + } + return +} + +func (c *conn) label() (s string) { + s = "format_name=" + c.FormatName + if c.Protocol != "" { + s += "\nprotocol=" + c.Protocol + } + if c.Source != "" { + s += "\nsource=" + c.Source + } + if c.URL != "" { + s += "\nurl=" + c.URL + } + if c.UserAgent != "" { + s += "\nuser_agent=" + c.UserAgent + } + return +} diff --git a/internal/streams/streams.go b/internal/streams/streams.go index 6d6fa773..ff0f5654 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -27,6 +27,7 @@ func Init() { } api.HandleFunc("api/streams", apiStreams) + api.HandleFunc("api/streams.dot", apiStreamsDOT) if cfg.Publish == nil { return diff --git a/www/index.html b/www/index.html index 6adf9f0a..63fedcec 100644 --- a/www/index.html +++ b/www/index.html @@ -139,7 +139,7 @@ const isChecked = checkboxStates[name] ? 'checked' : ''; tr.innerHTML = `` + - `` + + `` + ``; } diff --git a/www/main.js b/www/main.js index 2c15e071..714c9127 100644 --- a/www/main.js +++ b/www/main.js @@ -138,6 +138,7 @@ body.dark-mode hr {
  • Add
  • Config
  • Log
  • +
  • Net
  • 🌙 diff --git a/www/network.html b/www/network.html new file mode 100644 index 00000000..5193b6a9 --- /dev/null +++ b/www/network.html @@ -0,0 +1,44 @@ + + + + + go2rtc - Network + + + + + +
    + + + \ No newline at end of file From 31e57c2ff81e04f585135fcf0670fc150d5f9734 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Jun 2024 06:37:42 +0300 Subject: [PATCH 117/130] Fix errors output for webrtc client and server --- internal/webrtc/client.go | 10 ++++++++-- internal/webrtc/server.go | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/webrtc/client.go b/internal/webrtc/client.go index 4b8b1b9a..d42c51dd 100644 --- a/internal/webrtc/client.go +++ b/internal/webrtc/client.go @@ -77,10 +77,15 @@ func go2rtcClient(url string) (core.Producer, error) { // 2. Create PeerConnection pc, err := PeerConnection(true) if err != nil { - log.Error().Err(err).Caller().Send() return nil, err } + defer func() { + if err != nil { + _ = pc.Close() + } + }() + // waiter will wait PC error or WS error or nil (connection OK) var connState core.Waiter var connMu sync.Mutex @@ -133,7 +138,8 @@ func go2rtcClient(url string) (core.Producer, error) { } if msg.Type != "webrtc/answer" { - return nil, errors.New("wrong answer: " + msg.Type) + err = errors.New("wrong answer: " + msg.String()) + return nil, err } answer := msg.String() diff --git a/internal/webrtc/server.go b/internal/webrtc/server.go index 91a237db..f7365afa 100644 --- a/internal/webrtc/server.go +++ b/internal/webrtc/server.go @@ -65,6 +65,7 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) { url := r.URL.Query().Get("src") stream := streams.Get(url) if stream == nil { + http.Error(w, api.StreamNotFound, http.StatusNotFound) return } From 5d579596088f4a0694c1d12b5e4b18144ca1056b Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sun, 16 Jun 2024 08:56:57 +0300 Subject: [PATCH 118/130] fix(streams): handle missing codec_name in appendDOT function --- internal/streams/dot.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/internal/streams/dot.go b/internal/streams/dot.go index aa008c40..b9a2b773 100644 --- a/internal/streams/dot.go +++ b/internal/streams/dot.go @@ -77,12 +77,17 @@ func (n *node) codec() []byte { return b[:len(b)-1] } -func (n *node) appendDOT(dot []byte, group string) []byte { - dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", n.ID, group, n.Codec["codec_name"], n.codec()) +func (n *node) appendDOT(dot []byte, group string) ([]byte, error) { + codecName, ok := n.Codec["codec_name"] + if !ok { + return nil, fmt.Errorf("codec_name not found in Codec map") + } + + dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", n.ID, group, codecName, n.codec()) //for _, sink := range n.Childs { // dot = fmt.Appendf(dot, "%d -> %d;\n", n.ID, sink) //} - return dot + return dot, nil } type conn struct { @@ -111,7 +116,7 @@ func (c *conn) appendDOT(dot []byte, group string) []byte { for _, recv := range c.Receivers { dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", c.ID, recv.ID, humanBytes(recv.Bytes)) - dot = recv.appendDOT(dot, "node") + dot, _ = recv.appendDOT(dot, "node") // TODO: handle error for debug purposes } for _, send := range c.Senders { dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.Parent, c.ID, humanBytes(send.Bytes)) From 1b411b1fed4fa33d17739542ce53f2870a161998 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sun, 16 Jun 2024 10:18:45 +0300 Subject: [PATCH 119/130] refactor(streams): optimize label generation with strings.Builder feat(network): add periodic data fetching and network update --- internal/streams/dot.go | 15 ++++---- www/network.html | 83 +++++++++++++++++++++++++++++++---------- 2 files changed, 72 insertions(+), 26 deletions(-) diff --git a/internal/streams/dot.go b/internal/streams/dot.go index aa008c40..9aacccb5 100644 --- a/internal/streams/dot.go +++ b/internal/streams/dot.go @@ -146,19 +146,20 @@ func (c *conn) host() (s string) { return } -func (c *conn) label() (s string) { - s = "format_name=" + c.FormatName +func (c *conn) label() string { + var sb strings.Builder + sb.WriteString("format_name=" + c.FormatName) if c.Protocol != "" { - s += "\nprotocol=" + c.Protocol + sb.WriteString("\nprotocol=" + c.Protocol) } if c.Source != "" { - s += "\nsource=" + c.Source + sb.WriteString("\nsource=" + c.Source) } if c.URL != "" { - s += "\nurl=" + c.URL + sb.WriteString("\nurl=" + c.URL) } if c.UserAgent != "" { - s += "\nuser_agent=" + c.UserAgent + sb.WriteString("\nuser_agent=" + c.UserAgent) } - return + return sb.String() } diff --git a/www/network.html b/www/network.html index 5193b6a9..519e0eba 100644 --- a/www/network.html +++ b/www/network.html @@ -21,24 +21,69 @@ - -
    - + +
    + + \ No newline at end of file From a69eb8a66e576cbfe369c4af985ea37be5abd096 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sun, 16 Jun 2024 14:53:08 +0300 Subject: [PATCH 120/130] style(network): add flex-grow to network div and move script tag --- www/network.html | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/www/network.html b/www/network.html index 519e0eba..b0edd31c 100644 --- a/www/network.html +++ b/www/network.html @@ -18,11 +18,15 @@ height: 100%; width: 100%; } + + #network { + flex-grow: 1; + } -
    + - \ No newline at end of file From cb44d5431a854efd2a84af52d747e98aabc862b9 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sun, 16 Jun 2024 15:01:40 +0300 Subject: [PATCH 121/130] feat(network): preserve pan and scale on data reload --- www/network.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/www/network.html b/www/network.html index b0edd31c..18d4a640 100644 --- a/www/network.html +++ b/www/network.html @@ -68,6 +68,8 @@ network.storePositions(); } else { const positions = network.getPositions(); + const viewState = network.getViewPosition(); + const scale = network.getScale(); network.setData(data); @@ -76,6 +78,8 @@ network.moveNode(nodeId, positions[nodeId].x, positions[nodeId].y); } } + + network.moveTo({ position: viewState, scale: scale }); } } catch (error) { From 906f554d74fa7d2cf18cbb6314a3d309342089ef Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Jun 2024 15:19:50 +0300 Subject: [PATCH 122/130] Code refactoring after #1195 --- internal/streams/dot.go | 25 +++++++++++++++---------- pkg/core/codec.go | 8 +++++++- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/internal/streams/dot.go b/internal/streams/dot.go index b9a2b773..96aa4115 100644 --- a/internal/streams/dot.go +++ b/internal/streams/dot.go @@ -67,6 +67,13 @@ type node struct { var codecKeys = []string{"codec_name", "sample_rate", "channels", "profile", "level"} +func (n *node) name() string { + if name, ok := n.Codec["codec_name"].(string); ok { + return name + } + return "unknown" +} + func (n *node) codec() []byte { b := make([]byte, 0, 128) for _, k := range codecKeys { @@ -74,20 +81,18 @@ func (n *node) codec() []byte { b = fmt.Appendf(b, "%s=%v\n", k, v) } } - return b[:len(b)-1] + if l := len(b); l > 0 { + return b[:l-1] + } + return b } -func (n *node) appendDOT(dot []byte, group string) ([]byte, error) { - codecName, ok := n.Codec["codec_name"] - if !ok { - return nil, fmt.Errorf("codec_name not found in Codec map") - } - - dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", n.ID, group, codecName, n.codec()) +func (n *node) appendDOT(dot []byte, group string) []byte { + dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", n.ID, group, n.name(), n.codec()) //for _, sink := range n.Childs { // dot = fmt.Appendf(dot, "%d -> %d;\n", n.ID, sink) //} - return dot, nil + return dot } type conn struct { @@ -116,7 +121,7 @@ func (c *conn) appendDOT(dot []byte, group string) []byte { for _, recv := range c.Receivers { dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", c.ID, recv.ID, humanBytes(recv.Bytes)) - dot, _ = recv.appendDOT(dot, "node") // TODO: handle error for debug purposes + dot = recv.appendDOT(dot, "node") } for _, send := range c.Senders { dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.Parent, c.ID, humanBytes(send.Bytes)) diff --git a/pkg/core/codec.go b/pkg/core/codec.go index d07b8b74..9c6c6b79 100644 --- a/pkg/core/codec.go +++ b/pkg/core/codec.go @@ -69,8 +69,14 @@ func FFmpegCodecName(name string) string { return "vp9" case CodecAV1: return "av1" + case CodecELD: + return "aac/eld" + case CodecFLAC: + return "flac" + case CodecMP3: + return "mp3" } - return "" + return name } func (c *Codec) String() (s string) { From d8aed552bc54e98804d92e74993389cf18ea1469 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sun, 16 Jun 2024 15:22:33 +0300 Subject: [PATCH 123/130] fix(network): ensure consistent node positions by storing and reusing seed --- www/network.html | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/www/network.html b/www/network.html index 18d4a640..53f1ca68 100644 --- a/www/network.html +++ b/www/network.html @@ -31,13 +31,11 @@ let network; let nodes = new vis.DataSet(); let edges = new vis.DataSet(); + let seed = ""; /* global vis */ window.addEventListener('load', () => { const url = new URL('api/streams.dot' + location.search, location.href); const options = { - layout: { - randomSeed: "0.4597730541017021:1718519934576" - }, edges: { font: { align: 'middle' }, smooth: false, @@ -66,16 +64,19 @@ edges = new vis.DataSet(data.edges); network = new vis.Network(container, { nodes, edges }, options); network.storePositions(); + seed = network.getSeed(); } else { const positions = network.getPositions(); const viewState = network.getViewPosition(); const scale = network.getScale(); - + network.setOptions({layout: { + randomSeed: seed + }}) network.setData(data); for (const nodeId in positions) { if (positions.hasOwnProperty(nodeId)) { - network.moveNode(nodeId, positions[nodeId].x, positions[nodeId].y); + network.moveNode(nodeId, Math.floor(positions[nodeId].x), Math.floor(positions[nodeId].y)); } } From a56d3353806c00d9e8203427994499d191e13942 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Jun 2024 15:26:18 +0300 Subject: [PATCH 124/130] Fix homekit producer remote_addr --- pkg/homekit/producer.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/homekit/producer.go b/pkg/homekit/producer.go index c2781e27..451b9882 100644 --- a/pkg/homekit/producer.go +++ b/pkg/homekit/producer.go @@ -57,7 +57,8 @@ func Dial(rawURL string, server *srtp.Server) (*Client, error) { ID: core.NewID(), FormatName: "homekit", Protocol: "udp", - Source: conn.URL(), + RemoteAddr: conn.Conn.RemoteAddr().String(), + Source: rawURL, Transport: conn, }, hap: conn, From da5f060741f5939ee57c48c9d7d75e19baab5dea Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Jun 2024 19:03:57 +0300 Subject: [PATCH 125/130] Add killsignal and killtimeout to exec/rtsp --- internal/exec/closer.go | 39 ++++++++++++++++++++++++++++ internal/exec/exec.go | 53 ++++++++++++++++++++++---------------- internal/exec/pipe.go | 56 ----------------------------------------- 3 files changed, 70 insertions(+), 78 deletions(-) create mode 100644 internal/exec/closer.go delete mode 100644 internal/exec/pipe.go diff --git a/internal/exec/closer.go b/internal/exec/closer.go new file mode 100644 index 00000000..66d0e3ac --- /dev/null +++ b/internal/exec/closer.go @@ -0,0 +1,39 @@ +package exec + +import ( + "errors" + "net/url" + "os" + "os/exec" + "syscall" + "time" + + "github.com/AlexxIT/go2rtc/pkg/core" +) + +// closer support custom killsignal with custom killtimeout +type closer struct { + cmd *exec.Cmd + query url.Values +} + +func (c *closer) Close() (err error) { + sig := os.Kill + if s := c.query.Get("killsignal"); s != "" { + sig = syscall.Signal(core.Atoi(s)) + } + + log.Trace().Msgf("[exec] kill with signal=%d", sig) + err = c.cmd.Process.Signal(sig) + + if s := c.query.Get("killtimeout"); s != "" { + timeout := time.Duration(core.Atoi(s)) * time.Second + timer := time.AfterFunc(timeout, func() { + log.Trace().Msgf("[exec] kill after timeout=%s", s) + _ = c.cmd.Process.Kill() + }) + defer timer.Stop() // stop timer if Wait ends before timeout + } + + return errors.Join(err, c.cmd.Wait()) +} diff --git a/internal/exec/exec.go b/internal/exec/exec.go index ac1691d3..035317d9 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -1,10 +1,12 @@ package exec import ( + "bufio" "crypto/md5" "encoding/hex" "errors" "fmt" + "io" "net/url" "os" "os/exec" @@ -49,8 +51,10 @@ func Init() { } func execHandle(rawURL string) (core.Producer, error) { + rawURL, rawQuery, _ := strings.Cut(rawURL, "#") + query := streams.ParseQuery(rawQuery) + var path string - var query url.Values // RTSP flow should have `{output}` inside URL // pipe flow may have `#{params}` inside URL @@ -62,9 +66,6 @@ func execHandle(rawURL string) (core.Producer, error) { sum := md5.Sum([]byte(rawURL)) path = "/" + hex.EncodeToString(sum[:]) rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:] - } else if i = strings.IndexByte(rawURL, '#'); i > 0 { - query = streams.ParseQuery(rawURL[i+1:]) - rawURL = rawURL[:i] } args := shell.QuoteSplit(rawURL[5:]) // remove `exec:` @@ -74,23 +75,34 @@ func execHandle(rawURL string) (core.Producer, error) { debug: log.Debug().Enabled(), } - if path == "" { - return handlePipe(rawURL, cmd, query) - } - - return handleRTSP(rawURL, cmd, path) -} - -func handlePipe(source string, cmd *exec.Cmd, query url.Values) (core.Producer, error) { if query.Get("backchannel") == "1" { return stdin.NewClient(cmd) } - r, err := PipeCloser(cmd, query) + cl := &closer{cmd: cmd, query: query} + + if path == "" { + return handlePipe(rawURL, cmd, cl) + } + + return handleRTSP(rawURL, cmd, cl, path) +} + +func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, error) { + stdout, err := cmd.StdoutPipe() if err != nil { return nil, err } + rc := struct { + io.Reader + io.Closer + }{ + // add buffer for pipe reader to reduce syscall + bufio.NewReaderSize(stdout, core.BufferSize), + cl, + } + log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe") ts := time.Now() @@ -99,9 +111,9 @@ func handlePipe(source string, cmd *exec.Cmd, query url.Values) (core.Producer, return nil, err } - prod, err := magic.Open(r) + prod, err := magic.Open(rc) if err != nil { - _ = r.Close() + _ = rc.Close() return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr) } @@ -115,7 +127,7 @@ func handlePipe(source string, cmd *exec.Cmd, query url.Values) (core.Producer, return prod, nil } -func handleRTSP(source string, cmd *exec.Cmd, path string) (core.Producer, error) { +func handleRTSP(source string, cmd *exec.Cmd, cl io.Closer, path string) (core.Producer, error) { if log.Trace().Enabled() { cmd.Stdout = os.Stdout } @@ -147,9 +159,9 @@ func handleRTSP(source string, cmd *exec.Cmd, path string) (core.Producer, error }() select { - case <-time.After(time.Second * 60): - _ = cmd.Process.Kill() + case <-time.After(time.Minute): log.Error().Str("source", source).Msg("[exec] timeout") + _ = cl.Close() return nil, errors.New("exec: timeout") case <-done: // limit message size @@ -157,10 +169,7 @@ func handleRTSP(source string, cmd *exec.Cmd, path string) (core.Producer, error case prod := <-waiter: log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp") setRemoteInfo(prod, source, cmd.Args) - prod.OnClose = func() error { - log.Debug().Msgf("[exec] kill rtsp") - return errors.Join(cmd.Process.Kill(), cmd.Wait()) - } + prod.OnClose = cl.Close return prod, nil } } diff --git a/internal/exec/pipe.go b/internal/exec/pipe.go deleted file mode 100644 index 12ea136b..00000000 --- a/internal/exec/pipe.go +++ /dev/null @@ -1,56 +0,0 @@ -package exec - -import ( - "bufio" - "errors" - "io" - "net/url" - "os/exec" - "syscall" - "time" - - "github.com/AlexxIT/go2rtc/pkg/core" -) - -// PipeCloser - return StdoutPipe that Kill cmd on Close call -func PipeCloser(cmd *exec.Cmd, query url.Values) (io.ReadCloser, error) { - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, err - } - - // add buffer for pipe reader to reduce syscall - return &pipeCloser{bufio.NewReaderSize(stdout, core.BufferSize), stdout, cmd, query}, nil -} - -type pipeCloser struct { - io.Reader - io.Closer - cmd *exec.Cmd - query url.Values -} - -func (p *pipeCloser) Close() error { - return errors.Join(p.Closer.Close(), p.Kill(), p.Wait()) -} - -func (p *pipeCloser) Kill() error { - if s := p.query.Get("killsignal"); s != "" { - log.Trace().Msgf("[exec] kill with custom sig=%s", s) - sig := syscall.Signal(core.Atoi(s)) - return p.cmd.Process.Signal(sig) - } - return p.cmd.Process.Kill() -} - -func (p *pipeCloser) Wait() error { - if s := p.query.Get("killtimeout"); s != "" { - timeout := time.Duration(core.Atoi(s)) * time.Second - timer := time.AfterFunc(timeout, func() { - log.Trace().Msgf("[exec] kill after timeout=%s", s) - _ = p.cmd.Process.Kill() - }) - defer timer.Stop() // stop timer if Wait ends before timeout - } - return p.cmd.Wait() -} From bdc7ff1035b0b8a427445ea31e9b3004d0748da0 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Jun 2024 19:04:34 +0300 Subject: [PATCH 126/130] Fix forwarded remote_addr in the network --- pkg/core/connection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/connection.go b/pkg/core/connection.go index 1055c381..2c3f2196 100644 --- a/pkg/core/connection.go +++ b/pkg/core/connection.go @@ -96,7 +96,7 @@ func (c *Connection) SetRemoteAddr(s string) { if c.RemoteAddr == "" { c.RemoteAddr = s } else { - c.RemoteAddr += " forward " + c.RemoteAddr + c.RemoteAddr += " forwarded " + s } } From 5b481a27c67cdf1b75b02e295ce6e172eca58878 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sun, 16 Jun 2024 21:57:48 +0300 Subject: [PATCH 127/130] fix(network): enable autoResize in network settings --- www/network.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/network.html b/www/network.html index 53f1ca68..17c3570c 100644 --- a/www/network.html +++ b/www/network.html @@ -47,7 +47,7 @@ enabled: false, } }, - autoResize: false, + autoResize: true, locale: navigator.language.toLowerCase().split('-').slice(0, 2).join('-'), }; const container = document.getElementById('network'); From e6fa97c738560dd03374994818e2bdd032096051 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Jun 2024 22:12:52 +0300 Subject: [PATCH 128/130] Code refactoring after #1196 --- www/network.html | 99 ++++++++++++++++++++---------------------------- 1 file changed, 41 insertions(+), 58 deletions(-) diff --git a/www/network.html b/www/network.html index 17c3570c..7a4ff229 100644 --- a/www/network.html +++ b/www/network.html @@ -25,73 +25,56 @@ -
    - - + + update(); + }); + - \ No newline at end of file + From db6745e8ff034552ef87d4a45220428b87823e67 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 18 Jun 2024 20:35:17 +0300 Subject: [PATCH 129/130] Code refactoring after #1168 --- internal/app/app.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index cdbb870b..eb803584 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -45,17 +45,7 @@ func Init() { os.Exit(0) } - ppid := os.Getppid() - if ppid == 1 { - daemon = false - } else { - parent, err := os.FindProcess(ppid) - if err != nil || parent.Pid < 1 { - daemon = false - } - } - - if daemon { + if daemon && os.Getppid() != 1 { if runtime.GOOS == "windows" { fmt.Println("Daemon mode is not supported on Windows") os.Exit(1) From a4885c2c3abce58074d04878bba0d72105642a9b Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 18 Jun 2024 21:33:36 +0300 Subject: [PATCH 130/130] Update version to 1.9.4 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index d40851cc..98bd79e3 100644 --- a/main.go +++ b/main.go @@ -36,7 +36,7 @@ import ( ) func main() { - app.Version = "1.9.3" + app.Version = "1.9.4" // 1. Core modules: app, api/ws, streams
  • TimeTime Level Message
    ${ts.toLocaleString()}${escapeHTML(line['level'])}${escapeHTML(msg)}
    ${ts}${line['level']}${escapeHTML(msg)}
    ${online} / info / probe${online} / info / probe / net${links}