diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c00433fb..0bc21d11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: with: { name: go2rtc_win64, path: go2rtc.exe } - name: Build go2rtc_win32 - env: { GOOS: windows, GOARCH: 386 } + env: { GOOS: windows, GOARCH: 386, GOTOOLCHAIN: go1.20.14 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_win32 uses: actions/upload-artifact@v4 @@ -85,7 +85,7 @@ jobs: with: { name: go2rtc_linux_mipsel, path: go2rtc } - name: Build go2rtc_mac_amd64 - env: { GOOS: darwin, GOARCH: amd64 } + env: { GOOS: darwin, GOARCH: amd64, GOTOOLCHAIN: go1.20.14 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_mac_amd64 uses: actions/upload-artifact@v4 @@ -159,6 +159,7 @@ jobs: platforms: | linux/amd64 linux/386 + linux/arm/v6 linux/arm/v7 linux/arm64/v8 push: ${{ github.event_name != 'pull_request' }} diff --git a/Dockerfile b/Dockerfile index b3888820..7e235631 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,13 +3,18 @@ # 0. Prepare images ARG PYTHON_VERSION="3.11" ARG GO_VERSION="1.22" -ARG NGROK_VERSION="3" - -FROM python:${PYTHON_VERSION}-alpine AS base -FROM ngrok/ngrok:${NGROK_VERSION}-alpine AS ngrok -# 1. Build go2rtc binary +# 1. Download ngrok binary (for support arm/v6) +FROM alpine AS ngrok +ARG TARGETARCH +ARG TARGETOS + +ADD https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-${TARGETOS}-${TARGETARCH}.tgz / +RUN tar -xzf /ngrok-v3-stable-${TARGETOS}-${TARGETARCH}.tgz -C /bin + + +# 2. Build go2rtc binary FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build ARG TARGETPLATFORM ARG TARGETOS @@ -30,15 +35,8 @@ COPY . . RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath -# 2. Collect all files -FROM scratch AS rootfs - -COPY --from=build /build/go2rtc /usr/local/bin/ -COPY --from=ngrok /bin/ngrok /usr/local/bin/ - - # 3. Final image -FROM base +FROM python:${PYTHON_VERSION}-alpine AS base # Install ffmpeg, tini (for signal handling), # and other common tools for the echo source. @@ -56,7 +54,8 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver # Hardware: AMD and NVidia VDPAU (not sure about this) # RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total) -COPY --from=rootfs / / +COPY --from=build /build/go2rtc /usr/local/bin/ +COPY --from=ngrok /bin/ngrok /usr/local/bin/ ENTRYPOINT ["/sbin/tini", "--"] VOLUME /config diff --git a/README.md b/README.md index 33c14cca..21e760a6 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,8 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/): -- `go2rtc_win64.zip` - Windows 64-bit -- `go2rtc_win32.zip` - Windows 32-bit +- `go2rtc_win64.zip` - Windows 10+ 64-bit +- `go2rtc_win32.zip` - Windows 7+ 32-bit - `go2rtc_win_arm64.zip` - Windows ARM 64-bit - `go2rtc_linux_amd64` - Linux 64-bit - `go2rtc_linux_i386` - Linux 32-bit @@ -124,8 +124,8 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2 - `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS) - `go2rtc_linux_armv6` - Linux ARMv6 (for old Raspberry 1 and Zero) - `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks)) -- `go2rtc_mac_amd64.zip` - Mac Intel 64-bit -- `go2rtc_mac_arm64.zip` - Mac ARM 64-bit +- `go2rtc_mac_amd64.zip` - macOS 10.13+ Intel 64-bit +- `go2rtc_mac_arm64.zip` - macOS ARM 64-bit - `go2rtc_freebsd_amd64.zip` - FreeBSD Intel 64-bit - `go2rtc_freebsd_arm64.zip` - FreeBSD ARM 64-bit @@ -172,7 +172,7 @@ Available modules: - [api](#module-api) - HTTP API (important for WebRTC support) - [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support) - [webrtc](#module-webrtc) - WebRTC Server -- [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot Server +- [mp4](#module-mp4) - MSE, MP4 stream and MP4 snapshot Server - [hls](#module-hls) - HLS TS or fMP4 stream Server - [mjpeg](#module-mjpeg) - MJPEG Server - [ffmpeg](#source-ffmpeg) - FFmpeg integration @@ -650,10 +650,11 @@ This source type support Roborock vacuums with cameras. Known working models: - Roborock S6 MaxV - only video (the vacuum has no microphone) - Roborock S7 MaxV - video and two way audio +- Roborock Qrevo MaxV - video and two way audio -Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config. +Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock) or the [core integration](https://www.home-assistant.io/integrations/roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config. -If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 678) to the end of the roborock-link. +If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 789) to the end of the roborock-link. #### Source: WebRTC diff --git a/go.mod b/go.mod index d3cb791f..ecd32f3a 100644 --- a/go.mod +++ b/go.mod @@ -1,48 +1,49 @@ module github.com/AlexxIT/go2rtc -go 1.22 +go 1.20 require ( github.com/asticode/go-astits v1.13.0 github.com/expr-lang/expr v1.16.9 - github.com/gorilla/websocket v1.5.1 + github.com/gorilla/websocket v1.5.3 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 + github.com/miekg/dns v1.1.62 + github.com/pion/ice/v2 v2.3.36 + github.com/pion/interceptor v0.1.37 github.com/pion/rtcp v1.2.14 - github.com/pion/rtp v1.8.6 + github.com/pion/rtp v1.8.9 github.com/pion/sdp/v3 v3.0.9 - github.com/pion/srtp/v2 v2.0.18 + github.com/pion/srtp/v2 v2.0.20 github.com/pion/stun v0.6.1 - github.com/pion/webrtc/v3 v3.2.40 + github.com/pion/webrtc/v3 v3.3.4 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.24.0 + golang.org/x/crypto v0.28.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/asticode/go-astikit v0.30.0 // indirect + github.com/asticode/go-astikit v0.45.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/kr/pretty v0.2.1 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/pion/datachannel v1.5.6 // indirect - github.com/pion/dtls/v2 v2.2.11 // indirect + github.com/pion/datachannel v1.5.9 // indirect + github.com/pion/dtls/v2 v2.2.12 // 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 - github.com/pion/sctp v1.8.16 // indirect - github.com/pion/transport/v2 v2.2.5 // indirect + github.com/pion/sctp v1.8.33 // indirect + github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/turn/v2 v2.1.6 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect golang.org/x/tools v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index 727787ac..804ecc43 100644 --- a/go.sum +++ b/go.sum @@ -1,48 +1,45 @@ -github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA= github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= +github.com/asticode/go-astikit v0.45.0 h1:08to/jrbod9tchF2bJ9moW+RTDK7DBUxLdIeSE7v7Sw= +github.com/asticode/go-astikit v0.45.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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.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= 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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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.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/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= +github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= +github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE= 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/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/ice/v2 v2.3.36 h1:SopeXiVbbcooUg2EIR8sq4b13RQ8gzrkkldOVg+bBsc= +github.com/pion/ice/v2 v2.3.36/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= +github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= +github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= 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.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= @@ -53,40 +50,36 @@ github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9 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/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/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk= +github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= +github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw= +github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM= 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/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk= +github.com/pion/srtp/v2 v2.0.20/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/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/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= -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/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= 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/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= 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/pion/webrtc/v3 v3.3.4 h1:v2heQVnXTSqNRXcaFQVOhIOYkLMxOu1iJG8uy1djvkk= +github.com/pion/webrtc/v3 v3.3.4/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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= @@ -106,25 +99,19 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT 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/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= 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-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.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.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/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 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= @@ -133,17 +120,10 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug 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.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= 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/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 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,42 +140,29 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.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/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.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= 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.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.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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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.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.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.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= diff --git a/internal/api/ws/ws.go b/internal/api/ws/ws.go index 800a377d..1d945bfe 100644 --- a/internal/api/ws/ws.go +++ b/internal/api/ws/ws.go @@ -1,6 +1,7 @@ package ws import ( + "encoding/json" "io" "net/http" "net/url" @@ -38,20 +39,19 @@ type Message struct { Value any `json:"value,omitempty"` } -func (m *Message) String() string { +func (m *Message) String() (value string) { if s, ok := m.Value.(string); ok { return s } - return "" + return } -func (m *Message) GetString(key string) string { - if v, ok := m.Value.(map[string]any); ok { - if s, ok := v[key].(string); ok { - return s - } +func (m *Message) Unmarshal(v any) error { + b, err := json.Marshal(m.Value) + if err != nil { + return err } - return "" + return json.Unmarshal(b, v) } type WSHandler func(tr *Transport, msg *Message) error diff --git a/internal/app/README.md b/internal/app/README.md index 2460daa2..9ec3d9fc 100644 --- a/internal/app/README.md +++ b/internal/app/README.md @@ -19,15 +19,15 @@ go2rtc -c log.format=text -c /config/go2rtc.yaml -c rtsp.listen='' -c /usr/local ## Environment variables -Also go2rtc support templates for using environment variables in any part of config: +There is support for loading external variables into the config. First, they will be attempted to be loaded from [credential files](https://systemd.io/CREDENTIALS). If `CREDENTIALS_DIRECTORY` is not set, then the key will be loaded from an environment variable. If no environment variable is set, then the string will be left as-is. ```yaml streams: camera1: rtsp://rtsp:${CAMERA_PASSWORD}@192.168.1.123/av_stream/ch0 rtsp: - username: ${RTSP_USER:admin} # "admin" if env "RTSP_USER" not set - password: ${RTSP_PASS:secret} # "secret" if env "RTSP_PASS" not set + username: ${RTSP_USER:admin} # "admin" if "RTSP_USER" not set + password: ${RTSP_PASS:secret} # "secret" if "RTSP_PASS" not set ``` ## JSON Schema diff --git a/internal/doorbird/doorbird.go b/internal/doorbird/doorbird.go new file mode 100644 index 00000000..c56fc0f9 --- /dev/null +++ b/internal/doorbird/doorbird.go @@ -0,0 +1,36 @@ +package doorbird + +import ( + "net/url" + + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/doorbird" +) + +func Init() { + streams.RedirectFunc("doorbird", func(rawURL string) (string, error) { + u, err := url.Parse(rawURL) + if err != nil { + return "", err + } + + // https://www.doorbird.com/downloads/api_lan.pdf + switch u.Query().Get("media") { + case "video": + u.Path = "/bha-api/video.cgi" + case "audio": + u.Path = "/bha-api/audio-receive.cgi" + default: + return "", nil + } + + u.Scheme = "http" + + return u.String(), nil + }) + + streams.HandleFunc("doorbird", func(source string) (core.Producer, error) { + return doorbird.Dial(source) + }) +} diff --git a/internal/exec/exec.go b/internal/exec/exec.go index 035317d9..bce166e8 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -10,7 +10,6 @@ import ( "net/url" "os" "os/exec" - "slices" "strings" "sync" "time" @@ -230,7 +229,7 @@ func trimSpace(b []byte) []byte { func setRemoteInfo(info core.Info, source string, args []string) { info.SetSource(source) - if i := slices.Index(args, "-i"); i > 0 && i < len(args)-1 { + if i := core.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) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 062e5aaf..25d61e4b 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -2,7 +2,6 @@ package ffmpeg import ( "net/url" - "slices" "strings" "github.com/AlexxIT/go2rtc/internal/api" @@ -44,7 +43,7 @@ func Init() { return "", err } args := parseArgs(url[7:]) - if slices.Contains(args.Codecs, "auto") { + if core.Contains(args.Codecs, "auto") { return "", nil // force call streams.HandleFunc("ffmpeg") } return "exec:" + args.String(), nil @@ -180,6 +179,7 @@ func parseArgs(s string) *ffmpeg.Args { Version: verAV, } + var source = s var query url.Values if i := strings.IndexByte(s, '#'); i >= 0 { query = streams.ParseQuery(s[i+1:]) @@ -222,6 +222,10 @@ func parseArgs(s string) *ffmpeg.Args { default: s += "?video&audio" } + s += "&source=ffmpeg:" + url.QueryEscape(source) + for _, v := range query["query"] { + s += "&" + v + } args.Input = inputTemplate("rtsp", s, query) } else if i = strings.Index(s, "?"); i > 0 { switch s[:i] { diff --git a/internal/ffmpeg/ffmpeg_test.go b/internal/ffmpeg/ffmpeg_test.go index 3fc5d208..2ab1170d 100644 --- a/internal/ffmpeg/ffmpeg_test.go +++ b/internal/ffmpeg/ffmpeg_test.go @@ -31,7 +31,7 @@ func TestParseArgsFile(t *testing.T) { { 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}`, + 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 -pix_fmt:v yuv420p -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", @@ -53,85 +53,143 @@ func TestParseArgsFile(t *testing.T) { } func TestParseArgsDevice(t *testing.T) { - // [DEVICE] video will be output for MJPEG to pipe, with size 1920x1080 - args := parseArgs("device?video=0&video_size=1920x1080") - require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i "video=0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped - //args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma") - args = parseArgs("device?video=0&framerate=20#video=h265") - require.Equal(t, `ffmpeg -hide_banner -f dshow -framerate 20 -i "video=0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - args = parseArgs("device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)") - require.Equal(t, `ffmpeg -hide_banner -f dshow -i "video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) + tests := []struct { + name string + source string + expect string + }{ + { + name: "[DEVICE] video will be output for MJPEG to pipe, with size 1920x1080", + source: "device?video=0&video_size=1920x1080", + expect: `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i "video=0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped", + source: "device?video=0&framerate=20#video=h265", + expect: `ffmpeg -hide_banner -f dshow -framerate 20 -i "video=0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[DEVICE] video/audio", + source: "device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)", + expect: `ffmpeg -hide_banner -f dshow -i "video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)" -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 TestParseArgsIpCam(t *testing.T) { - // [HTTP] video will be copied - args := parseArgs("http://example.com") - require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [HTTP-MJPEG] video will be transcoded to H264 - args = parseArgs("http://example.com#video=h264") - 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 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [HLS] video will be copied, audio will be skipped - args = parseArgs("https://example.com#video=copy") - require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i https://example.com -c:v copy -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [RTSP] video will be copied without transcoding codecs - args = parseArgs("rtsp://example.com") - require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -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") - require.Equal(t, `ffmpeg -hide_banner -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 libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP - args = parseArgs("rtsp://example.com#input=rtsp/udp") - require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [RTMP] video will be copied, changing RTSP transport from TCP to UDP+TCP - args = parseArgs("rtmp://example.com#input=rtsp/udp") - require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) + tests := []struct { + name string + source string + expect string + }{ + { + name: "[HTTP] video will be copied", + source: "http://example.com", + expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[HTTP-MJPEG] video will be transcoded to H264", + source: "http://example.com#video=h264", + 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 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[HLS] video will be copied, audio will be skipped", + source: "https://example.com#video=copy", + expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i https://example.com -c:v copy -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[RTSP] video will be copied without transcoding codecs", + source: "rtsp://example.com", + expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -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", + expect: `ffmpeg -hide_banner -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 libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP", + source: "rtsp://example.com#input=rtsp/udp", + expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[RTMP] video will be copied, changing RTSP transport from TCP to UDP+TCP", + source: "rtmp://example.com#input=rtsp/udp", + expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -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 TestParseArgsAudio(t *testing.T) { - // [AUDIO] audio will be transcoded to AAC, video will be skipped - args := parseArgs("rtsp:///example.com#audio=aac") - 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 aac -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [AUDIO] audio will be transcoded to AAC/16000, video will be skipped - args = parseArgs("rtsp:///example.com#audio=aac/16000") - 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 aac -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [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 -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") - 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 pcm_mulaw -ar:a 8000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [AUDIO] audio will be transcoded to PCMU/16000, video will be skipped - args = parseArgs("rtsp:///example.com#audio=pcmu/16000") - 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 pcm_mulaw -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [AUDIO] audio will be transcoded to PCMU/48000, video will be skipped - args = parseArgs("rtsp:///example.com#audio=pcmu/48000") - 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 pcm_mulaw -ar:a 48000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [AUDIO] audio will be transcoded to PCMA, video will be skipped - args = parseArgs("rtsp:///example.com#audio=pcma") - 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 pcm_alaw -ar:a 8000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [AUDIO] audio will be transcoded to PCMA/16000, video will be skipped - args = parseArgs("rtsp:///example.com#audio=pcma/16000") - 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 pcm_alaw -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [AUDIO] audio will be transcoded to PCMA/48000, video will be skipped - args = parseArgs("rtsp:///example.com#audio=pcma/48000") - 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 pcm_alaw -ar:a 48000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) + tests := []struct { + name string + source string + expect string + }{ + { + name: "[AUDIO] audio will be transcoded to AAC, video will be skipped", + source: "rtsp://example.com#audio=aac", + expect: `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 aac -vn -f adts -`, + }, + { + name: "[AUDIO] audio will be transcoded to AAC/16000, video will be skipped", + source: "rtsp://example.com#audio=aac/16000", + expect: `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 aac -ar:a 16000 -ac:a 1 -vn -f adts -`, + }, + { + name: "[AUDIO] audio will be transcoded to OPUS, video will be skipped", + source: "rtsp://example.com#audio=opus", + expect: `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}`, + }, + { + name: "[AUDIO] audio will be transcoded to PCMU, video will be skipped", + source: "rtsp://example.com#audio=pcmu", + expect: `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 pcm_mulaw -ar:a 8000 -ac:a 1 -vn -f wav -`, + }, + { + name: "[AUDIO] audio will be transcoded to PCMU/16000, video will be skipped", + source: "rtsp://example.com#audio=pcmu/16000", + expect: `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 pcm_mulaw -ar:a 16000 -ac:a 1 -vn -f wav -`, + }, + { + name: "[AUDIO] audio will be transcoded to PCMU/48000, video will be skipped", + source: "rtsp://example.com#audio=pcmu/48000", + expect: `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 pcm_mulaw -ar:a 48000 -ac:a 1 -vn -f wav -`, + }, + { + name: "[AUDIO] audio will be transcoded to PCMA, video will be skipped", + source: "rtsp://example.com#audio=pcma", + expect: `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 pcm_alaw -ar:a 8000 -ac:a 1 -vn -f wav -`, + }, + { + name: "[AUDIO] audio will be transcoded to PCMA/16000, video will be skipped", + source: "rtsp://example.com#audio=pcma/16000", + expect: `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 pcm_alaw -ar:a 16000 -ac:a 1 -vn -f wav -`, + }, + { + name: "[AUDIO] audio will be transcoded to PCMA/48000, video will be skipped", + source: "rtsp://example.com#audio=pcma/48000", + expect: `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 pcm_alaw -ar:a 48000 -ac:a 1 -vn -f wav -`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + args := parseArgs(test.source) + require.Equal(t, test.expect, args.String()) + }) + } } func TestParseArgsHwVaapi(t *testing.T) { diff --git a/internal/http/http.go b/internal/http/http.go index a35439d5..4b0560c1 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -14,6 +14,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/image" "github.com/AlexxIT/go2rtc/pkg/magic" "github.com/AlexxIT/go2rtc/pkg/mpjpeg" + "github.com/AlexxIT/go2rtc/pkg/pcm" "github.com/AlexxIT/go2rtc/pkg/tcp" ) @@ -87,6 +88,9 @@ func do(req *http.Request) (core.Producer, error) { return image.Open(res) case ct == "multipart/x-mixed-replace": return mpjpeg.Open(res.Body) + //https://www.iana.org/assignments/media-types/audio/basic + case ct == "audio/basic": + return pcm.Open(res.Body) } return magic.Open(res.Body) diff --git a/internal/rtmp/rtmp.go b/internal/rtmp/rtmp.go index afc363a9..b3d7f932 100644 --- a/internal/rtmp/rtmp.go +++ b/internal/rtmp/rtmp.go @@ -133,7 +133,7 @@ func streamsHandle(url string) (core.Producer, error) { func streamsConsumerHandle(url string) (core.Consumer, func(), error) { cons := flv.NewConsumer() run := func() { - wr, err := rtmp.DialPublish(url) + wr, err := rtmp.DialPublish(url, cons) if err != nil { return } diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index 230bdece..0fe135f8 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -147,6 +147,7 @@ func tcpHandler(conn *rtsp.Conn) { var closer func() trace := log.Trace().Enabled() + level := zerolog.WarnLevel conn.Listen(func(msg any) { if trace { @@ -188,8 +189,18 @@ func tcpHandler(conn *rtsp.Conn) { conn.PacketSize = uint16(core.Atoi(s)) } + // param name like ffmpeg style https://ffmpeg.org/ffmpeg-protocols.html + if s := query.Get("log_level"); s != "" { + if lvl, err := zerolog.ParseLevel(s); err == nil { + level = lvl + } + } + + // will help to protect looping requests to same source + conn.Connection.Source = query.Get("source") + if err := stream.AddConsumer(conn); err != nil { - log.Warn().Err(err).Str("stream", name).Msg("[rtsp]") + log.WithLevel(level).Err(err).Str("stream", name).Msg("[rtsp]") return } @@ -227,7 +238,7 @@ func tcpHandler(conn *rtsp.Conn) { if err := conn.Accept(); err != nil { if err != io.EOF { - log.Warn().Err(err).Caller().Send() + log.WithLevel(level).Err(err).Caller().Send() } if closer != nil { closer() diff --git a/internal/streams/add_consumer.go b/internal/streams/add_consumer.go index eb767691..7400ce6e 100644 --- a/internal/streams/add_consumer.go +++ b/internal/streams/add_consumer.go @@ -22,6 +22,12 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) { producers: for prodN, prod := range s.producers { + // check for loop request, ex. `camera1: ffmpeg:camera1` + if info, ok := cons.(core.Info); ok && prod.url == info.GetSource() { + log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN) + continue + } + if prodErrors[prodN] != nil { log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN) continue @@ -129,7 +135,7 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error for _, media := range prodMedias { if media.Direction == core.DirectionRecvonly { for _, codec := range media.Codecs { - prod = appendString(prod, codec.PrintName()) + prod = appendString(prod, media.Kind+":"+codec.PrintName()) } } } @@ -137,7 +143,7 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error for _, media := range consMedias { if media.Direction == core.DirectionSendonly { for _, codec := range media.Codecs { - cons = appendString(cons, codec.PrintName()) + cons = appendString(cons, media.Kind+":"+codec.PrintName()) } } } diff --git a/internal/streams/api.go b/internal/streams/api.go index d64c4846..d6042974 100644 --- a/internal/streams/api.go +++ b/internal/streams/api.go @@ -48,12 +48,12 @@ func apiStreams(w http.ResponseWriter, r *http.Request) { name = src } - if New(name, src) == nil { + if New(name, query["src"]...) == nil { http.Error(w, "", http.StatusBadRequest) return } - if err := app.PatchConfig(name, src, "streams"); err != nil { + if err := app.PatchConfig(name, query["src"], "streams"); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } diff --git a/internal/streams/play.go b/internal/streams/play.go index 7ada66e6..9bec7258 100644 --- a/internal/streams/play.go +++ b/internal/streams/play.go @@ -103,7 +103,7 @@ func (s *Stream) Play(source string) error { } func (s *Stream) AddInternalProducer(conn core.Producer) { - producer := &Producer{conn: conn, state: stateInternal} + producer := &Producer{conn: conn, state: stateInternal, url: "internal"} s.mu.Lock() s.producers = append(s.producers, producer) s.mu.Unlock() diff --git a/internal/streams/stream.go b/internal/streams/stream.go index bb832694..569e63ee 100644 --- a/internal/streams/stream.go +++ b/internal/streams/stream.go @@ -21,6 +21,12 @@ func NewStream(source any) *Stream { return &Stream{ producers: []*Producer{NewProducer(source)}, } + case []string: + s := new(Stream) + for _, str := range source { + s.producers = append(s.producers, NewProducer(str)) + } + return s case []any: s := new(Stream) for _, src := range source { @@ -70,7 +76,7 @@ func (s *Stream) RemoveConsumer(cons core.Consumer) { } func (s *Stream) AddProducer(prod core.Producer) { - producer := &Producer{conn: prod, state: stateExternal} + producer := &Producer{conn: prod, state: stateExternal, url: "external"} s.mu.Lock() s.producers = append(s.producers, producer) s.mu.Unlock() diff --git a/internal/streams/streams.go b/internal/streams/streams.go index ff0f5654..b1038423 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -56,13 +56,18 @@ func Validate(source string) error { return nil } -func New(name string, source string) *Stream { - if Validate(source) != nil { - return nil +func New(name string, sources ...string) *Stream { + for _, source := range sources { + if Validate(source) != nil { + return nil + } } - stream := NewStream(source) + stream := NewStream(sources) + + streamsMu.Lock() streams[name] = stream + streamsMu.Unlock() return stream } @@ -95,6 +100,10 @@ func Patch(name string, source string) *Stream { return nil } + if Validate(source) != nil { + return nil + } + // check an existing stream with this name if stream, ok := streams[name]; ok { stream.SetSource(source) @@ -102,7 +111,9 @@ func Patch(name string, source string) *Stream { } // create new stream with this name - return New(name, source) + stream := NewStream(source) + streams[name] = stream + return stream } func GetOrPatch(query url.Values) *Stream { diff --git a/internal/webrtc/candidates.go b/internal/webrtc/candidates.go index b92c4656..adbfb4a7 100644 --- a/internal/webrtc/candidates.go +++ b/internal/webrtc/candidates.go @@ -2,10 +2,10 @@ package webrtc import ( "net" - "slices" "strings" "github.com/AlexxIT/go2rtc/internal/api/ws" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/webrtc" pion "github.com/pion/webrtc/v3" ) @@ -75,14 +75,14 @@ func FilterCandidate(candidate *pion.ICECandidate) bool { // host candidate should be in the hosts list if candidate.Typ == pion.ICECandidateTypeHost && filters.Candidates != nil { - if !slices.Contains(filters.Candidates, candidate.Address) { + if !core.Contains(filters.Candidates, candidate.Address) { return false } } if filters.Networks != nil { networkType := NetworkType(candidate.Protocol.String(), candidate.Address) - if !slices.Contains(filters.Networks, networkType) { + if !core.Contains(filters.Networks, networkType) { return false } } diff --git a/internal/webrtc/webrtc.go b/internal/webrtc/webrtc.go index 8b4943c3..fe25c919 100644 --- a/internal/webrtc/webrtc.go +++ b/internal/webrtc/webrtc.go @@ -40,15 +40,17 @@ func Init() { AddCandidate(network, candidate) } + var err error + // create pionAPI with custom codecs list and custom network settings - serverAPI, err := webrtc.NewServerAPI(network, address, &filters) + serverAPI, err = webrtc.NewServerAPI(network, address, &filters) if err != nil { log.Error().Err(err).Caller().Send() return } // use same API for WebRTC server and client if no address - clientAPI := serverAPI + clientAPI = serverAPI if address != "" { log.Info().Str("addr", cfg.Mod.Listen).Msg("[webrtc] listen") @@ -81,11 +83,13 @@ func Init() { streams.HandleFunc("webrtc", streamsHandler) } +var serverAPI, clientAPI *pion.API + var log zerolog.Logger var PeerConnection func(active bool) (*pion.PeerConnection, error) -func asyncHandler(tr *ws.Transport, msg *ws.Message) error { +func asyncHandler(tr *ws.Transport, msg *ws.Message) (err error) { var stream *streams.Stream var mode core.Mode @@ -104,8 +108,30 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error { return errors.New(api.StreamNotFound) } + var offer struct { + Type string `json:"type"` + SDP string `json:"sdp"` + ICEServers []pion.ICEServer `json:"ice_servers"` + } + + // V2 - json/object exchange, V1 - raw SDP exchange + apiV2 := msg.Type == "webrtc" + + if apiV2 { + if err = msg.Unmarshal(&offer); err != nil { + return err + } + } else { + offer.SDP = msg.String() + } + // create new PeerConnection instance - pc, err := PeerConnection(false) + var pc *pion.PeerConnection + if offer.ICEServers == nil { + pc, err = PeerConnection(false) + } else { + pc, err = serverAPI.NewPeerConnection(pion.Configuration{ICEServers: offer.ICEServers}) + } if err != nil { log.Error().Err(err).Caller().Send() return err @@ -145,20 +171,10 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error { } }) - // V2 - json/object exchange, V1 - raw SDP exchange - apiV2 := msg.Type == "webrtc" + log.Trace().Msgf("[webrtc] offer:\n%s", offer.SDP) // 1. SetOffer, so we can get remote client codecs - var offer string - if apiV2 { - offer = msg.GetString("sdp") - } else { - offer = msg.String() - } - - log.Trace().Msgf("[webrtc] offer:\n%s", offer) - - if err = conn.SetOffer(offer); err != nil { + if err = conn.SetOffer(offer.SDP); err != nil { log.Warn().Err(err).Caller().Send() return err } diff --git a/internal/webrtc/webrtc_test.go b/internal/webrtc/webrtc_test.go new file mode 100644 index 00000000..e014c31c --- /dev/null +++ b/internal/webrtc/webrtc_test.go @@ -0,0 +1,38 @@ +package webrtc + +import ( + "encoding/json" + "testing" + + "github.com/AlexxIT/go2rtc/internal/api/ws" + pion "github.com/pion/webrtc/v3" + "github.com/stretchr/testify/require" +) + +func TestWebRTCAPIv1(t *testing.T) { + raw := `{"type":"webrtc/offer","value":"v=0\n..."}` + msg := new(ws.Message) + err := json.Unmarshal([]byte(raw), msg) + require.Nil(t, err) + + require.Equal(t, "v=0\n...", msg.String()) +} + +func TestWebRTCAPIv2(t *testing.T) { + raw := `{"type":"webrtc","value":{"type":"offer","sdp":"v=0\n...","ice_servers":[{"urls":["stun:stun.l.google.com:19302"]}]}}` + msg := new(ws.Message) + err := json.Unmarshal([]byte(raw), msg) + require.Nil(t, err) + + var offer struct { + Type string `json:"type"` + SDP string `json:"sdp"` + ICEServers []pion.ICEServer `json:"ice_servers"` + } + err = msg.Unmarshal(&offer) + require.Nil(t, err) + + require.Equal(t, "offer", offer.Type) + require.Equal(t, "v=0\n...", offer.SDP) + require.Equal(t, "stun:stun.l.google.com:19302", offer.ICEServers[0].URLs[0]) +} diff --git a/main.go b/main.go index 98bd79e3..d5c59ffc 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/bubble" "github.com/AlexxIT/go2rtc/internal/debug" + "github.com/AlexxIT/go2rtc/internal/doorbird" "github.com/AlexxIT/go2rtc/internal/dvrip" "github.com/AlexxIT/go2rtc/internal/echo" "github.com/AlexxIT/go2rtc/internal/exec" @@ -36,7 +37,7 @@ import ( ) func main() { - app.Version = "1.9.4" + app.Version = "1.9.7" // 1. Core modules: app, api/ws, streams @@ -82,6 +83,7 @@ func main() { bubble.Init() // bubble source expr.Init() // expr source gopro.Init() // gopro source + doorbird.Init() // doorbird source // 6. Helper modules diff --git a/pkg/aac/adts.go b/pkg/aac/adts.go index d5e7828e..94a13ad7 100644 --- a/pkg/aac/adts.go +++ b/pkg/aac/adts.go @@ -9,7 +9,6 @@ import ( ) func IsADTS(b []byte) bool { - _ = b[1] return len(b) > 7 && b[0] == 0xFF && b[1]&0xF6 == 0xF0 } diff --git a/pkg/aac/rtp.go b/pkg/aac/rtp.go index b5ae4a10..1faa2e27 100644 --- a/pkg/aac/rtp.go +++ b/pkg/aac/rtp.go @@ -22,6 +22,15 @@ func RTPDepay(handler core.HandlerFunc) core.HandlerFunc { //log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker) if len(packet.Payload) < int(2+headersSize) { + // In very rare cases noname cameras may send data not according to the standard + // https://github.com/AlexxIT/go2rtc/issues/1328 + if IsADTS(packet.Payload) { + clone := *packet + clone.Version = RTPPacketVersionAAC + clone.Timestamp = timestamp + clone.Payload = clone.Payload[ADTSHeaderSize:] + handler(&clone) + } return } diff --git a/pkg/aac/rtp_test.go b/pkg/aac/rtp_test.go new file mode 100644 index 00000000..c541b255 --- /dev/null +++ b/pkg/aac/rtp_test.go @@ -0,0 +1,33 @@ +package aac + +import ( + "encoding/hex" + "testing" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" + "github.com/stretchr/testify/require" +) + +func TestBuggy_RTSP_AAC(t *testing.T) { + // https: //github.com/AlexxIT/go2rtc/issues/1328 + payload, _ := hex.DecodeString("fff16080431ffc211ad4458aa309a1c0a8761a230502b7c74b2b5499252a010555e32e460128303c8ace4fd3260d654a424f7e7c65eddc96735fc6f1ac0edf94fdefa0e0bd6370da1c07b9c0e77a9d6e86b196a1ac7439dcafadcffcf6d89f60ac67f8884868e931383ad3e40cf5495470d1f606ef6f7624d285b951ebfa0e42641ab98f1371182b237d14f1bd16ad714fa2f1c6a7d23ebde7a0e34a2eca156a608a4caec49d9dca4b6fe2a09e9cdbf762c5b4148a3914abb7959c991228b0837b5988334b9fc18b8fac689b5ca1e4661573bbb8b253a86cae7ec14ace49969a9a76fd571ab6e650764cb59114d61dcedf07ac61b39e4ac66adebfd0d0ab45d518dd3c161049823f150864d977cf0855172ac8482e4b25fe911325d19617558c5405af74aff5492e4599bee53f2dbdf0503730af37078550f84c956b7ee89aae83c154fa2fa6e6792c5ddd5cd5cf6bb96bf055fee7f93bed59ffb039daee5ea7e5593cb194e9091e417c67d8f73026a6a6ae056e808f7c65c03d1b9197d3709ceb63bc7b979f7ba71df5e7c6395d99d6ea229000a6bc16fb4346d6b27d32f5d8d1200736d9366d59c0c9547210813b602473da9c46f9015bbb37594c1dd90cd6a36e96bd5d6a1445ab93c9e65505ec2c722bb4cc27a10600139a48c83594dde145253c386f6627d8c6e5102fe3828a590c709bc87f55b37e97d1ae72b017b09c6bb2c13299817bb45cc67318e10b6822075b97c6a03ec1c0") + packet := &rtp.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: true, + SequenceNumber: 36944, + Timestamp: 4217191328, + SSRC: 12892774, + }, + Payload: payload, + } + + var size int + + RTPDepay(func(packet *core.Packet) { + size = len(packet.Payload) + })(packet) + + require.Equal(t, len(payload), size+ADTSHeaderSize) +} diff --git a/pkg/bubble/client.go b/pkg/bubble/client.go index 5afba779..7a71d555 100644 --- a/pkg/bubble/client.go +++ b/pkg/bubble/client.go @@ -231,7 +231,7 @@ func (c *Client) Handle() error { Header: rtp.Header{ Timestamp: core.Now90000(), }, - Payload: annexb.EncodeToAVCC(b[6:], false), + Payload: annexb.EncodeToAVCC(b[6:]), } c.videoTrack.WriteRTP(pkt) } else { diff --git a/pkg/core/codec.go b/pkg/core/codec.go index 9c6c6b79..b138df28 100644 --- a/pkg/core/codec.go +++ b/pkg/core/codec.go @@ -157,7 +157,12 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec { } } - if c.Name == "" { + switch c.Name { + case "PCM": + // https://www.reddit.com/r/Hikvision/comments/17elxex/comment/k642g2r/ + // check pkg/rtsp/rtsp_test.go TestHikvisionPCM + c.Name = CodecPCML + case "": // https://en.wikipedia.org/wiki/RTP_payload_formats switch payloadType { case "0": diff --git a/pkg/core/connection.go b/pkg/core/connection.go index 2c3f2196..cc0f43e4 100644 --- a/pkg/core/connection.go +++ b/pkg/core/connection.go @@ -25,6 +25,7 @@ type Info interface { SetSource(string) SetURL(string) WithRequest(*http.Request) + GetSource() string } // Connection just like webrtc.PeerConnection @@ -123,6 +124,10 @@ func (c *Connection) WithRequest(r *http.Request) { c.UserAgent = r.UserAgent() } +func (c *Connection) GetSource() string { + return c.Source +} + // Create like os.Create, init Consumer with existing Transport func Create(w io.Writer) (*Connection, error) { return &Connection{Transport: w}, nil diff --git a/pkg/core/media.go b/pkg/core/media.go index 2284d0cd..72ab58c6 100644 --- a/pkg/core/media.go +++ b/pkg/core/media.go @@ -124,9 +124,13 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) { codec := media.Codecs[0] - name := codec.Name - if name == CodecELD { + switch codec.Name { + case CodecELD: name = CodecAAC + case CodecPCML: + name = CodecPCM // beacuse we using pcm.LittleToBig for RTSP server + default: + name = codec.Name } md := &sdp.MediaDescription{ diff --git a/pkg/core/slices.go b/pkg/core/slices.go new file mode 100644 index 00000000..747d813f --- /dev/null +++ b/pkg/core/slices.go @@ -0,0 +1,43 @@ +package core + +// This code copied from go1.21 for backward support in go1.20. +// We need to support go1.20 for Windows 7 + +// Index returns the index of the first occurrence of v in s, +// or -1 if not present. +func Index[S ~[]E, E comparable](s S, v E) int { + for i := range s { + if v == s[i] { + return i + } + } + return -1 +} + +// Contains reports whether v is present in s. +func Contains[S ~[]E, E comparable](s S, v E) bool { + return Index(s, v) >= 0 +} + +type Ordered interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | + ~float32 | ~float64 | + ~string +} + +// Max returns the maximal value in x. It panics if x is empty. +// For floating-point E, Max propagates NaNs (any NaN value in x +// forces the output to be NaN). +func Max[S ~[]E, E Ordered](x S) E { + if len(x) < 1 { + panic("slices.Max: empty list") + } + m := x[0] + for i := 1; i < len(x); i++ { + if x[i] > m { + m = x[i] + } + } + return m +} diff --git a/pkg/doorbird/backchannel.go b/pkg/doorbird/backchannel.go new file mode 100644 index 00000000..e5f3257c --- /dev/null +++ b/pkg/doorbird/backchannel.go @@ -0,0 +1,100 @@ +package doorbird + +import ( + "fmt" + "net" + "net/http" + "net/url" + "time" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +type Client struct { + core.Connection + conn net.Conn +} + +func Dial(rawURL string) (*Client, error) { + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + user := u.User.Username() + pass, _ := u.User.Password() + + rawURL = fmt.Sprintf("http://%s/bha-api/audio-transmit.cgi?http-user=%s&http-password=%s", u.Host, user, pass) + + req, err := http.NewRequest("POST", rawURL, nil) + if err != nil { + return nil, err + } + req.Header = http.Header{ + "Content-Type": []string{"audio/basic"}, + "Content-Length": []string{"9999999"}, + "Connection": []string{"Keep-Alive"}, + "Cache-Control": []string{"no-cache"}, + } + + if u.Port() == "" { + u.Host += ":80" + } + + conn, err := net.DialTimeout("tcp", u.Host, core.ConnDialTimeout) + if err != nil { + return nil, err + } + + _ = conn.SetWriteDeadline(time.Now().Add(core.ConnDeadline)) + if err = req.Write(conn); err != nil { + return nil, err + } + + medias := []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecPCMU, ClockRate: 8000}, + }, + }, + } + + return &Client{ + core.Connection{ + ID: core.NewID(), + FormatName: "doorbird", + Protocol: "http", + URL: rawURL, + Medias: medias, + Transport: conn, + }, + conn, + }, nil +} + +func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + return nil, core.ErrCantGetTrack +} + +func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { + sender := core.NewSender(media, track.Codec) + + sender.Handler = func(pkt *rtp.Packet) { + _ = c.conn.SetWriteDeadline(time.Now().Add(core.ConnDeadline)) + if n, err := c.conn.Write(pkt.Payload); err == nil { + c.Send += n + } + } + + sender.HandleRTP(track) + c.Senders = append(c.Senders, sender) + return nil +} + +func (c *Client) Start() (err error) { + _, err = c.conn.Read(nil) + return +} diff --git a/pkg/dvrip/producer.go b/pkg/dvrip/producer.go index c87017b4..4f49da1e 100644 --- a/pkg/dvrip/producer.go +++ b/pkg/dvrip/producer.go @@ -53,7 +53,7 @@ func (c *Producer) Start() error { packet := &rtp.Packet{ Header: rtp.Header{Timestamp: c.videoTS}, - Payload: annexb.EncodeToAVCC(payload, false), + Payload: annexb.EncodeToAVCC(payload), } //log.Printf("[AVC] %v, len: %d, ts: %10d", h265.Types(payload), len(payload), packet.Timestamp) @@ -146,7 +146,7 @@ func (c *Producer) probe() error { c.videoTS = binary.LittleEndian.Uint32(ts) c.videoDT = 90000 / uint32(fps) - payload := annexb.EncodeToAVCC(b[16:], false) + payload := annexb.EncodeToAVCC(b[16:]) c.addVideoTrack(b[4], payload) case 0xFA: // audio diff --git a/pkg/h264/annexb/annexb.go b/pkg/h264/annexb/annexb.go index 13f06622..26614a82 100644 --- a/pkg/h264/annexb/annexb.go +++ b/pkg/h264/annexb/annexb.go @@ -11,64 +11,60 @@ const startAUD = StartCode + "\x09\xF0" const startAUDstart = startAUD + StartCode // EncodeToAVCC -// will change original slice data! -// safeAppend should be used if original slice has useful data after end (part of other slice) // // FFmpeg MPEG-TS: 00000001 AUD 00000001 SPS 00000001 PPS 000001 IFrame // FFmpeg H264: 00000001 SPS 00000001 PPS 000001 IFrame 00000001 PFrame -func EncodeToAVCC(b []byte, safeAppend bool) []byte { - const minSize = len(StartCode) + 1 - - // 1. Check frist "start code" - if len(b) < len(startAUDstart) || string(b[:len(StartCode)]) != StartCode { - return nil - } - - // 2. Skip Access unit delimiter (AUD) from FFmpeg - if string(b[:len(startAUDstart)]) == startAUDstart { - b = b[6:] - } - +// Reolink: 000001 AUD 000001 VPS 00000001 SPS 00000001 PPS 00000001 IDR 00000001 IDR +func EncodeToAVCC(annexb []byte) (avc []byte) { var start int - for i, n := minSize, len(b)-minSize; i < n; { - // 3. Check "start code" (first 2 bytes) - if b[i] != 0 || b[i+1] != 0 { - i++ - continue - } + avc = make([]byte, 0, len(annexb)+4) // init memory with little overhead - // 4. Check "start code" (3 bytes size or 4 bytes size) - if b[i+2] == 1 { - if safeAppend { - // protect original slice from "damage" - b = bytes.Clone(b) - safeAppend = false + for i := 0; ; i++ { + var offset int + + if i+3 < len(annexb) { + // search next separator + if annexb[i] == 0 && annexb[i+1] == 0 { + if annexb[i+2] == 1 { + offset = 3 // 00 00 01 + } else if annexb[i+2] == 0 && annexb[i+3] == 1 { + offset = 4 // 00 00 00 01 + } else { + continue + } + } else { + continue } - - // convert start code from 3 bytes to 4 bytes - b = append(b, 0) - copy(b[i+1:], b[i:]) - n++ - } else if b[i+2] != 0 || b[i+3] != 1 { - i++ - continue + } else { + i = len(annexb) // move i to data end } - // 5. Set size for previous AU - size := uint32(i - start - len(StartCode)) - binary.BigEndian.PutUint32(b[start:], size) + if start != 0 { + size := uint32(i - start) + avc = binary.BigEndian.AppendUint32(avc, size) + avc = append(avc, annexb[start:i]...) + } - start = i + // sometimes FFmpeg put separator at the end + if i += offset; i == len(annexb) { + break + } - i += minSize + if isAUD(annexb[i]) { + start = 0 // skip this NALU + } else { + start = i // save this position + } } - // 6. Set size for last AU - size := uint32(len(b) - start - len(StartCode)) - binary.BigEndian.PutUint32(b[start:], size) + return +} - return b +func isAUD(b byte) bool { + const h264 = 9 + const h265 = 35 << 1 + return b&0b0001_1111 == h264 || b&0b0111_1110 == h265 } func DecodeAVCC(b []byte, safeClone bool) []byte { diff --git a/pkg/h264/annexb/annexb_test.go b/pkg/h264/annexb/annexb_test.go new file mode 100644 index 00000000..7220f570 --- /dev/null +++ b/pkg/h264/annexb/annexb_test.go @@ -0,0 +1,85 @@ +package annexb + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func decode(s string) []byte { + b, _ := hex.DecodeString(strings.ReplaceAll(s, " ", "")) + return b +} + +func naluTypes(avcc []byte) (types []byte) { + for { + types = append(types, avcc[4]) + + size := 4 + binary.BigEndian.Uint32(avcc) + if size < uint32(len(avcc)) { + avcc = avcc[size:] + } else { + break + } + } + return +} + +func TestFFmpegH264(t *testing.T) { + // ffmpeg -re -i bbb.mp4 -c copy -f h264 - + s := "000000016764001fac2484014016ec0440000003004000000c23c60c92 0000000168ee32c8b0 00000165888080033ffef5f8454f32cb1bb4203f854dd69bc2ca91b2bce1fb3527440000030000030000030000030050999841d1afd324aea000000300000f600011c0001b40004e40011f0003b80010800059000238000be0005e000220001100000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300004041 00000001" + b := EncodeToAVCC(decode(s)) + require.True(t, bytes.HasSuffix(b, []byte{0x40, 0x41})) + n := naluTypes(b) + require.Equal(t, []byte{0x67, 0x68, 0x65}, n) +} + +func TestFFmpegMPEGTSH264(t *testing.T) { + // ffmpeg -re -i bbb.mp4 -c copy -f mpegts - + s := "00000001 09f0 000000016764001fac2484014016ec0440000003004000000c23c60c92 0000000168ee32c8b0 00000165888080033ffef5f8454f32cb1bb4203f854dd69bc2ca91b2bce1fb3527440000030000030000030000030050999841d1afd324aea000000300000f600011c0001b40004e40011f0003b80010800059000238000be0005e000220001100000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300004041" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x67, 0x68, 0x65}, n) +} + +func TestFFmpegHEVC(t *testing.T) { + // ffmpeg -re -i bbb.mp4 -c libx265 -preset superfast -tune zerolatency -f hevc - + s := "0000000140010c01ffff01600000030090000003000003005dba0240 0000000142010101600000030090000003000003005da00280802d165bab930bc05a7080000003008000000c04 000000014401c1718312 0000014e0105ffffffffffffffffff2b2ca2de09b51747dbbb55a4fe7fc2fc4e7832363520286275696c642032303829202d20332e352b3131312d6330616631386464353a5b57696e646f77735d5b4743432031332e322e305d5b3634206269745d20386269742b31306269742b3132626974202d20482e3236352f4845564320636f646563202d20436f7079726967687420323031332d3230313820286329204d756c7469636f7265776172" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x40, 0x42, 0x44, 0x4e}, n) +} + +func TestFFmpegHEVC2(t *testing.T) { + // ffmpeg -re -i bbb.mp4 -c libx265 -preset superfast -tune zerolatency -f hevc - + s := "0000000140010c01ffff01600000030090000003000003005dba0240 0000000142010101600000030090000003000003005da00280802d165bab930bc05a7080000003008000000c04 000000014401c1718312 0000014e0105ffffffffffffffffff2b2ca2de09b51747dbbb55a4fe7fc2fc4e7832363520286275696c642032303829202d20332e352b3131312d6330616631386464353a5b57696e646f77735d5b4743432031332e322e305d5b3634206269745d20386269742b31306269742b3132626974202d20482e3236352f4845564320636f646563202d20436f7079726967687420323031332d3230313820286329204d756c7469636f7265776172652c20496e63202d20687474703a2f2f783236352e6f7267202d206f7074696f6e733a2063707569643d31303439303731206672616d652d746872656164733d32206e756d612d706f6f6c733d3420777070206e6f2d706d6f6465206e6f2d706d65206e6f2d70736e72206e6f2d7373696d206c6f672d6c6576656c3d322062697464657074683d3820696e7075742d6373703d31206670733d32342f3120696e7075742d7265733d313238307837323020696e7465726c6163653d3020746f74616c2d6672616d65733d30206c6576656c2d6964633d3020686967682d746965723d31207568642d62643d30207265663d31206e6f2d616c6c6f772d6e6f6e2d636f6e666f726d616e6365207265706561742d6865616465727320616e6e657862206e6f2d617564206e6f2d656f62206e6f2d656f73206e6f2d68726420696e666f20686173683d302074656d706f72616c2d6c61796572733d30206f70656e2d676f70206d696e2d6b6579696e743d3234206b6579696e743d32353020676f702d6c6f6f6b61686561643d3020626672616d65733d3020622d61646170743d30206e6f2d622d707972616d696420626672616d652d626961733d302072632d6c6f6f6b61686561643d30206c6f6f6b61686561642d736c696365733d34207363656e656375743d30206e6f2d686973742d7363656e65637574207261646c3d30206e6f2d73706c696365206e6f2d696e7472612d72656672657368206374753d3332206d696e2d63752d73697a653d38206e6f2d72656374206e6f2d616d70206d61782d74752d73697a653d33322074752d696e7465722d64657074683d312074752d696e7472612d64657074683d31206c696d69742d74753d302072646f712d6c6576656c3d302064796e616d69632d72643d302e3030206e6f2d7373696d2d7264207369676e68696465206e6f2d74736b6970206e722d696e7472613d30206e722d696e7465723d30206e6f2d636f6e73747261696e65642d696e747261207374726f6e672d696e7472612d736d6f6f7468696e67206d61782d6d657267653d32206c696d69742d726566733d30206e6f2d6c696d69742d6d6f646573206d653d31207375626d653d31206d6572616e67653d35372074656d706f72616c2d6d7670206e6f2d6672616d652d647570206e6f2d686d65206e6f2d77656967687470206e6f2d77656967687462206e6f2d616e616c797a652d7372632d70696373206465626c6f636b3d303a30206e6f2d73616f206e6f2d73616f2d6e6f6e2d6465626c6f636b2072643d322073656c6563746976652d73616f3d30206561726c792d736b69702072736b697020666173742d696e747261206e6f2d74736b69702d66617374206e6f2d63752d6c6f73736c657373206e6f2d622d696e747261206e6f2d73706c697472642d736b697020726470656e616c74793d30207073792d72643d322e3030207073792d72646f713d302e3030206e6f2d72642d726566696e65206e6f2d6c6f73736c65737320636271706f6666733d3020637271706f6666733d302072633d637266206372663d32382e302071636f6d703d302e3630207170737465703d342073746174732d77726974653d302073746174732d726561643d30206970726174696f3d312e34302061712d6d6f64653d302061712d737472656e6774683d302e3030206e6f2d637574726565207a6f6e652d636f756e743d30206e6f2d7374726963742d6362722071672d73697a653d3332206e6f2d72632d677261696e2071706d61783d36392071706d696e3d30206e6f2d636f6e73742d766276207361723d31206f7665727363616e3d3020766964656f666f726d61743d352072616e67653d3020636f6c6f727072696d3d32207472616e736665723d3220636f6c6f726d61747269783d32206368726f6d616c6f633d31206368726f6d616c6f632d746f703d30206368726f6d616c6f632d626f74746f6d3d3020646973706c61792d77696e646f773d3020636c6c3d302c30206d696e2d6c756d613d30206d61782d6c756d613d323535206c6f67322d6d61782d706f632d6c73623d38207675692d74696d696e672d696e666f207675692d6872642d696e666f20736c696365733d31206e6f2d6f70742d71702d707073206e6f2d6f70742d7265662d6c6973742d6c656e6774682d707073206e6f2d6d756c74692d706173732d6f70742d727073207363656e656375742d626961733d302e3035206e6f2d6f70742d63752d64656c74612d7170206e6f2d61712d6d6f74696f6e206e6f2d6864723130206e6f2d68647231302d6f7074206e6f2d6468647231302d6f7074206e6f2d6964722d7265636f766572792d73656920616e616c797369732d72657573652d6c6576656c3d3020616e616c797369732d736176652d72657573652d6c6576656c3d3020616e616c797369732d6c6f61642d72657573652d6c6576656c3d30207363616c652d666163746f723d3020726566696e652d696e7472613d3020726566696e652d696e7465723d3020726566696e652d6d763d3120726566696e652d6374752d646973746f7274696f6e3d30206e6f2d6c696d69742d73616f206374752d696e666f3d30206e6f2d6c6f77706173732d64637420726566696e652d616e616c797369732d747970653d3020636f70792d7069633d31206d61782d617573697a652d666163746f723d312e30206e6f2d64796e616d69632d726566696e65206e6f2d73696e676c652d736569206e6f2d686576632d6171206e6f2d737674206e6f2d6669656c642071702d61646170746174696f6e2d72616e67653d312e3030207363656e656375742d61776172652d71703d30636f6e666f726d616e63652d77696e646f772d6f6666736574732072696768743d3020626f74746f6d3d30206465636f6465722d6d61782d726174653d30206e6f2d7662762d6c6976652d6d756c74692d70617373206e6f2d6d63737466206e6f2d7362726380 0000012801adc2e5bca307b9ce6b18b5ad6a525294a6d117ffd3917322eebaeda718a0000003000003000003021207706824da718a00000300000300000300044408d5db4e31400000030000030000030012500c2725a000000300000300000300002a600e4880000003000003000003000019301180000003000003000003000007d400000300000300000300000300010b000003000003000003000003001810000003000003000003000003019100000300000300000300000d38000003000003000003000067c000000300000300000300025e000003000003000003000c58000003000003000003002b60000003000003000003007f80000003000003000003016300000300000300000303b2000003000003000006e400000300000300000e18000003000003000018d00000030000030000292000000300000300003ce00000030000030000030000030000030000bb80" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x40, 0x42, 0x44, 0x4e, 0x28}, n) +} + +func TestFFmpegMPEGTSHEVC(t *testing.T) { + // ffmpeg -re -i bbb.mp4 -c libx265 -preset superfast -tune zerolatency -an -f mpegts - + s := "00000001460150 0000000140010c01ffff01600000030090000003000003005dba0240 0000000142010101600000030090000003000003005da00280802d165bab930bc05a7080000003008000000c04 000000014401c1718312 0000014e0105ffffffffffffffffff2b2ca2de09b51747dbbb55a4fe7fc2fc4e7832363520286275696c642032303829202d20332e352b3131312d6330616631386464353a5b57696e646f77735d5b4743432031332e322e305d5b3634206269745d20386269742b31306269742b3132626974202d20482e3236352f4845564320636f646563202d20436f7079726967687420323031332d3230313820286329204d756c7469636f7265776172652c20496e63202d20687474703a2f2f783236352e6f7267202d206f7074696f6e733a2063707569643d31303439303731206672616d652d746872656164733d32206e756d612d706f6f6c733d3420777070206e6f2d706d6f6465206e6f2d706d65206e6f2d70736e72206e6f2d7373696d206c6f672d6c6576656c3d322062697464657074683d3820696e7075742d6373703d31206670733d32342f3120696e7075742d7265733d313238307837323020696e7465726c6163653d3020746f74616c2d6672616d65733d30206c6576656c2d6964633d3020686967682d746965723d31207568642d62643d30207265663d31206e6f2d616c6c6f772d6e6f6e2d636f6e666f726d616e6365207265706561742d6865616465727320616e6e657862206e6f2d617564206e6f2d656f62206e6f2d656f73206e6f2d68726420696e666f20686173683d302074656d706f72616c2d6c61796572733d30206f70656e2d676f70206d696e2d6b6579696e743d3234206b6579696e743d32353020676f702d6c6f6f6b61686561643d3020626672616d65733d3020622d61646170743d30206e6f2d622d707972616d696420626672616d652d626961733d302072632d6c6f6f6b61686561643d30206c6f6f6b61686561642d736c696365733d34207363656e656375743d30206e6f2d686973742d7363656e65637574207261646c3d30206e6f2d73706c696365206e6f2d696e7472612d72656672657368206374753d3332206d696e2d63752d73697a653d38206e6f2d72656374206e6f2d616d70206d61782d74752d73697a653d33322074752d696e7465722d64657074683d312074752d696e7472612d64657074683d31206c696d69742d74753d302072646f712d6c6576656c3d302064796e616d69632d72643d302e3030206e6f2d7373696d2d7264207369676e68696465206e6f2d74736b6970206e722d696e7472613d30206e722d696e7465723d30206e6f2d636f6e73747261696e65642d696e747261207374726f6e672d696e7472612d736d6f6f7468696e67206d61782d6d657267653d32206c696d69742d726566733d30206e6f2d6c696d69742d6d6f646573206d653d31207375626d653d31206d6572616e67653d35372074656d706f72616c2d6d7670206e6f2d6672616d652d647570206e6f2d686d65206e6f2d77656967687470206e6f2d77656967687462206e6f2d616e616c797a652d7372632d70696373206465626c6f636b3d303a30206e6f2d73616f206e6f2d73616f2d6e6f6e2d6465626c6f636b2072643d322073656c6563746976652d73616f3d30206561726c792d736b69702072736b697020666173742d696e747261206e6f2d74736b69702d66617374206e6f2d63752d6c6f73736c657373206e6f2d622d696e747261206e6f2d73706c697472642d736b697020726470656e616c74793d30207073792d72643d322e3030207073792d72646f713d302e3030206e6f2d72642d726566696e65206e6f2d6c6f73736c65737320636271706f6666733d3020637271706f6666733d302072633d637266206372663d32382e302071636f6d703d302e3630207170737465703d342073746174732d77726974653d302073746174732d726561643d30206970726174696f3d312e34302061712d6d6f64653d302061712d737472656e6774683d302e3030206e6f2d637574726565207a6f6e652d636f756e743d30206e6f2d7374726963742d6362722071672d73697a653d3332206e6f2d72632d677261696e2071706d61783d36392071706d696e3d30206e6f2d636f6e73742d766276207361723d31206f7665727363616e3d3020766964656f666f726d61743d352072616e67653d3020636f6c6f727072696d3d32207472616e736665723d3220636f6c6f726d61747269783d32206368726f6d616c6f633d31206368726f6d616c6f632d746f703d30206368726f6d616c6f632d626f74746f6d3d3020646973706c61792d77696e646f773d3020636c6c3d302c30206d696e2d6c756d613d30206d61782d6c756d613d323535206c6f67322d6d61782d706f632d6c73623d38207675692d74696d696e672d696e666f207675692d6872642d696e666f20736c696365733d31206e6f2d6f70742d71702d707073206e6f2d6f70742d7265662d6c6973742d6c656e6774682d707073206e6f2d6d756c74692d706173732d6f70742d727073207363656e656375742d626961733d302e3035206e6f2d6f70742d63752d64656c74612d7170206e6f2d61712d6d6f74696f6e206e6f2d6864723130206e6f2d68647231302d6f7074206e6f2d6468647231302d6f7074206e6f2d6964722d7265636f766572792d73656920616e616c797369732d72657573652d6c6576656c3d3020616e616c797369732d736176652d72657573652d6c6576656c3d3020616e616c797369732d6c6f61642d72657573652d6c6576656c3d30207363616c652d666163746f723d3020726566696e652d696e7472613d3020726566696e652d696e7465723d3020726566696e652d6d763d3120726566696e652d6374752d646973746f7274696f6e3d30206e6f2d6c696d69742d73616f206374752d696e666f3d30206e6f2d6c6f77706173732d64637420726566696e652d616e616c797369732d747970653d3020636f70792d7069633d31206d61782d617573697a652d666163746f723d312e30206e6f2d64796e616d69632d726566696e65206e6f2d73696e676c652d736569206e6f2d686576632d6171206e6f2d737674206e6f2d6669656c642071702d61646170746174696f6e2d72616e67653d312e3030207363656e656375742d61776172652d71703d30636f6e666f726d616e63652d77696e646f772d6f6666736574732072696768743d3020626f74746f6d3d30206465636f6465722d6d61782d726174653d30206e6f2d7662762d6c6976652d6d756c74692d70617373206e6f2d6d63737466206e6f2d7362726380 0000012801adc2e5bca307b9ce6b18b5ad6a525294a6d117ffd3917322eebaeda718a0000003000003000003021207706824da718a00000300000300000300044408d5db4e31400000030000030000030012500c2725a000000300000300000300002a600e4880000003000003000003000019301180000003000003000003000007d400000300000300000300000300010b000003000003000003000003001810000003000003000003000003019100000300000300000300000d38000003000003000003000067c000000300000300000300025e000003000003000003000c58000003000003000003002b60000003000003000003007f80000003000003000003016300000300000300000303b2000003000003000006e400000300000300000e18000003000003000018d00000030000030000292000000300000300003ce00000030000030000030000030000030000bb80" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x40, 0x42, 0x44, 0x4e, 0x28}, n) +} + +func TestReolink(t *testing.T) { + s := "000001460150 00000140010C01FFFF01600000030000030000030000030096AC09 0000000142010101600000030000030000030000030096A001E020021C7F8AAD3BA24BB804000013D800018CE008 000000014401C072F0941E3648 000000012601" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x40, 0x42, 0x44, 0x26}, n) +} + +func TestDahua(t *testing.T) { + s := "00000001460150 00000140010c01ffff01400000030000030000030000030099ac0900 0000000142010101400000030000030000030000030099a001402005a1fe5aee46c1ae550400 000000014401c073c04c9000 000000012601" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x40, 0x42, 0x44, 0x26}, n) +} diff --git a/pkg/homekit/consumer.go b/pkg/homekit/consumer.go index 1c665233..ea83146f 100644 --- a/pkg/homekit/consumer.go +++ b/pkg/homekit/consumer.go @@ -3,7 +3,7 @@ package homekit import ( "fmt" "io" - "math/rand/v2" + "math/rand" "net" "time" diff --git a/pkg/homekit/helpers.go b/pkg/homekit/helpers.go index 89c63dc3..a1719671 100644 --- a/pkg/homekit/helpers.go +++ b/pkg/homekit/helpers.go @@ -2,7 +2,6 @@ package homekit import ( "encoding/hex" - "slices" "github.com/AlexxIT/go2rtc/pkg/aac" "github.com/AlexxIT/go2rtc/pkg/core" @@ -22,8 +21,8 @@ func videoToMedia(codecs []camera.VideoCodec) *core.Media { for _, codec := range codecs { for _, param := range codec.CodecParams { // get best profile and level - profileID := slices.Max(param.ProfileID) - level := slices.Max(param.Level) + profileID := core.Max(param.ProfileID) + level := core.Max(param.Level) profile := videoProfiles[profileID] + videoLevels[level] mediaCodec := &core.Codec{ Name: videoCodecs[codec.CodecType], diff --git a/pkg/kasa/producer.go b/pkg/kasa/producer.go index 22d10216..697c19e8 100644 --- a/pkg/kasa/producer.go +++ b/pkg/kasa/producer.go @@ -113,7 +113,7 @@ func (c *Producer) Start() error { Header: rtp.Header{ Timestamp: uint32(ts * 90000), }, - Payload: annexb.EncodeToAVCC(body, false), + Payload: annexb.EncodeToAVCC(body), } video.WriteRTP(pkt) } @@ -168,7 +168,7 @@ func (c *Producer) probe() error { } waitVideo = false - body = annexb.EncodeToAVCC(body, false) + body = annexb.EncodeToAVCC(body) codec := h264.AVCCToCodec(body) media = &core.Media{ Kind: core.KindVideo, diff --git a/pkg/magic/bitstream/producer.go b/pkg/magic/bitstream/producer.go index b84f049b..5f00f41e 100644 --- a/pkg/magic/bitstream/producer.go +++ b/pkg/magic/bitstream/producer.go @@ -25,7 +25,7 @@ func Open(r io.Reader) (*Producer, error) { return nil, err } - buf = annexb.EncodeToAVCC(buf, false) // won't break original buffer + buf = annexb.EncodeToAVCC(buf) // won't break original buffer var codec *core.Codec var format string @@ -82,7 +82,7 @@ func (c *Producer) Start() error { if len(c.Receivers) > 0 { pkt := &rtp.Packet{ Header: rtp.Header{Timestamp: core.Now90000()}, - Payload: annexb.EncodeToAVCC(buf[:i], true), + Payload: annexb.EncodeToAVCC(buf[:i]), } c.Receivers[0].WriteRTP(pkt) diff --git a/pkg/mjpeg/mjpeg_test.go b/pkg/mjpeg/mjpeg_test.go new file mode 100644 index 00000000..586f8c80 --- /dev/null +++ b/pkg/mjpeg/mjpeg_test.go @@ -0,0 +1,13 @@ +package mjpeg + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRFC2435(t *testing.T) { + lqt, cqt := MakeTables(71) + require.Equal(t, byte(9), lqt[0]) + require.Equal(t, byte(10), cqt[0]) +} diff --git a/pkg/mjpeg/rfc2435.go b/pkg/mjpeg/rfc2435.go index f7e41330..44307896 100644 --- a/pkg/mjpeg/rfc2435.go +++ b/pkg/mjpeg/rfc2435.go @@ -2,21 +2,24 @@ package mjpeg // RFC 2435. Appendix A -var jpeg_luma_quantizer = []byte{ - 16, 11, 10, 16, 24, 40, 51, 61, - 12, 12, 14, 19, 26, 58, 60, 55, - 14, 13, 16, 24, 40, 57, 69, 56, - 14, 17, 22, 29, 51, 87, 80, 62, - 18, 22, 37, 56, 68, 109, 103, 77, - 24, 35, 55, 64, 81, 104, 113, 92, - 49, 64, 78, 87, 103, 121, 120, 101, - 72, 92, 95, 98, 112, 100, 103, 99, +// don't know why two tables are not respect RFC +// https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/rtpdec_jpeg.c + +var jpeg_luma_quantizer = [64]byte{ + 16, 11, 12, 14, 12, 10, 16, 14, + 13, 14, 18, 17, 16, 19, 24, 40, + 26, 24, 22, 22, 24, 49, 35, 37, + 29, 40, 58, 51, 61, 60, 57, 51, + 56, 55, 64, 72, 92, 78, 64, 68, + 87, 69, 55, 56, 80, 109, 81, 87, + 95, 98, 103, 104, 103, 62, 77, 113, + 121, 112, 100, 120, 92, 101, 103, 99, } -var jpeg_chroma_quantizer = []byte{ - 17, 18, 24, 47, 99, 99, 99, 99, - 18, 21, 26, 66, 99, 99, 99, 99, - 24, 26, 56, 99, 99, 99, 99, 99, - 47, 66, 99, 99, 99, 99, 99, 99, +var jpeg_chroma_quantizer = [64]byte{ + 17, 18, 18, 24, 21, 24, 47, 26, + 26, 47, 99, 66, 56, 66, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, @@ -37,7 +40,7 @@ func MakeTables(q byte) (lqt, cqt []byte) { if q < 50 { factor = 5000 / factor - } else if q > 99 { + } else { factor = 200 - factor*2 } @@ -140,22 +143,35 @@ var chm_ac_symbols = []byte{ func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte { // Appendix A from https://www.rfc-editor.org/rfc/rfc2435 - p = append(p, 0xFF, 0xD8) + p = append(p, 0xFF, + 0xD8, // SOI + ) p = MakeQuantHeader(p, lqt, 0) p = MakeQuantHeader(p, cqt, 1) if t == 0 { - t = 0x21 + t = 0x21 // hsamp = 2, vsamp = 1 } else { - t = 0x22 + t = 0x22 // hsamp = 2, vsamp = 2 } - p = append(p, - 0xFF, 0xC0, 0, 17, 8, + p = append(p, 0xFF, + 0xC0, // SOF + 0, 17, // size + 8, // bits per component byte(h>>8), byte(h&0xFF), byte(w>>8), byte(w&0xFF), - 3, 0, t, 0, 1, 0x11, 1, 2, 0x11, 1, + 3, // number of components + 0, // comp 0 + t, + 0, // quant table 0 + 1, // comp 1 + 0x11, // hsamp = 1, vsamp = 1 + 1, // quant table 1 + 2, // comp 2 + 0x11, // hsamp = 1, vsamp = 1 + 1, // quant table 1 ) p = MakeHuffmanHeader(p, lum_dc_codelens, lum_dc_symbols, 0, 0) @@ -163,7 +179,20 @@ func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte { p = MakeHuffmanHeader(p, chm_dc_codelens, chm_dc_symbols, 1, 0) p = MakeHuffmanHeader(p, chm_ac_codelens, chm_ac_symbols, 1, 1) - return append(p, 0xFF, 0xDA, 0, 12, 3, 0, 0, 1, 0x11, 2, 0x11, 0, 63, 0) + return append(p, 0xFF, + 0xDA, // SOS + 0, 12, // size + 3, // 3 components + 0, // comp 0 + 0, // huffman table 0 + 1, // comp 1 + 0x11, // huffman table 1 + 2, // comp 2 + 0x11, // huffman table 1 + 0, // first DCT coeff + 63, // last DCT coeff + 0, // sucessive approx + ) } func MakeQuantHeader(p []byte, qt []byte, tableNo byte) []byte { diff --git a/pkg/mpegts/demuxer.go b/pkg/mpegts/demuxer.go index a3efc2c9..08ccca39 100644 --- a/pkg/mpegts/demuxer.go +++ b/pkg/mpegts/demuxer.go @@ -364,7 +364,7 @@ func (p *PES) GetPacket() (pkt *rtp.Packet) { Header: rtp.Header{ PayloadType: p.StreamType, }, - Payload: annexb.EncodeToAVCC(p.Payload, false), + Payload: annexb.EncodeToAVCC(p.Payload), } if p.DTS != 0 { diff --git a/pkg/mpjpeg/multipart.go b/pkg/mpjpeg/multipart.go index abceea43..ca8924e5 100644 --- a/pkg/mpjpeg/multipart.go +++ b/pkg/mpjpeg/multipart.go @@ -18,15 +18,21 @@ func Next(rd *bufio.Reader) (http.Header, []byte, error) { return nil, nil, err } - if strings.HasPrefix(s, "--") { - break - } - if s == "\r\n" { continue } - return nil, nil, errors.New("multipart: wrong boundary: " + s) + if !strings.HasPrefix(s, "--") { + return nil, nil, errors.New("multipart: wrong boundary: " + s) + } + + // Foscam G2 has a awful implementation of MJPEG + // https://github.com/AlexxIT/go2rtc/issues/1258 + if b, _ := rd.Peek(2); string(b) == "--" { + continue + } + + break } tp := textproto.NewReader(rd) @@ -50,7 +56,5 @@ func Next(rd *bufio.Reader) (http.Header, []byte, error) { return nil, nil, err } - _, _ = rd.Discard(2) // skip "\r\n" - return http.Header(header), buf, nil } diff --git a/pkg/nest/api.go b/pkg/nest/api.go index 5e0d3407..9d187054 100644 --- a/pkg/nest/api.go +++ b/pkg/nest/api.go @@ -53,6 +53,8 @@ func NewAPI(clientID, clientSecret, refreshToken string) (*API, error) { if err != nil { return nil, err } + defer res.Body.Close() + if res.StatusCode != 200 { return nil, errors.New("nest: wrong status: " + res.Status) } @@ -92,6 +94,7 @@ func (a *API) GetDevices(projectID string) (map[string]string, error) { if err != nil { return nil, err } + defer res.Body.Close() if res.StatusCode != 200 { return nil, errors.New("nest: wrong status: " + res.Status) @@ -157,6 +160,7 @@ func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) { if err != nil { return "", err } + defer res.Body.Close() if res.StatusCode != 200 { return "", errors.New("nest: wrong status: " + res.Status) @@ -211,6 +215,7 @@ func (a *API) ExtendStream() error { if err != nil { return err } + defer res.Body.Close() if res.StatusCode != 200 { return errors.New("nest: wrong status: " + res.Status) diff --git a/pkg/pcm/producer.go b/pkg/pcm/producer.go new file mode 100644 index 00000000..8a957f6d --- /dev/null +++ b/pkg/pcm/producer.go @@ -0,0 +1,55 @@ +package pcm + +import ( + "io" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +type Producer struct { + core.Connection + rd io.Reader +} + +func Open(rd io.Reader) (*Producer, error) { + medias := []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + {Name: core.CodecPCMU, ClockRate: 8000}, + }, + }, + } + return &Producer{ + core.Connection{ + ID: core.NewID(), + FormatName: "pcm", + Medias: medias, + Transport: rd, + }, + rd, + }, nil +} + +func (c *Producer) Start() error { + for { + payload := make([]byte, 1024) + if _, err := io.ReadFull(c.rd, payload); err != nil { + return err + } + + c.Recv += 1024 + + if len(c.Receivers) == 0 { + continue + } + + pkt := &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: payload, + } + c.Receivers[0].WriteRTP(pkt) + } +} diff --git a/pkg/rtmp/client.go b/pkg/rtmp/client.go index 138d727d..c9e9ad17 100644 --- a/pkg/rtmp/client.go +++ b/pkg/rtmp/client.go @@ -35,7 +35,7 @@ func DialPlay(rawURL string) (*flv.Producer, error) { return client.Producer() } -func DialPublish(rawURL string) (io.Writer, error) { +func DialPublish(rawURL string, cons *flv.Consumer) (io.Writer, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err @@ -55,6 +55,11 @@ func DialPublish(rawURL string) (io.Writer, error) { return nil, err } + cons.FormatName = "rtmp" + cons.Protocol = "rtmp" + cons.RemoteAddr = conn.RemoteAddr().String() + cons.URL = rawURL + return client, nil } diff --git a/pkg/rtsp/consumer.go b/pkg/rtsp/consumer.go index b6df188f..860ed113 100644 --- a/pkg/rtsp/consumer.go +++ b/pkg/rtsp/consumer.go @@ -162,6 +162,8 @@ func (c *Conn) packetWriter(codec *core.Codec, channel, payloadType uint8) core. case core.CodecJPEG: handlerFunc = mjpeg.RTPPay(handlerFunc) } + } else if codec.Name == core.CodecPCML { + handlerFunc = pcm.LittleToBig(handlerFunc) } else if c.PacketSize != 0 { switch codec.Name { case core.CodecH264: diff --git a/pkg/rtsp/helpers.go b/pkg/rtsp/helpers.go index c0f02f5b..6b07342d 100644 --- a/pkg/rtsp/helpers.go +++ b/pkg/rtsp/helpers.go @@ -28,8 +28,10 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) { sd := &sdp.SessionDescription{} if err := sd.Unmarshal(rawSDP); err != nil { // fix multiple `s=` https://github.com/AlexxIT/WebRTC/issues/417 - re, _ := regexp.Compile("\ns=[^\n]+") - rawSDP = re.ReplaceAll(rawSDP, nil) + rawSDP = regexp.MustCompile("\ns=[^\n]+").ReplaceAll(rawSDP, nil) + + // fix broken `c=` https://github.com/AlexxIT/go2rtc/issues/1426 + rawSDP = regexp.MustCompile("\nc=[^\n]+").ReplaceAll(rawSDP, nil) // fix SDP header for some cameras if i := bytes.Index(rawSDP, []byte("\nm=")); i > 0 { @@ -38,8 +40,13 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) { // Fix invalid media type (errSDPInvalidValue) caused by // some TP-LINK IP camera, e.g. TL-IPC44GW - m := regexp.MustCompile("m=application/[^ ]+") - rawSDP = m.ReplaceAll(rawSDP, []byte("m=application")) + for _, b := range regexp.MustCompile("m=[^ ]+ ").FindAll(rawSDP, -1) { + switch string(b[2 : len(b)-1]) { + case "audio", "video", "application": + default: + rawSDP = bytes.Replace(rawSDP, b, []byte("m=application "), 1) + } + } if err == io.EOF { rawSDP = append(rawSDP, '\n') diff --git a/pkg/rtsp/rtsp_test.go b/pkg/rtsp/rtsp_test.go index 7eb317a7..14c99803 100644 --- a/pkg/rtsp/rtsp_test.go +++ b/pkg/rtsp/rtsp_test.go @@ -3,6 +3,7 @@ package rtsp import ( "testing" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/stretchr/testify/assert" ) @@ -159,3 +160,110 @@ a=control:trackID=2 assert.Equal(t, "recvonly", medias[0].Direction) assert.Equal(t, "recvonly", medias[1].Direction) } + +func TestBugSDP6(t *testing.T) { + // https://github.com/AlexxIT/go2rtc/issues/1278 + s := `v=0 +o=- 3730506281693 1 IN IP4 172.20.0.215 +s=IP camera Live streaming +i=stream1 +t=0 0 +a=tool:LIVE555 Streaming Media v2014.02.04 +a=type:broadcast +a=control:* +a=range:npt=0- +a=x-qt-text-nam:IP camera Live streaming +a=x-qt-text-inf:stream1 +m=video 0 RTP/AVP 26 +c=IN IP4 172.20.0.215 +b=AS:1500 +a=x-bufferdelay:0.55000 +a=x-dimensions:1280,960 +a=control:track1 +m=audio 0 RTP/AVP 0 +c=IN IP4 172.20.0.215 +b=AS:64 +a=x-bufferdelay:0.55000 +a=control:track2 +m=application 0 RTP/AVP 107 +c=IN IP4 172.20.0.215 +b=AS:1 +a=x-bufferdelay:0.55000 +a=rtpmap:107 vnd.onvif.metadata/90000/500 +a=control:track4 +m=vana 0 RTP/AVP 108 +c=IN IP4 172.20.0.215 +b=AS:1 +a=x-bufferdelay:0.55000 +a=rtpmap:108 video.analysis/90000/500 +a=control:track5 +` + medias, err := UnmarshalSDP([]byte(s)) + assert.Nil(t, err) + assert.Len(t, medias, 4) +} + +func TestBugSDP7(t *testing.T) { + // https://github.com/AlexxIT/go2rtc/issues/1426 + s := `v=0 +o=- 1001 1 IN +s=VCP IPC Realtime stream +m=video 0 RTP/AVP 105 +c=IN +a=control:rtsp://1.0.1.113/media/video2/video +a=rtpmap:105 H264/90000 +a=fmtp:105 profile-level-id=640016; packetization-mode=1; sprop-parameter-sets=Z2QAFqw7UFAX/LCAAAH0AABOIEI=,aOqPLA== +a=recvonly +m=audio 0 RTP/AVP 0 +c=IN +a=fmtp:0 RTCP=0 +a=control:rtsp://1.0.1.113/media/video2/audio1 +a=recvonly +m=audio 0 RTP/AVP 0 +c=IN +a=control:rtsp://1.0.1.113/media/video2/backchannel +a=rtpmap:0 PCMA/8000 +a=rtpmap:0 PCMU/8000 +a=sendonly +m=application 0 RTP/AVP 107 +c=IN +a=control:rtsp://1.0.1.113/media/video2/metadata +a=rtpmap:107 vnd.onvif.metadata/90000 +a=fmtp:107 DecoderTag=h3c-v3 RTCP=0 +a=recvonly +` + medias, err := UnmarshalSDP([]byte(s)) + assert.Nil(t, err) + assert.Len(t, medias, 4) +} + +func TestHikvisionPCM(t *testing.T) { + s := `v=0 +o=- 1721969533379665 1721969533379665 IN IP4 192.168.1.12 +s=Media Presentation +e=NONE +b=AS:5100 +t=0 0 +a=control:rtsp://192.168.1.12:554/Streaming/channels/101/ +m=video 0 RTP/AVP 96 +c=IN IP4 0.0.0.0 +b=AS:5000 +a=recvonly +a=x-dimensions:3200,1800 +a=control:rtsp://192.168.1.12:554/Streaming/channels/101/trackID=1 +a=rtpmap:96 H264/90000 +a=fmtp:96 profile-level-id=420029; packetization-mode=1; sprop-parameter-sets=Z2QAM6wVFKAyAOP5f/AAEAAWyAAAH0AAB1MAIA==,aO48sA== +m=audio 0 RTP/AVP 11 +c=IN IP4 0.0.0.0 +b=AS:50 +a=recvonly +a=control:rtsp://192.168.1.12:554/Streaming/channels/101/trackID=2 +a=rtpmap:11 PCM/48000 +a=Media_header:MEDIAINFO=494D4B4801030000040000010170011080BB0000007D000000000000000000000000000000000000; +a=appversion:1.0 +` + medias, err := UnmarshalSDP([]byte(s)) + assert.Nil(t, err) + assert.Len(t, medias, 2) + assert.Equal(t, core.CodecPCML, medias[1].Codecs[0].Name) +} diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go index d538b961..75df671f 100644 --- a/pkg/shell/shell.go +++ b/pkg/shell/shell.go @@ -3,6 +3,7 @@ package shell import ( "os" "os/signal" + "path/filepath" "regexp" "strings" "syscall" @@ -51,6 +52,13 @@ func ReplaceEnvVars(text string) string { dok = true } + if dir, vok := os.LookupEnv("CREDENTIALS_DIRECTORY"); vok { + value, err := os.ReadFile(filepath.Join(dir, key)) + if err == nil { + return strings.TrimSpace(string(value)) + } + } + if value, vok := os.LookupEnv(key); vok { return value } diff --git a/pkg/webrtc/api.go b/pkg/webrtc/api.go index f63cabfd..0361e6b4 100644 --- a/pkg/webrtc/api.go +++ b/pkg/webrtc/api.go @@ -2,8 +2,8 @@ package webrtc import ( "net" - "slices" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/interceptor" "github.com/pion/webrtc/v3" ) @@ -47,7 +47,7 @@ func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error if filters != nil && filters.Interfaces != nil { s.SetIncludeLoopbackCandidate(true) s.SetInterfaceFilter(func(name string) bool { - return slices.Contains(filters.Interfaces, name) + return core.Contains(filters.Interfaces, name) }) } else { // disable listen on Hassio docker interfaces @@ -59,7 +59,7 @@ func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error if filters != nil && filters.IPs != nil { s.SetIncludeLoopbackCandidate(true) s.SetIPFilter(func(ip net.IP) bool { - return slices.Contains(filters.IPs, ip.String()) + return core.Contains(filters.IPs, ip.String()) }) } diff --git a/pkg/webrtc/conn.go b/pkg/webrtc/conn.go index 3e3ecc4f..5bc16ede 100644 --- a/pkg/webrtc/conn.go +++ b/pkg/webrtc/conn.go @@ -29,6 +29,7 @@ func NewConn(pc *webrtc.PeerConnection) *Conn { Connection: core.Connection{ ID: core.NewID(), FormatName: "webrtc", + Transport: pc, }, pc: pc, } diff --git a/pkg/webrtc/consumer.go b/pkg/webrtc/consumer.go index 2dcab436..fb90442c 100644 --- a/pkg/webrtc/consumer.go +++ b/pkg/webrtc/consumer.go @@ -64,6 +64,10 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv } case core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecPCML: + // Fix audio quality https://github.com/AlexxIT/WebRTC/issues/500 + // should be before ResampleToG711, because it will be called last + sender.Handler = pcm.RepackG711(false, sender.Handler) + if codec.ClockRate == 0 { if codec.Name == core.CodecPCM || codec.Name == core.CodecPCML { codec.Name = core.CodecPCMA @@ -71,9 +75,6 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv codec.ClockRate = 8000 sender.Handler = pcm.ResampleToG711(track.Codec, 8000, sender.Handler) } - - // Fix audio quality https://github.com/AlexxIT/WebRTC/issues/500 - sender.Handler = pcm.RepackG711(false, sender.Handler) } // TODO: rewrite this dirty logic diff --git a/scripts/README.md b/scripts/README.md index efcef154..36f667b2 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,3 +1,11 @@ +## Versions + +[Go 1.20](https://go.dev/doc/go1.20) is last version with support Windows 7 and macOS 10.13. +Go 1.21 support only Windows 10 and macOS 10.15. + +So we will set `go 1.20` (minimum version) inside `go.mod` file. And will use env `GOTOOLCHAIN=go1.20.14` for building +`win32` and `mac_amd64` binaries. All other binaries will use latest go version. + ## Build - UPX-3.96 pack broken bin for `linux_mipsel` @@ -32,6 +40,7 @@ go list -deps .\cmd\go2rtc_rtsp\ - github.com/sigurn/crc8 - github.com/pion/ice/v2 - github.com/google/uuid + - github.com/wlynxg/anet - github.com/rs/zerolog - github.com/mattn/go-colorable - github.com/mattn/go-isatty diff --git a/scripts/build.cmd b/scripts/build.cmd index 8b355f76..85dd9531 100644 --- a/scripts/build.cmd +++ b/scripts/build.cmd @@ -1,15 +1,18 @@ @ECHO OFF +@SET GOTOOLCHAIN= @SET GOOS=windows @SET GOARCH=amd64 @SET FILENAME=go2rtc_win64.zip go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc.exe +@SET GOTOOLCHAIN=go1.20.14 @SET GOOS=windows @SET GOARCH=386 @SET FILENAME=go2rtc_win32.zip go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc.exe +@SET GOTOOLCHAIN= @SET GOOS=windows @SET GOARCH=arm64 @SET FILENAME=go2rtc_win_arm64.zip @@ -47,11 +50,13 @@ go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME% @SET FILENAME=go2rtc_linux_mipsel go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME% +@SET GOTOOLCHAIN=go1.20.14 @SET GOOS=darwin @SET GOARCH=amd64 @SET FILENAME=go2rtc_mac_amd64.zip go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc +@SET GOTOOLCHAIN= @SET GOOS=darwin @SET GOARCH=arm64 @SET FILENAME=go2rtc_mac_arm64.zip