diff --git a/README.md b/README.md index a4715c9d..3bfe3a48 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,7 @@ Supported for sources: - [TP-Link Tapo](#source-tapo) cameras - [Hikvision ISAPI](#source-isapi) cameras - [Roborock vacuums](#source-roborock) models with cameras +- [Exec](#source-exec) audio on server - [Any Browser](#incoming-browser) as IP-camera Two way audio can be used in browser with [WebRTC](#module-webrtc) technology. The browser will give access to the microphone only for HTTPS sites ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https)). @@ -407,19 +408,21 @@ Exec source can run any external application and expect data from it. Two transp If you want to use **RTSP** transport - the command must contain the `{output}` argument in any place. On launch, it will be replaced by the local address of the RTSP server. -**pipe** reads data from app stdout in different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**. +**pipe** reads data from app stdout in different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**. Also pipe can write data to app stdin in two formats: **PCMA** and **PCM/48000**. The source can be used with: - [FFmpeg](https://ffmpeg.org/) - go2rtc ffmpeg source just a shortcut to exec source +- [FFplay](https://ffmpeg.org/ffplay.html) - play audio on your server - [GStreamer](https://gstreamer.freedesktop.org/) - [Raspberry Pi Cameras](https://www.raspberrypi.com/documentation/computers/camera_software.html) - any your own software -Pipe commands support two parameters (format: `exec:{command}#{param1}#{param2}`): +Pipe commands support parameters (format: `exec:{command}#{param1}#{param2}`): - `killsignal` - signal which will be send to stop the process (numeric form) - `killtimeout` - time in seconds for forced termination with sigkill +- `backchannel` - enable backchannel for two-way audio ```yaml streams: @@ -427,6 +430,8 @@ streams: picam_h264: exec:libcamera-vid -t 0 --inline -o - picam_mjpeg: exec:libcamera-vid -t 0 --codec mjpeg -o - canon: exec:gphoto2 --capture-movie --stdout#killsignal=2#killtimeout=5 + play_pcma: exec:ffplay -fflags nobuffer -f alaw -ar 8000 -i -#backchannel=1 + play_pcm48k: exec:ffplay -fflags nobuffer -f s16be -ar 48000 -i -#backchannel=1 ``` #### Source: Echo diff --git a/go.mod b/go.mod index 059cd2a3..b1ba4b4c 100644 --- a/go.mod +++ b/go.mod @@ -4,44 +4,45 @@ go 1.21 require ( github.com/asticode/go-astits v1.13.0 - github.com/expr-lang/expr v1.15.7 + github.com/expr-lang/expr v1.16.5 github.com/gorilla/websocket v1.5.1 - github.com/miekg/dns v1.1.57 - github.com/pion/ice/v2 v2.3.11 - github.com/pion/interceptor v0.1.25 - github.com/pion/rtcp v1.2.13 - github.com/pion/rtp v1.8.3 - github.com/pion/sdp/v3 v3.0.6 + github.com/miekg/dns v1.1.59 + 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.24 - github.com/rs/zerolog v1.31.0 - github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 + 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.8.4 + github.com/stretchr/testify v1.9.0 github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 - golang.org/x/crypto v0.17.0 + golang.org/x/crypto v0.23.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/asticode/go-astikit v0.30.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/google/uuid v1.5.0 // indirect + 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.5 // indirect - github.com/pion/dtls/v2 v2.2.8 // 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 - github.com/pion/mdns v0.0.9 // indirect + github.com/pion/mdns v0.0.12 // indirect github.com/pion/randutil v0.1.0 // indirect - github.com/pion/sctp v1.8.9 // indirect - github.com/pion/transport/v2 v2.2.4 // indirect - github.com/pion/turn/v2 v2.1.4 // indirect + github.com/pion/sctp v1.8.16 // indirect + 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.14.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/tools v0.16.1 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.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 ) diff --git a/go.sum b/go.sum index cc738a92..09b0abb9 100644 --- a/go.sum +++ b/go.sum @@ -6,31 +6,14 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.15.7 h1:BK0JcWUkoW6nrbLBo6xCKhz4BvH5DSOOu1Gx5lucyZo= -github.com/expr-lang/expr v1.15.7/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -43,149 +26,125 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= -github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8= -github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= +github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= +github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= +github.com/pion/datachannel v1.5.6 h1:1IxKJntfSlYkpUj8LlYRSWpYiTTC02nUrOE8T3DqGeg= +github.com/pion/datachannel v1.5.6/go.mod h1:1eKT6Q85pRnr2mHiWHxJwO50SfZRtWHTsNIVb/NfGW4= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= -github.com/pion/dtls/v2 v2.2.8 h1:BUroldfiIbV9jSnC6cKOMnyiORRWrWWpV11JUyEu5OA= -github.com/pion/dtls/v2 v2.2.8/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= -github.com/pion/ice/v2 v2.3.11 h1:rZjVmUwyT55cmN8ySMpL7rsS8KYsJERsrxJLLxpKhdw= -github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E= -github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc= -github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y= +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= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI= -github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4= -github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc= +github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= +github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= -github.com/pion/rtcp v1.2.13 h1:+EQijuisKwm/8VBs8nWllr0bIndR7Lf7cZG200mpbNo= -github.com/pion/rtcp v1.2.13/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= -github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8= +github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= +github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= -github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs= -github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g= -github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI= -github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw= -github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= +github.com/pion/rtp v1.8.6 h1:MTmn/b0aWWsAzux2AmP8WGllusBVw4NPYPVFFd7jUPw= +github.com/pion/rtp v1.8.6/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= +github.com/pion/sctp v1.8.13/go.mod h1:YKSgO/bO/6aOMP9LCie1DuD7m+GamiK2yIiPM6vH+GA= +github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY= +github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE= +github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= +github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo= github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA= github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= -github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40= -github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc= github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= -github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo= github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= -github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v2 v2.2.5 h1:iyi25i/21gQck4hfRhomF6SktmUQjRsRW4WJdhfc3Kc= +github.com/pion/transport/v2 v2.2.5/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4= +github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0= github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= -github.com/pion/turn/v2 v2.1.4 h1:2xn8rduI5W6sCZQkEnIUDAkrBQNl2eYIBCHMZ3QMmP8= -github.com/pion/turn/v2 v2.1.4/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= -github.com/pion/webrtc/v3 v3.2.24 h1:MiFL5DMo2bDaaIFWr0DDpwiV/L4EGbLZb+xoRvfEo1Y= -github.com/pion/webrtc/v3 v3.2.24/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs= +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= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= -github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= -github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs= -github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= +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/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= github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI= github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +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-20201020160332-67f06af15bc9/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= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -194,57 +153,42 @@ golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 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.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= -golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 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= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/ws/ws.go b/internal/api/ws/ws.go index 5b8a35c3..c8187dec 100644 --- a/internal/api/ws/ws.go +++ b/internal/api/ws/ws.go @@ -11,7 +11,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/app" "github.com/gorilla/websocket" - "github.com/rs/zerolog/log" + "github.com/rs/zerolog" ) func Init() { @@ -23,11 +23,15 @@ func Init() { app.LoadConfig(&cfg) + log = app.GetLogger("api") + initWS(cfg.Mod.Origin) api.HandleFunc("api/ws", apiWS) } +var log zerolog.Logger + // Message - struct for data exchange in Web API type Message struct { Type string `json:"type"` diff --git a/internal/app/app.go b/internal/app/app.go index d67b9025..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.8.5" +var Version = "1.9.1" var UserAgent = "go2rtc/" + Version var ConfigPath string diff --git a/internal/exec/exec.go b/internal/exec/exec.go index aefab201..a160136c 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "errors" "fmt" + "io" "net/url" "os" "os/exec" @@ -19,6 +20,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/magic" pkg "github.com/AlexxIT/go2rtc/pkg/rtsp" "github.com/AlexxIT/go2rtc/pkg/shell" + "github.com/AlexxIT/go2rtc/pkg/stdin" "github.com/rs/zerolog" ) @@ -48,37 +50,41 @@ func Init() { func execHandle(rawURL string) (core.Producer, error) { var path string + var query url.Values - rawURL, rawQuery, _ := strings.Cut(rawURL, "#") - - args := shell.QuoteSplit(rawURL[5:]) // remove `exec:` - for i, arg := range args { - if arg == "{output}" { - if rtsp.Port == "" { - return nil, errors.New("rtsp module disabled") - } - - sum := md5.Sum([]byte(rawURL)) - path = "/" + hex.EncodeToString(sum[:]) - args[i] = "rtsp://127.0.0.1:" + rtsp.Port + path - break + // RTSP flow should have `{output}` inside URL + // pipe flow may have `#{params}` inside URL + if i := strings.Index(rawURL, "{output}"); i > 0 { + if rtsp.Port == "" { + return nil, errors.New("exec: rtsp module disabled") } + + 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:` cmd := exec.Command(args[0], args[1:]...) if log.Debug().Enabled() { cmd.Stderr = os.Stderr } if path == "" { - query := streams.ParseQuery(rawQuery) return handlePipe(rawURL, cmd, query) } - return handleRTSP(rawURL, path, cmd) + return handleRTSP(rawURL, cmd, path) } func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error) { + if query.Get("backchannel") == "1" { + return stdin.NewClient(cmd) + } + r, err := PipeCloser(cmd, query) if err != nil { return nil, err @@ -96,15 +102,23 @@ func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error return prod, err } -func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) { +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 } - ch := make(chan core.Producer) + waiter := make(chan core.Producer) waitersMu.Lock() - waiters[path] = ch + waiters[path] = waiter waitersMu.Unlock() defer func() { @@ -122,16 +136,9 @@ func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) { return nil, err } - chErr := make(chan error) - + done := make(chan error, 1) go func() { - err := cmd.Wait() - // unblocking write to channel - select { - case chErr <- err: - default: - log.Trace().Str("url", url).Msg("[exec] close") - } + done <- cmd.Wait() }() select { @@ -139,9 +146,10 @@ func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) { _ = cmd.Process.Kill() log.Error().Str("url", url).Msg("[exec] timeout") return nil, errors.New("timeout") - case err := <-chErr: - return nil, fmt.Errorf("exec: %s", err) - case prod := <-ch: + case <-done: + // limit message size + return nil, errors.New("exec: " + stderr.String()) + case prod := <-waiter: log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run") return prod, nil } @@ -154,3 +162,22 @@ var ( waiters = map[string]chan core.Producer{} waitersMu sync.Mutex ) + +type limitBuffer struct { + buf []byte + n int +} + +func (l *limitBuffer) 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) { + if l.n < cap(l.buf) { + l.n += copy(l.buf[l.n:], p) + } + return len(p), nil +} diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 5d94d3c1..e815f39b 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -7,6 +7,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/ffmpeg/device" "github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware" + "github.com/AlexxIT/go2rtc/internal/ffmpeg/virtual" "github.com/AlexxIT/go2rtc/internal/rtsp" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/ffmpeg" @@ -145,7 +146,7 @@ func parseArgs(s string) *ffmpeg.Args { } var query url.Values - if i := strings.IndexByte(s, '#'); i > 0 { + if i := strings.IndexByte(s, '#'); i >= 0 { query = streams.ParseQuery(s[i+1:]) args.Video = len(query["video"]) args.Audio = len(query["audio"]) @@ -193,6 +194,11 @@ func parseArgs(s string) *ffmpeg.Args { 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 { args.Input = inputTemplate("file", s, query) } diff --git a/internal/ffmpeg/ffmpeg_test.go b/internal/ffmpeg/ffmpeg_test.go index 7fa3ebc8..d5a39284 100644 --- a/internal/ffmpeg/ffmpeg_test.go +++ b/internal/ffmpeg/ffmpeg_test.go @@ -7,29 +7,48 @@ import ( ) func TestParseArgsFile(t *testing.T) { - // [FILE] all tracks will be copied without transcoding codecs - args := parseArgs("/media/bbb.mp4") - require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [FILE] video will be transcoded to H264, audio will be skipped - args = parseArgs("/media/bbb.mp4#video=h264") - require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [FILE] video will be copied, audio will be transcoded to pcmu - args = parseArgs("/media/bbb.mp4#video=copy#audio=pcmu") - require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v copy -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped - args = parseArgs("/media/bbb.mp4#video=h265#rotate=-90") - require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [FILE] video will be output for MJPEG to pipe, audio will be skipped - args = parseArgs("/media/bbb.mp4#video=mjpeg") - require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -`, args.String()) - - // https://github.com/AlexxIT/go2rtc/issues/509 - args = parseArgs("ffmpeg:test.mp4#raw=-ss 00:00:20") - require.Equal(t, `ffmpeg -hide_banner -re -i ffmpeg:test.mp4 -ss 00:00:20 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) + tests := []struct { + name string + source string + expect string + }{ + { + name: "[FILE] all tracks will be copied without transcoding codecs", + source: "/media/bbb.mp4", + expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[FILE] video will be transcoded to H264, audio will be skipped", + source: "/media/bbb.mp4#video=h264", + expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[FILE] video will be copied, audio will be transcoded to pcmu", + source: "/media/bbb.mp4#video=copy#audio=pcmu", + expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v copy -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped", + source: "/media/bbb.mp4#video=h265#rotate=-90", + expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[FILE] video will be output for MJPEG to pipe, audio will be skipped", + source: "/media/bbb.mp4#video=mjpeg", + expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -`, + }, + { + name: "https://github.com/AlexxIT/go2rtc/issues/509", + source: "ffmpeg:test.mp4#raw=-ss 00:00:20", + expect: `ffmpeg -hide_banner -re -i ffmpeg:test.mp4 -ss 00:00:20 -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()) + }) + } } func TestParseArgsDevice(t *testing.T) { @@ -87,7 +106,7 @@ func TestParseArgsAudio(t *testing.T) { // [AUDIO] audio will be transcoded to OPUS, video will be skipped args = parseArgs("rtsp:///example.com#audio=opus") - require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -application:a lowdelay -frame_duration 20 -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) + require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -application:a lowdelay -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) // [AUDIO] audio will be transcoded to PCMU, video will be skipped args = parseArgs("rtsp:///example.com#audio=pcmu") @@ -115,28 +134,46 @@ func TestParseArgsAudio(t *testing.T) { } func TestParseArgsHwVaapi(t *testing.T) { - // [HTTP-MJPEG] video will be transcoded to H264 - args := parseArgs("http:///example.com#video=h264#hardware=vaapi") - require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [RTSP] video with rotation, should be transcoded, so select H264 - args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=vaapi") - require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,transpose_vaapi=4,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [RTSP] video with resize to 1280x720, should be transcoded, so select H265 - args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=vaapi") - require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=1280:720:out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [FILE] video will be output for MJPEG to pipe, audio will be skipped - args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=vaapi") - require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -f mjpeg -`, args.String()) - - // [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265 - args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=vaapi") - require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) + tests := []struct { + name string + source string + expect string + }{ + { + name: "[HTTP-MJPEG] video will be transcoded to H264", + source: "http:///example.com#video=h264#hardware=vaapi", + expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[RTSP] video with rotation, should be transcoded, so select H264", + source: "rtsp://example.com#video=h264#rotate=180#hardware=vaapi", + expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,transpose_vaapi=4,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[RTSP] video with resize to 1280x720, should be transcoded, so select H265", + source: "rtsp://example.com#video=h265#width=1280#height=720#hardware=vaapi", + expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[FILE] video will be output for MJPEG to pipe, audio will be skipped", + source: "/media/bbb.mp4#video=mjpeg#hardware=vaapi", + expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, + }, + { + name: "[DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265", + source: "device?video=0&video_size=1920x1080#video=h265#hardware=vaapi", + expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -f dshow -video_size 1920x1080 -i "video=0" -c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload" -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()) + }) + } } -func TestParseArgsHwV4l2m2m(t *testing.T) { +func _TestParseArgsHwV4l2m2m(t *testing.T) { // [HTTP-MJPEG] video will be transcoded to H264 args := parseArgs("http:///example.com#video=h264#hardware=v4l2m2m") require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) @@ -166,7 +203,7 @@ func TestParseArgsHwRKMPP(t *testing.T) { require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1 -height 320 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) } -func TestParseArgsHwCuda(t *testing.T) { +func _TestParseArgsHwCuda(t *testing.T) { // [HTTP-MJPEG] video will be transcoded to H264 args := parseArgs("http:///example.com#video=h264#hardware=cuda") require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) @@ -184,7 +221,7 @@ func TestParseArgsHwCuda(t *testing.T) { require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) } -func TestParseArgsHwDxva2(t *testing.T) { +func _TestParseArgsHwDxva2(t *testing.T) { // [HTTP-MJPEG] video will be transcoded to H264 args := parseArgs("http:///example.com#video=h264#hardware=dxva2") require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) @@ -206,7 +243,7 @@ func TestParseArgsHwDxva2(t *testing.T) { require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) } -func TestParseArgsHwVideotoolbox(t *testing.T) { +func _TestParseArgsHwVideotoolbox(t *testing.T) { // [HTTP-MJPEG] video will be transcoded to H264 args := parseArgs("http:///example.com#video=h264#hardware=videotoolbox") require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) @@ -226,16 +263,32 @@ func TestParseArgsHwVideotoolbox(t *testing.T) { func TestDeckLink(t *testing.T) { args := parseArgs(`DeckLink SDI (2)#video=h264#hardware=vaapi#input=-format_code Hp29 -f decklink -i "{input}"`) - require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -format_code Hp29 -f decklink -i "DeckLink SDI (2)" -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) + require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -format_code Hp29 -f decklink -i "DeckLink SDI (2)" -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) } func TestDrawText(t *testing.T) { - args := parseArgs("http:///example.com#video=h264#drawtext=fontsize=12") - require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - args = parseArgs("http:///example.com#video=h264#width=640#drawtext=fontsize=12") - require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=640:-1,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - args = parseArgs("http:///example.com#video=h264#width=640#drawtext=fontsize=12#hardware=vaapi") - require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "scale=640:-1:out_color_matrix=bt709:out_range=tv,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}',hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) + tests := []struct { + name string + source string + expect string + }{ + { + source: "http:///example.com#video=h264#drawtext=fontsize=12", + expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + source: "http:///example.com#video=h264#width=640#drawtext=fontsize=12", + expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=640:-1,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + source: "http:///example.com#video=h264#width=640#drawtext=fontsize=12#hardware=vaapi", + expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "scale=640:-1,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}',hwupload" -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/jpeg_test.go b/internal/ffmpeg/jpeg_test.go index b1d459b3..299d225a 100644 --- a/internal/ffmpeg/jpeg_test.go +++ b/internal/ffmpeg/jpeg_test.go @@ -19,5 +19,5 @@ func TestParseQuery(t *testing.T) { query, err = url.ParseQuery("hw=vaapi") require.Nil(t, err) args = parseQuery(query) - require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -i - -c:v mjpeg_vaapi -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, args.String()) + require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -i - -c:v mjpeg_vaapi -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, args.String()) } diff --git a/internal/ffmpeg/virtual/virtual.go b/internal/ffmpeg/virtual/virtual.go new file mode 100644 index 00000000..4a791982 --- /dev/null +++ b/internal/ffmpeg/virtual/virtual.go @@ -0,0 +1,59 @@ +package virtual + +import ( + "net/url" +) + +func GetInput(src string) (string, error) { + query, err := url.ParseQuery(src) + if err != nil { + return "", err + } + + // set defaults (using Add instead of Set) + query.Add("source", "testsrc") + query.Add("size", "1920x1080") + query.Add("decimals", "2") + + // https://ffmpeg.org/ffmpeg-filters.html + source := query.Get("source") + input := "-re -f lavfi -i " + source + + 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" + case "1080": + value = "1920x1080" + case "2K": + value = "2560x1440" + case "4K": + value = "3840x2160" + case "8K": + value = "7680x4230" // https://reolink.com/blog/8k-resolution/ + } + case "decimals": + if source != "testsrc" { + continue + } + default: + continue + } + + input += sep + key + "=" + value + sep = ":" // next separator + } + + if s := query.Get("format"); s != "" { + input += ",format=" + s + } + + return input, nil +} diff --git a/internal/mjpeg/init.go b/internal/mjpeg/init.go index 6cf10bf4..f2519a61 100644 --- a/internal/mjpeg/init.go +++ b/internal/mjpeg/init.go @@ -57,6 +57,8 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) { return } log.Debug().Msgf("[mjpeg] transcoding time=%s", time.Since(ts)) + case core.CodecJPEG: + b = mjpeg.FixJPEG(b) } h := w.Header() @@ -163,7 +165,7 @@ func handlerWS(tr *ws.Transport, _ *ws.Message) error { cons.UserAgent = tr.Request.UserAgent() if err := stream.AddConsumer(cons); err != nil { - log.Error().Err(err).Caller().Send() + log.Debug().Err(err).Msg("[mjpeg] add consumer") return err } 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() diff --git a/internal/streams/add_consumer.go b/internal/streams/add_consumer.go index d97a4266..eb767691 100644 --- a/internal/streams/add_consumer.go +++ b/internal/streams/add_consumer.go @@ -3,18 +3,17 @@ package streams import ( "errors" "strings" - "sync/atomic" "github.com/AlexxIT/go2rtc/pkg/core" ) func (s *Stream) AddConsumer(cons core.Consumer) (err error) { - // support for multiple simultaneous requests from different consumers - consN := atomic.AddInt32(&s.requests, 1) - 1 + // support for multiple simultaneous pending from different consumers + consN := s.pending.Add(1) - 1 - var prodErrors []error + var prodErrors = make([]error, len(s.producers)) var prodMedias []*core.Media - var prods []*Producer // matched producers for consumer + var prodStarts []*Producer // Step 1. Get consumer medias consMedias := cons.GetMedias() @@ -23,15 +22,20 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) { producers: for prodN, prod := range s.producers { + if prodErrors[prodN] != nil { + log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN) + continue + } + if err = prod.Dial(); err != nil { - log.Trace().Err(err).Msgf("[streams] skip prod=%s", prod.url) - prodErrors = append(prodErrors, err) + log.Trace().Err(err).Msgf("[streams] dial cons=%d prod=%d", consN, prodN) + prodErrors[prodN] = err continue } // Step 2. Get producer medias (not tracks yet) for _, prodMedia := range prod.GetMedias() { - log.Trace().Msgf("[streams] check prod=%d media=%s", prodN, prodMedia) + log.Trace().Msgf("[streams] check cons=%d prod=%d media=%s", consN, prodN, prodMedia) prodMedias = append(prodMedias, prodMedia) // Step 3. Match consumer/producer codecs list @@ -44,11 +48,12 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) { switch prodMedia.Direction { case core.DirectionRecvonly: - log.Trace().Msgf("[streams] match prod=%d => cons=%d", prodN, consN) + log.Trace().Msgf("[streams] match cons=%d <= prod=%d", consN, prodN) // Step 4. Get recvonly track from producer if track, err = prod.GetTrack(prodMedia, prodCodec); err != nil { log.Info().Err(err).Msg("[streams] can't get track") + prodErrors[prodN] = err continue } // Step 5. Add track to consumer @@ -68,11 +73,12 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) { // Step 5. Add track to producer if err = prod.AddTrack(prodMedia, prodCodec, track); err != nil { log.Info().Err(err).Msg("[streams] can't add track") + prodErrors[prodN] = err continue } } - prods = append(prods, prod) + prodStarts = append(prodStarts, prod) if !consMedia.MatchAll() { break producers @@ -82,11 +88,11 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) { } // stop producers if they don't have readers - if atomic.AddInt32(&s.requests, -1) == 0 { + if s.pending.Add(-1) == 0 { s.stopProducers() } - if len(prods) == 0 { + if len(prodStarts) == 0 { return formatError(consMedias, prodMedias, prodErrors) } @@ -95,7 +101,7 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) { s.mu.Unlock() // there may be duplicates, but that's not a problem - for _, prod := range prods { + for _, prod := range prodStarts { prod.start() } @@ -103,6 +109,20 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) { } func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error { + // 1. Return errors if any not nil + var text string + + for _, err := range prodErrors { + if err != nil { + text = appendString(text, err.Error()) + } + } + + if len(text) != 0 { + return errors.New("streams: " + text) + } + + // 2. Return "codecs not matched" if prodMedias != nil { var prod, cons string @@ -125,16 +145,7 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error return errors.New("streams: codecs not matched: " + prod + " => " + cons) } - if prodErrors != nil { - var text string - - for _, err := range prodErrors { - text = appendString(text, err.Error()) - } - - return errors.New("streams: " + text) - } - + // 3. Return unknown error return errors.New("streams: unknown error") } diff --git a/internal/streams/producer.go b/internal/streams/producer.go index 6d3cf2b9..5a25dba5 100644 --- a/internal/streams/producer.go +++ b/internal/streams/producer.go @@ -245,10 +245,10 @@ func (p *Producer) stop() { switch p.state { case stateExternal: - log.Debug().Msgf("[streams] can't stop external producer") + log.Trace().Msgf("[streams] skip stop external producer") return case stateNone: - log.Debug().Msgf("[streams] can't stop none producer") + log.Trace().Msgf("[streams] skip stop none producer") return case stateStart: p.workerID++ diff --git a/internal/streams/stream.go b/internal/streams/stream.go index 0a8108e2..49c58e77 100644 --- a/internal/streams/stream.go +++ b/internal/streams/stream.go @@ -3,6 +3,7 @@ package streams import ( "encoding/json" "sync" + "sync/atomic" "github.com/AlexxIT/go2rtc/pkg/core" ) @@ -11,7 +12,7 @@ type Stream struct { producers []*Producer consumers []core.Consumer mu sync.Mutex - requests int32 + pending atomic.Int32 } func NewStream(source any) *Stream { diff --git a/internal/streams/stream_test.go b/internal/streams/stream_test.go index b2e88dc6..99fa9229 100644 --- a/internal/streams/stream_test.go +++ b/internal/streams/stream_test.go @@ -10,7 +10,7 @@ import ( func TestRecursion(t *testing.T) { // create stream with some source - stream1 := New("from_yaml", "does not matter") + stream1 := New("from_yaml", "does_not_matter") require.Len(t, streams, 1) // ask another unnamed stream that links go2rtc diff --git a/internal/webrtc/client.go b/internal/webrtc/client.go index 18622dc4..ae1a455b 100644 --- a/internal/webrtc/client.go +++ b/internal/webrtc/client.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "strings" + "sync" "time" "github.com/AlexxIT/go2rtc/internal/api/ws" @@ -82,6 +83,7 @@ func go2rtcClient(url string) (core.Producer, error) { // waiter will wait PC error or WS error or nil (connection OK) var connState core.Waiter + var connMu sync.Mutex prod := webrtc.NewConn(pc) prod.Desc = "WebRTC/WebSocket async" @@ -91,7 +93,9 @@ func go2rtcClient(url string) (core.Producer, error) { case *pion.ICECandidate: s := msg.ToJSON().Candidate log.Trace().Str("candidate", s).Msg("[webrtc] local ") + connMu.Lock() _ = conn.WriteJSON(&ws.Message{Type: "webrtc/candidate", Value: s}) + connMu.Unlock() case pion.PeerConnectionState: switch msg { @@ -118,9 +122,9 @@ func go2rtcClient(url string) (core.Producer, error) { // 4. Send offer msg := &ws.Message{Type: "webrtc/offer", Value: offer} - if err = conn.WriteJSON(msg); err != nil { - return nil, err - } + connMu.Lock() + _ = conn.WriteJSON(msg) + connMu.Unlock() // 5. Get answer if err = conn.ReadJSON(msg); err != nil { diff --git a/pkg/core/helpers.go b/pkg/core/helpers.go index 8e91da33..0c367e2c 100644 --- a/pkg/core/helpers.go +++ b/pkg/core/helpers.go @@ -38,15 +38,6 @@ func RandString(size, base byte) string { return string(b) } -func Any(errs ...error) error { - for _, err := range errs { - if err != nil { - return err - } - } - return nil -} - func Between(s, sub1, sub2 string) string { i := strings.Index(s, sub1) if i < 0 { diff --git a/pkg/flv/amf/amf_test.go b/pkg/flv/amf/amf_test.go index 077aec48..f22308e6 100644 --- a/pkg/flv/amf/amf_test.go +++ b/pkg/flv/amf/amf_test.go @@ -141,7 +141,7 @@ func TestNewReader(t *testing.T) { name: "obs-connect", actual: "020007636f6e6e656374003ff000000000000003000361707002000c617070312f73747265616d3100047479706502000a6e6f6e70726976617465000e737570706f727473476f4177617901010008666c61736856657202001f464d4c452f332e302028636f6d70617469626c653b20464d53632f312e3029000673776655726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d310005746355726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d31000009", expect: []any{ - "connect", 1, + "connect", float64(1), map[string]any{ "app": "app1/stream1", "flashVer": "FMLE/3.0 (compatible; FMSc/1.0)", diff --git a/pkg/h264/payloader.go b/pkg/h264/payloader.go index cebaaf7c..efc89986 100644 --- a/pkg/h264/payloader.go +++ b/pkg/h264/payloader.go @@ -67,11 +67,15 @@ func EmitNalus(nals []byte, isAVC bool, emit func([]byte)) { } } else { for { - end := 4 + binary.BigEndian.Uint32(nals) - emit(nals[4:end]) - if int(end) >= len(nals) { + n := uint32(len(nals)) + if n < 4 { break } + end := 4 + binary.BigEndian.Uint32(nals) + if n < end { + break + } + emit(nals[4:end]) nals = nals[end:] } } 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) 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) } diff --git a/pkg/mjpeg/helpers.go b/pkg/mjpeg/helpers.go new file mode 100644 index 00000000..21000b9b --- /dev/null +++ b/pkg/mjpeg/helpers.go @@ -0,0 +1,35 @@ +package mjpeg + +import ( + "bytes" + "image/jpeg" +) + +// FixJPEG - reencode JPEG if it has wrong header +// +// for example, this app produce "bad" images: +// https://github.com/jacksonliam/mjpg-streamer +// +// and they can't be uploaded to the Telegram servers: +// {"ok":false,"error_code":400,"description":"Bad Request: IMAGE_PROCESS_FAILED"} +func FixJPEG(b []byte) []byte { + // skip non-JPEG + if len(b) < 10 || b[0] != 0xFF || b[1] != 0xD8 { + return b + } + // skip if header OK for imghdr library + // https://docs.python.org/3/library/imghdr.html + if string(b[2:4]) == "\xFF\xDB" || string(b[6:10]) == "JFIF" || string(b[6:10]) == "Exif" { + return b + } + + img, err := jpeg.Decode(bytes.NewReader(b)) + if err != nil { + return b + } + buf := bytes.NewBuffer(nil) + if err = jpeg.Encode(buf, img, nil); err != nil { + return b + } + return buf.Bytes() +} 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 +} diff --git a/pkg/stdin/client.go b/pkg/stdin/client.go new file mode 100644 index 00000000..51db30ee --- /dev/null +++ b/pkg/stdin/client.go @@ -0,0 +1,33 @@ +package stdin + +import ( + "os/exec" + + "github.com/AlexxIT/go2rtc/pkg/core" +) + +type Client struct { + cmd *exec.Cmd + + medias []*core.Media + sender *core.Sender + send int +} + +func NewClient(cmd *exec.Cmd) (*Client, error) { + c := &Client{ + cmd: cmd, + medias: []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecPCMA, ClockRate: 8000}, + {Name: core.CodecPCM}, + }, + }, + }, + } + + return c, nil +} diff --git a/pkg/stdin/consumer.go b/pkg/stdin/consumer.go new file mode 100644 index 00000000..a1284948 --- /dev/null +++ b/pkg/stdin/consumer.go @@ -0,0 +1,61 @@ +package stdin + +import ( + "encoding/json" + "errors" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +func (c *Client) GetMedias() []*core.Media { + return c.medias +} + +func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + return nil, core.ErrCantGetTrack +} + +func (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { + if c.sender == nil { + stdin, err := c.cmd.StdinPipe() + if err != nil { + return err + } + + c.sender = core.NewSender(media, track.Codec) + c.sender.Handler = func(packet *rtp.Packet) { + _, _ = stdin.Write(packet.Payload) + c.send += len(packet.Payload) + } + } + + c.sender.HandleRTP(track) + return nil +} + +func (c *Client) Start() (err error) { + return c.cmd.Run() +} + +func (c *Client) Stop() (err error) { + if c.sender != nil { + c.sender.Close() + } + if c.cmd.Process == nil { + return nil + } + return errors.Join(c.cmd.Process.Kill(), c.cmd.Wait()) +} + +func (c *Client) MarshalJSON() ([]byte, error) { + info := &core.Info{ + Type: "Exec active consumer", + Medias: c.medias, + Send: c.send, + } + if c.sender != nil { + info.Senders = []*core.Sender{c.sender} + } + return json.Marshal(info) +} diff --git a/pkg/webrtc/helpers.go b/pkg/webrtc/helpers.go index 9a494792..90dd72a1 100644 --- a/pkg/webrtc/helpers.go +++ b/pkg/webrtc/helpers.go @@ -47,6 +47,9 @@ func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*core.Media continue } + // skip non-media codecs to avoid confusing users in info and logs + media.Codecs = SkipNonMediaCodecs(media.Codecs) + medias = append(medias, media) } } @@ -54,6 +57,21 @@ func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*core.Media return } +func SkipNonMediaCodecs(input []*core.Codec) (output []*core.Codec) { + for _, codec := range input { + switch codec.Name { + case "RTX", "RED", "ULPFEC", "FLEXFEC-03": + continue + case "CN", "TELEPHONE-EVENT": + continue // https://datatracker.ietf.org/doc/html/rfc7874 + } + // VP8, VP9, H264, H265, AV1 + // OPUS, G722, PCMU, PCMA + output = append(output, codec) + } + return +} + // WithResampling - will add for consumer: PCMA/0, PCMU/0, PCM/0, PCML/0 // so it can add resampling for PCMA/PCMU and repack for PCM/PCML func WithResampling(medias []*core.Media) []*core.Media { diff --git a/pkg/webrtc/track.go b/pkg/webrtc/track.go index 547fafb1..3102abd1 100644 --- a/pkg/webrtc/track.go +++ b/pkg/webrtc/track.go @@ -1,6 +1,8 @@ package webrtc import ( + "sync" + "github.com/pion/rtp" "github.com/pion/webrtc/v3" ) @@ -12,6 +14,7 @@ type Track struct { sequence uint16 ssrc uint32 writer webrtc.TrackLocalWriter + mu sync.Mutex } func NewTrack(kind string) *Track { @@ -23,8 +26,10 @@ func NewTrack(kind string) *Track { } func (t *Track) Bind(context webrtc.TrackLocalContext) (webrtc.RTPCodecParameters, error) { + t.mu.Lock() t.ssrc = uint32(context.SSRC()) t.writer = context.WriteStream() + t.mu.Unlock() for _, parameters := range context.CodecParameters() { // return first parameters @@ -35,7 +40,9 @@ func (t *Track) Bind(context webrtc.TrackLocalContext) (webrtc.RTPCodecParameter } func (t *Track) Unbind(context webrtc.TrackLocalContext) error { + t.mu.Lock() t.writer = nil + t.mu.Unlock() return nil } @@ -55,19 +62,22 @@ func (t *Track) Kind() webrtc.RTPCodecType { return webrtc.NewRTPCodecType(t.kind) } -func (t *Track) WriteRTP(payloadType uint8, packet *rtp.Packet) error { +func (t *Track) WriteRTP(payloadType uint8, packet *rtp.Packet) (err error) { + // using mutex because Unbind https://github.com/AlexxIT/go2rtc/issues/994 + t.mu.Lock() + // in case when we start WriteRTP before Track.Bind - if t.writer == nil { - return nil + if t.writer != nil { + // important to have internal counter if input packets from different sources + t.sequence++ + + header := packet.Header + header.SSRC = t.ssrc + header.PayloadType = payloadType + header.SequenceNumber = t.sequence + _, err = t.writer.WriteRTP(&header, packet.Payload) } - // important to have internal counter if input packets from different sources - t.sequence++ - - header := packet.Header - header.SSRC = t.ssrc - header.PayloadType = payloadType - header.SequenceNumber = t.sequence - _, err := t.writer.WriteRTP(&header, packet.Payload) - return err + t.mu.Unlock() + return } diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 00000000..0814ba48 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,82 @@ +#!/bin/sh + +check_command() { + if ! command -v $1 &> /dev/null + then + echo "Error: $1 could not be found. Please install it." + exit 1 + fi +} + +# Check for required commands +check_command go +check_command 7z +check_command upx + +# Windows amd64 +export GOOS=windows +export GOARCH=amd64 +FILENAME="go2rtc_win64.zip" +go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc.exe + +# Windows 386 +export GOOS=windows +export GOARCH=386 +FILENAME="go2rtc_win32.zip" +go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc.exe + +# Windows arm64 +export GOOS=windows +export GOARCH=arm64 +FILENAME="go2rtc_win_arm64.zip" +go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc.exe + +# Linux amd64 +export GOOS=linux +export GOARCH=amd64 +FILENAME="go2rtc_linux_amd64" +go build -ldflags "-s -w" -trimpath -o $FILENAME && upx --lzma --force-overwrite -q --no-progress $FILENAME + +# Linux 386 +export GOOS=linux +export GOARCH=386 +FILENAME="go2rtc_linux_i386" +go build -ldflags "-s -w" -trimpath -o $FILENAME && upx --lzma --force-overwrite -q --no-progress $FILENAME + +# Linux arm64 +export GOOS=linux +export GOARCH=arm64 +FILENAME="go2rtc_linux_arm64" +go build -ldflags "-s -w" -trimpath -o $FILENAME && upx --lzma --force-overwrite -q --no-progress $FILENAME + +# Linux arm v7 +export GOOS=linux +export GOARCH=arm +export GOARM=7 +FILENAME="go2rtc_linux_arm" +go build -ldflags "-s -w" -trimpath -o $FILENAME && upx --lzma --force-overwrite -q --no-progress $FILENAME + +# Linux arm v6 +export GOOS=linux +export GOARCH=arm +export GOARM=6 +FILENAME="go2rtc_linux_armv6" +go build -ldflags "-s -w" -trimpath -o $FILENAME && upx --lzma --force-overwrite -q --no-progress $FILENAME + +# Linux mipsle +export GOOS=linux +export GOARCH=mipsle +FILENAME="go2rtc_linux_mipsel" +go build -ldflags "-s -w" -trimpath -o $FILENAME && upx --lzma --force-overwrite -q --no-progress $FILENAME + +# Darwin amd64 +export GOOS=darwin +export GOARCH=amd64 +FILENAME="go2rtc_mac_amd64.zip" +go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc + +# Darwin arm64 +export GOOS=darwin +export GOARCH=arm64 +FILENAME="go2rtc_mac_arm64.zip" +go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc \ No newline at end of file diff --git a/www/editor.html b/www/editor.html index 3e0de699..8ace0881 100644 --- a/www/editor.html +++ b/www/editor.html @@ -46,7 +46,7 @@ r = await fetch('api/config', {method: 'POST', body: editor.getValue()}); if (r.ok) { alert('OK'); - fetch('api/restart', {method: 'POST'}); + await fetch('api/restart', {method: 'POST'}); } else { alert(await r.text()); } diff --git a/www/log.html b/www/log.html index fa5d3615..d2e3cbf4 100644 --- a/www/log.html +++ b/www/log.html @@ -19,17 +19,30 @@ height: 100%; } - - - - table tbody td { font-size: 13px; vertical-align: top; } - + .info { + color: #0174DF; + } + .debug { + color: #808080; + } + + .error { + color: #DF0101; + } + + .trace { + color: #585858; + } + + .warn { + color: #FF9966; + } @@ -69,8 +82,8 @@ .replace(/\n/g, '
'); } - let reverseBtn = document.getElementById('reverse'); - let update = document.getElementById('update'); + const reverseBtn = document.getElementById('reverse'); + const update = document.getElementById('update'); let reverseOrder = false; let autoUpdateEnabled = true; @@ -89,7 +102,7 @@ const msg = Object.keys(line).reduce((msg, key) => { return KEYS.indexOf(key) < 0 ? `${msg} ${key}=${line[key]}` : msg; }, line['message']); - return `${ts.toLocaleString()}${line['level']}${escapeHTML(msg)}`; + return `${ts.toLocaleString()}${escapeHTML(line['level'])}${escapeHTML(msg)}`; }).join(''); } diff --git a/www/main.js b/www/main.js index bb2bfec6..2c15e071 100644 --- a/www/main.js +++ b/www/main.js @@ -179,7 +179,8 @@ document.addEventListener('DOMContentLoaded', () => { // Update the editor theme based on the dark mode state const updateEditorTheme = () => { if (typeof editor !== 'undefined') { - editor.setTheme(isDarkModeEnabled() ? "ace/theme/tomorrow_night_eighties" : "ace/theme/github"); } + editor.setTheme(isDarkModeEnabled() ? 'ace/theme/tomorrow_night_eighties' : 'ace/theme/github'); + } }; // Initial update for dark mode and toggle button