Compare commits

..

65 Commits

Author SHA1 Message Date
Alex X 199fdd6728 Update version to 1.9.8 2025-01-03 16:24:31 +03:00
Alex X 4035e91672 Fix ONVIF XML tag parsing in some cases 2025-01-03 15:08:38 +03:00
Alex X bc9194d740 Update go dependencies 2025-01-03 13:57:15 +03:00
Alex X f601c47218 Improve ONVIF server 2025-01-03 13:19:40 +03:00
Alex X 2c3219ffcb Merge pull request #1520 from acortelyou/feat/unifi
Extend onvif server to support Unifi Protect
2024-12-30 20:33:33 +03:00
Alex Cortelyou cf88bf9c23 Remove inaccurate comments 2024-12-29 16:22:49 -08:00
Alex Cortelyou b8303b9a22 Remove optional fields, normalize indentation 2024-12-29 16:16:49 -08:00
Alex X a3f084dcde RTMP server enhancement to support OpenIPC cameras 2024-12-29 22:37:04 +03:00
Alex X 0d6b8fc6fc Fix OPUS/48000/1 for RTSP from some cameras #1506 2024-12-29 11:44:56 +03:00
Alex Cortelyou 159d9425a7 Remove non-essential fields 2024-12-24 11:08:18 -08:00
Alex Cortelyou 3a50b3678d Extend onvif server to support Unifi Protect 2024-12-23 23:43:39 -08:00
Alex X 8ecaabfce9 Add support VIGI cameras #1470 2024-12-16 20:25:01 +03:00
Alex X f1ba5e95ec Fix parsing RTSP Transport header #1235 2024-12-06 12:34:31 +03:00
Alex X d8c0f9d1d9 Update support doorbird source #1060 2024-12-05 10:55:14 +03:00
Alex X d7cdc8b3b0 Merge pull request #1477 from oeiber/patch-1
Removing additional '&' in rawURL
2024-11-24 19:00:38 +03:00
oeiber 5b53ca7cf1 Removing double additional '&' in rawURL 2024-11-24 16:19:58 +01:00
Alex X 194d1dae51 Add support doorbird source #1060 2024-11-24 13:09:13 +03:00
Alex X 25145f72e5 Fix broken incoming sources after v1.9.7 #1458 2024-11-14 19:39:26 +03:00
Alex X dbe9e4aade Update version to 1.9.7 2024-11-11 20:20:53 +03:00
Alex X 715be4dad0 Merge pull request #1450 from edenhaus/ffmpeg-codec-not-matched-error
Lower codec not matched error for ffmpeg to debug
2024-11-11 18:05:52 +03:00
Alex X 570b7d0d97 Code refactoring for #1450 2024-11-11 17:49:22 +03:00
Alex X 80ac0ab17f Merge pull request #1448 from edenhaus/imporve-codec-not-matched-error
Improve codec not matched error by including kind
2024-11-11 16:37:25 +03:00
Alex X 9ee8174d5f Code refactoring for #1448 2024-11-11 16:36:51 +03:00
Robert Resch 831aa03c9f Implement suggestion 2024-11-11 11:16:12 +01:00
Robert Resch d372597bdb Lower codec not matched error for ffmpeg to debug 2024-11-11 09:27:21 +01:00
Alex X 172437b6fc Merge pull request #1449 from amarshall/credentials-dir
Read from credential files
2024-11-11 07:11:29 +03:00
Andrew Marshall 7640a42bfc Read from credential files
See https://systemd.io/CREDENTIALS/. This will also work for Docker
Secrets by setting `CREDENTIALS_DIRECTORY=/run/secrets`.
2024-11-10 17:33:22 -05:00
Robert Resch fde04bd625 Improve codec not matched error by including kind 2024-11-10 19:27:59 +01:00
Alex X ad14a5ccba Merge pull request #1447 from Jerome1998/patch-1
Updated Roborock part in the README.md file
2024-11-10 16:44:16 +03:00
Jerome 2348d12e9d Update README.md 2024-11-10 13:13:31 +01:00
Alex X 5cafc05e13 Merge pull request #1446 from eltociear/patch-1
docs: update README.md
2024-11-10 07:08:37 +03:00
Ikko Eltociear Ashimine e982257271 docs: update README.md
shapshot -> snapshot
2024-11-10 09:00:37 +09:00
Alex X 340fd81778 Fix loop request, ex. camera1: ffmpeg:camera1 2024-11-09 18:17:41 +03:00
Alex X 2c34a17d88 Fix stop for webrtc stream #1428 2024-11-02 20:50:33 +03:00
Alex X 6b005a666e Fix yet another broken SDP from CN cameras #1426 2024-11-01 12:09:44 +03:00
Alex X 1d1bcb0a63 Code refactoring for UnmarshalSDP 2024-11-01 12:08:06 +03:00
Alex X 3f5f1328e7 Fix webrtc:ws source after 1.9.5 #1425 2024-10-31 20:09:11 +03:00
Alex X 8cca8decde Update version to 1.9.6 2024-10-29 17:50:00 +03:00
Alex X be5bbd3b9b Fix FFmpeg tests 2024-10-29 14:39:54 +03:00
Alex X 3f94a754e4 Fix WebRTC card stuck in loading #1417 2024-10-29 14:39:37 +03:00
Alex X 780f378fb1 Update version to 1.9.5 2024-10-28 22:47:55 +03:00
Alex X b874c17bcb Update dependencies 2024-10-28 22:47:36 +03:00
Alex X 16e4831499 Add the option to pass ICE servers with an async WebRTC offer #1408 2024-10-24 23:31:21 +03:00
Alex X 9d709f0db8 Merge pull request #1407 from edenhaus/streams-api-multiple-sources
Extend streams API to allow multiple sources
2024-10-24 20:47:19 +03:00
Alex X a8d394efd7 Update PUT /api/streams for support multiple src params 2024-10-24 20:46:31 +03:00
Robert Resch 95a5283c86 Extend streams API to allow multiple sources 2024-10-22 16:31:31 +02:00
Alex X ef7d898747 Merge pull request #1355 from michelepra/concurrent_map_fix
data race for streams map
2024-09-27 21:11:18 +03:00
Michele Prà 388c408080 defer used wisely 2024-09-27 18:14:41 +02:00
Alex X 7b77e41253 Add support arm/v6 to Dockerfile 2024-09-22 07:24:25 +03:00
Alex X c0bfebf3a4 Merge pull request #1362 from edenhaus/armhf
Build the docker image for linux/arm/v6
2024-09-20 13:50:02 +03:00
Robert Resch 6f9f1c3a35 Build the docker image for linux/arm/v6 2024-09-19 16:48:37 +02:00
Michele Prà 8128edad43 Update streams.go 2024-09-16 16:42:22 +02:00
Michele Prà eb8a13d8c2 data race for streams map
https://go.dev/doc/articles/race_detector
2024-09-16 12:42:34 +02:00
Alex X 8399edce6a Fix RTSP AAC audio from very buggy noname camera #1328 2024-09-05 11:58:05 +03:00
Alex X 2311d5eabe Change go version to 1.20 for Windows 7 support 2024-09-01 17:54:01 +03:00
Alex X afc8f4fdf6 Merge pull request #1297 from cthach/fix-nest-extend-stream
fix(nest): Resource leak due to lack of closing HTTP response bodies
2024-08-07 16:38:38 +03:00
Chris Thach 66de2f91b6 Fix resource leak in Nest source due to lack of closing HTTP response bodies 2024-08-06 22:25:55 +00:00
Alex X bd88695e59 Fix AnnexB parsing in some cases 2024-08-04 10:18:24 +03:00
Alex X d559ec0208 Fix wrong media values in SDP for some cameras #1278 2024-07-26 17:00:16 +03:00
Alex X ed99025bd6 Add support S16LE (PCM-LE) for RTSP server 2024-07-26 14:47:42 +03:00
Alex X 57d48f53e0 Fix PCM audio quality for WebRTC 2024-07-26 14:15:53 +03:00
Alex X 68fa42249e Fix PCM audio from Hikvision cameras 2024-07-26 14:01:43 +03:00
Alex X c5bc761a52 Fix RTSP MJPEG source quality in some cases #559 2024-07-26 07:55:15 +03:00
Alex X 3762bdbccd Fix mjpeg source for Foscam G2 camera #1258 2024-07-18 13:52:50 +03:00
Alex X eaae7aee39 Fix stream info for publishing RTMP 2024-06-19 06:53:27 +03:00
70 changed files with 1712 additions and 681 deletions
+3 -2
View File
@@ -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' }}
+13 -14
View File
@@ -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
+8 -7
View File
@@ -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
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
@@ -170,7 +170,7 @@ Available modules:
- [api](#module-api) - HTTP API (important for WebRTC support)
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
- [webrtc](#module-webrtc) - WebRTC Server
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot Server
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 snapshot Server
- [hls](#module-hls) - HLS TS or fMP4 stream Server
- [mjpeg](#module-mjpeg) - MJPEG Server
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
@@ -648,10 +648,11 @@ This source type support Roborock vacuums with cameras. Known working models:
- Roborock S6 MaxV - only video (the vacuum has no microphone)
- Roborock S7 MaxV - video and two way audio
- Roborock Qrevo MaxV - video and two way audio
Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config.
Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock) or the [core integration](https://www.home-assistant.io/integrations/roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config.
If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 678) to the end of the roborock-link.
If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 789) to the end of the roborock-link.
#### Source: WebRTC
+72
View File
@@ -0,0 +1,72 @@
package main
import (
"log"
"net"
"net/url"
"os"
"github.com/AlexxIT/go2rtc/pkg/onvif"
)
func main() {
var rawURL = os.Args[1]
var operation = os.Args[2]
var token string
if len(os.Args) > 3 {
token = os.Args[3]
}
client, err := onvif.NewClient(rawURL)
if err != nil {
log.Panic(err)
}
var b []byte
switch operation {
case onvif.ServiceGetServiceCapabilities:
b, err = client.MediaRequest(operation)
case onvif.DeviceGetCapabilities,
onvif.DeviceGetDeviceInformation,
onvif.DeviceGetDiscoveryMode,
onvif.DeviceGetDNS,
onvif.DeviceGetHostname,
onvif.DeviceGetNetworkDefaultGateway,
onvif.DeviceGetNetworkInterfaces,
onvif.DeviceGetNetworkProtocols,
onvif.DeviceGetNTP,
onvif.DeviceGetScopes,
onvif.DeviceGetServices,
onvif.DeviceGetSystemDateAndTime,
onvif.DeviceSystemReboot:
b, err = client.DeviceRequest(operation)
case onvif.MediaGetProfiles, onvif.MediaGetVideoSources:
b, err = client.MediaRequest(operation)
case onvif.MediaGetProfile:
b, err = client.GetProfile(token)
case onvif.MediaGetVideoSourceConfiguration:
b, err = client.GetVideoSourceConfiguration(token)
case onvif.MediaGetStreamUri:
b, err = client.GetStreamUri(token)
case onvif.MediaGetSnapshotUri:
b, err = client.GetSnapshotUri(token)
default:
log.Printf("unknown action\n")
}
if err != nil {
log.Printf("%s\n", err)
}
u, err := url.Parse(rawURL)
if err != nil {
log.Fatal(err)
}
host, _, _ := net.SplitHostPort(u.Host)
if err = os.WriteFile(host+"_"+operation+".xml", b, 0644); err != nil {
log.Printf("%s\n", err)
}
}
+24 -22
View File
@@ -1,48 +1,50 @@
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/pion/rtcp v1.2.14
github.com/pion/rtp v1.8.6
github.com/miekg/dns v1.1.62
github.com/pion/ice/v2 v2.3.37
github.com/pion/interceptor v0.1.37
github.com/pion/rtcp v1.2.15
github.com/pion/rtp v1.8.10
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.5
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/stretchr/testify v1.10.0
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
golang.org/x/crypto v0.24.0
golang.org/x/crypto v0.31.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.10 // 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.35 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v2 v2.1.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/tools v0.22.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/tools v0.24.0 // indirect
)
+58 -82
View File
@@ -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.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
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.37 h1:ObIdaNDu1rCo7hObhs34YSBcO7fjslJMZV0ux+uZWh0=
github.com/pion/ice/v2 v2.3.37/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=
@@ -50,43 +47,40 @@ github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYF
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
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.10 h1:puphjdbjPB+L+NFaVuZ5h6bt1g5q4kFIoI+r5q/g0CU=
github.com/pion/rtp v1.8.10/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
github.com/pion/sctp v1.8.35 h1:qwtKvNK1Wc5tHMIYgTDJhfZk7vATGVHhXbUDfHbYwzA=
github.com/pion/sctp v1.8.35/go.mod h1:EcXP8zCYVTRy3W9xtOF7wJm1L1aXfKRQzaM33SjQlzg=
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/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
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.5 h1:ZsSzaMz/i9nblPdiAkZoP+E6Kmjw+jnyq3bEmU3EtRg=
github.com/pion/webrtc/v3 v3.3.5/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=
@@ -102,53 +96,42 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
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/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
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.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
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=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -160,44 +143,37 @@ 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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.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/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+8 -8
View File
@@ -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
+3 -3
View File
@@ -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
+36
View File
@@ -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)
})
}
+1 -2
View File
@@ -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)
+6 -2
View File
@@ -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] {
+132 -74
View File
@@ -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) {
+4
View File
@@ -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)
+25
View File
@@ -0,0 +1,25 @@
# ONVIF
A regular camera has a single video source (`GetVideoSources`) and two profiles (`GetProfiles`).
Go2rtc has one video source and one profile per stream.
## Tested clients
Go2rtc works as ONVIF server:
- Happytime onvif client (windows)
- Home Assistant ONVIF integration (linux)
- Onvier (android)
- ONVIF Device Manager (windows)
PS. Support only TCP transport for RTSP protocol. UDP and HTTP transports - unsupported yet.
## Tested cameras
Go2rtc works as ONVIF client:
- Dahua IPC-K42
- OpenIPC
- Reolink RLC-520A
- TP-Link Tapo TC60
@@ -55,49 +55,65 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
return
}
action := onvif.GetRequestAction(b)
if action == "" {
operation := onvif.GetRequestAction(b)
if operation == "" {
http.Error(w, "malformed request body", http.StatusBadRequest)
return
}
log.Trace().Msgf("[onvif] %s", action)
log.Trace().Msgf("[onvif] server request %s %s:\n%s", r.Method, r.RequestURI, b)
var res string
switch operation {
case onvif.DeviceGetNetworkInterfaces, // important for Hass
onvif.DeviceGetSystemDateAndTime, // important for Hass
onvif.DeviceGetDiscoveryMode,
onvif.DeviceGetDNS,
onvif.DeviceGetHostname,
onvif.DeviceGetNetworkDefaultGateway,
onvif.DeviceGetNetworkProtocols,
onvif.DeviceGetNTP,
onvif.DeviceGetScopes:
b = onvif.StaticResponse(operation)
switch action {
case onvif.ActionGetCapabilities:
case onvif.DeviceGetCapabilities:
// important for Hass: Media section
res = onvif.GetCapabilitiesResponse(r.Host)
b = onvif.GetCapabilitiesResponse(r.Host)
case onvif.ActionGetSystemDateAndTime:
// important for Hass
res = onvif.GetSystemDateAndTimeResponse()
case onvif.DeviceGetServices:
b = onvif.GetServicesResponse(r.Host)
case onvif.ActionGetNetworkInterfaces:
// important for Hass: none
res = onvif.GetNetworkInterfacesResponse()
case onvif.ActionGetDeviceInformation:
case onvif.DeviceGetDeviceInformation:
// important for Hass: SerialNumber (unique server ID)
res = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
b = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
case onvif.ActionGetServiceCapabilities:
case onvif.ServiceGetServiceCapabilities:
// important for Hass
res = onvif.GetServiceCapabilitiesResponse()
// TODO: check path links to media
b = onvif.GetMediaServiceCapabilitiesResponse()
case onvif.ActionSystemReboot:
res = onvif.SystemRebootResponse()
case onvif.DeviceSystemReboot:
b = onvif.StaticResponse(operation)
time.AfterFunc(time.Second, func() {
os.Exit(0)
})
case onvif.ActionGetProfiles:
// important for Hass: H264 codec, width, height
res = onvif.GetProfilesResponse(streams.GetAll())
case onvif.MediaGetVideoSources:
b = onvif.GetVideoSourcesResponse(streams.GetAll())
case onvif.ActionGetStreamUri:
case onvif.MediaGetProfiles:
// important for Hass: H264 codec, width, height
b = onvif.GetProfilesResponse(streams.GetAll())
case onvif.MediaGetProfile:
token := onvif.FindTagValue(b, "ProfileToken")
b = onvif.GetProfileResponse(token)
case onvif.MediaGetVideoSourceConfiguration:
token := onvif.FindTagValue(b, "ConfigurationToken")
b = onvif.GetVideoSourceConfigurationResponse(token)
case onvif.MediaGetStreamUri:
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -105,16 +121,22 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
}
uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken")
res = onvif.GetStreamUriResponse(uri)
b = onvif.GetStreamUriResponse(uri)
case onvif.MediaGetSnapshotUri:
uri := "http://" + r.Host + "/api/frame.jpeg?src=" + onvif.FindTagValue(b, "ProfileToken")
b = onvif.GetSnapshotUriResponse(uri)
default:
http.Error(w, "unsupported action", http.StatusBadRequest)
http.Error(w, "unsupported operation", http.StatusBadRequest)
log.Debug().Msgf("[onvif] unsupported request:\n%s", b)
return
}
log.Trace().Msgf("[onvif] server response:\n%s", b)
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
if _, err = w.Write([]byte(res)); err != nil {
if _, err = w.Write(b); err != nil {
log.Error().Err(err).Caller().Send()
}
}
@@ -160,7 +182,7 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
}
if l := log.Trace(); l.Enabled() {
b, _ := client.GetProfiles()
b, _ := client.MediaRequest(onvif.MediaGetProfiles)
l.Msgf("[onvif] src=%s profiles:\n%s", src, b)
}
+1 -1
View File
@@ -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
}
+13 -2
View File
@@ -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()
+8 -2
View File
@@ -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())
}
}
}
+2 -2
View File
@@ -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)
}
+1 -1
View File
@@ -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()
+7 -1
View File
@@ -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()
+16 -5
View File
@@ -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 {
+4
View File
@@ -15,4 +15,8 @@ func Init() {
streams.HandleFunc("tapo", func(source string) (core.Producer, error) {
return tapo.Dial(source)
})
streams.HandleFunc("vigi", func(source string) (core.Producer, error) {
return tapo.Dial(source)
})
}
+3 -3
View File
@@ -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
}
}
+32 -16
View File
@@ -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
}
+38
View File
@@ -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])
}
+3 -1
View File
@@ -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.8"
// 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
-1
View File
@@ -9,7 +9,6 @@ import (
)
func IsADTS(b []byte) bool {
_ = b[1]
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF6 == 0xF0
}
+9
View File
@@ -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
}
+33
View File
@@ -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)
}
+1 -1
View File
@@ -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 {
+6 -1
View File
@@ -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":
+5
View File
@@ -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
+6 -2
View File
@@ -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{
+43
View File
@@ -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
}
+93
View File
@@ -0,0 +1,93 @@
package doorbird
import (
"fmt"
"net"
"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()
if u.Port() == "" {
u.Host += ":80"
}
conn, err := net.DialTimeout("tcp", u.Host, core.ConnDialTimeout)
if err != nil {
return nil, err
}
s := fmt.Sprintf("POST /bha-api/audio-transmit.cgi?http-user=%s&http-password=%s HTTP/1.0\r\n", user, pass) +
"Content-Type: audio/basic\r\n" +
"Content-Length: 9999999\r\n" +
"Connection: Keep-Alive\r\n" +
"Cache-Control: no-cache\r\n" +
"\r\n"
_ = conn.SetWriteDeadline(time.Now().Add(core.ConnDeadline))
if _, err = conn.Write([]byte(s)); 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
}
+2 -2
View File
@@ -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
+27 -15
View File
@@ -140,23 +140,29 @@ func (c *Producer) probe() error {
// 1. Empty video/audio flag
// 2. MedaData without stereo key for AAC
// 3. Audio header after Video keyframe tag
waitType := []byte{TagData}
timeout := time.Now().Add(core.ProbeTimeout)
for len(waitType) != 0 && time.Now().Before(timeout) {
// OpenIPC camera sends:
// 1. Empty video/audio flag
// 2. No MetaData packet
// 3. Sends a video packet in more than 3 seconds
waitVideo := true
waitAudio := true
timeout := time.Now().Add(time.Second * 5)
for (waitVideo || waitAudio) && time.Now().Before(timeout) {
pkt, err := c.readPacket()
if err != nil {
return err
}
if i := bytes.IndexByte(waitType, pkt.PayloadType); i < 0 {
continue
} else {
waitType = append(waitType[:i], waitType[i+1:]...)
}
//log.Printf("%d %0.20s", pkt.PayloadType, pkt.Payload)
switch pkt.PayloadType {
case TagAudio:
if !waitAudio {
continue
}
_ = pkt.Payload[1] // bounds
codecID := pkt.Payload[0] >> 4 // SoundFormat
@@ -179,8 +185,13 @@ func (c *Producer) probe() error {
Codecs: []*core.Codec{codec},
}
c.Medias = append(c.Medias, media)
waitAudio = false
case TagVideo:
if !waitVideo {
continue
}
var codec *core.Codec
if isExHeader(pkt.Payload) {
@@ -213,19 +224,20 @@ func (c *Producer) probe() error {
Codecs: []*core.Codec{codec},
}
c.Medias = append(c.Medias, media)
waitVideo = false
case TagData:
if !bytes.Contains(pkt.Payload, []byte("onMetaData")) {
waitType = append(waitType, TagData)
continue
}
// Dahua cameras doesn't send videocodecid
if bytes.Contains(pkt.Payload, []byte("videocodecid")) ||
bytes.Contains(pkt.Payload, []byte("width")) ||
bytes.Contains(pkt.Payload, []byte("framerate")) {
waitType = append(waitType, TagVideo)
if !bytes.Contains(pkt.Payload, []byte("videocodecid")) &&
!bytes.Contains(pkt.Payload, []byte("width")) &&
!bytes.Contains(pkt.Payload, []byte("framerate")) {
waitVideo = false
}
if bytes.Contains(pkt.Payload, []byte("audiocodecid")) {
waitType = append(waitType, TagAudio)
if !bytes.Contains(pkt.Payload, []byte("audiocodecid")) {
waitAudio = false
}
}
}
+40 -44
View File
@@ -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 {
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -3,7 +3,7 @@ package homekit
import (
"fmt"
"io"
"math/rand/v2"
"math/rand"
"net"
"time"
+2 -3
View File
@@ -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],
+2 -2
View File
@@ -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,
+2 -2
View File
@@ -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)
+13
View File
@@ -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])
}
+51 -22
View File
@@ -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 {
+1 -1
View File
@@ -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 {
+11 -7
View File
@@ -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
}
+5
View File
@@ -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)
+38
View File
@@ -0,0 +1,38 @@
## Profiles
- Profile A - For access control configuration
- Profile C - For door control and event management
- Profile S - For basic video streaming
- Video streaming and configuration
- Profile T - For advanced video streaming
- H.264 / H.265 video compression
- Imaging settings
- Motion alarm and tampering events
- Metadata streaming
- Bi-directional audio
## Services
https://www.onvif.org/profiles/specifications/
- https://www.onvif.org/ver10/device/wsdl/devicemgmt.wsdl
- https://www.onvif.org/ver20/imaging/wsdl/imaging.wsdl
- https://www.onvif.org/ver10/media/wsdl/media.wsdl
## TMP
| | Dahua | Reolink | TP-Link |
|------------------------|---------|---------|---------|
| GetCapabilities | no auth | no auth | no auth |
| GetServices | no auth | no auth | no auth |
| GetServiceCapabilities | no auth | no auth | auth |
| GetSystemDateAndTime | no auth | no auth | no auth |
| GetNetworkInterfaces | auth | auth | auth |
| GetDeviceInformation | auth | auth | auth |
| GetProfiles | auth | auth | auth |
| GetScopes | auth | auth | auth |
- Dahua - onvif://192.168.10.90:80
- Reolink - onvif://192.168.10.92:8000
- TP-Link - onvif://192.168.10.91:2020/onvif/device_service
-
+26 -87
View File
@@ -2,8 +2,6 @@ package onvif
import (
"bytes"
"crypto/sha1"
"encoding/base64"
"errors"
"html"
"io"
@@ -12,8 +10,6 @@ import (
"regexp"
"strings"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
)
const PathDevice = "/onvif/device_service"
@@ -41,7 +37,7 @@ func NewClient(rawURL string) (*Client, error) {
client.deviceURL = baseURL + u.Path
}
b, err := client.GetCapabilities()
b, err := client.DeviceRequest(DeviceGetCapabilities)
if err != nil {
return nil, err
}
@@ -95,7 +91,7 @@ func (c *Client) GetURI() (string, error) {
}
func (c *Client) GetName() (string, error) {
b, err := c.GetDeviceInformation()
b, err := c.DeviceRequest(DeviceGetDeviceInformation)
if err != nil {
return "", err
}
@@ -104,7 +100,7 @@ func (c *Client) GetName() (string, error) {
}
func (c *Client) GetProfilesTokens() ([]string, error) {
b, err := c.GetProfiles()
b, err := c.MediaRequest(MediaGetProfiles)
if err != nil {
return nil, err
}
@@ -127,86 +123,53 @@ func (c *Client) HasSnapshots() bool {
return strings.Contains(string(b), `SnapshotUri="true"`)
}
func (c *Client) GetCapabilities() ([]byte, error) {
func (c *Client) GetProfile(token string) ([]byte, error) {
return c.Request(
c.deviceURL,
`<tds:GetCapabilities xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<tds:Category>All</tds:Category>
</tds:GetCapabilities>`,
c.mediaURL, `<trt:GetProfile><trt:ProfileToken>`+token+`</trt:ProfileToken></trt:GetProfile>`,
)
}
func (c *Client) GetNetworkInterfaces() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:GetNetworkInterfaces xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
)
}
func (c *Client) GetDeviceInformation() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:GetDeviceInformation xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
)
}
func (c *Client) GetProfiles() ([]byte, error) {
return c.Request(
c.mediaURL, `<trt:GetProfiles xmlns:trt="http://www.onvif.org/ver10/media/wsdl"/>`,
)
func (c *Client) GetVideoSourceConfiguration(token string) ([]byte, error) {
return c.Request(c.mediaURL, `<trt:GetVideoSourceConfiguration>
<trt:ConfigurationToken>`+token+`</trt:ConfigurationToken>
</trt:GetVideoSourceConfiguration>`)
}
func (c *Client) GetStreamUri(token string) ([]byte, error) {
return c.Request(
c.mediaURL,
`<trt:GetStreamUri xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
return c.Request(c.mediaURL, `<trt:GetStreamUri>
<trt:StreamSetup>
<tt:Stream>RTP-Unicast</tt:Stream>
<tt:Transport><tt:Protocol>RTSP</tt:Protocol></tt:Transport>
</trt:StreamSetup>
<trt:ProfileToken>`+token+`</trt:ProfileToken>
</trt:GetStreamUri>`,
)
</trt:GetStreamUri>`)
}
func (c *Client) GetSnapshotUri(token string) ([]byte, error) {
return c.Request(
c.imaginURL,
`<trt:GetSnapshotUri xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
<trt:ProfileToken>`+token+`</trt:ProfileToken>
</trt:GetSnapshotUri>`,
)
}
func (c *Client) GetSystemDateAndTime() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:GetSystemDateAndTime xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
c.imaginURL, `<trt:GetSnapshotUri><trt:ProfileToken>`+token+`</trt:ProfileToken></trt:GetSnapshotUri>`,
)
}
func (c *Client) GetServiceCapabilities() ([]byte, error) {
// some cameras answer GetServiceCapabilities for media only for path = "/onvif/media"
return c.Request(
c.mediaURL, `<trt:GetServiceCapabilities xmlns:trt="http://www.onvif.org/ver10/media/wsdl"/>`,
c.mediaURL, `<trt:GetServiceCapabilities />`,
)
}
func (c *Client) SystemReboot() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:SystemReboot xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
)
func (c *Client) DeviceRequest(operation string) ([]byte, error) {
if operation == DeviceGetServices {
operation = `<tds:GetServices><tds:IncludeCapability>true</tds:IncludeCapability></tds:GetServices>`
} else {
operation = `<tds:` + operation + `/>`
}
return c.Request(c.deviceURL, operation)
}
func (c *Client) GetServices() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:GetServices xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<tds:IncludeCapability>true</tds:IncludeCapability>
</tds:GetServices>`,
)
}
func (c *Client) GetScopes() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:GetScopes xmlns:tds="http://www.onvif.org/ver10/device/wsdl" />`,
)
func (c *Client) MediaRequest(operation string) ([]byte, error) {
operation = `<trt:` + operation + `/>`
return c.Request(c.mediaURL, operation)
}
func (c *Client) Request(url, body string) ([]byte, error) {
@@ -214,35 +177,11 @@ func (c *Client) Request(url, body string) ([]byte, error) {
return nil, errors.New("onvif: unsupported service")
}
buf := bytes.NewBuffer(nil)
buf.WriteString(
`<?xml version="1.0" encoding="UTF-8"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">`,
)
if user := c.url.User; user != nil {
nonce := core.RandString(16, 36)
created := time.Now().UTC().Format(time.RFC3339Nano)
pass, _ := user.Password()
h := sha1.New()
h.Write([]byte(nonce + created + pass))
buf.WriteString(`<s:Header>
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsse:UsernameToken>
<wsse:Username>` + user.Username() + `</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">` + base64.StdEncoding.EncodeToString(h.Sum(nil)) + `</wsse:Password>
<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">` + base64.StdEncoding.EncodeToString([]byte(nonce)) + `</wsse:Nonce>
<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">` + created + `</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</s:Header>`)
}
buf.WriteString(`<s:Body>` + body + `</s:Body></s:Envelope>`)
e := NewEnvelopeWithUser(c.url.User)
e.Append(body)
client := &http.Client{Timeout: time.Second * 5000}
res, err := client.Post(url, `application/soap+xml;charset=utf-8`, buf)
res, err := client.Post(url, `application/soap+xml;charset=utf-8`, bytes.NewReader(e.Bytes()))
if err != nil {
return nil, err
}
+79
View File
@@ -0,0 +1,79 @@
package onvif
import (
"crypto/sha1"
"encoding/base64"
"fmt"
"net/url"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
)
type Envelope struct {
buf []byte
}
const (
prefix1 = `<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
`
prefix2 = `<s:Body>
`
suffix = `
</s:Body>
</s:Envelope>`
)
func NewEnvelope() *Envelope {
e := &Envelope{buf: make([]byte, 0, 1024)}
e.Append(prefix1, prefix2)
return e
}
func NewEnvelopeWithUser(user *url.Userinfo) *Envelope {
if user == nil {
return NewEnvelope()
}
nonce := core.RandString(16, 36)
created := time.Now().UTC().Format(time.RFC3339Nano)
pass, _ := user.Password()
h := sha1.New()
h.Write([]byte(nonce + created + pass))
e := &Envelope{buf: make([]byte, 0, 1024)}
e.Append(prefix1)
e.Appendf(`<s:Header>
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsse:UsernameToken>
<wsse:Username>%s</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">%s</wsse:Password>
<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">%s</wsse:Nonce>
<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">%s</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</s:Header>
`,
user.Username(),
base64.StdEncoding.EncodeToString(h.Sum(nil)),
base64.StdEncoding.EncodeToString([]byte(nonce)),
created)
e.Append(prefix2)
return e
}
func (e *Envelope) Append(args ...string) {
for _, s := range args {
e.buf = append(e.buf, s...)
}
}
func (e *Envelope) Appendf(format string, args ...any) {
e.buf = fmt.Appendf(e.buf, format, args...)
}
func (e *Envelope) Bytes() []byte {
return append(e.buf, suffix...)
}
+24 -1
View File
@@ -1,6 +1,7 @@
package onvif
import (
"fmt"
"net"
"regexp"
"strconv"
@@ -11,7 +12,7 @@ import (
)
func FindTagValue(b []byte, tag string) string {
re := regexp.MustCompile(`(?s)[:<]` + tag + `>([^<]+)`)
re := regexp.MustCompile(`(?s)<(?:\w+:)?` + tag + `\b[^>]*>([^<]+)`)
m := re.FindSubmatch(b)
if len(m) != 2 {
return ""
@@ -106,3 +107,25 @@ func atoi(s string) int {
}
return i
}
func GetPosixTZ(current time.Time) string {
// Thanks to https://github.com/Path-Variable/go-posix-time
_, offset := current.Zone()
if current.IsDST() {
_, end := current.ZoneBounds()
endPlus1 := end.Add(time.Hour * 25)
_, offset = endPlus1.Zone()
}
var prefix string
if offset < 0 {
prefix = "GMT+"
offset = -offset / 60
} else {
prefix = "GMT-"
offset = offset / 60
}
return prefix + fmt.Sprintf("%02d:%02d", offset/60, offset%60)
}
+28
View File
@@ -84,6 +84,34 @@ func TestGetStreamUri(t *testing.T) {
</SOAP-ENV:Envelope>`,
url: "rtsp://192.168.5.53:8090/profile1=r",
},
{
name: "go2rtc 1.9.4",
xml: `<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<trt:GetStreamUriResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
<trt:MediaUri>
<tt:Uri xmlns:tt="http://www.onvif.org/ver10/schema">rtsp://192.168.1.123:8554/rtsp-dahua1</tt:Uri>
</trt:MediaUri>
</trt:GetStreamUriResponse>
</s:Body>
</s:Envelope>`,
url: "rtsp://192.168.1.123:8554/rtsp-dahua1",
},
{
name: "go2rtc 1.9.8",
xml: `<?xml version="1.0" encoding="utf-8" standalone="no"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
<s:Body>
<trt:GetStreamUriResponse>
<trt:MediaUri>
<tt:Uri>rtsp://192.168.1.123:8554/rtsp-dahua2</tt:Uri>
</trt:MediaUri>
</trt:GetStreamUriResponse>
</s:Body>
</s:Envelope>
`,
url: "rtsp://192.168.1.123:8554/rtsp-dahua2",
},
}
for _, test := range tests {
+201 -155
View File
@@ -2,30 +2,40 @@ package onvif
import (
"bytes"
"fmt"
"regexp"
"strconv"
"time"
)
const (
ActionGetCapabilities = "GetCapabilities"
ActionGetSystemDateAndTime = "GetSystemDateAndTime"
ActionGetNetworkInterfaces = "GetNetworkInterfaces"
ActionGetDeviceInformation = "GetDeviceInformation"
ActionGetServiceCapabilities = "GetServiceCapabilities"
ActionGetProfiles = "GetProfiles"
ActionGetStreamUri = "GetStreamUri"
ActionSystemReboot = "SystemReboot"
const ServiceGetServiceCapabilities = "GetServiceCapabilities"
ActionGetServices = "GetServices"
ActionGetScopes = "GetScopes"
ActionGetVideoSources = "GetVideoSources"
ActionGetAudioSources = "GetAudioSources"
ActionGetVideoSourceConfigurations = "GetVideoSourceConfigurations"
ActionGetAudioSourceConfigurations = "GetAudioSourceConfigurations"
ActionGetVideoEncoderConfigurations = "GetVideoEncoderConfigurations"
ActionGetAudioEncoderConfigurations = "GetAudioEncoderConfigurations"
const (
DeviceGetCapabilities = "GetCapabilities"
DeviceGetDeviceInformation = "GetDeviceInformation"
DeviceGetDiscoveryMode = "GetDiscoveryMode"
DeviceGetDNS = "GetDNS"
DeviceGetHostname = "GetHostname"
DeviceGetNetworkDefaultGateway = "GetNetworkDefaultGateway"
DeviceGetNetworkInterfaces = "GetNetworkInterfaces"
DeviceGetNetworkProtocols = "GetNetworkProtocols"
DeviceGetNTP = "GetNTP"
DeviceGetScopes = "GetScopes"
DeviceGetServices = "GetServices"
DeviceGetSystemDateAndTime = "GetSystemDateAndTime"
DeviceSystemReboot = "SystemReboot"
)
const (
MediaGetAudioEncoderConfigurations = "GetAudioEncoderConfigurations"
MediaGetAudioSources = "GetAudioSources"
MediaGetAudioSourceConfigurations = "GetAudioSourceConfigurations"
MediaGetProfile = "GetProfile"
MediaGetProfiles = "GetProfiles"
MediaGetSnapshotUri = "GetSnapshotUri"
MediaGetStreamUri = "GetStreamUri"
MediaGetVideoEncoderConfigurations = "GetVideoEncoderConfigurations"
MediaGetVideoSources = "GetVideoSources"
MediaGetVideoSourceConfiguration = "GetVideoSourceConfiguration"
MediaGetVideoSourceConfigurations = "GetVideoSourceConfigurations"
)
func GetRequestAction(b []byte) string {
@@ -42,163 +52,199 @@ func GetRequestAction(b []byte) string {
return string(m[1])
}
func GetCapabilitiesResponse(host string) string {
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<tds:GetCapabilitiesResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<tds:Capabilities xmlns:tt="http://www.onvif.org/ver10/schema">
<tt:Device>
<tt:XAddr>http://` + host + `/onvif/device_service</tt:XAddr>
</tt:Device>
<tt:Media>
<tt:XAddr>http://` + host + `/onvif/media_service</tt:XAddr>
<tt:StreamingCapabilities>
<tt:RTPMulticast>false</tt:RTPMulticast>
<tt:RTP_TCP>false</tt:RTP_TCP>
<tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP>
</tt:StreamingCapabilities>
</tt:Media>
</tds:Capabilities>
</tds:GetCapabilitiesResponse>
</s:Body>
</s:Envelope>`
func GetCapabilitiesResponse(host string) []byte {
e := NewEnvelope()
e.Append(`<tds:GetCapabilitiesResponse>
<tds:Capabilities>
<tt:Device>
<tt:XAddr>http://`, host, `/onvif/device_service</tt:XAddr>
</tt:Device>
<tt:Media>
<tt:XAddr>http://`, host, `/onvif/media_service</tt:XAddr>
<tt:StreamingCapabilities>
<tt:RTPMulticast>false</tt:RTPMulticast>
<tt:RTP_TCP>false</tt:RTP_TCP>
<tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP>
</tt:StreamingCapabilities>
</tt:Media>
</tds:Capabilities>
</tds:GetCapabilitiesResponse>`)
return e.Bytes()
}
func GetSystemDateAndTimeResponse() string {
func GetServicesResponse(host string) []byte {
e := NewEnvelope()
e.Append(`<tds:GetServicesResponse>
<tds:Service>
<tds:Namespace>http://www.onvif.org/ver10/device/wsdl</tds:Namespace>
<tds:XAddr>http://`, host, `/onvif/device_service</tds:XAddr>
<tds:Version><tt:Major>2</tt:Major><tt:Minor>5</tt:Minor></tds:Version>
</tds:Service>
<tds:Service>
<tds:Namespace>http://www.onvif.org/ver10/media/wsdl</tds:Namespace>
<tds:XAddr>http://`, host, `/onvif/media_service</tds:XAddr>
<tds:Version><tt:Major>2</tt:Major><tt:Minor>5</tt:Minor></tds:Version>
</tds:Service>
</tds:GetServicesResponse>`)
return e.Bytes()
}
func GetSystemDateAndTimeResponse() []byte {
loc := time.Now()
utc := loc.UTC()
return fmt.Sprintf(`<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<tds:GetSystemDateAndTimeResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<tds:SystemDateAndTime xmlns:tt="http://www.onvif.org/ver10/schema">
<tt:DateTimeType>NTP</tt:DateTimeType>
<tt:DaylightSavings>false</tt:DaylightSavings>
<tt:TimeZone>
<tt:TZ>GMT%s</tt:TZ>
</tt:TimeZone>
<tt:UTCDateTime>
<tt:Time>
<tt:Hour>%d</tt:Hour>
<tt:Minute>%d</tt:Minute>
<tt:Second>%d</tt:Second>
</tt:Time>
<tt:Date>
<tt:Year>%d</tt:Year>
<tt:Month>%d</tt:Month>
<tt:Day>%d</tt:Day>
</tt:Date>
</tt:UTCDateTime>
<tt:LocalDateTime>
<tt:Time>
<tt:Hour>%d</tt:Hour>
<tt:Minute>%d</tt:Minute>
<tt:Second>%d</tt:Second>
</tt:Time>
<tt:Date>
<tt:Year>%d</tt:Year>
<tt:Month>%d</tt:Month>
<tt:Day>%d</tt:Day>
</tt:Date>
</tt:LocalDateTime>
</tds:SystemDateAndTime>
</tds:GetSystemDateAndTimeResponse>
</s:Body>
</s:Envelope>`,
loc.Format("-07:00"),
e := NewEnvelope()
e.Appendf(`<tds:GetSystemDateAndTimeResponse>
<tds:SystemDateAndTime>
<tt:DateTimeType>NTP</tt:DateTimeType>
<tt:DaylightSavings>true</tt:DaylightSavings>
<tt:TimeZone>
<tt:TZ>%s</tt:TZ>
</tt:TimeZone>
<tt:UTCDateTime>
<tt:Time><tt:Hour>%d</tt:Hour><tt:Minute>%d</tt:Minute><tt:Second>%d</tt:Second></tt:Time>
<tt:Date><tt:Year>%d</tt:Year><tt:Month>%d</tt:Month><tt:Day>%d</tt:Day></tt:Date>
</tt:UTCDateTime>
<tt:LocalDateTime>
<tt:Time><tt:Hour>%d</tt:Hour><tt:Minute>%d</tt:Minute><tt:Second>%d</tt:Second></tt:Time>
<tt:Date><tt:Year>%d</tt:Year><tt:Month>%d</tt:Month><tt:Day>%d</tt:Day></tt:Date>
</tt:LocalDateTime>
</tds:SystemDateAndTime>
</tds:GetSystemDateAndTimeResponse>`,
GetPosixTZ(loc),
utc.Hour(), utc.Minute(), utc.Second(), utc.Year(), utc.Month(), utc.Day(),
loc.Hour(), loc.Minute(), loc.Second(), loc.Year(), loc.Month(), loc.Day(),
)
return e.Bytes()
}
func GetNetworkInterfacesResponse() string {
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<tds:GetNetworkInterfacesResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>
</s:Body>
</s:Envelope>`
func GetDeviceInformationResponse(manuf, model, firmware, serial string) []byte {
e := NewEnvelope()
e.Append(`<tds:GetDeviceInformationResponse>
<tds:Manufacturer>`, manuf, `</tds:Manufacturer>
<tds:Model>`, model, `</tds:Model>
<tds:FirmwareVersion>`, firmware, `</tds:FirmwareVersion>
<tds:SerialNumber>`, serial, `</tds:SerialNumber>
<tds:HardwareId>1.00</tds:HardwareId>
</tds:GetDeviceInformationResponse>`)
return e.Bytes()
}
func GetDeviceInformationResponse(manuf, model, firmware, serial string) string {
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<tds:GetDeviceInformationResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<tds:Manufacturer>` + manuf + `</tds:Manufacturer>
<tds:Model>` + model + `</tds:Model>
<tds:FirmwareVersion>` + firmware + `</tds:FirmwareVersion>
<tds:SerialNumber>` + serial + `</tds:SerialNumber>
<tds:HardwareId>1.00</tds:HardwareId>
</tds:GetDeviceInformationResponse>
</s:Body>
</s:Envelope>`
func GetMediaServiceCapabilitiesResponse() []byte {
e := NewEnvelope()
e.Append(`<trt:GetServiceCapabilitiesResponse>
<trt:Capabilities SnapshotUri="true" Rotation="false" VideoSourceMode="false" OSD="false" TemporaryOSDText="false" EXICompression="false">
<trt:StreamingCapabilities RTPMulticast="false" RTP_TCP="false" RTP_RTSP_TCP="true" NonAggregateControl="false" NoRTSPStreaming="false" />
</trt:Capabilities>
</trt:GetServiceCapabilitiesResponse>`)
return e.Bytes()
}
func GetServiceCapabilitiesResponse() string {
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<trt:GetServiceCapabilitiesResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
<trt:Capabilities SnapshotUri="false" Rotation="false" VideoSourceMode="false" OSD="false" TemporaryOSDText="false" EXICompression="false">
<trt:StreamingCapabilities RTPMulticast="false" RTP_TCP="false" RTP_RTSP_TCP="true" NonAggregateControl="false" NoRTSPStreaming="false" />
</trt:Capabilities>
</trt:GetServiceCapabilitiesResponse>
</s:Body>
</s:Envelope>`
func GetProfilesResponse(names []string) []byte {
e := NewEnvelope()
e.Append(`<trt:GetProfilesResponse>
`)
for _, name := range names {
appendProfile(e, "Profiles", name)
}
e.Append(`</trt:GetProfilesResponse>`)
return e.Bytes()
}
func SystemRebootResponse() string {
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<tds:SystemRebootResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<tds:Message>system reboot in 1 second...</tds:Message>
</tds:SystemRebootResponse>
</s:Body>
</s:Envelope>`
func GetProfileResponse(name string) []byte {
e := NewEnvelope()
e.Append(`<trt:GetProfileResponse>
`)
appendProfile(e, "Profile", name)
e.Append(`</trt:GetProfileResponse>`)
return e.Bytes()
}
func GetProfilesResponse(names []string) string {
buf := bytes.NewBuffer(nil)
buf.WriteString(`<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<trt:GetProfilesResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">`)
func appendProfile(e *Envelope, tag, name string) {
e.Append(`<trt:`, tag, ` token="`, name, `" fixed="true">
<tt:Name>`, name, `</tt:Name>
<tt:VideoSourceConfiguration token="`, name, `">
<tt:Name>VSC</tt:Name>
<tt:SourceToken>`, name, `</tt:SourceToken>
<tt:Bounds x="0" y="0" width="1920" height="1080"></tt:Bounds>
</tt:VideoSourceConfiguration>
<tt:VideoEncoderConfiguration token="vec">
<tt:Name>VEC</tt:Name>
<tt:Encoding>H264</tt:Encoding>
<tt:Resolution><tt:Width>1920</tt:Width><tt:Height>1080</tt:Height></tt:Resolution>
</tt:VideoEncoderConfiguration>
</trt:`, tag, `>
`)
}
for i, name := range names {
buf.WriteString(`
<trt:Profiles token="` + name + `" fixed="true">
<tt:Name>` + name + `</tt:Name>
<tt:VideoEncoderConfiguration token="` + strconv.Itoa(i) + `">
<tt:Encoding>H264</tt:Encoding>
<tt:Resolution>
<tt:Width>1920</tt:Width>
<tt:Height>1080</tt:Height>
</tt:Resolution>
</tt:VideoEncoderConfiguration>
</trt:Profiles>`)
func GetVideoSourceConfigurationResponse(name string) []byte {
e := NewEnvelope()
e.Append(`<trt:GetVideoSourceConfigurationResponse>
<trt:Configuration token="`, name, `">
<tt:Name>VSC</tt:Name>
<tt:SourceToken>`, name, `</tt:SourceToken>
<tt:Bounds x="0" y="0" width="1920" height="1080"></tt:Bounds>
</trt:Configuration>
</trt:GetVideoSourceConfigurationResponse>`)
return e.Bytes()
}
func GetVideoSourcesResponse(names []string) []byte {
e := NewEnvelope()
e.Append(`<trt:GetVideoSourcesResponse>
`)
for _, name := range names {
e.Append(`<trt:VideoSources token="`, name, `">
<tt:Framerate>30.000000</tt:Framerate>
<tt:Resolution><tt:Width>1920</tt:Width><tt:Height>1080</tt:Height></tt:Resolution>
</trt:VideoSources>
`)
}
e.Append(`</trt:GetVideoSourcesResponse>`)
return e.Bytes()
}
func GetStreamUriResponse(uri string) []byte {
e := NewEnvelope()
e.Append(`<trt:GetStreamUriResponse><trt:MediaUri><tt:Uri>`, uri, `</tt:Uri></trt:MediaUri></trt:GetStreamUriResponse>`)
return e.Bytes()
}
func GetSnapshotUriResponse(uri string) []byte {
e := NewEnvelope()
e.Append(`<trt:GetSnapshotUriResponse><trt:MediaUri><tt:Uri>`, uri, `</tt:Uri></trt:MediaUri></trt:GetSnapshotUriResponse>`)
return e.Bytes()
}
func StaticResponse(operation string) []byte {
switch operation {
case DeviceGetSystemDateAndTime:
return GetSystemDateAndTimeResponse()
}
buf.WriteString(`
</trt:GetProfilesResponse>
</s:Body>
</s:Envelope>`)
return buf.String()
e := NewEnvelope()
e.Append(responses[operation])
b := e.Bytes()
if operation == DeviceGetNetworkInterfaces {
println()
}
return b
}
func GetStreamUriResponse(uri string) string {
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<trt:GetStreamUriResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
<trt:MediaUri>
<tt:Uri xmlns:tt="http://www.onvif.org/ver10/schema">` + uri + `</tt:Uri>
</trt:MediaUri>
</trt:GetStreamUriResponse>
</s:Body>
</s:Envelope>`
var responses = map[string]string{
DeviceGetDiscoveryMode: `<tds:GetDiscoveryModeResponse><tds:DiscoveryMode>Discoverable</tds:DiscoveryMode></tds:GetDiscoveryModeResponse>`,
DeviceGetDNS: `<tds:GetDNSResponse><tds:DNSInformation /></tds:GetDNSResponse>`,
DeviceGetHostname: `<tds:GetHostnameResponse><tds:HostnameInformation /></tds:GetHostnameResponse>`,
DeviceGetNetworkDefaultGateway: `<tds:GetNetworkDefaultGatewayResponse><tds:NetworkGateway /></tds:GetNetworkDefaultGatewayResponse>`,
DeviceGetNTP: `<tds:GetNTPResponse><tds:NTPInformation /></tds:GetNTPResponse>`,
DeviceSystemReboot: `<tds:SystemRebootResponse><tds:Message>OK</tds:Message></tds:SystemRebootResponse>`,
DeviceGetNetworkInterfaces: `<tds:GetNetworkInterfacesResponse />`,
DeviceGetNetworkProtocols: `<tds:GetNetworkProtocolsResponse />`,
DeviceGetScopes: `<tds:GetScopesResponse>
<tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/name/go2rtc</tt:ScopeItem></tds:Scopes>
<tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/location/github</tt:ScopeItem></tds:Scopes>
<tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/Profile/Streaming</tt:ScopeItem></tds:Scopes>
<tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/type/Network_Video_Transmitter</tt:ScopeItem></tds:Scopes>
</tds:GetScopesResponse>`,
}
+55
View File
@@ -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)
}
}
+6 -1
View File
@@ -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
}
+7 -6
View File
@@ -117,10 +117,6 @@ func (c *Conn) acceptCommand(b []byte) error {
}
}
if c.App == "" {
return fmt.Errorf("rtmp: read command %x", b)
}
payload := amf.EncodeItems(
"_result", tID,
map[string]any{"fmsVer": "FMS/3,0,1,123"},
@@ -129,9 +125,16 @@ func (c *Conn) acceptCommand(b []byte) error {
return c.writeMessage(3, TypeCommand, 0, payload)
case CommandReleaseStream:
// if app is empty - will use key as app
if c.App == "" && len(items) == 4 {
c.App, _ = items[3].(string)
}
payload := amf.EncodeItems("_result", tID, nil)
return c.writeMessage(3, TypeCommand, 0, payload)
case CommandFCPublish: // no response
case CommandCreateStream:
payload := amf.EncodeItems("_result", tID, nil, 1)
return c.writeMessage(3, TypeCommand, 0, payload)
@@ -140,8 +143,6 @@ func (c *Conn) acceptCommand(b []byte) error {
c.Intent = cmd
c.streamID = 1
case CommandFCPublish: // no response
default:
println("rtmp: unknown command: " + cmd)
}
+2
View File
@@ -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:
+20 -6
View File
@@ -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')
@@ -63,8 +70,15 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) {
// Check buggy SDP with fmtp for H264 on another track
// https://github.com/AlexxIT/WebRTC/issues/419
for _, codec := range media.Codecs {
if codec.Name == core.CodecH264 && codec.FmtpLine == "" {
codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions)
switch codec.Name {
case core.CodecH264:
if codec.FmtpLine == "" {
codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions)
}
case core.CodecOpus:
// fix OPUS for some cameras https://datatracker.ietf.org/doc/html/rfc7587
codec.ClockRate = 48000
codec.Channels = 2
}
}
+108
View File
@@ -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)
}
+4 -4
View File
@@ -149,7 +149,7 @@ func (c *Conn) Accept() error {
}
const transport = "RTP/AVP/TCP;unicast;interleaved="
if strings.HasPrefix(tr, transport) {
if tr = core.Between(tr, "interleaved=", ";"); tr != "" {
c.session = core.RandString(8, 10)
c.state = StateSetup
@@ -157,13 +157,13 @@ func (c *Conn) Accept() error {
if i := reqTrackID(req); i >= 0 && i < len(c.Senders) {
// mark sender as SETUP
c.Senders[i].Media.ID = MethodSetup
tr = fmt.Sprintf("RTP/AVP/TCP;unicast;interleaved=%d-%d", i*2, i*2+1)
res.Header.Set("Transport", tr)
tr = fmt.Sprintf("%d-%d", i*2, i*2+1)
res.Header.Set("Transport", transport+tr)
} else {
res.Status = "400 Bad Request"
}
} else {
res.Header.Set("Transport", tr[:len(transport)+3])
res.Header.Set("Transport", transport+tr)
}
} else {
res.Status = "461 Unsupported transport"
+8
View File
@@ -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
}
+74 -32
View File
@@ -27,7 +27,7 @@ import (
type Client struct {
core.Listener
url string
url *url.URL
medias []*core.Media
receivers []*core.Receiver
@@ -52,17 +52,15 @@ type cbcMode interface {
SetIV([]byte)
}
func Dial(url string) (*Client, error) {
var err error
c := &Client{url: url}
if c.conn1, err = c.newConn(); err != nil {
return nil, err
}
return c, nil
}
func (c *Client) newConn() (net.Conn, error) {
u, err := url.Parse(c.url)
// Dial support different urls:
// - tapo://{cloud-password}@192.168.1.123 - auth to Tapo cameras
// with cloud password (autodetect hash method)
// - tapo://admin:{hashed-cloud-password}@192.168.1.123 - auth to Tapo cameras
// with pre-hashed cloud password
// - vigi://admin:{password}@192.168.1.123 - auth to Vigi cameras with password
// for admin account (other not supported)
func Dial(rawURL string) (*Client, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
@@ -71,21 +69,31 @@ func (c *Client) newConn() (net.Conn, error) {
u.Host += ":8800"
}
req, err := http.NewRequest("POST", "http://"+u.Host+"/stream", nil)
c := &Client{url: u}
if c.conn1, err = c.newConn(); err != nil {
return nil, err
}
return c, nil
}
func (c *Client) newConn() (net.Conn, error) {
req, err := http.NewRequest("POST", "http://"+c.url.Host+"/stream", nil)
if err != nil {
return nil, err
}
query := u.Query()
query := c.url.Query()
if deviceId := query.Get("deviceId"); deviceId != "" {
req.URL.RawQuery = "deviceId=" + deviceId
}
req.URL.User = u.User
req.Header.Set("Content-Type", "multipart/mixed; boundary=--client-stream-boundary--")
conn, res, err := dial(req)
username := c.url.User.Username()
password, _ := c.url.User.Password()
conn, res, err := dial(req, c.url.Scheme, username, password)
if err != nil {
return nil, err
}
@@ -95,7 +103,7 @@ func (c *Client) newConn() (net.Conn, error) {
}
if c.decrypt == nil {
c.newDectypter(res)
c.newDectypter(res, c.url.Scheme, username, password)
}
channel := query.Get("channel")
@@ -119,14 +127,18 @@ func (c *Client) newConn() (net.Conn, error) {
return conn, nil
}
func (c *Client) newDectypter(res *http.Response) {
username := res.Request.URL.User.Username()
password, _ := res.Request.URL.User.Password()
func (c *Client) newDectypter(res *http.Response, brand, username, password string) {
exchange := res.Header.Get("Key-Exchange")
nonce := core.Between(exchange, `nonce="`, `"`)
// extract nonce from response
// cipher="AES_128_CBC" username="admin" padding="PKCS7_16" algorithm="MD5" nonce="***"
nonce := res.Header.Get("Key-Exchange")
nonce = core.Between(nonce, `nonce="`, `"`)
if brand == "tapo" && password == "" {
if strings.Contains(exchange, `encrypt_type="3"`) {
password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username)))
} else {
password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
}
username = "admin"
}
key := md5.Sum([]byte(nonce + ":" + password))
iv := md5.Sum([]byte(username + ":" + nonce))
@@ -263,16 +275,12 @@ func (c *Client) Request(conn net.Conn, body []byte) (string, error) {
}
}
func dial(req *http.Request) (net.Conn, *http.Response, error) {
func dial(req *http.Request, brand, username, password string) (net.Conn, *http.Response, error) {
conn, err := net.DialTimeout("tcp", req.URL.Host, core.ConnDialTimeout)
if err != nil {
return nil, nil, err
}
username := req.URL.User.Username()
password, _ := req.URL.User.Password()
req.URL.User = nil
if err = req.Write(conn); err != nil {
return nil, nil, err
}
@@ -291,7 +299,7 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) {
return nil, nil, fmt.Errorf("Expected StatusCode to be %d, received %d", http.StatusUnauthorized, res.StatusCode)
}
if password == "" {
if brand == "tapo" && password == "" {
// support cloud password in place of username
if strings.Contains(auth, `encrypt_type="3"`) {
password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username)))
@@ -299,6 +307,8 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) {
password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
}
username = "admin"
} else if brand == "vigi" && username == "admin" {
password = securityEncode(password)
}
realm := tcp.Between(auth, `realm="`, `"`)
@@ -331,7 +341,39 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) {
return nil, nil, err
}
req.URL.User = url.UserPassword(username, password)
return conn, res, nil
}
const (
keyShort = "RDpbLfCPsJZ7fiv"
keyLong = "yLwVl0zKqws7LgKPRQ84Mdt708T1qQ3Ha7xv3H7NyU84p21BriUWBU43odz3iP4rBL3cD02KZciXTysVXiV8ngg6vL48rPJyAUw0HurW20xqxv9aYb4M9wK1Ae0wlro510qXeU07kV57fQMc8L6aLgMLwygtc0F10a0Dg70TOoouyFhdysuRMO51yY5ZlOZZLEal1h0t9YQW0Ko7oBwmCAHoic4HYbUyVeU3sfQ1xtXcPcf1aT303wAQhv66qzW"
)
func securityEncode(s string) string {
size := len(s)
var n int // max
if size > len(keyShort) {
n = size
} else {
n = len(keyShort)
}
b := make([]byte, n)
for i := 0; i < n; i++ {
c1 := 187
c2 := 187
if i >= size {
c1 = int(keyShort[i])
} else if i >= len(keyShort) {
c2 = int(s[i])
} else {
c1 = int(keyShort[i])
c2 = int(s[i])
}
b[i] = keyLong[(c1^c2)%len(keyLong)]
}
return string(b)
}
+1 -1
View File
@@ -77,7 +77,7 @@ func (c *Client) Stop() error {
func (c *Client) MarshalJSON() ([]byte, error) {
info := &core.Connection{
ID: core.ID(c),
FormatName: "tapo",
FormatName: c.url.Scheme,
Protocol: "http",
Medias: c.medias,
Recv: c.recv,
+3 -3
View File
@@ -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())
})
}
+1
View File
@@ -29,6 +29,7 @@ func NewConn(pc *webrtc.PeerConnection) *Conn {
Connection: core.Connection{
ID: core.NewID(),
FormatName: "webrtc",
Transport: pc,
},
pc: pc,
}
+4 -3
View File
@@ -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
+9
View File
@@ -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
+5
View File
@@ -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