Merge branch 'AlexxIT:master' into onvif-client
This commit is contained in:
@@ -19,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with: { go-version: '1.22' }
|
with: { go-version: '1.24' }
|
||||||
|
|
||||||
- name: Build go2rtc_win64
|
- name: Build go2rtc_win64
|
||||||
env: { GOOS: windows, GOARCH: amd64 }
|
env: { GOOS: windows, GOARCH: amd64 }
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.22'
|
go-version: '1.24'
|
||||||
|
|
||||||
- name: Build Go binary
|
- name: Build Go binary
|
||||||
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
go2rtc.yaml
|
go2rtc.yaml
|
||||||
go2rtc.json
|
go2rtc.json
|
||||||
|
|
||||||
|
go2rtc_freebsd*
|
||||||
go2rtc_linux*
|
go2rtc_linux*
|
||||||
go2rtc_mac*
|
go2rtc_mac*
|
||||||
go2rtc_win*
|
go2rtc_win*
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# 0. Prepare images
|
# 0. Prepare images
|
||||||
ARG PYTHON_VERSION="3.11"
|
ARG PYTHON_VERSION="3.11"
|
||||||
ARG GO_VERSION="1.22"
|
ARG GO_VERSION="1.24"
|
||||||
|
|
||||||
|
|
||||||
# 1. Download ngrok binary (for support arm/v6)
|
# 1. Download ngrok binary (for support arm/v6)
|
||||||
@@ -42,7 +42,7 @@ FROM python:${PYTHON_VERSION}-alpine AS base
|
|||||||
# and other common tools for the echo source.
|
# and other common tools for the echo source.
|
||||||
# alsa-plugins-pulse for ALSA support (+0MB)
|
# alsa-plugins-pulse for ALSA support (+0MB)
|
||||||
# font-droid for FFmpeg drawtext filter (+2MB)
|
# font-droid for FFmpeg drawtext filter (+2MB)
|
||||||
RUN apk add --no-cache tini ffmpeg bash curl jq alsa-plugins-pulse font-droid
|
RUN apk add --no-cache tini ffmpeg ffplay bash curl jq alsa-plugins-pulse font-droid
|
||||||
|
|
||||||
# Hardware Acceleration for Intel CPU (+50MB)
|
# Hardware Acceleration for Intel CPU (+50MB)
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
|
|||||||
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks))
|
- `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` - macOS 10.13+ Intel 64-bit
|
- `go2rtc_mac_amd64.zip` - macOS 10.13+ Intel 64-bit
|
||||||
- `go2rtc_mac_arm64.zip` - macOS ARM 64-bit
|
- `go2rtc_mac_arm64.zip` - macOS ARM 64-bit
|
||||||
|
- `go2rtc_freebsd_amd64.zip` - FreeBSD 64-bit
|
||||||
|
- `go2rtc_freebsd_arm64.zip` - FreeBSD ARM 64-bit
|
||||||
|
|
||||||
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
||||||
|
|
||||||
@@ -231,7 +233,7 @@ streams:
|
|||||||
sonoff_camera: rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
|
sonoff_camera: rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
|
||||||
dahua_camera:
|
dahua_camera:
|
||||||
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
|
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
|
||||||
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1
|
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1#backchannel=0
|
||||||
amcrest_doorbell:
|
amcrest_doorbell:
|
||||||
- rtsp://username:password@192.168.1.123:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
|
- rtsp://username:password@192.168.1.123:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
|
||||||
unifi_camera: rtspx://192.168.1.123:7441/fD6ouM72bWoFijxK
|
unifi_camera: rtspx://192.168.1.123:7441/fD6ouM72bWoFijxK
|
||||||
@@ -241,7 +243,7 @@ streams:
|
|||||||
**Recommendations**
|
**Recommendations**
|
||||||
|
|
||||||
- **Amcrest Doorbell** users may want to disable two way audio, because with an active stream you won't have a call button working. You need to add `#backchannel=0` to the end of your RTSP link in YAML config file
|
- **Amcrest Doorbell** users may want to disable two way audio, because with an active stream you won't have a call button working. You need to add `#backchannel=0` to the end of your RTSP link in YAML config file
|
||||||
- **Dahua Doorbell** users may want to change backchannel [audio codec](https://github.com/AlexxIT/go2rtc/issues/52)
|
- **Dahua Doorbell** users may want to change [audio codec](https://github.com/AlexxIT/go2rtc/issues/49#issuecomment-2127107379) for proper 2-way audio. Make sure not to request backchannel multiple times by adding `#backchannel=0` to other stream sources of the same doorbell. The `unicast=true&proto=Onvif` is preferred for 2-way audio as this makes the doorbell accept multiple codecs for the incoming audio
|
||||||
- **Reolink** users may want NOT to use RTSP protocol at all, some camera models have a very awful unusable stream implementation
|
- **Reolink** users may want NOT to use RTSP protocol at all, some camera models have a very awful unusable stream implementation
|
||||||
- **Ubiquiti UniFi** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81)
|
- **Ubiquiti UniFi** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81)
|
||||||
- **TP-Link Tapo** users may skip login and password, because go2rtc support login [without them](https://drmnsamoliu.github.io/video.html)
|
- **TP-Link Tapo** users may skip login and password, because go2rtc support login [without them](https://drmnsamoliu.github.io/video.html)
|
||||||
@@ -350,7 +352,7 @@ streams:
|
|||||||
mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg#video=h264
|
mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg#video=h264
|
||||||
|
|
||||||
# [RTSP] video with rotation, should be transcoded, so select H264
|
# [RTSP] video with rotation, should be transcoded, so select H264
|
||||||
rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
|
rotate: ffmpeg:rtsp://12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
|
||||||
```
|
```
|
||||||
|
|
||||||
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/internal/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
|
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/internal/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
|
||||||
@@ -680,6 +682,10 @@ Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC proto
|
|||||||
|
|
||||||
Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-streams/), using WebRTC protocol. You need to specify signalling WebSocket URL with all credentials in query params, `client_id` and `ice_servers` list in [JSON format](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer).
|
Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-streams/), using WebRTC protocol. You need to specify signalling WebSocket URL with all credentials in query params, `client_id` and `ice_servers` list in [JSON format](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer).
|
||||||
|
|
||||||
|
**switchbot**
|
||||||
|
|
||||||
|
Support connection to [SwitchBot](https://us.switch-bot.com/) cameras that are based on Kinesis Video Streams. Specifically, this includes [Pan/Tilt Cam Plus 2K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-2k) and [Pan/Tilt Cam Plus 3K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-3k). `Outdoor Spotlight Cam 1080P`, `Outdoor Spotlight Cam 2K`, `Pan/Tilt Cam`, `Pan/Tilt Cam 2K`, `Indoor Cam` are based on Tuya, so this feature is not available.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
streams:
|
streams:
|
||||||
webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1
|
webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1
|
||||||
@@ -687,6 +693,7 @@ streams:
|
|||||||
webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}]
|
webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}]
|
||||||
webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze
|
webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze
|
||||||
webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}]
|
webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}]
|
||||||
|
webrtc-switchbot: webrtc:wss://...amazonaws.com/?...#format=switchbot#resolution=hd#client_id=...#ice_servers=[{...},{...}]
|
||||||
```
|
```
|
||||||
|
|
||||||
**PS.** For `kinesis` sources you can use [echo](#source-echo) to get connection params using `bash`/`python` or any other script language.
|
**PS.** For `kinesis` sources you can use [echo](#source-echo) to get connection params using `bash`/`python` or any other script language.
|
||||||
@@ -881,7 +888,7 @@ Read more about [codecs filters](#codecs-filters).
|
|||||||
|
|
||||||
You can get any stream as RTMP-stream: `rtmp://192.168.1.123/{stream_name}`. Only H264/AAC codecs supported right now.
|
You can get any stream as RTMP-stream: `rtmp://192.168.1.123/{stream_name}`. Only H264/AAC codecs supported right now.
|
||||||
|
|
||||||
[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has differnt problems with this format.
|
[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has different problems with this format.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
rtmp:
|
rtmp:
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
## Example
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go run examples/onvif_client/main.go http://admin:password@192.168.10.90 GetAudioEncoderConfigurations
|
||||||
|
```
|
||||||
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@@ -41,7 +40,13 @@ func main() {
|
|||||||
onvif.DeviceGetSystemDateAndTime,
|
onvif.DeviceGetSystemDateAndTime,
|
||||||
onvif.DeviceSystemReboot:
|
onvif.DeviceSystemReboot:
|
||||||
b, err = client.DeviceRequest(operation)
|
b, err = client.DeviceRequest(operation)
|
||||||
case onvif.MediaGetProfiles, onvif.MediaGetVideoSources:
|
case onvif.MediaGetProfiles,
|
||||||
|
onvif.MediaGetVideoEncoderConfigurations,
|
||||||
|
onvif.MediaGetVideoSources,
|
||||||
|
onvif.MediaGetVideoSourceConfigurations,
|
||||||
|
onvif.MediaGetAudioEncoderConfigurations,
|
||||||
|
onvif.MediaGetAudioSources,
|
||||||
|
onvif.MediaGetAudioSourceConfigurations:
|
||||||
b, err = client.MediaRequest(operation)
|
b, err = client.MediaRequest(operation)
|
||||||
case onvif.MediaGetProfile:
|
case onvif.MediaGetProfile:
|
||||||
b, err = client.GetProfile(token)
|
b, err = client.GetProfile(token)
|
||||||
@@ -64,9 +69,7 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
host, _, _ := net.SplitHostPort(u.Host)
|
if err = os.WriteFile(u.Hostname()+"_"+operation+".xml", b, 0644); err != nil {
|
||||||
|
|
||||||
if err = os.WriteFile(host+"_"+operation+".xml", b, 0644); err != nil {
|
|
||||||
log.Printf("%s\n", err)
|
log.Printf("%s\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ go 1.20
|
|||||||
require (
|
require (
|
||||||
github.com/asticode/go-astits v1.13.0
|
github.com/asticode/go-astits v1.13.0
|
||||||
github.com/expr-lang/expr v1.16.9
|
github.com/expr-lang/expr v1.16.9
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/miekg/dns v1.1.62
|
github.com/miekg/dns v1.1.63
|
||||||
github.com/pion/ice/v2 v2.3.37
|
github.com/pion/ice/v2 v2.3.37
|
||||||
github.com/pion/interceptor v0.1.37
|
github.com/pion/interceptor v0.1.37
|
||||||
github.com/pion/rtcp v1.2.15
|
github.com/pion/rtcp v1.2.15
|
||||||
github.com/pion/rtp v1.8.10
|
github.com/pion/rtp v1.8.11
|
||||||
github.com/pion/sdp/v3 v3.0.9
|
github.com/pion/sdp/v3 v3.0.10
|
||||||
github.com/pion/srtp/v2 v2.0.20
|
github.com/pion/srtp/v2 v2.0.20
|
||||||
github.com/pion/stun v0.6.1
|
github.com/pion/stun v0.6.1
|
||||||
github.com/pion/webrtc/v3 v3.3.5
|
github.com/pion/webrtc/v3 v3.3.5
|
||||||
@@ -21,30 +22,29 @@ require (
|
|||||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||||
golang.org/x/crypto v0.31.0
|
golang.org/x/crypto v0.33.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/asticode/go-astikit v0.45.0 // indirect
|
github.com/asticode/go-astikit v0.52.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/kr/pretty v0.3.1 // indirect
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/pion/datachannel v1.5.10 // indirect
|
github.com/pion/datachannel v1.5.10 // indirect
|
||||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
github.com/pion/logging v0.2.3 // indirect
|
||||||
github.com/pion/mdns v0.0.12 // indirect
|
github.com/pion/mdns v0.0.12 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/sctp v1.8.35 // indirect
|
github.com/pion/sctp v1.8.36 // indirect
|
||||||
github.com/pion/transport/v2 v2.2.10 // indirect
|
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/wlynxg/anet v0.0.5 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
golang.org/x/mod v0.20.0 // indirect
|
golang.org/x/mod v0.20.0 // indirect
|
||||||
golang.org/x/net v0.33.0 // indirect
|
golang.org/x/net v0.35.0 // indirect
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/tools v0.24.0 // indirect
|
golang.org/x/tools v0.24.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
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.52.0 h1:kTl2XjgiVQhUl1H7kim7NhmTtCMwVBbPrXKqhQhbk8Y=
|
||||||
github.com/asticode/go-astikit v0.45.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
github.com/asticode/go-astikit v0.52.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
|
||||||
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
|
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/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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
@@ -23,14 +23,15 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
|
||||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
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/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.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||||
@@ -40,8 +41,9 @@ 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/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 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
|
||||||
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
|
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/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||||
|
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||||
|
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||||
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
||||||
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
|
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
|
||||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
@@ -50,12 +52,12 @@ github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9
|
|||||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
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.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||||
github.com/pion/rtp v1.8.10 h1:puphjdbjPB+L+NFaVuZ5h6bt1g5q4kFIoI+r5q/g0CU=
|
github.com/pion/rtp v1.8.11 h1:17xjnY5WO5hgO6SD3/NTIUPvSFw/PbLsIJyz1r1yNIk=
|
||||||
github.com/pion/rtp v1.8.10/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
|
github.com/pion/rtp v1.8.11/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
|
||||||
github.com/pion/sctp v1.8.35 h1:qwtKvNK1Wc5tHMIYgTDJhfZk7vATGVHhXbUDfHbYwzA=
|
github.com/pion/sctp v1.8.36 h1:owNudmnz1xmhfYje5L/FCav3V9wpPRePHle3Zi+P+M0=
|
||||||
github.com/pion/sctp v1.8.35/go.mod h1:EcXP8zCYVTRy3W9xtOF7wJm1L1aXfKRQzaM33SjQlzg=
|
github.com/pion/sctp v1.8.36/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA=
|
||||||
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||||
github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk=
|
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/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 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||||
@@ -90,13 +92,11 @@ github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDf
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.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.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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
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 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
|
||||||
@@ -110,13 +110,12 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
|||||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
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.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
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.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-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-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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
@@ -125,13 +124,13 @@ 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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
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.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.11.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-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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -146,8 +145,8 @@ golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.11.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.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.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.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-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.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
@@ -166,14 +165,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
|
|||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
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 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
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=
|
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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
# only debian 13 (trixie) has latest ffmpeg
|
# only debian 13 (trixie) has latest ffmpeg
|
||||||
# https://packages.debian.org/trixie/ffmpeg
|
# https://packages.debian.org/trixie/ffmpeg
|
||||||
ARG DEBIAN_VERSION="trixie-slim"
|
ARG DEBIAN_VERSION="trixie-slim"
|
||||||
ARG GO_VERSION="1.22-bookworm"
|
ARG GO_VERSION="1.24-bookworm"
|
||||||
ARG NGROK_VERSION="3"
|
ARG NGROK_VERSION="3"
|
||||||
|
|
||||||
FROM debian:${DEBIAN_VERSION} AS base
|
FROM debian:${DEBIAN_VERSION} AS base
|
||||||
|
|||||||
+2
-4
@@ -69,6 +69,8 @@ func Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Mod.Listen != "" {
|
if cfg.Mod.Listen != "" {
|
||||||
|
_, port, _ := net.SplitHostPort(cfg.Mod.Listen)
|
||||||
|
Port, _ = strconv.Atoi(port)
|
||||||
go listen("tcp", cfg.Mod.Listen)
|
go listen("tcp", cfg.Mod.Listen)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,10 +94,6 @@ func listen(network, address string) {
|
|||||||
|
|
||||||
log.Info().Str("addr", address).Msg("[api] listen")
|
log.Info().Str("addr", address).Msg("[api] listen")
|
||||||
|
|
||||||
if network == "tcp" {
|
|
||||||
Port = ln.Addr().(*net.TCPAddr).Port
|
|
||||||
}
|
|
||||||
|
|
||||||
server := http.Server{
|
server := http.Server{
|
||||||
Handler: Handler,
|
Handler: Handler,
|
||||||
ReadHeaderTimeout: 5 * time.Second, // Example: Set to 5 seconds
|
ReadHeaderTimeout: 5 * time.Second, // Example: Set to 5 seconds
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/www"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/www"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initStatic(staticDir string) {
|
func initStatic(staticDir string) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func LoadConfig(v any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func PatchConfig(key string, value any, path ...string) error {
|
func PatchConfig(path []string, value any) error {
|
||||||
if ConfigPath == "" {
|
if ConfigPath == "" {
|
||||||
return errors.New("config file disabled")
|
return errors.New("config file disabled")
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ func PatchConfig(key string, value any, path ...string) error {
|
|||||||
// empty config is OK
|
// empty config is OK
|
||||||
b, _ := os.ReadFile(ConfigPath)
|
b, _ := os.ReadFile(ConfigPath)
|
||||||
|
|
||||||
b, err := yaml.Patch(b, key, value, path...)
|
b, err := yaml.Patch(b, path, value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-15
@@ -3,12 +3,14 @@ package app
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/mattn/go-isatty"
|
"github.com/mattn/go-isatty"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
var MemoryLog = newBuffer(16)
|
var MemoryLog = newBuffer()
|
||||||
|
|
||||||
func GetLogger(module string) zerolog.Logger {
|
func GetLogger(module string) zerolog.Logger {
|
||||||
if s, ok := modules[module]; ok {
|
if s, ok := modules[module]; ok {
|
||||||
@@ -38,11 +40,17 @@ func initLogger() {
|
|||||||
|
|
||||||
var writer io.Writer
|
var writer io.Writer
|
||||||
|
|
||||||
switch modules["output"] {
|
switch output, path, _ := strings.Cut(modules["output"], ":"); output {
|
||||||
case "stderr":
|
case "stderr":
|
||||||
writer = os.Stderr
|
writer = os.Stderr
|
||||||
case "stdout":
|
case "stdout":
|
||||||
writer = os.Stdout
|
writer = os.Stdout
|
||||||
|
case "file":
|
||||||
|
if path == "" {
|
||||||
|
path = "go2rtc.log"
|
||||||
|
}
|
||||||
|
// if fail - only MemoryLog will be available
|
||||||
|
writer, _ = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
timeFormat := modules["time"]
|
timeFormat := modules["time"]
|
||||||
@@ -99,15 +107,19 @@ var modules = map[string]string{
|
|||||||
"time": zerolog.TimeFormatUnixMs,
|
"time": zerolog.TimeFormatUnixMs,
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunkSize = 1 << 16
|
const (
|
||||||
|
chunkCount = 16
|
||||||
|
chunkSize = 1 << 16
|
||||||
|
)
|
||||||
|
|
||||||
type circularBuffer struct {
|
type circularBuffer struct {
|
||||||
chunks [][]byte
|
chunks [][]byte
|
||||||
r, w int
|
r, w int
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBuffer(chunks int) *circularBuffer {
|
func newBuffer() *circularBuffer {
|
||||||
b := &circularBuffer{chunks: make([][]byte, 0, chunks)}
|
b := &circularBuffer{chunks: make([][]byte, 0, chunkCount)}
|
||||||
// create first chunk
|
// create first chunk
|
||||||
b.chunks = append(b.chunks, make([]byte, 0, chunkSize))
|
b.chunks = append(b.chunks, make([]byte, 0, chunkSize))
|
||||||
return b
|
return b
|
||||||
@@ -116,16 +128,17 @@ func newBuffer(chunks int) *circularBuffer {
|
|||||||
func (b *circularBuffer) Write(p []byte) (n int, err error) {
|
func (b *circularBuffer) Write(p []byte) (n int, err error) {
|
||||||
n = len(p)
|
n = len(p)
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
// check if chunk has size
|
// check if chunk has size
|
||||||
if len(b.chunks[b.w])+n > chunkSize {
|
if len(b.chunks[b.w])+n > chunkSize {
|
||||||
// increase write chunk index
|
// increase write chunk index
|
||||||
if b.w++; b.w == cap(b.chunks) {
|
if b.w++; b.w == chunkCount {
|
||||||
b.w = 0
|
b.w = 0
|
||||||
}
|
}
|
||||||
// check overflow
|
// check overflow
|
||||||
if b.r == b.w {
|
if b.r == b.w {
|
||||||
// increase read chunk index
|
// increase read chunk index
|
||||||
if b.r++; b.r == cap(b.chunks) {
|
if b.r++; b.r == chunkCount {
|
||||||
b.r = 0
|
b.r = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,29 +153,34 @@ func (b *circularBuffer) Write(p []byte) (n int, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
b.chunks[b.w] = append(b.chunks[b.w], p...)
|
b.chunks[b.w] = append(b.chunks[b.w], p...)
|
||||||
|
b.mu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *circularBuffer) WriteTo(w io.Writer) (n int64, err error) {
|
func (b *circularBuffer) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
for i := b.r; ; {
|
buf := make([]byte, 0, chunkCount*chunkSize)
|
||||||
var nn int
|
|
||||||
if nn, err = w.Write(b.chunks[i]); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
n += int64(nn)
|
|
||||||
|
|
||||||
|
// use temp buffer inside mutex because w.Write can take some time
|
||||||
|
b.mu.Lock()
|
||||||
|
for i := b.r; ; {
|
||||||
|
buf = append(buf, b.chunks[i]...)
|
||||||
if i == b.w {
|
if i == b.w {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if i++; i == cap(b.chunks) {
|
if i++; i == chunkCount {
|
||||||
i = 0
|
i = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
nn, err := w.Write(buf)
|
||||||
|
return int64(nn), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *circularBuffer) Reset() {
|
func (b *circularBuffer) Reset() {
|
||||||
|
b.mu.Lock()
|
||||||
b.chunks[0] = b.chunks[0][:0]
|
b.chunks[0] = b.chunks[0][:0]
|
||||||
b.r = 0
|
b.r = 0
|
||||||
b.w = 0
|
b.w = 0
|
||||||
|
b.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package exec
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
// closer support custom killsignal with custom killtimeout
|
|
||||||
type closer struct {
|
|
||||||
cmd *exec.Cmd
|
|
||||||
query url.Values
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *closer) Close() (err error) {
|
|
||||||
sig := os.Kill
|
|
||||||
if s := c.query.Get("killsignal"); s != "" {
|
|
||||||
sig = syscall.Signal(core.Atoi(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Trace().Msgf("[exec] kill with signal=%d", sig)
|
|
||||||
err = c.cmd.Process.Signal(sig)
|
|
||||||
|
|
||||||
if s := c.query.Get("killtimeout"); s != "" {
|
|
||||||
timeout := time.Duration(core.Atoi(s)) * time.Second
|
|
||||||
timer := time.AfterFunc(timeout, func() {
|
|
||||||
log.Trace().Msgf("[exec] kill after timeout=%s", s)
|
|
||||||
_ = c.cmd.Process.Kill()
|
|
||||||
})
|
|
||||||
defer timer.Stop() // stop timer if Wait ends before timeout
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Join(err, c.cmd.Wait())
|
|
||||||
}
|
|
||||||
+37
-23
@@ -9,9 +9,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
@@ -49,7 +49,7 @@ func Init() {
|
|||||||
log = app.GetLogger("exec")
|
log = app.GetLogger("exec")
|
||||||
}
|
}
|
||||||
|
|
||||||
func execHandle(rawURL string) (core.Producer, error) {
|
func execHandle(rawURL string) (prod core.Producer, err error) {
|
||||||
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||||
query := streams.ParseQuery(rawQuery)
|
query := streams.ParseQuery(rawQuery)
|
||||||
|
|
||||||
@@ -67,39 +67,55 @@ func execHandle(rawURL string) (core.Producer, error) {
|
|||||||
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
|
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
|
||||||
}
|
}
|
||||||
|
|
||||||
args := shell.QuoteSplit(rawURL[5:]) // remove `exec:`
|
cmd := shell.NewCommand(rawURL[5:]) // remove `exec:`
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
|
||||||
cmd.Stderr = &logWriter{
|
cmd.Stderr = &logWriter{
|
||||||
buf: make([]byte, 512),
|
buf: make([]byte, 512),
|
||||||
debug: log.Debug().Enabled(),
|
debug: log.Debug().Enabled(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s := query.Get("killsignal"); s != "" {
|
||||||
|
sig := syscall.Signal(core.Atoi(s))
|
||||||
|
cmd.Cancel = func() error {
|
||||||
|
log.Debug().Msgf("[exec] kill with signal=%d", sig)
|
||||||
|
return cmd.Process.Signal(sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s := query.Get("killtimeout"); s != "" {
|
||||||
|
cmd.WaitDelay = time.Duration(core.Atoi(s)) * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
if query.Get("backchannel") == "1" {
|
if query.Get("backchannel") == "1" {
|
||||||
return stdin.NewClient(cmd)
|
return stdin.NewClient(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
cl := &closer{cmd: cmd, query: query}
|
|
||||||
|
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return handlePipe(rawURL, cmd, cl)
|
prod, err = handlePipe(rawURL, cmd)
|
||||||
|
} else {
|
||||||
|
prod, err = handleRTSP(rawURL, cmd, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
return handleRTSP(rawURL, cmd, cl, path)
|
if err != nil {
|
||||||
|
_ = cmd.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, error) {
|
func handlePipe(source string, cmd *shell.Command) (core.Producer, error) {
|
||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rc := struct {
|
rd := struct {
|
||||||
io.Reader
|
io.Reader
|
||||||
io.Closer
|
io.Closer
|
||||||
}{
|
}{
|
||||||
// add buffer for pipe reader to reduce syscall
|
// add buffer for pipe reader to reduce syscall
|
||||||
bufio.NewReaderSize(stdout, core.BufferSize),
|
bufio.NewReaderSize(stdout, core.BufferSize),
|
||||||
cl,
|
// stop cmd on close pipe call
|
||||||
|
cmd,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe")
|
log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe")
|
||||||
@@ -110,9 +126,8 @@ func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, erro
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
prod, err := magic.Open(rc)
|
prod, err := magic.Open(rd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = rc.Close()
|
|
||||||
return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr)
|
return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +141,7 @@ func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, erro
|
|||||||
return prod, nil
|
return prod, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRTSP(source string, cmd *exec.Cmd, cl io.Closer, path string) (core.Producer, error) {
|
func handleRTSP(source string, cmd *shell.Command, path string) (core.Producer, error) {
|
||||||
if log.Trace().Enabled() {
|
if log.Trace().Enabled() {
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
}
|
}
|
||||||
@@ -152,23 +167,22 @@ func handleRTSP(source string, cmd *exec.Cmd, cl io.Closer, path string) (core.P
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
done := make(chan error, 1)
|
timeout := time.NewTimer(30 * time.Second)
|
||||||
go func() {
|
defer timeout.Stop()
|
||||||
done <- cmd.Wait()
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-time.After(time.Minute):
|
case <-timeout.C:
|
||||||
|
// haven't received data from app in timeout
|
||||||
log.Error().Str("source", source).Msg("[exec] timeout")
|
log.Error().Str("source", source).Msg("[exec] timeout")
|
||||||
_ = cl.Close()
|
|
||||||
return nil, errors.New("exec: timeout")
|
return nil, errors.New("exec: timeout")
|
||||||
case <-done:
|
case <-cmd.Done():
|
||||||
// limit message size
|
// app fail before we receive any data
|
||||||
return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr)
|
return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr)
|
||||||
case prod := <-waiter:
|
case prod := <-waiter:
|
||||||
|
// app started successfully
|
||||||
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp")
|
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp")
|
||||||
setRemoteInfo(prod, source, cmd.Args)
|
setRemoteInfo(prod, source, cmd.Args)
|
||||||
prod.OnClose = cl.Close
|
prod.OnClose = cmd.Close
|
||||||
return prod, nil
|
return prod, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func apiPair(id, url string) error {
|
|||||||
|
|
||||||
streams.New(id, conn.URL())
|
streams.New(id, conn.URL())
|
||||||
|
|
||||||
return app.PatchConfig(id, conn.URL(), "streams")
|
return app.PatchConfig([]string{"streams", id}, conn.URL())
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiUnpair(id string) error {
|
func apiUnpair(id string) error {
|
||||||
@@ -112,7 +112,7 @@ func apiUnpair(id string) error {
|
|||||||
return errors.New(api.StreamNotFound)
|
return errors.New(api.StreamNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
rawURL := findHomeKitURL(stream)
|
rawURL := findHomeKitURL(stream.Sources())
|
||||||
if rawURL == "" {
|
if rawURL == "" {
|
||||||
return errors.New("not homekit source")
|
return errors.New("not homekit source")
|
||||||
}
|
}
|
||||||
@@ -123,15 +123,15 @@ func apiUnpair(id string) error {
|
|||||||
|
|
||||||
streams.Delete(id)
|
streams.Delete(id)
|
||||||
|
|
||||||
return app.PatchConfig(id, nil, "streams")
|
return app.PatchConfig([]string{"streams", id}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func findHomeKitURLs() map[string]*url.URL {
|
func findHomeKitURLs() map[string]*url.URL {
|
||||||
urls := map[string]*url.URL{}
|
urls := map[string]*url.URL{}
|
||||||
for id, stream := range streams.Streams() {
|
for name, sources := range streams.GetAllSources() {
|
||||||
if rawURL := findHomeKitURL(stream); rawURL != "" {
|
if rawURL := findHomeKitURL(sources); rawURL != "" {
|
||||||
if u, err := url.Parse(rawURL); err == nil {
|
if u, err := url.Parse(rawURL); err == nil {
|
||||||
urls[id] = u
|
urls[name] = u
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-24
@@ -79,7 +79,7 @@ func Init() {
|
|||||||
Handler: homekit.ServerHandler(srv),
|
Handler: homekit.ServerHandler(srv),
|
||||||
}
|
}
|
||||||
|
|
||||||
if url := findHomeKitURL(stream); url != "" {
|
if url := findHomeKitURL(stream.Sources()); url != "" {
|
||||||
// 1. Act as transparent proxy for HomeKit camera
|
// 1. Act as transparent proxy for HomeKit camera
|
||||||
dial := func() (net.Conn, error) {
|
dial := func() (net.Conn, error) {
|
||||||
client, err := homekit.Dial(url, srtp.Server)
|
client, err := homekit.Dial(url, srtp.Server)
|
||||||
@@ -118,8 +118,8 @@ func Init() {
|
|||||||
servers[host] = srv
|
servers[host] = srv
|
||||||
}
|
}
|
||||||
|
|
||||||
api.HandleFunc(hap.PathPairSetup, hapPairSetup)
|
api.HandleFunc(hap.PathPairSetup, hapHandler)
|
||||||
api.HandleFunc(hap.PathPairVerify, hapPairVerify)
|
api.HandleFunc(hap.PathPairVerify, hapHandler)
|
||||||
|
|
||||||
log.Trace().Msgf("[homekit] mdns: %s", entries)
|
log.Trace().Msgf("[homekit] mdns: %s", entries)
|
||||||
|
|
||||||
@@ -148,13 +148,19 @@ func streamHandler(rawURL string) (core.Producer, error) {
|
|||||||
return client, err
|
return client, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func hapPairSetup(w http.ResponseWriter, r *http.Request) {
|
func resolve(host string) *server {
|
||||||
srv, ok := servers[r.Host]
|
if len(servers) == 1 {
|
||||||
if !ok {
|
for _, srv := range servers {
|
||||||
log.Error().Msg("[homekit] unknown host: " + r.Host)
|
return srv
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if srv, ok := servers[host]; ok {
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hapHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
conn, rw, err := w.(http.Hijacker).Hijack()
|
conn, rw, err := w.(http.Hijacker).Hijack()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -162,32 +168,29 @@ func hapPairSetup(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
if err = srv.hap.PairSetup(r, rw, conn); err != nil {
|
// Can support multiple HomeKit cameras on single port ONLY for Apple devices.
|
||||||
log.Error().Err(err).Caller().Send()
|
// Doesn't support Home Assistant and any other open source projects
|
||||||
}
|
// because they don't send the host header in requests.
|
||||||
}
|
srv := resolve(r.Host)
|
||||||
|
if srv == nil {
|
||||||
func hapPairVerify(w http.ResponseWriter, r *http.Request) {
|
|
||||||
srv, ok := servers[r.Host]
|
|
||||||
if !ok {
|
|
||||||
log.Error().Msg("[homekit] unknown host: " + r.Host)
|
log.Error().Msg("[homekit] unknown host: " + r.Host)
|
||||||
|
_ = hap.WriteBackoff(rw)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, rw, err := w.(http.Hijacker).Hijack()
|
switch r.RequestURI {
|
||||||
if err != nil {
|
case hap.PathPairSetup:
|
||||||
return
|
err = srv.hap.PairSetup(r, rw, conn)
|
||||||
|
case hap.PathPairVerify:
|
||||||
|
err = srv.hap.PairVerify(r, rw, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer conn.Close()
|
if err != nil && err != io.EOF {
|
||||||
|
|
||||||
if err = srv.hap.PairVerify(r, rw, conn); err != nil && err != io.EOF {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func findHomeKitURL(stream *streams.Stream) string {
|
func findHomeKitURL(sources []string) string {
|
||||||
sources := stream.Sources()
|
|
||||||
if len(sources) == 0 {
|
if len(sources) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ func (s *server) DelPair(conn net.Conn, id string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) PatchConfig() {
|
func (s *server) PatchConfig() {
|
||||||
if err := app.PatchConfig("pairings", s.pairings, "homekit", s.stream); err != nil {
|
if err := app.PatchConfig([]string{"homekit", s.stream, "pairings"}, s.pairings); err != nil {
|
||||||
log.Error().Err(err).Msgf(
|
log.Error().Err(err).Msgf(
|
||||||
"[homekit] can't save %s pairings=%v", s.stream, s.pairings,
|
"[homekit] can't save %s pairings=%v", s.stream, s.pairings,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package nest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
@@ -38,11 +39,12 @@ func apiNest(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var items []*api.Source
|
var items []*api.Source
|
||||||
|
|
||||||
for name, deviceID := range devices {
|
for _, device := range devices {
|
||||||
query.Set("device_id", deviceID)
|
query.Set("device_id", device.DeviceID)
|
||||||
|
query.Set("protocols", strings.Join(device.Protocols, ","))
|
||||||
|
|
||||||
items = append(items, &api.Source{
|
items = append(items, &api.Source{
|
||||||
Name: name, URL: "nest:?" + query.Encode(),
|
Name: device.Name, URL: "nest:?" + query.Encode(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-3
@@ -72,7 +72,11 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
|
|||||||
onvif.DeviceGetNetworkDefaultGateway,
|
onvif.DeviceGetNetworkDefaultGateway,
|
||||||
onvif.DeviceGetNetworkProtocols,
|
onvif.DeviceGetNetworkProtocols,
|
||||||
onvif.DeviceGetNTP,
|
onvif.DeviceGetNTP,
|
||||||
onvif.DeviceGetScopes:
|
onvif.DeviceGetScopes,
|
||||||
|
onvif.MediaGetVideoEncoderConfigurations,
|
||||||
|
onvif.MediaGetAudioEncoderConfigurations,
|
||||||
|
onvif.MediaGetAudioSources,
|
||||||
|
onvif.MediaGetAudioSourceConfigurations:
|
||||||
b = onvif.StaticResponse(operation)
|
b = onvif.StaticResponse(operation)
|
||||||
|
|
||||||
case onvif.DeviceGetCapabilities:
|
case onvif.DeviceGetCapabilities:
|
||||||
@@ -99,16 +103,20 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
case onvif.MediaGetVideoSources:
|
case onvif.MediaGetVideoSources:
|
||||||
b = onvif.GetVideoSourcesResponse(streams.GetAll())
|
b = onvif.GetVideoSourcesResponse(streams.GetAllNames())
|
||||||
|
|
||||||
case onvif.MediaGetProfiles:
|
case onvif.MediaGetProfiles:
|
||||||
// important for Hass: H264 codec, width, height
|
// important for Hass: H264 codec, width, height
|
||||||
b = onvif.GetProfilesResponse(streams.GetAll())
|
b = onvif.GetProfilesResponse(streams.GetAllNames())
|
||||||
|
|
||||||
case onvif.MediaGetProfile:
|
case onvif.MediaGetProfile:
|
||||||
token := onvif.FindTagValue(b, "ProfileToken")
|
token := onvif.FindTagValue(b, "ProfileToken")
|
||||||
b = onvif.GetProfileResponse(token)
|
b = onvif.GetProfileResponse(token)
|
||||||
|
|
||||||
|
case onvif.MediaGetVideoSourceConfigurations:
|
||||||
|
// important for Happytime Onvif Client
|
||||||
|
b = onvif.GetVideoSourceConfigurationsResponse(streams.GetAllNames())
|
||||||
|
|
||||||
case onvif.MediaGetVideoSourceConfiguration:
|
case onvif.MediaGetVideoSourceConfiguration:
|
||||||
token := onvif.FindTagValue(b, "ConfigurationToken")
|
token := onvif.FindTagValue(b, "ConfigurationToken")
|
||||||
b = onvif.GetVideoSourceConfigurationResponse(token)
|
b = onvif.GetVideoSourceConfigurationResponse(token)
|
||||||
@@ -129,6 +137,7 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
http.Error(w, "unsupported operation", http.StatusBadRequest)
|
http.Error(w, "unsupported operation", http.StatusBadRequest)
|
||||||
|
log.Warn().Msgf("[onvif] unsupported operation: %s", operation)
|
||||||
log.Debug().Msgf("[onvif] unsupported request:\n%s", b)
|
log.Debug().Msgf("[onvif] unsupported request:\n%s", b)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
## Tested client
|
||||||
|
|
||||||
|
| From | To | Comment |
|
||||||
|
|--------|---------------------------------|---------|
|
||||||
|
| go2rtc | Reolink RLC-520A fw. v3.1.0.801 | OK |
|
||||||
|
|
||||||
|
**go2rtc.yaml**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
rtmp-reolink1: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password
|
||||||
|
rtmp-reolink2: rtmp://192.168.10.92/bcs/channel0_sub.bcs?channel=0&stream=1&user=admin&password=password
|
||||||
|
rtmp-reolink3: rtmp://192.168.10.92/bcs/channel0_ext.bcs?channel=0&stream=1&user=admin&password=password
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tested server
|
||||||
|
|
||||||
|
| From | To | Comment |
|
||||||
|
|------------------------|--------|---------------------|
|
||||||
|
| OBS 31.0.2 | go2rtc | OK |
|
||||||
|
| OpenIPC 2.5.03.02-lite | go2rtc | OK |
|
||||||
|
| FFmpeg 6.1 | go2rtc | OK |
|
||||||
|
| GoPro Black 12 | go2rtc | OK, 1080p, 5000kbps |
|
||||||
|
|
||||||
|
**go2rtc.yaml**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
rtmp:
|
||||||
|
listen: :1935
|
||||||
|
streams:
|
||||||
|
tmp:
|
||||||
|
```
|
||||||
|
|
||||||
|
**OBS**
|
||||||
|
|
||||||
|
Settings > Stream:
|
||||||
|
|
||||||
|
- Service: Custom
|
||||||
|
- Server: rtmp://192.168.10.101/tmp
|
||||||
|
- Stream Key: <empty>
|
||||||
|
- Use auth: <disabled>
|
||||||
|
|
||||||
|
**OpenIPC**
|
||||||
|
|
||||||
|
WebUI > Majestic > Settings > Outgoing
|
||||||
|
|
||||||
|
- Enable
|
||||||
|
- Address: rtmp://192.168.10.101/tmp
|
||||||
|
- Save
|
||||||
|
- Restart
|
||||||
|
|
||||||
|
**FFmpeg**
|
||||||
|
|
||||||
|
```shell
|
||||||
|
ffmpeg -re -i bbb.mp4 -c copy -f flv rtmp://192.168.10.101/tmp
|
||||||
|
```
|
||||||
|
|
||||||
|
**GoPro**
|
||||||
|
|
||||||
|
GoPro Quik > Camera > Translation > Other
|
||||||
+20
-1
@@ -1,6 +1,7 @@
|
|||||||
package rtsp
|
package rtsp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -185,6 +186,22 @@ func tcpHandler(conn *rtsp.Conn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.Get("backchannel") == "1" {
|
||||||
|
conn.Medias = append(conn.Medias, &core.Media{
|
||||||
|
Kind: core.KindAudio,
|
||||||
|
Direction: core.DirectionRecvonly,
|
||||||
|
Codecs: []*core.Codec{
|
||||||
|
{Name: core.CodecOpus, ClockRate: 48000, Channels: 2},
|
||||||
|
{Name: core.CodecPCM, ClockRate: 16000},
|
||||||
|
{Name: core.CodecPCMA, ClockRate: 16000},
|
||||||
|
{Name: core.CodecPCMU, ClockRate: 16000},
|
||||||
|
{Name: core.CodecPCM, ClockRate: 8000},
|
||||||
|
{Name: core.CodecPCMA, ClockRate: 8000},
|
||||||
|
{Name: core.CodecPCMU, ClockRate: 8000},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if s := query.Get("pkt_size"); s != "" {
|
if s := query.Get("pkt_size"); s != "" {
|
||||||
conn.PacketSize = uint16(core.Atoi(s))
|
conn.PacketSize = uint16(core.Atoi(s))
|
||||||
}
|
}
|
||||||
@@ -237,7 +254,9 @@ func tcpHandler(conn *rtsp.Conn) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err := conn.Accept(); err != nil {
|
if err := conn.Accept(); err != nil {
|
||||||
if err != io.EOF {
|
if errors.Is(err, rtsp.FailedAuth) {
|
||||||
|
log.Warn().Str("remote_addr", conn.Connection.RemoteAddr).Msg("[rtsp] failed authentication")
|
||||||
|
} else if err != io.EOF {
|
||||||
log.WithLevel(level).Err(err).Caller().Send()
|
log.WithLevel(level).Err(err).Caller().Send()
|
||||||
}
|
}
|
||||||
if closer != nil {
|
if closer != nil {
|
||||||
|
|||||||
@@ -1,8 +1,55 @@
|
|||||||
## Testing notes
|
## Examples
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
streams:
|
streams:
|
||||||
test1-basic: ffmpeg:virtual?video#video=h264
|
# known RTSP sources
|
||||||
test2-reconnect: ffmpeg:virtual?video&duration=10#video=h264
|
rtsp-dahua1: rtsp://admin:password@192.168.10.90/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
|
||||||
test3-execkill: exec:./examples/rtsp_client/rtsp_client/rtsp_client {output}
|
rtsp-dahua2: rtsp://admin:password@192.168.10.90/cam/realmonitor?channel=1&subtype=1
|
||||||
|
rtsp-tplink1: rtsp://admin:password@192.168.10.91/stream1
|
||||||
|
rtsp-tplink2: rtsp://admin:password@192.168.10.91/stream2
|
||||||
|
rtsp-reolink1: rtsp://admin:password@192.168.10.92/h264Preview_01_main
|
||||||
|
rtsp-reolink2: rtsp://admin:password@192.168.10.92/h264Preview_01_sub
|
||||||
|
rtsp-sonoff1: rtsp://admin:password@192.168.10.93/av_stream/ch0
|
||||||
|
rtsp-sonoff2: rtsp://admin:password@192.168.10.93/av_stream/ch1
|
||||||
|
|
||||||
|
# known RTMP sources
|
||||||
|
rtmp-reolink1: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password
|
||||||
|
rtmp-reolink2: rtmp://192.168.10.92/bcs/channel0_sub.bcs?channel=0&stream=1&user=admin&password=password
|
||||||
|
rtmp-reolink3: rtmp://192.168.10.92/bcs/channel0_ext.bcs?channel=0&stream=1&user=admin&password=password
|
||||||
|
|
||||||
|
# known HTTP sources
|
||||||
|
http-reolink1: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=admin&password=password
|
||||||
|
http-reolink2: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_sub.bcs&user=admin&password=password
|
||||||
|
http-reolink3: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=admin&password=password
|
||||||
|
|
||||||
|
# known ONVIF sources
|
||||||
|
onvif-dahua1: onvif://admin:password@192.168.10.90?subtype=MediaProfile00000
|
||||||
|
onvif-dahua2: onvif://admin:password@192.168.10.90?subtype=MediaProfile00001
|
||||||
|
onvif-dahua3: onvif://admin:password@192.168.10.90?subtype=MediaProfile00000&snapshot
|
||||||
|
onvif-tplink1: onvif://admin:password@192.168.10.91:2020?subtype=profile_1
|
||||||
|
onvif-tplink2: onvif://admin:password@192.168.10.91:2020?subtype=profile_2
|
||||||
|
onvif-reolink1: onvif://admin:password@192.168.10.92:8000?subtype=000
|
||||||
|
onvif-reolink2: onvif://admin:password@192.168.10.92:8000?subtype=001
|
||||||
|
onvif-reolink3: onvif://admin:password@192.168.10.92:8000?subtype=000&snapshot
|
||||||
|
onvif-openipc1: onvif://admin:password@192.168.10.95:80?subtype=PROFILE_000
|
||||||
|
onvif-openipc2: onvif://admin:password@192.168.10.95:80?subtype=PROFILE_001
|
||||||
|
|
||||||
|
# some EXEC examples
|
||||||
|
exec-h264-pipe: exec:ffmpeg -re -i bbb.mp4 -c copy -f h264 -
|
||||||
|
exec-flv-pipe: exec:ffmpeg -re -i bbb.mp4 -c copy -f flv -
|
||||||
|
exec-mpegts-pipe: exec:ffmpeg -re -i bbb.mp4 -c copy -f mpegts -
|
||||||
|
exec-adts-pipe: exec:ffmpeg -re -i bbb.mp4 -c copy -f adts -
|
||||||
|
exec-mjpeg-pipe: exec:ffmpeg -re -i bbb.mp4 -c mjpeg -f mjpeg -
|
||||||
|
exec-hevc-pipe: exec:ffmpeg -re -i bbb.mp4 -c libx265 -preset superfast -tune zerolatency -f hevc -
|
||||||
|
exec-wav-pipe: exec:ffmpeg -re -i bbb.mp4 -c pcm_alaw -ar 8000 -ac 1 -f wav -
|
||||||
|
exec-y4m-pipe: exec:ffmpeg -re -i bbb.mp4 -c rawvideo -f yuv4mpegpipe -
|
||||||
|
exec-pcma-pipe: exec:ffmpeg -re -i numb.mp3 -c:a pcm_alaw -ar:a 8000 -ac:a 1 -f wav -
|
||||||
|
exec-pcmu-pipe: exec:ffmpeg -re -i numb.mp3 -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -f wav -
|
||||||
|
exec-s16le-pipe: exec:ffmpeg -re -i numb.mp3 -c:a pcm_s16le -ar:a 16000 -ac:a 1 -f wav -
|
||||||
|
|
||||||
|
# some FFmpeg examples
|
||||||
|
ffmpeg-video-h264: ffmpeg:virtual?video#video=h264
|
||||||
|
ffmpeg-video-4K: ffmpeg:virtual?video&size=4K#video=h264
|
||||||
|
ffmpeg-video-10s: ffmpeg:virtual?video&duration=10#video=h264
|
||||||
|
ffmpeg-video-src2: ffmpeg:virtual?video=testsrc2&size=2K#video=h264
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func apiStreams(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := app.PatchConfig(name, query["src"], "streams"); err != nil {
|
if err := app.PatchConfig([]string{"streams", name}, query["src"]); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ func apiStreams(w http.ResponseWriter, r *http.Request) {
|
|||||||
case "DELETE":
|
case "DELETE":
|
||||||
delete(streams, src)
|
delete(streams, src)
|
||||||
|
|
||||||
if err := app.PatchConfig(src, nil, "streams"); err != nil {
|
if err := app.PatchConfig([]string{"streams", src}, nil); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,5 +171,6 @@ func (c *conn) label() string {
|
|||||||
if c.UserAgent != "" {
|
if c.UserAgent != "" {
|
||||||
sb.WriteString("\nuser_agent=" + c.UserAgent)
|
sb.WriteString("\nuser_agent=" + c.UserAgent)
|
||||||
}
|
}
|
||||||
return sb.String()
|
// escape quotes https://github.com/AlexxIT/go2rtc/issues/1603
|
||||||
|
return strings.ReplaceAll(sb.String(), `"`, `'`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,11 +47,12 @@ func NewStream(source any) *Stream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stream) Sources() (sources []string) {
|
func (s *Stream) Sources() []string {
|
||||||
|
sources := make([]string, 0, len(s.producers))
|
||||||
for _, prod := range s.producers {
|
for _, prod := range s.producers {
|
||||||
sources = append(sources, prod.url)
|
sources = append(sources, prod.url)
|
||||||
}
|
}
|
||||||
return
|
return sources
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stream) SetSource(source string) {
|
func (s *Stream) SetSource(source string) {
|
||||||
|
|||||||
+34
-17
@@ -42,10 +42,6 @@ func Init() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Get(name string) *Stream {
|
|
||||||
return streams[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
var sanitize = regexp.MustCompile(`\s`)
|
var sanitize = regexp.MustCompile(`\s`)
|
||||||
|
|
||||||
// Validate - not allow creating dynamic streams with spaces in the source
|
// Validate - not allow creating dynamic streams with spaces in the source
|
||||||
@@ -68,6 +64,7 @@ func New(name string, sources ...string) *Stream {
|
|||||||
streamsMu.Lock()
|
streamsMu.Lock()
|
||||||
streams[name] = stream
|
streams[name] = stream
|
||||||
streamsMu.Unlock()
|
streamsMu.Unlock()
|
||||||
|
|
||||||
return stream
|
return stream
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +121,7 @@ func GetOrPatch(query url.Values) *Stream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check if src is stream name
|
// check if src is stream name
|
||||||
if stream, ok := streams[source]; ok {
|
if stream := Get(source); stream != nil {
|
||||||
return stream
|
return stream
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,21 +136,41 @@ func GetOrPatch(query url.Values) *Stream {
|
|||||||
return Patch(source, source)
|
return Patch(source, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAll() (names []string) {
|
var log zerolog.Logger
|
||||||
|
|
||||||
|
// streams map
|
||||||
|
|
||||||
|
var streams = map[string]*Stream{}
|
||||||
|
var streamsMu sync.Mutex
|
||||||
|
|
||||||
|
func Get(name string) *Stream {
|
||||||
|
streamsMu.Lock()
|
||||||
|
defer streamsMu.Unlock()
|
||||||
|
return streams[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func Delete(name string) {
|
||||||
|
streamsMu.Lock()
|
||||||
|
defer streamsMu.Unlock()
|
||||||
|
delete(streams, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllNames() []string {
|
||||||
|
streamsMu.Lock()
|
||||||
|
names := make([]string, 0, len(streams))
|
||||||
for name := range streams {
|
for name := range streams {
|
||||||
names = append(names, name)
|
names = append(names, name)
|
||||||
}
|
}
|
||||||
return
|
streamsMu.Unlock()
|
||||||
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
func Streams() map[string]*Stream {
|
func GetAllSources() map[string][]string {
|
||||||
return streams
|
streamsMu.Lock()
|
||||||
|
sources := make(map[string][]string, len(streams))
|
||||||
|
for name, stream := range streams {
|
||||||
|
sources[name] = stream.Sources()
|
||||||
|
}
|
||||||
|
streamsMu.Unlock()
|
||||||
|
return sources
|
||||||
}
|
}
|
||||||
|
|
||||||
func Delete(id string) {
|
|
||||||
delete(streams, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
var log zerolog.Logger
|
|
||||||
var streams = map[string]*Stream{}
|
|
||||||
var streamsMu sync.Mutex
|
|
||||||
|
|||||||
@@ -11,13 +11,15 @@ If an external connection via STUN is used:
|
|||||||
|
|
||||||
- Uses [UDP hole punching](https://en.wikipedia.org/wiki/UDP_hole_punching) technology to bypass NAT even if you not open your server to the World
|
- Uses [UDP hole punching](https://en.wikipedia.org/wiki/UDP_hole_punching) technology to bypass NAT even if you not open your server to the World
|
||||||
- For about 20% of users, the techology will not work because of the [Symmetric NAT](https://tomchen.github.io/symmetric-nat-test/)
|
- For about 20% of users, the techology will not work because of the [Symmetric NAT](https://tomchen.github.io/symmetric-nat-test/)
|
||||||
- UDP is not suitable for transmitting 2K and 4K high bitrate video over open networks because of the high loss rate
|
- UDP is not suitable for transmitting 2K and 4K high bitrate video over open networks because of the high loss rate:
|
||||||
|
- https://habr.com/ru/companies/flashphoner/articles/480006/
|
||||||
|
- https://www.youtube.com/watch?v=FXVg2ckuKfs
|
||||||
|
|
||||||
## Default config
|
## Default config
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
webrtc:
|
webrtc:
|
||||||
listen: ":8555/tcp"
|
listen: ":8555"
|
||||||
ice_servers:
|
ice_servers:
|
||||||
- urls: [ "stun:stun.l.google.com:19302" ]
|
- urls: [ "stun:stun.l.google.com:19302" ]
|
||||||
```
|
```
|
||||||
@@ -29,7 +31,7 @@ webrtc:
|
|||||||
```yaml
|
```yaml
|
||||||
webrtc:
|
webrtc:
|
||||||
# fix local TCP or UDP or both ports for WebRTC media
|
# fix local TCP or UDP or both ports for WebRTC media
|
||||||
listen: ":8555/tcp" # address of your local server
|
listen: ":8555" # address of your local server
|
||||||
|
|
||||||
# add additional host candidates manually
|
# add additional host candidates manually
|
||||||
# order is important, the first will have a higher priority
|
# order is important, the first will have a higher priority
|
||||||
@@ -54,16 +56,19 @@ webrtc:
|
|||||||
# use `candidates: []` to remove all auto discovery candidates
|
# use `candidates: []` to remove all auto discovery candidates
|
||||||
candidates: [ 192.168.1.123 ]
|
candidates: [ 192.168.1.123 ]
|
||||||
|
|
||||||
|
# enable localhost candidates
|
||||||
|
loopback: true
|
||||||
|
|
||||||
# list of network types to be used for connection
|
# list of network types to be used for connection
|
||||||
# including candidates from the `listen` option
|
# including candidates from the `listen` option
|
||||||
networks: [ udp4, udp6, tcp4, tcp6 ]
|
networks: [ udp4, udp6, tcp4, tcp6 ]
|
||||||
|
|
||||||
# list of interfaces to be used for connection
|
# list of interfaces to be used for connection
|
||||||
# not related to the `listen` option
|
# including interfaces from unspecified `listen` option (empty host)
|
||||||
interfaces: [ eno1 ]
|
interfaces: [ eno1 ]
|
||||||
|
|
||||||
# list of host IP-addresses to be used for connection
|
# list of host IP-addresses to be used for connection
|
||||||
# not related to the `listen` option
|
# including IPs from unspecified `listen` option (empty host)
|
||||||
ips: [ 192.168.1.123 ]
|
ips: [ 192.168.1.123 ]
|
||||||
|
|
||||||
# range for random UDP ports [min, max] to be used for connection
|
# range for random UDP ports [min, max] to be used for connection
|
||||||
@@ -71,14 +76,16 @@ webrtc:
|
|||||||
udp_ports: [ 50000, 50100 ]
|
udp_ports: [ 50000, 50100 ]
|
||||||
```
|
```
|
||||||
|
|
||||||
By default go2rtc uses **fixed TCP** port and multiple **random UDP** ports for each WebRTC connection - `listen: ":8555/tcp"`.
|
By default go2rtc uses **fixed TCP** port and **fixed UDP** ports for each **direct** WebRTC connection - `listen: ":8555"`.
|
||||||
|
|
||||||
You can set **fixed TCP** and **fixed UDP** port for all connections - `listen: ":8555"`. This may has lower performance, but it's your choice.
|
You can set **fixed TCP** and **random UDP** port for all connections - `listen: ":8555/tcp"`.
|
||||||
|
|
||||||
Don't know why, but you can disable TCP port and leave only random UDP ports - `listen: ""`.
|
Don't know why, but you can disable TCP port and leave only random UDP ports - `listen: ""`.
|
||||||
|
|
||||||
## Config filters
|
## Config filters
|
||||||
|
|
||||||
|
**Importan!** By default go2rtc exclude all Docker-like candidates (`172.16.0.0/12`). This can not be disabled.
|
||||||
|
|
||||||
Filters allow you to exclude unnecessary candidates. Extra candidates don't make your connection worse or better. But the wrong filter settings can break everything. Skip this setting if you don't understand it.
|
Filters allow you to exclude unnecessary candidates. Extra candidates don't make your connection worse or better. But the wrong filter settings can break everything. Skip this setting if you don't understand it.
|
||||||
|
|
||||||
For example, go2rtc is installed on the host system. And there are unnecessary interfaces. You can keep only the relevant via `interfaces` or `ips` options. You can also exclude IPv6 candidates if your server supports them but your home network does not.
|
For example, go2rtc is installed on the host system. And there are unnecessary interfaces. You can keep only the relevant via `interfaces` or `ips` options. You can also exclude IPv6 candidates if your server supports them but your home network does not.
|
||||||
@@ -97,8 +104,6 @@ For example, go2rtc inside closed docker container (ex. [Frigate](https://frigat
|
|||||||
webrtc:
|
webrtc:
|
||||||
listen: ":8555" # use fixed TCP and UDP ports
|
listen: ":8555" # use fixed TCP and UDP ports
|
||||||
candidates: [ 192.168.1.2:8555 ] # add manual host candidate (use docker port forwarding)
|
candidates: [ 192.168.1.2:8555 ] # add manual host candidate (use docker port forwarding)
|
||||||
filters:
|
|
||||||
candidates: [] # skip all internal docker candidates
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Userful links
|
## Userful links
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/xnet"
|
||||||
pion "github.com/pion/webrtc/v3"
|
pion "github.com/pion/webrtc/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,6 +74,11 @@ func FilterCandidate(candidate *pion.ICECandidate) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove any Docker-like IP from candidates
|
||||||
|
if ip := net.ParseIP(candidate.Address); ip != nil && xnet.Docker.Contains(ip) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// host candidate should be in the hosts list
|
// host candidate should be in the hosts list
|
||||||
if candidate.Typ == pion.ICECandidateTypeHost && filters.Candidates != nil {
|
if candidate.Typ == pion.ICECandidateTypeHost && filters.Candidates != nil {
|
||||||
if !core.Contains(filters.Candidates, candidate.Address) {
|
if !core.Contains(filters.Candidates, candidate.Address) {
|
||||||
|
|||||||
@@ -41,9 +41,11 @@ func streamsHandler(rawURL string) (core.Producer, error) {
|
|||||||
// https://aws.amazon.com/kinesis/video-streams/
|
// https://aws.amazon.com/kinesis/video-streams/
|
||||||
// https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html
|
// https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html
|
||||||
// https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc
|
// https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc
|
||||||
return kinesisClient(rawURL, query, "webrtc/kinesis")
|
return kinesisClient(rawURL, query, "webrtc/kinesis", nil)
|
||||||
} else if format == "openipc" {
|
} else if format == "openipc" {
|
||||||
return openIPCClient(rawURL, query)
|
return openIPCClient(rawURL, query)
|
||||||
|
} else if format == "switchbot" {
|
||||||
|
return switchbotClient(rawURL, query)
|
||||||
} else {
|
} else {
|
||||||
return go2rtcClient(rawURL)
|
return go2rtcClient(rawURL)
|
||||||
}
|
}
|
||||||
@@ -54,6 +56,8 @@ func streamsHandler(rawURL string) (core.Producer, error) {
|
|||||||
} else if format == "wyze" {
|
} else if format == "wyze" {
|
||||||
// https://github.com/mrlt8/docker-wyze-bridge
|
// https://github.com/mrlt8/docker-wyze-bridge
|
||||||
return wyzeClient(rawURL)
|
return wyzeClient(rawURL)
|
||||||
|
} else if format == "creality" {
|
||||||
|
return crealityClient(rawURL)
|
||||||
} else {
|
} else {
|
||||||
return whepClient(rawURL)
|
return whepClient(rawURL)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package webrtc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://github.com/AlexxIT/go2rtc/issues/1600
|
||||||
|
func crealityClient(url string) (core.Producer, error) {
|
||||||
|
pc, err := PeerConnection(true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prod := webrtc.NewConn(pc)
|
||||||
|
prod.FormatName = "webrtc/creality"
|
||||||
|
prod.Mode = core.ModeActiveProducer
|
||||||
|
prod.Protocol = "http"
|
||||||
|
prod.URL = url
|
||||||
|
|
||||||
|
medias := []*core.Media{
|
||||||
|
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||||
|
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: return webrtc.SessionDescription
|
||||||
|
offer, err := prod.CreateCompleteOffer(medias)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := offerToB64(offer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "plain/text")
|
||||||
|
|
||||||
|
// TODO: change http.DefaultClient settings
|
||||||
|
client := http.Client{Timeout: time.Second * 5000}
|
||||||
|
defer client.CloseIdleConnections()
|
||||||
|
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer, err := answerFromB64(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = prod.SetAnswer(answer); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return prod, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func offerToB64(sdp string) (io.Reader, error) {
|
||||||
|
// JS object
|
||||||
|
v := map[string]string{
|
||||||
|
"type": "offer",
|
||||||
|
"sdp": sdp,
|
||||||
|
}
|
||||||
|
|
||||||
|
// bytes
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// base64, why? who knows...
|
||||||
|
s := base64.StdEncoding.EncodeToString(b)
|
||||||
|
|
||||||
|
return strings.NewReader(s), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func answerFromB64(r io.Reader) (string, error) {
|
||||||
|
// base64
|
||||||
|
b, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// bytes
|
||||||
|
if b, err = base64.StdEncoding.DecodeString(string(b)); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// JS object
|
||||||
|
var v map[string]string
|
||||||
|
if err = json.Unmarshal(b, &v); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// string "v=0..."
|
||||||
|
return v["sdp"], nil
|
||||||
|
}
|
||||||
@@ -34,7 +34,10 @@ func (k kinesisResponse) String() string {
|
|||||||
return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload)
|
return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func kinesisClient(rawURL string, query url.Values, format string) (core.Producer, error) {
|
func kinesisClient(
|
||||||
|
rawURL string, query url.Values, format string,
|
||||||
|
sdpOffer func(prod *webrtc.Conn, query url.Values) (any, error),
|
||||||
|
) (core.Producer, error) {
|
||||||
// 1. Connect to signalign server
|
// 1. Connect to signalign server
|
||||||
conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)
|
conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -108,23 +111,33 @@ func kinesisClient(rawURL string, query url.Values, format string) (core.Produce
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
var payload any
|
||||||
|
|
||||||
|
if sdpOffer == nil {
|
||||||
medias := []*core.Media{
|
medias := []*core.Media{
|
||||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||||
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Create offer
|
// 4. Create offer
|
||||||
offer, err := prod.CreateOffer(medias)
|
var offer string
|
||||||
if err != nil {
|
if offer, err = prod.CreateOffer(medias); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Send offer
|
// 5. Send offer
|
||||||
req.Action = "SDP_OFFER"
|
payload = pion.SessionDescription{
|
||||||
req.Payload, _ = json.Marshal(pion.SessionDescription{
|
|
||||||
Type: pion.SDPTypeOffer,
|
Type: pion.SDPTypeOffer,
|
||||||
SDP: offer,
|
SDP: offer,
|
||||||
})
|
}
|
||||||
|
} else {
|
||||||
|
if payload, err = sdpOffer(prod, query); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Action = "SDP_OFFER"
|
||||||
|
req.Payload, _ = json.Marshal(payload)
|
||||||
if err = conn.WriteJSON(req); err != nil {
|
if err = conn.WriteJSON(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -218,5 +231,5 @@ func wyzeClient(rawURL string) (core.Producer, error) {
|
|||||||
"ice_servers": []string{string(kvs.Servers)},
|
"ice_servers": []string{string(kvs.Servers)},
|
||||||
}
|
}
|
||||||
|
|
||||||
return kinesisClient(kvs.URL, query, "webrtc/wyze")
|
return kinesisClient(kvs.URL, query, "webrtc/wyze", nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package webrtc
|
package webrtc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -62,8 +63,8 @@ func syncHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// 2. application/sdp - receive/response SDP via WebRTC-HTTP Egress Protocol (WHEP)
|
// 2. application/sdp - receive/response SDP via WebRTC-HTTP Egress Protocol (WHEP)
|
||||||
// 3. other - receive/response raw SDP
|
// 3. other - receive/response raw SDP
|
||||||
func outputWebRTC(w http.ResponseWriter, r *http.Request) {
|
func outputWebRTC(w http.ResponseWriter, r *http.Request) {
|
||||||
url := r.URL.Query().Get("src")
|
u := r.URL.Query().Get("src")
|
||||||
stream := streams.Get(url)
|
stream := streams.Get(u)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
@@ -87,6 +88,21 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
offer = desc.SDP
|
offer = desc.SDP
|
||||||
|
|
||||||
|
case "application/x-www-form-urlencoded":
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
offerB64 := r.Form.Get("data")
|
||||||
|
b, err := base64.StdEncoding.DecodeString(offerB64)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
offer = string(b)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -124,6 +140,11 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
err = json.NewEncoder(w).Encode(v)
|
err = json.NewEncoder(w).Encode(v)
|
||||||
|
|
||||||
|
case "application/x-www-form-urlencoded":
|
||||||
|
w.Header().Set("Content-Type", mediaType)
|
||||||
|
answerB64 := base64.StdEncoding.EncodeToString([]byte(answer))
|
||||||
|
_, err = w.Write([]byte(answerB64))
|
||||||
|
|
||||||
case MimeSDP:
|
case MimeSDP:
|
||||||
w.Header().Set("Content-Type", mediaType)
|
w.Header().Set("Content-Type", mediaType)
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package webrtc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func switchbotClient(rawURL string, query url.Values) (core.Producer, error) {
|
||||||
|
return kinesisClient(rawURL, query, "webrtc/switchbot", func(prod *webrtc.Conn, query url.Values) (any, error) {
|
||||||
|
medias := []*core.Media{
|
||||||
|
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||||
|
}
|
||||||
|
|
||||||
|
offer, err := prod.CreateOffer(medias)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v := struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
SDP string `json:"sdp"`
|
||||||
|
Resolution int `json:"resolution"`
|
||||||
|
PlayType int `json:"play_type"`
|
||||||
|
}{
|
||||||
|
Type: "offer",
|
||||||
|
SDP: offer,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch query.Get("resolution") {
|
||||||
|
case "hd":
|
||||||
|
v.Resolution = 0
|
||||||
|
case "sd":
|
||||||
|
v.Resolution = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ func Init() {
|
|||||||
} `yaml:"webrtc"`
|
} `yaml:"webrtc"`
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Mod.Listen = ":8555/tcp"
|
cfg.Mod.Listen = ":8555"
|
||||||
cfg.Mod.IceServers = []pion.ICEServer{
|
cfg.Mod.IceServers = []pion.ICEServer{
|
||||||
{URLs: []string{"stun:stun.l.google.com:19302"}},
|
{URLs: []string{"stun:stun.l.google.com:19302"}},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package webtorrent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/webtorrent"
|
"github.com/AlexxIT/go2rtc/pkg/webtorrent"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"net/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var upgrader *websocket.Upgrader
|
var upgrader *websocket.Upgrader
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app.Version = "1.9.8"
|
app.Version = "1.9.9"
|
||||||
|
|
||||||
// 1. Core modules: app, api/ws, streams
|
// 1. Core modules: app, api/ws, streams
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
||||||
|
|
||||||
|
if media.Direction != "" {
|
||||||
|
md.WithPropertyAttribute(media.Direction)
|
||||||
|
}
|
||||||
|
|
||||||
if media.ID != "" {
|
if media.ID != "" {
|
||||||
md.WithValueAttribute("control", media.ID)
|
md.WithValueAttribute("control", media.ID)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-4
@@ -140,6 +140,7 @@ func (s *Sender) Start() {
|
|||||||
s.done = make(chan struct{})
|
s.done = make(chan struct{})
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
// for range on nil chan is OK
|
||||||
for packet := range s.buf {
|
for packet := range s.buf {
|
||||||
s.Output(packet)
|
s.Output(packet)
|
||||||
}
|
}
|
||||||
@@ -148,7 +149,7 @@ func (s *Sender) Start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Sender) Wait() {
|
func (s *Sender) Wait() {
|
||||||
if done := s.done; s.done != nil {
|
if done := s.done; done != nil {
|
||||||
<-done
|
<-done
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,10 +166,12 @@ func (s *Sender) State() string {
|
|||||||
|
|
||||||
func (s *Sender) Close() {
|
func (s *Sender) Close() {
|
||||||
// close buffer if exists
|
// close buffer if exists
|
||||||
if buf := s.buf; buf != nil {
|
s.mu.Lock()
|
||||||
s.buf = nil
|
if s.buf != nil {
|
||||||
defer close(buf)
|
close(s.buf) // exit from for range loop
|
||||||
|
s.buf = nil // prevent writing to closed chan
|
||||||
}
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
s.Node.Close()
|
s.Node.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSenser(t *testing.T) {
|
||||||
|
recv := make(chan *Packet) // blocking receiver
|
||||||
|
|
||||||
|
sender := NewSender(nil, &Codec{})
|
||||||
|
sender.Output = func(packet *Packet) {
|
||||||
|
recv <- packet
|
||||||
|
}
|
||||||
|
require.Equal(t, "new", sender.State())
|
||||||
|
|
||||||
|
sender.Start()
|
||||||
|
require.Equal(t, "connected", sender.State())
|
||||||
|
|
||||||
|
sender.Input(&Packet{})
|
||||||
|
sender.Input(&Packet{})
|
||||||
|
|
||||||
|
require.Equal(t, 2, sender.Packets)
|
||||||
|
require.Equal(t, 0, sender.Drops)
|
||||||
|
|
||||||
|
// important to read one before close
|
||||||
|
// because goroutine in Start() can run with nil chan
|
||||||
|
// it's OK in real life, but bad for test
|
||||||
|
_, ok := <-recv
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
sender.Close()
|
||||||
|
require.Equal(t, "closed", sender.State())
|
||||||
|
|
||||||
|
sender.Input(&Packet{})
|
||||||
|
|
||||||
|
require.Equal(t, 2, sender.Packets)
|
||||||
|
require.Equal(t, 1, sender.Drops)
|
||||||
|
|
||||||
|
// read 2nd
|
||||||
|
_, ok = <-recv
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
// read 3rd
|
||||||
|
select {
|
||||||
|
case <-recv:
|
||||||
|
ok = true
|
||||||
|
default:
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
require.False(t, ok)
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package h265
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package h265
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
)
|
)
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -2,12 +2,21 @@ package camera
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestNilCharacter(t *testing.T) {
|
||||||
|
var res SetupEndpoints
|
||||||
|
char := &hap.Character{}
|
||||||
|
err := char.ReadTLV8(&res)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
require.NotNil(t, strings.Contains(err.Error(), "can't read value"))
|
||||||
|
}
|
||||||
|
|
||||||
type testTLV8 struct {
|
type testTLV8 struct {
|
||||||
name string
|
name string
|
||||||
value string
|
value string
|
||||||
|
|||||||
+10
-3
@@ -3,6 +3,7 @@ package hap
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@@ -126,11 +127,17 @@ func (c *Character) Write(v any) (err error) {
|
|||||||
|
|
||||||
// ReadTLV8 value to right struct
|
// ReadTLV8 value to right struct
|
||||||
func (c *Character) ReadTLV8(v any) (err error) {
|
func (c *Character) ReadTLV8(v any) (err error) {
|
||||||
return tlv8.UnmarshalBase64(c.Value.(string), v)
|
if s, ok := c.Value.(string); ok {
|
||||||
|
return tlv8.UnmarshalBase64(s, v)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("hap: can't read value: %v", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Character) ReadBool() bool {
|
func (c *Character) ReadBool() (bool, error) {
|
||||||
return c.Value.(bool)
|
if v, ok := c.Value.(bool); ok {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("hap: can't read value: %v", c.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Character) String() string {
|
func (c *Character) String() string {
|
||||||
|
|||||||
@@ -235,3 +235,18 @@ func WriteResponse(w *bufio.Writer, statusCode int, contentType string, body []b
|
|||||||
}
|
}
|
||||||
return w.Flush()
|
return w.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WriteBackoff(rw *bufio.ReadWriter) error {
|
||||||
|
plainM2 := struct {
|
||||||
|
State byte `tlv8:"6"`
|
||||||
|
Error byte `tlv8:"7"`
|
||||||
|
}{
|
||||||
|
State: StateM2,
|
||||||
|
Error: 3, // BackoffError
|
||||||
|
}
|
||||||
|
body, err := tlv8.Marshal(plainM2)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body)
|
||||||
|
}
|
||||||
|
|||||||
+2
-1
@@ -2,8 +2,9 @@ package hass
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type API struct {
|
type API struct {
|
||||||
|
|||||||
+12
-37
@@ -10,10 +10,14 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/xnet"
|
||||||
"github.com/miekg/dns" // awesome library for parsing mDNS records
|
"github.com/miekg/dns" // awesome library for parsing mDNS records
|
||||||
)
|
)
|
||||||
|
|
||||||
const ServiceHAP = "_hap._tcp.local." // HomeKit Accessory Protocol
|
const (
|
||||||
|
ServiceDNSSD = "_services._dns-sd._udp.local."
|
||||||
|
ServiceHAP = "_hap._tcp.local." // HomeKit Accessory Protocol
|
||||||
|
)
|
||||||
|
|
||||||
type ServiceEntry struct {
|
type ServiceEntry struct {
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
@@ -153,6 +157,7 @@ type Browser struct {
|
|||||||
Service string
|
Service string
|
||||||
|
|
||||||
Addr net.Addr
|
Addr net.Addr
|
||||||
|
Nets []*net.IPNet
|
||||||
Recv net.PacketConn
|
Recv net.PacketConn
|
||||||
Sends []net.PacketConn
|
Sends []net.PacketConn
|
||||||
|
|
||||||
@@ -165,7 +170,9 @@ type Browser struct {
|
|||||||
// Receiver will get multicast responses on senders requests.
|
// Receiver will get multicast responses on senders requests.
|
||||||
func (b *Browser) ListenMulticastUDP() error {
|
func (b *Browser) ListenMulticastUDP() error {
|
||||||
// 1. Collect IPv4 interfaces
|
// 1. Collect IPv4 interfaces
|
||||||
ip4s, err := InterfacesIP4()
|
nets, err := xnet.IPNets(func(ip net.IP) bool {
|
||||||
|
return !xnet.Docker.Contains(ip)
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -182,11 +189,12 @@ func (b *Browser) ListenMulticastUDP() error {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
for _, ip4 := range ip4s {
|
for _, ipn := range nets {
|
||||||
conn, err := lc1.ListenPacket(ctx, "udp4", ip4.String()+":5353") // same port important
|
conn, err := lc1.ListenPacket(ctx, "udp4", ipn.IP.String()+":5353") // same port important
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
b.Nets = append(b.Nets, ipn)
|
||||||
b.Sends = append(b.Sends, conn)
|
b.Sends = append(b.Sends, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,36 +372,3 @@ func NewServiceEntries(msg *dns.Msg, ip net.IP) (entries []*ServiceEntry) {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func InterfacesIP4() ([]net.IP, error) {
|
|
||||||
intfs, err := net.Interfaces()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ips []net.IP
|
|
||||||
|
|
||||||
loop:
|
|
||||||
for _, intf := range intfs {
|
|
||||||
if intf.Flags&net.FlagUp == 0 || intf.Flags&net.FlagLoopback != 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs, err := intf.Addrs()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, addr := range addrs {
|
|
||||||
switch v := addr.(type) {
|
|
||||||
case *net.IPNet:
|
|
||||||
if ip := v.IP.To4(); ip != nil {
|
|
||||||
ips = append(ips, ip)
|
|
||||||
continue loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ips, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package mdns
|
package mdns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDiscovery(t *testing.T) {
|
func TestDiscovery(t *testing.T) {
|
||||||
|
|||||||
+72
-67
@@ -20,7 +20,11 @@ func Serve(service string, entries []*ServiceEntry) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Browser) Serve(entries []*ServiceEntry) error {
|
func (b *Browser) Serve(entries []*ServiceEntry) error {
|
||||||
var msg dns.Msg
|
names := make(map[string]*ServiceEntry, len(entries))
|
||||||
|
for _, entry := range entries {
|
||||||
|
name := entry.name() + "." + b.Service
|
||||||
|
names[name] = entry
|
||||||
|
}
|
||||||
|
|
||||||
buf := make([]byte, 1500)
|
buf := make([]byte, 1500)
|
||||||
for {
|
for {
|
||||||
@@ -29,51 +33,86 @@ func (b *Browser) Serve(entries []*ServiceEntry) error {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = msg.Unpack(buf[:n]); err != nil {
|
var req dns.Msg // request
|
||||||
|
if err = req.Unpack(buf[:n]); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !HasQuestionPTP(&msg, b.Service) {
|
// skip messages without Questions
|
||||||
|
if req.Question == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteIP := addr.(*net.UDPAddr).IP
|
remoteIP := addr.(*net.UDPAddr).IP
|
||||||
localIP := MatchLocalIP(remoteIP)
|
localIP := b.MatchLocalIP(remoteIP)
|
||||||
|
|
||||||
|
// skip messages from unknown networks (can be docker network)
|
||||||
if localIP == nil {
|
if localIP == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
answer, err := NewDNSAnswer(entries, b.Service, localIP).Pack()
|
var res dns.Msg // response
|
||||||
|
for _, q := range req.Question {
|
||||||
|
if q.Qtype != dns.TypePTR || q.Qclass != dns.ClassINET {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if q.Name == ServiceDNSSD {
|
||||||
|
AppendDNSSD(&res, b.Service)
|
||||||
|
} else if q.Name == b.Service {
|
||||||
|
for _, entry := range entries {
|
||||||
|
AppendEntry(&res, entry, b.Service, localIP)
|
||||||
|
}
|
||||||
|
} else if entry, ok := names[q.Name]; ok {
|
||||||
|
AppendEntry(&res, entry, b.Service, localIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Answer == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
res.MsgHdr.Response = true
|
||||||
|
res.MsgHdr.Authoritative = true
|
||||||
|
|
||||||
|
data, err := res.Pack()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, send := range b.Sends {
|
for _, send := range b.Sends {
|
||||||
_, _ = send.WriteTo(answer, MulticastAddr)
|
_, _ = send.WriteTo(data, MulticastAddr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func HasQuestionPTP(msg *dns.Msg, name string) bool {
|
func (b *Browser) MatchLocalIP(remote net.IP) net.IP {
|
||||||
for _, q := range msg.Question {
|
for _, ipn := range b.Nets {
|
||||||
if q.Qtype == dns.TypePTR && q.Name == name {
|
if ipn.Contains(remote) {
|
||||||
return true
|
return ipn.IP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDNSAnswer(entries []*ServiceEntry, service string, ip net.IP) *dns.Msg {
|
func AppendDNSSD(msg *dns.Msg, service string) {
|
||||||
msg := dns.Msg{
|
msg.Answer = append(
|
||||||
MsgHdr: dns.MsgHdr{
|
msg.Answer,
|
||||||
Response: true,
|
&dns.PTR{
|
||||||
Authoritative: true,
|
Hdr: dns.RR_Header{
|
||||||
|
Name: ServiceDNSSD, // _services._dns-sd._udp.local.
|
||||||
|
Rrtype: dns.TypePTR, // 12
|
||||||
|
Class: dns.ClassINET, // 1
|
||||||
|
Ttl: 4500,
|
||||||
},
|
},
|
||||||
}
|
Ptr: service, // _home-assistant._tcp.local.
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
for _, entry := range entries {
|
func AppendEntry(msg *dns.Msg, entry *ServiceEntry, service string, ip net.IP) {
|
||||||
ptrName := entry.name() + "." + service
|
ptrName := entry.name() + "." + service
|
||||||
srvName := entry.name() + ".local."
|
srvName := entry.name() + ".local."
|
||||||
|
|
||||||
@@ -81,77 +120,43 @@ func NewDNSAnswer(entries []*ServiceEntry, service string, ip net.IP) *dns.Msg {
|
|||||||
msg.Answer,
|
msg.Answer,
|
||||||
&dns.PTR{
|
&dns.PTR{
|
||||||
Hdr: dns.RR_Header{
|
Hdr: dns.RR_Header{
|
||||||
Name: service,
|
Name: service, // _home-assistant._tcp.local.
|
||||||
Rrtype: dns.TypePTR,
|
Rrtype: dns.TypePTR, // 12
|
||||||
Class: dns.ClassINET,
|
Class: dns.ClassINET, // 1
|
||||||
Ttl: 4500,
|
Ttl: 4500,
|
||||||
},
|
},
|
||||||
Ptr: ptrName,
|
Ptr: ptrName, // Home\ Assistant._home-assistant._tcp.local.
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
msg.Extra = append(
|
msg.Extra = append(
|
||||||
msg.Extra,
|
msg.Extra,
|
||||||
&dns.TXT{
|
&dns.TXT{
|
||||||
Hdr: dns.RR_Header{
|
Hdr: dns.RR_Header{
|
||||||
Name: ptrName,
|
Name: ptrName, // Home\ Assistant._home-assistant._tcp.local.
|
||||||
Rrtype: dns.TypeTXT,
|
Rrtype: dns.TypeTXT, // 16
|
||||||
Class: ClassCacheFlush,
|
Class: ClassCacheFlush, // 32769
|
||||||
Ttl: 4500,
|
Ttl: 4500,
|
||||||
},
|
},
|
||||||
Txt: entry.TXT(),
|
Txt: entry.TXT(),
|
||||||
},
|
},
|
||||||
&dns.SRV{
|
&dns.SRV{
|
||||||
Hdr: dns.RR_Header{
|
Hdr: dns.RR_Header{
|
||||||
Name: ptrName,
|
Name: ptrName, // Home\ Assistant._home-assistant._tcp.local.
|
||||||
Rrtype: dns.TypeSRV,
|
Rrtype: dns.TypeSRV, // 33
|
||||||
Class: ClassCacheFlush,
|
Class: ClassCacheFlush, // 32769
|
||||||
Ttl: 120,
|
Ttl: 120,
|
||||||
Rdlength: 0,
|
|
||||||
},
|
},
|
||||||
Port: entry.Port,
|
Port: entry.Port, // 8123
|
||||||
Target: srvName,
|
Target: srvName, // 963f1fa82b7142809711cebe7c826322.local.
|
||||||
},
|
},
|
||||||
&dns.A{
|
&dns.A{
|
||||||
Hdr: dns.RR_Header{
|
Hdr: dns.RR_Header{
|
||||||
Name: srvName,
|
Name: srvName, // 963f1fa82b7142809711cebe7c826322.local.
|
||||||
Rrtype: dns.TypeA,
|
Rrtype: dns.TypeA, // 1
|
||||||
Class: ClassCacheFlush,
|
Class: ClassCacheFlush, // 32769
|
||||||
Ttl: 120,
|
Ttl: 120,
|
||||||
Rdlength: 0,
|
|
||||||
},
|
},
|
||||||
A: ip,
|
A: ip,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
return &msg
|
|
||||||
}
|
|
||||||
|
|
||||||
func MatchLocalIP(remote net.IP) net.IP {
|
|
||||||
intfs, err := net.Interfaces()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, intf := range intfs {
|
|
||||||
if intf.Flags&net.FlagUp == 0 || intf.Flags&net.FlagLoopback != 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs, err := intf.Addrs()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, addr := range addrs {
|
|
||||||
switch v := addr.(type) {
|
|
||||||
case *net.IPNet:
|
|
||||||
if local := v.IP.To4(); local != nil && v.Contains(remote) {
|
|
||||||
return local
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -3,10 +3,11 @@ package mjpeg
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/pion/rtp"
|
|
||||||
"image"
|
"image"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RTPDepay(handlerFunc core.HandlerFunc) core.HandlerFunc {
|
func RTPDepay(handlerFunc core.HandlerFunc) core.HandlerFunc {
|
||||||
|
|||||||
+147
-14
@@ -17,9 +17,15 @@ type API struct {
|
|||||||
|
|
||||||
StreamProjectID string
|
StreamProjectID string
|
||||||
StreamDeviceID string
|
StreamDeviceID string
|
||||||
StreamSessionID string
|
|
||||||
StreamExpiresAt time.Time
|
StreamExpiresAt time.Time
|
||||||
|
|
||||||
|
// WebRTC
|
||||||
|
StreamSessionID string
|
||||||
|
|
||||||
|
// RTSP
|
||||||
|
StreamToken string
|
||||||
|
StreamExtensionToken string
|
||||||
|
|
||||||
extendTimer *time.Timer
|
extendTimer *time.Timer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +33,12 @@ type Auth struct {
|
|||||||
AccessToken string
|
AccessToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeviceInfo struct {
|
||||||
|
Name string
|
||||||
|
DeviceID string
|
||||||
|
Protocols []string
|
||||||
|
}
|
||||||
|
|
||||||
var cache = map[string]*API{}
|
var cache = map[string]*API{}
|
||||||
var cacheMu sync.Mutex
|
var cacheMu sync.Mutex
|
||||||
|
|
||||||
@@ -80,7 +92,7 @@ func NewAPI(clientID, clientSecret, refreshToken string) (*API, error) {
|
|||||||
return api, nil
|
return api, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) GetDevices(projectID string) (map[string]string, error) {
|
func (a *API) GetDevices(projectID string) ([]DeviceInfo, error) {
|
||||||
uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectID + "/devices"
|
uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectID + "/devices"
|
||||||
req, err := http.NewRequest("GET", uri, nil)
|
req, err := http.NewRequest("GET", uri, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -108,24 +120,30 @@ func (a *API) GetDevices(projectID string) (map[string]string, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
devices := map[string]string{}
|
devices := make([]DeviceInfo, 0, len(resv.Devices))
|
||||||
|
|
||||||
for _, device := range resv.Devices {
|
for _, device := range resv.Devices {
|
||||||
|
// only RTSP and WEB_RTC available (both supported)
|
||||||
if len(device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols) == 0 {
|
if len(device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols[0] != "WEB_RTC" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
i := strings.LastIndexByte(device.Name, '/')
|
i := strings.LastIndexByte(device.Name, '/')
|
||||||
if i <= 0 {
|
if i <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
name := device.Traits.SdmDevicesTraitsInfo.CustomName
|
name := device.Traits.SdmDevicesTraitsInfo.CustomName
|
||||||
devices[name] = device.Name[i+1:]
|
// Devices configured through the Nest app use the container/room name as opposed to the customName trait
|
||||||
|
if name == "" && len(device.ParentRelations) > 0 {
|
||||||
|
name = device.ParentRelations[0].DisplayName
|
||||||
|
}
|
||||||
|
|
||||||
|
devices = append(devices, DeviceInfo{
|
||||||
|
Name: name,
|
||||||
|
DeviceID: device.Name[i+1:],
|
||||||
|
Protocols: device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return devices, nil
|
return devices, nil
|
||||||
@@ -190,11 +208,20 @@ func (a *API) ExtendStream() error {
|
|||||||
var reqv struct {
|
var reqv struct {
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
Params struct {
|
Params struct {
|
||||||
MediaSessionID string `json:"mediaSessionId"`
|
MediaSessionID string `json:"mediaSessionId,omitempty"`
|
||||||
|
StreamExtensionToken string `json:"streamExtensionToken,omitempty"`
|
||||||
} `json:"params"`
|
} `json:"params"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.StreamToken != "" {
|
||||||
|
// RTSP
|
||||||
|
reqv.Command = "sdm.devices.commands.CameraLiveStream.ExtendRtspStream"
|
||||||
|
reqv.Params.StreamExtensionToken = a.StreamExtensionToken
|
||||||
|
} else {
|
||||||
|
// WebRTC
|
||||||
reqv.Command = "sdm.devices.commands.CameraLiveStream.ExtendWebRtcStream"
|
reqv.Command = "sdm.devices.commands.CameraLiveStream.ExtendWebRtcStream"
|
||||||
reqv.Params.MediaSessionID = a.StreamSessionID
|
reqv.Params.MediaSessionID = a.StreamSessionID
|
||||||
|
}
|
||||||
|
|
||||||
b, err := json.Marshal(reqv)
|
b, err := json.Marshal(reqv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -225,6 +252,8 @@ func (a *API) ExtendStream() error {
|
|||||||
Results struct {
|
Results struct {
|
||||||
ExpiresAt time.Time `json:"expiresAt"`
|
ExpiresAt time.Time `json:"expiresAt"`
|
||||||
MediaSessionID string `json:"mediaSessionId"`
|
MediaSessionID string `json:"mediaSessionId"`
|
||||||
|
StreamExtensionToken string `json:"streamExtensionToken"`
|
||||||
|
StreamToken string `json:"streamToken"`
|
||||||
} `json:"results"`
|
} `json:"results"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,6 +263,111 @@ func (a *API) ExtendStream() error {
|
|||||||
|
|
||||||
a.StreamSessionID = resv.Results.MediaSessionID
|
a.StreamSessionID = resv.Results.MediaSessionID
|
||||||
a.StreamExpiresAt = resv.Results.ExpiresAt
|
a.StreamExpiresAt = resv.Results.ExpiresAt
|
||||||
|
a.StreamExtensionToken = resv.Results.StreamExtensionToken
|
||||||
|
a.StreamToken = resv.Results.StreamToken
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *API) GenerateRtspStream(projectID, deviceID string) (string, error) {
|
||||||
|
var reqv struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
Params struct{} `json:"params"`
|
||||||
|
}
|
||||||
|
reqv.Command = "sdm.devices.commands.CameraLiveStream.GenerateRtspStream"
|
||||||
|
|
||||||
|
b, err := json.Marshal(reqv)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" +
|
||||||
|
projectID + "/devices/" + deviceID + ":executeCommand"
|
||||||
|
req, err := http.NewRequest("POST", uri, bytes.NewReader(b))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+a.Token)
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: time.Second * 5000}
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return "", errors.New("nest: wrong status: " + res.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resv struct {
|
||||||
|
Results struct {
|
||||||
|
StreamURLs map[string]string `json:"streamUrls"`
|
||||||
|
StreamExtensionToken string `json:"streamExtensionToken"`
|
||||||
|
StreamToken string `json:"streamToken"`
|
||||||
|
ExpiresAt time.Time `json:"expiresAt"`
|
||||||
|
} `json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.NewDecoder(res.Body).Decode(&resv); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := resv.Results.StreamURLs["rtspUrl"]; !ok {
|
||||||
|
return "", errors.New("nest: failed to generate rtsp url")
|
||||||
|
}
|
||||||
|
|
||||||
|
a.StreamProjectID = projectID
|
||||||
|
a.StreamDeviceID = deviceID
|
||||||
|
a.StreamToken = resv.Results.StreamToken
|
||||||
|
a.StreamExtensionToken = resv.Results.StreamExtensionToken
|
||||||
|
a.StreamExpiresAt = resv.Results.ExpiresAt
|
||||||
|
|
||||||
|
return resv.Results.StreamURLs["rtspUrl"], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *API) StopRTSPStream() error {
|
||||||
|
if a.StreamProjectID == "" || a.StreamDeviceID == "" {
|
||||||
|
return errors.New("nest: tried to stop rtsp stream without a project or device ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
var reqv struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
Params struct {
|
||||||
|
StreamExtensionToken string `json:"streamExtensionToken"`
|
||||||
|
} `json:"params"`
|
||||||
|
}
|
||||||
|
reqv.Command = "sdm.devices.commands.CameraLiveStream.StopRtspStream"
|
||||||
|
reqv.Params.StreamExtensionToken = a.StreamExtensionToken
|
||||||
|
|
||||||
|
b, err := json.Marshal(reqv)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" +
|
||||||
|
a.StreamProjectID + "/devices/" + a.StreamDeviceID + ":executeCommand"
|
||||||
|
req, err := http.NewRequest("POST", uri, bytes.NewReader(b))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+a.Token)
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: time.Second * 5000}
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return errors.New("nest: wrong status: " + res.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.StreamProjectID = ""
|
||||||
|
a.StreamDeviceID = ""
|
||||||
|
a.StreamExtensionToken = ""
|
||||||
|
a.StreamToken = ""
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -266,10 +400,10 @@ type Device struct {
|
|||||||
//SdmDevicesTraitsCameraClipPreview struct {
|
//SdmDevicesTraitsCameraClipPreview struct {
|
||||||
//} `json:"sdm.devices.traits.CameraClipPreview"`
|
//} `json:"sdm.devices.traits.CameraClipPreview"`
|
||||||
} `json:"traits"`
|
} `json:"traits"`
|
||||||
//ParentRelations []struct {
|
ParentRelations []struct {
|
||||||
// Parent string `json:"parent"`
|
Parent string `json:"parent"`
|
||||||
// DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
//} `json:"parentRelations"`
|
} `json:"parentRelations"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) StartExtendStreamTimer() {
|
func (a *API) StartExtendStreamTimer() {
|
||||||
@@ -282,7 +416,6 @@ func (a *API) StartExtendStreamTimer() {
|
|||||||
duration = time.Until(a.StreamExpiresAt.Add(-30 * time.Second))
|
duration = time.Until(a.StreamExpiresAt.Add(-30 * time.Second))
|
||||||
a.extendTimer.Reset(duration)
|
a.extendTimer.Reset(duration)
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) StopExtendStreamTimer() {
|
func (a *API) StopExtendStreamTimer() {
|
||||||
|
|||||||
+71
-13
@@ -3,18 +3,25 @@ package nest
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||||
pion "github.com/pion/webrtc/v3"
|
pion "github.com/pion/webrtc/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type WebRTCClient struct {
|
||||||
conn *webrtc.Conn
|
conn *webrtc.Conn
|
||||||
api *API
|
api *API
|
||||||
}
|
}
|
||||||
|
|
||||||
func Dial(rawURL string) (*Client, error) {
|
type RTSPClient struct {
|
||||||
|
conn *rtsp.Conn
|
||||||
|
api *API
|
||||||
|
}
|
||||||
|
|
||||||
|
func Dial(rawURL string) (core.Producer, error) {
|
||||||
u, err := url.Parse(rawURL)
|
u, err := url.Parse(rawURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -36,6 +43,42 @@ func Dial(rawURL string) (*Client, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protocols := strings.Split(query.Get("protocols"), ",")
|
||||||
|
if len(protocols) > 0 && protocols[0] == "RTSP" {
|
||||||
|
return rtspConn(nestAPI, rawURL, projectID, deviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to WEB_RTC for backwards compataiility
|
||||||
|
return rtcConn(nestAPI, rawURL, projectID, deviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WebRTCClient) GetMedias() []*core.Media {
|
||||||
|
return c.conn.GetMedias()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WebRTCClient) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||||
|
return c.conn.GetTrack(media, codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WebRTCClient) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||||
|
return c.conn.AddTrack(media, codec, track)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WebRTCClient) Start() error {
|
||||||
|
c.api.StartExtendStreamTimer()
|
||||||
|
return c.conn.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WebRTCClient) Stop() error {
|
||||||
|
c.api.StopExtendStreamTimer()
|
||||||
|
return c.conn.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WebRTCClient) MarshalJSON() ([]byte, error) {
|
||||||
|
return c.conn.MarshalJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
func rtcConn(nestAPI *API, rawURL, projectID, deviceID string) (*WebRTCClient, error) {
|
||||||
rtcAPI, err := webrtc.NewAPI()
|
rtcAPI, err := webrtc.NewAPI()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -77,31 +120,46 @@ func Dial(rawURL string) (*Client, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Client{conn: conn, api: nestAPI}, nil
|
return &WebRTCClient{conn: conn, api: nestAPI}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetMedias() []*core.Media {
|
func rtspConn(nestAPI *API, rawURL, projectID, deviceID string) (*RTSPClient, error) {
|
||||||
return c.conn.GetMedias()
|
rtspURL, err := nestAPI.GenerateRtspStream(projectID, deviceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rtspClient := rtsp.NewClient(rtspURL)
|
||||||
|
if err := rtspClient.Dial(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rtspClient.Describe(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &RTSPClient{conn: rtspClient, api: nestAPI}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
func (c *RTSPClient) GetMedias() []*core.Media {
|
||||||
|
result := c.conn.GetMedias()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RTSPClient) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||||
return c.conn.GetTrack(media, codec)
|
return c.conn.GetTrack(media, codec)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
func (c *RTSPClient) Start() error {
|
||||||
return c.conn.AddTrack(media, codec, track)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Start() error {
|
|
||||||
c.api.StartExtendStreamTimer()
|
c.api.StartExtendStreamTimer()
|
||||||
return c.conn.Start()
|
return c.conn.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Stop() error {
|
func (c *RTSPClient) Stop() error {
|
||||||
|
c.api.StopRTSPStream()
|
||||||
c.api.StopExtendStreamTimer()
|
c.api.StopExtendStreamTimer()
|
||||||
return c.conn.Stop()
|
return c.conn.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
func (c *RTSPClient) MarshalJSON() ([]byte, error) {
|
||||||
return c.conn.MarshalJSON()
|
return c.conn.MarshalJSON()
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -3,10 +3,11 @@ package ngrok
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"io"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Ngrok struct {
|
type Ngrok struct {
|
||||||
|
|||||||
+5
-2
@@ -169,9 +169,12 @@ func (c *Client) GetServiceCapabilities() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) DeviceRequest(operation string) ([]byte, error) {
|
func (c *Client) DeviceRequest(operation string) ([]byte, error) {
|
||||||
if operation == DeviceGetServices {
|
switch operation {
|
||||||
|
case DeviceGetServices:
|
||||||
operation = `<tds:GetServices><tds:IncludeCapability>true</tds:IncludeCapability></tds:GetServices>`
|
operation = `<tds:GetServices><tds:IncludeCapability>true</tds:IncludeCapability></tds:GetServices>`
|
||||||
} else {
|
case DeviceGetCapabilities:
|
||||||
|
operation = `<tds:GetCapabilities><tds:Category>All</tds:Category></tds:GetCapabilities>`
|
||||||
|
default:
|
||||||
operation = `<tds:` + operation + `/>`
|
operation = `<tds:` + operation + `/>`
|
||||||
}
|
}
|
||||||
return c.Request(c.deviceURL, operation)
|
return c.Request(c.deviceURL, operation)
|
||||||
|
|||||||
+35
-9
@@ -179,16 +179,33 @@ func appendProfile(e *Envelope, tag, name string) {
|
|||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetVideoSourceConfigurationsResponse(names []string) []byte {
|
||||||
|
e := NewEnvelope()
|
||||||
|
e.Append(`<trt:GetVideoSourceConfigurationsResponse>
|
||||||
|
`)
|
||||||
|
for _, name := range names {
|
||||||
|
appendProfile(e, "Configurations", name)
|
||||||
|
}
|
||||||
|
e.Append(`</trt:GetVideoSourceConfigurationsResponse>`)
|
||||||
|
return e.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
func GetVideoSourceConfigurationResponse(name string) []byte {
|
func GetVideoSourceConfigurationResponse(name string) []byte {
|
||||||
e := NewEnvelope()
|
e := NewEnvelope()
|
||||||
e.Append(`<trt:GetVideoSourceConfigurationResponse>
|
e.Append(`<trt:GetVideoSourceConfigurationResponse>
|
||||||
<trt:Configuration token="`, name, `">
|
`)
|
||||||
|
appendVideoSourceConfiguration(e, "Configuration", name)
|
||||||
|
e.Append(`</trt:GetVideoSourceConfigurationResponse>`)
|
||||||
|
return e.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendVideoSourceConfiguration(e *Envelope, tag, name string) {
|
||||||
|
e.Append(`<trt:`, tag, ` token="`, name, `" fixed="true">
|
||||||
<tt:Name>VSC</tt:Name>
|
<tt:Name>VSC</tt:Name>
|
||||||
<tt:SourceToken>`, name, `</tt:SourceToken>
|
<tt:SourceToken>`, name, `</tt:SourceToken>
|
||||||
<tt:Bounds x="0" y="0" width="1920" height="1080"></tt:Bounds>
|
<tt:Bounds x="0" y="0" width="1920" height="1080"></tt:Bounds>
|
||||||
</trt:Configuration>
|
</trt:`, tag, `>
|
||||||
</trt:GetVideoSourceConfigurationResponse>`)
|
`)
|
||||||
return e.Bytes()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetVideoSourcesResponse(names []string) []byte {
|
func GetVideoSourcesResponse(names []string) []byte {
|
||||||
@@ -226,11 +243,7 @@ func StaticResponse(operation string) []byte {
|
|||||||
|
|
||||||
e := NewEnvelope()
|
e := NewEnvelope()
|
||||||
e.Append(responses[operation])
|
e.Append(responses[operation])
|
||||||
b := e.Bytes()
|
return e.Bytes()
|
||||||
if operation == DeviceGetNetworkInterfaces {
|
|
||||||
println()
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var responses = map[string]string{
|
var responses = map[string]string{
|
||||||
@@ -249,4 +262,17 @@ var responses = map[string]string{
|
|||||||
<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/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:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/type/Network_Video_Transmitter</tt:ScopeItem></tds:Scopes>
|
||||||
</tds:GetScopesResponse>`,
|
</tds:GetScopesResponse>`,
|
||||||
|
|
||||||
|
MediaGetVideoEncoderConfigurations: `<trt:GetVideoEncoderConfigurationsResponse>
|
||||||
|
<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:RateControl />
|
||||||
|
</tt:VideoEncoderConfiguration>
|
||||||
|
</trt:GetVideoEncoderConfigurationsResponse>`,
|
||||||
|
|
||||||
|
MediaGetAudioEncoderConfigurations: `<trt:GetAudioEncoderConfigurationsResponse />`,
|
||||||
|
MediaGetAudioSources: `<trt:GetAudioSourcesResponse />`,
|
||||||
|
MediaGetAudioSourceConfigurations: `<trt:GetAudioSourceConfigurationsResponse />`,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
v2 "github.com/AlexxIT/go2rtc/pkg/pcm"
|
v2 "github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPCMUtoPCM(t *testing.T) {
|
func TestPCMUtoPCM(t *testing.T) {
|
||||||
|
|||||||
+1
-2
@@ -514,7 +514,6 @@ func (c *Client) Stop() error {
|
|||||||
|
|
||||||
if c.prod != nil {
|
if c.prod != nil {
|
||||||
_ = c.prod.Stop()
|
_ = c.prod.Stop()
|
||||||
c.prod = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.ws != nil {
|
if c.ws != nil {
|
||||||
@@ -538,5 +537,5 @@ func (c *Client) MarshalJSON() ([]byte, error) {
|
|||||||
return webrtcProd.MarshalJSON()
|
return webrtcProd.MarshalJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("ring: can't marshal")
|
return json.Marshal(c.prod)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func NewSnapshotProducer(client *RingRestClient, camera *CameraData) *SnapshotPr
|
|||||||
ID: core.NewID(),
|
ID: core.NewID(),
|
||||||
FormatName: "ring/snapshot",
|
FormatName: "ring/snapshot",
|
||||||
Protocol: "https",
|
Protocol: "https",
|
||||||
|
RemoteAddr: "app-snaps.ring.com",
|
||||||
Medias: []*core.Media{
|
Medias: []*core.Media{
|
||||||
{
|
{
|
||||||
Kind: core.KindVideo,
|
Kind: core.KindVideo,
|
||||||
@@ -43,7 +44,7 @@ func (p *SnapshotProducer) Start() error {
|
|||||||
// Fetch snapshot
|
// Fetch snapshot
|
||||||
response, err := p.client.Request("GET", fmt.Sprintf("https://app-snaps.ring.com/snapshots/next/%d", int(p.camera.ID)), nil)
|
response, err := p.client.Request("GET", fmt.Sprintf("https://app-snaps.ring.com/snapshots/next/%d", int(p.camera.ID)), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get snapshot: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pkt := &rtp.Packet{
|
pkt := &rtp.Packet{
|
||||||
@@ -51,10 +52,7 @@ func (p *SnapshotProducer) Start() error {
|
|||||||
Payload: response,
|
Payload: response,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send to all receivers
|
p.Receivers[0].WriteRTP(pkt)
|
||||||
for _, receiver := range p.Receivers {
|
|
||||||
receiver.WriteRTP(pkt)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -7,11 +7,12 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserInfo struct {
|
type UserInfo struct {
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
## Tests
|
||||||
|
|
||||||
|
- go2rtc rtmp client => Reolink
|
||||||
|
- go2rtc rtmp server <= Dahua
|
||||||
|
- go2rtc rtmp publish => YouTube
|
||||||
|
- go2rtc rtmp publish => Telegram
|
||||||
|
|
||||||
## Logs
|
## Logs
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+15
-6
@@ -46,7 +46,7 @@ func (c *Conn) Close() error {
|
|||||||
return c.conn.Close()
|
return c.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) readResponse(transID float64) ([]any, error) {
|
func (c *Conn) readResponse(wait func(items []any) bool) ([]any, error) {
|
||||||
for {
|
for {
|
||||||
msgType, _, b, err := c.readMessage()
|
msgType, _, b, err := c.readMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -59,7 +59,7 @@ func (c *Conn) readResponse(transID float64) ([]any, error) {
|
|||||||
c.rdPacketSize = binary.BigEndian.Uint32(b)
|
c.rdPacketSize = binary.BigEndian.Uint32(b)
|
||||||
case TypeCommand:
|
case TypeCommand:
|
||||||
items, _ := amf.NewReader(b).ReadItems()
|
items, _ := amf.NewReader(b).ReadItems()
|
||||||
if len(items) >= 3 && (items[1] == transID || items[1] == float64(0)) {
|
if wait(items) {
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,7 +250,9 @@ func (c *Conn) writeConnect() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err := c.readResponse(1)
|
v, err := c.readResponse(func(items []any) bool {
|
||||||
|
return len(items) >= 3 && items[0] == "_result" && items[1] == float64(1)
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -280,7 +282,9 @@ func (c *Conn) writeCreateStream() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err := c.readResponse(4)
|
v, err := c.readResponse(func(items []any) bool {
|
||||||
|
return len(items) >= 3 && items[0] == "_result" && items[1] == float64(4)
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -301,7 +305,10 @@ func (c *Conn) writePublish() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err := c.readResponse(5)
|
// YouTube can response with "onBWDone 0"
|
||||||
|
v, err := c.readResponse(func(items []any) bool {
|
||||||
|
return len(items) >= 3 && items[0] == "onStatus"
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -321,7 +328,9 @@ func (c *Conn) writePlay() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reolink response with ID=0, other software respose with ID=5
|
// Reolink response with ID=0, other software respose with ID=5
|
||||||
v, err := c.readResponse(5)
|
v, err := c.readResponse(func(items []any) bool {
|
||||||
|
return len(items) >= 3 && items[0] == "onStatus"
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-7
@@ -2,10 +2,13 @@ package rtmp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
|
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
|
||||||
@@ -34,23 +37,54 @@ func NewServer(conn net.Conn) (*Conn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) serverHandshake() error {
|
func (c *Conn) serverHandshake() error {
|
||||||
b := make([]byte, 1+1536)
|
// based on https://rtmp.veriskope.com/docs/spec/
|
||||||
// read C0+C1
|
_ = c.conn.SetDeadline(time.Now().Add(core.ConnDeadline))
|
||||||
|
|
||||||
|
// read C0
|
||||||
|
b := make([]byte, 1)
|
||||||
if _, err := io.ReadFull(c.rd, b); err != nil {
|
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// write S0+S1, skip random
|
|
||||||
|
if b[0] != 3 {
|
||||||
|
return errors.New("rtmp: wrong handshake")
|
||||||
|
}
|
||||||
|
|
||||||
|
// write S0
|
||||||
|
if _, err := c.conn.Write([]byte{3}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b = make([]byte, 1536)
|
||||||
|
|
||||||
|
// write S1
|
||||||
|
tsS1 := nowMS()
|
||||||
|
binary.BigEndian.PutUint32(b, tsS1)
|
||||||
|
binary.BigEndian.PutUint32(b[4:], 0)
|
||||||
|
_, _ = rand.Read(b[8:])
|
||||||
if _, err := c.conn.Write(b); err != nil {
|
if _, err := c.conn.Write(b); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// read S1, skip check
|
|
||||||
if _, err := io.ReadFull(c.rd, make([]byte, 1536)); err != nil {
|
// read C1
|
||||||
|
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// write C1
|
|
||||||
if _, err := c.conn.Write(b[1:]); err != nil {
|
// write S2
|
||||||
|
tsS2 := nowMS()
|
||||||
|
binary.BigEndian.PutUint32(b, tsS1)
|
||||||
|
binary.BigEndian.PutUint32(b[4:], tsS2)
|
||||||
|
if _, err := c.conn.Write(b); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// read C2
|
||||||
|
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = c.conn.SetDeadline(time.Time{})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,3 +195,7 @@ func (c *Conn) WriteStart() error {
|
|||||||
payload := amf.EncodeItems("onStatus", 0, nil, map[string]any{"code": code})
|
payload := amf.EncodeItems("onStatus", 0, nil, map[string]any{"code": code})
|
||||||
return c.writeMessage(3, TypeCommand, 0, payload)
|
return c.writeMessage(3, TypeCommand, 0, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nowMS() uint32 {
|
||||||
|
return uint32(time.Now().UnixNano() / int64(time.Millisecond))
|
||||||
|
}
|
||||||
|
|||||||
+2
-4
@@ -237,13 +237,11 @@ func (c *Conn) SetupMedia(media *core.Media) (byte, error) {
|
|||||||
rawURL := media.ID // control
|
rawURL := media.ID // control
|
||||||
if !strings.Contains(rawURL, "://") {
|
if !strings.Contains(rawURL, "://") {
|
||||||
rawURL = c.URL.String()
|
rawURL = c.URL.String()
|
||||||
if !strings.HasSuffix(rawURL, "/") {
|
// prefix check for https://github.com/AlexxIT/go2rtc/issues/1236
|
||||||
|
if !strings.HasSuffix(rawURL, "/") && !strings.HasPrefix(media.ID, "/") {
|
||||||
rawURL += "/"
|
rawURL += "/"
|
||||||
}
|
}
|
||||||
rawURL += media.ID
|
rawURL += media.ID
|
||||||
} else if strings.HasPrefix(rawURL, "rtsp://rtsp://") {
|
|
||||||
// fix https://github.com/AlexxIT/go2rtc/issues/830
|
|
||||||
rawURL = rawURL[7:]
|
|
||||||
}
|
}
|
||||||
trackURL, err := urlParse(rawURL)
|
trackURL, err := urlParse(rawURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -75,6 +75,16 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) {
|
|||||||
if codec.FmtpLine == "" {
|
if codec.FmtpLine == "" {
|
||||||
codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions)
|
codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions)
|
||||||
}
|
}
|
||||||
|
case core.CodecH265:
|
||||||
|
if codec.FmtpLine != "" {
|
||||||
|
// all three parameters are needed for a valid fmtp line
|
||||||
|
// https://github.com/AlexxIT/go2rtc/pull/1588
|
||||||
|
if !strings.Contains(codec.FmtpLine, "sprop-vps=") ||
|
||||||
|
!strings.Contains(codec.FmtpLine, "sprop-sps=") ||
|
||||||
|
!strings.Contains(codec.FmtpLine, "sprop-pps=") {
|
||||||
|
codec.FmtpLine = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
case core.CodecOpus:
|
case core.CodecOpus:
|
||||||
// fix OPUS for some cameras https://datatracker.ietf.org/doc/html/rfc7587
|
// fix OPUS for some cameras https://datatracker.ietf.org/doc/html/rfc7587
|
||||||
codec.ClockRate = 48000
|
codec.ClockRate = 48000
|
||||||
@@ -107,6 +117,7 @@ func findFmtpLine(payloadType uint8, descriptions []*sdp.MediaDescription) strin
|
|||||||
// 1. Content-Base: rtsp://::ffff:192.168.1.123/onvif/profile.1/
|
// 1. Content-Base: rtsp://::ffff:192.168.1.123/onvif/profile.1/
|
||||||
// 2. Content-Base: rtsp://rtsp://turret2-cam.lan:554/stream1/
|
// 2. Content-Base: rtsp://rtsp://turret2-cam.lan:554/stream1/
|
||||||
func urlParse(rawURL string) (*url.URL, error) {
|
func urlParse(rawURL string) (*url.URL, error) {
|
||||||
|
// fix https://github.com/AlexxIT/go2rtc/issues/830
|
||||||
if strings.HasPrefix(rawURL, "rtsp://rtsp://") {
|
if strings.HasPrefix(rawURL, "rtsp://rtsp://") {
|
||||||
rawURL = rawURL[7:]
|
rawURL = rawURL[7:]
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-1
@@ -19,18 +19,29 @@ func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, e
|
|||||||
c.stateMu.Lock()
|
c.stateMu.Lock()
|
||||||
defer c.stateMu.Unlock()
|
defer c.stateMu.Unlock()
|
||||||
|
|
||||||
|
var channel byte
|
||||||
|
|
||||||
|
switch c.mode {
|
||||||
|
case core.ModeActiveProducer:
|
||||||
if c.state == StatePlay {
|
if c.state == StatePlay {
|
||||||
if err := c.Reconnect(); err != nil {
|
if err := c.Reconnect(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
channel, err := c.SetupMedia(media)
|
var err error
|
||||||
|
channel, err = c.SetupMedia(media)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.state = StateSetup
|
c.state = StateSetup
|
||||||
|
case core.ModePassiveConsumer:
|
||||||
|
// Backchannel
|
||||||
|
channel = byte(len(c.Senders)) * 2
|
||||||
|
default:
|
||||||
|
return nil, errors.New("rtsp: wrong mode for GetTrack")
|
||||||
|
}
|
||||||
|
|
||||||
track := core.NewReceiver(media, codec)
|
track := core.NewReceiver(media, codec)
|
||||||
track.ID = channel
|
track.ID = channel
|
||||||
|
|||||||
+29
-10
@@ -13,6 +13,8 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var FailedAuth = errors.New("failed authentication")
|
||||||
|
|
||||||
func NewServer(conn net.Conn) *Conn {
|
func NewServer(conn net.Conn) *Conn {
|
||||||
return &Conn{
|
return &Conn{
|
||||||
Connection: core.Connection{
|
Connection: core.Connection{
|
||||||
@@ -45,7 +47,7 @@ func (c *Conn) Accept() error {
|
|||||||
|
|
||||||
c.Fire(req)
|
c.Fire(req)
|
||||||
|
|
||||||
if !c.auth.Validate(req) {
|
if valid, empty := c.auth.Validate(req); !valid {
|
||||||
res := &tcp.Response{
|
res := &tcp.Response{
|
||||||
Status: "401 Unauthorized",
|
Status: "401 Unauthorized",
|
||||||
Header: map[string][]string{"Www-Authenticate": {`Basic realm="go2rtc"`}},
|
Header: map[string][]string{"Www-Authenticate": {`Basic realm="go2rtc"`}},
|
||||||
@@ -54,8 +56,13 @@ func (c *Conn) Accept() error {
|
|||||||
if err = c.WriteResponse(res); err != nil {
|
if err = c.WriteResponse(res); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if empty {
|
||||||
|
// eliminate false positive: ffmpeg sends first request without
|
||||||
|
// authorization header even if the user provides credentials
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
return FailedAuth
|
||||||
|
}
|
||||||
|
|
||||||
// Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN
|
// Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN
|
||||||
// Sender: OPTIONS > ANNOUNCE > SETUP... > RECORD > TEARDOWN
|
// Sender: OPTIONS > ANNOUNCE > SETUP... > RECORD > TEARDOWN
|
||||||
@@ -129,6 +136,16 @@ func (c *Conn) Accept() error {
|
|||||||
medias = append(medias, media)
|
medias = append(medias, media)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i, track := range c.Receivers {
|
||||||
|
media := &core.Media{
|
||||||
|
Kind: core.GetKind(track.Codec.Name),
|
||||||
|
Direction: core.DirectionSendonly,
|
||||||
|
Codecs: []*core.Codec{track.Codec},
|
||||||
|
ID: "trackID=" + strconv.Itoa(i+len(c.Senders)),
|
||||||
|
}
|
||||||
|
medias = append(medias, media)
|
||||||
|
}
|
||||||
|
|
||||||
res.Body, err = core.MarshalSDP(c.SessionName, medias)
|
res.Body, err = core.MarshalSDP(c.SessionName, medias)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -141,29 +158,31 @@ func (c *Conn) Accept() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case MethodSetup:
|
case MethodSetup:
|
||||||
tr := req.Header.Get("Transport")
|
|
||||||
|
|
||||||
res := &tcp.Response{
|
res := &tcp.Response{
|
||||||
Header: map[string][]string{},
|
Header: map[string][]string{},
|
||||||
Request: req,
|
Request: req,
|
||||||
}
|
}
|
||||||
|
|
||||||
const transport = "RTP/AVP/TCP;unicast;interleaved="
|
// Test if client requests TCP transport, otherwise return 461 Transport not supported
|
||||||
if tr = core.Between(tr, "interleaved=", ";"); tr != "" {
|
// This allows smart clients who initially requested UDP to fall back on TCP transport
|
||||||
|
if tr := req.Header.Get("Transport"); strings.HasPrefix(tr, "RTP/AVP/TCP") {
|
||||||
c.session = core.RandString(8, 10)
|
c.session = core.RandString(8, 10)
|
||||||
c.state = StateSetup
|
c.state = StateSetup
|
||||||
|
|
||||||
if c.mode == core.ModePassiveConsumer {
|
if c.mode == core.ModePassiveConsumer {
|
||||||
if i := reqTrackID(req); i >= 0 && i < len(c.Senders) {
|
if i := reqTrackID(req); i >= 0 && i < len(c.Senders)+len(c.Receivers) {
|
||||||
// mark sender as SETUP
|
if i < len(c.Senders) {
|
||||||
c.Senders[i].Media.ID = MethodSetup
|
c.Senders[i].Media.ID = MethodSetup
|
||||||
tr = fmt.Sprintf("%d-%d", i*2, i*2+1)
|
} else {
|
||||||
res.Header.Set("Transport", transport+tr)
|
c.Receivers[i-len(c.Senders)].Media.ID = MethodSetup
|
||||||
|
}
|
||||||
|
tr = fmt.Sprintf("RTP/AVP/TCP;unicast;interleaved=%d-%d", i*2, i*2+1)
|
||||||
|
res.Header.Set("Transport", tr)
|
||||||
} else {
|
} else {
|
||||||
res.Status = "400 Bad Request"
|
res.Status = "400 Bad Request"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.Header.Set("Transport", transport+tr)
|
res.Header.Set("Transport", tr)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.Status = "461 Unsupported transport"
|
res.Status = "461 Unsupported transport"
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package shell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Command like exec.Cmd, but with support:
|
||||||
|
// - io.Closer interface
|
||||||
|
// - Wait from multiple places
|
||||||
|
// - Done channel
|
||||||
|
type Command struct {
|
||||||
|
*exec.Cmd
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommand(s string) *Command {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
args := QuoteSplit(s)
|
||||||
|
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
|
||||||
|
cmd.SysProcAttr = procAttr
|
||||||
|
return &Command{cmd, ctx, cancel, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) Start() error {
|
||||||
|
if err := c.Cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
c.err = c.Cmd.Wait()
|
||||||
|
c.cancel() // release context resources
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) Wait() error {
|
||||||
|
<-c.ctx.Done()
|
||||||
|
return c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) Run() error {
|
||||||
|
if err := c.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) Done() <-chan struct{} {
|
||||||
|
return c.ctx.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) Close() error {
|
||||||
|
c.cancel()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package shell
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
var procAttr *syscall.SysProcAttr
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package shell
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
// will stop child if parent died (even with SIGKILL)
|
||||||
|
var procAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGTERM}
|
||||||
+9
-2
@@ -65,6 +65,13 @@ func (s *Server) DelSession(session *Session) {
|
|||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) GetSession(ssrc uint32) (session *Session) {
|
||||||
|
s.mu.Lock()
|
||||||
|
session = s.sessions[ssrc]
|
||||||
|
s.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handle() error {
|
func (s *Server) handle() error {
|
||||||
b := make([]byte, 2048)
|
b := make([]byte, 2048)
|
||||||
for {
|
for {
|
||||||
@@ -80,14 +87,14 @@ func (s *Server) handle() error {
|
|||||||
case 99, 110, 0x80 | 99, 0x80 | 110:
|
case 99, 110, 0x80 | 99, 0x80 | 110:
|
||||||
// this is default position for SSRC in RTP packet
|
// this is default position for SSRC in RTP packet
|
||||||
ssrc := binary.BigEndian.Uint32(b[8:])
|
ssrc := binary.BigEndian.Uint32(b[8:])
|
||||||
if session, ok := s.sessions[ssrc]; ok {
|
if session := s.GetSession(ssrc); session != nil {
|
||||||
session.ReadRTP(b[:n])
|
session.ReadRTP(b[:n])
|
||||||
}
|
}
|
||||||
|
|
||||||
case 200, 201, 202, 203, 204, 205, 206, 207:
|
case 200, 201, 202, 203, 204, 205, 206, 207:
|
||||||
// this is default position for SSRC in RTCP packet
|
// this is default position for SSRC in RTCP packet
|
||||||
ssrc := binary.BigEndian.Uint32(b[4:])
|
ssrc := binary.BigEndian.Uint32(b[4:])
|
||||||
if session, ok := s.sessions[ssrc]; ok {
|
if session := s.GetSession(ssrc); session != nil {
|
||||||
session.ReadRTCP(b[:n])
|
session.ReadRTCP(b[:n])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package stdin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
@@ -42,10 +41,7 @@ func (c *Client) Stop() (err error) {
|
|||||||
if c.sender != nil {
|
if c.sender != nil {
|
||||||
c.sender.Close()
|
c.sender.Close()
|
||||||
}
|
}
|
||||||
if c.cmd.Process == nil {
|
return c.cmd.Close()
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return errors.Join(c.cmd.Process.Kill(), c.cmd.Wait())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||||
|
|||||||
+3
-4
@@ -1,21 +1,20 @@
|
|||||||
package stdin
|
package stdin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Deprecated: should be rewritten to core.Connection
|
// Deprecated: should be rewritten to core.Connection
|
||||||
type Client struct {
|
type Client struct {
|
||||||
cmd *exec.Cmd
|
cmd *shell.Command
|
||||||
|
|
||||||
medias []*core.Media
|
medias []*core.Media
|
||||||
sender *core.Sender
|
sender *core.Sender
|
||||||
send int
|
send int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(cmd *exec.Cmd) (*Client, error) {
|
func NewClient(cmd *shell.Command) (*Client, error) {
|
||||||
c := &Client{
|
c := &Client{
|
||||||
cmd: cmd,
|
cmd: cmd,
|
||||||
medias: []*core.Media{
|
medias: []*core.Media{
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ func dial(req *http.Request, brand, username, password string) (net.Conn, *http.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
_, _ = io.Copy(io.Discard, res.Body) // discard leftovers
|
||||||
_ = res.Body.Close() // ignore response body
|
_ = res.Body.Close() // ignore response body
|
||||||
|
|
||||||
auth := res.Header.Get("WWW-Authenticate")
|
auth := res.Header.Get("WWW-Authenticate")
|
||||||
|
|||||||
+4
-4
@@ -85,14 +85,14 @@ func (a *Auth) Write(req *Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) Validate(req *Request) bool {
|
func (a *Auth) Validate(req *Request) (valid, empty bool) {
|
||||||
if a == nil {
|
if a == nil {
|
||||||
return true
|
return true, true
|
||||||
}
|
}
|
||||||
|
|
||||||
header := req.Header.Get("Authorization")
|
header := req.Header.Get("Authorization")
|
||||||
if header == "" {
|
if header == "" {
|
||||||
return false
|
return false, true
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.Method == AuthUnknown {
|
if a.Method == AuthUnknown {
|
||||||
@@ -100,7 +100,7 @@ func (a *Auth) Validate(req *Request) bool {
|
|||||||
a.header = "Basic " + B64(a.user, a.pass)
|
a.header = "Basic " + B64(a.user, a.pass)
|
||||||
}
|
}
|
||||||
|
|
||||||
return header == a.header
|
return header == a.header, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) ReadNone(res *Response) bool {
|
func (a *Auth) ReadNone(res *Response) bool {
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import (
|
|||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Dial(address string) (net.Conn, error) {
|
func Dial(address string) (net.Conn, error) {
|
||||||
|
|||||||
+48
-17
@@ -4,6 +4,8 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/xnet"
|
||||||
|
"github.com/pion/ice/v2"
|
||||||
"github.com/pion/interceptor"
|
"github.com/pion/interceptor"
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v3"
|
||||||
)
|
)
|
||||||
@@ -18,6 +20,7 @@ func NewAPI() (*webrtc.API, error) {
|
|||||||
|
|
||||||
type Filters struct {
|
type Filters struct {
|
||||||
Candidates []string `yaml:"candidates"`
|
Candidates []string `yaml:"candidates"`
|
||||||
|
Loopback bool `yaml:"loopback"`
|
||||||
Interfaces []string `yaml:"interfaces"`
|
Interfaces []string `yaml:"interfaces"`
|
||||||
IPs []string `yaml:"ips"`
|
IPs []string `yaml:"ips"`
|
||||||
Networks []string `yaml:"networks"`
|
Networks []string `yaml:"networks"`
|
||||||
@@ -44,39 +47,53 @@ func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error
|
|||||||
// fix https://github.com/pion/webrtc/pull/2407
|
// fix https://github.com/pion/webrtc/pull/2407
|
||||||
s.SetDTLSInsecureSkipHelloVerify(true)
|
s.SetDTLSInsecureSkipHelloVerify(true)
|
||||||
|
|
||||||
|
if filters != nil && filters.Loopback {
|
||||||
|
s.SetIncludeLoopbackCandidate(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
var interfaceFilter func(name string) bool
|
||||||
if filters != nil && filters.Interfaces != nil {
|
if filters != nil && filters.Interfaces != nil {
|
||||||
s.SetIncludeLoopbackCandidate(true)
|
interfaceFilter = func(name string) bool {
|
||||||
s.SetInterfaceFilter(func(name string) bool {
|
|
||||||
return core.Contains(filters.Interfaces, name)
|
return core.Contains(filters.Interfaces, name)
|
||||||
})
|
}
|
||||||
} else {
|
} else {
|
||||||
// disable listen on Hassio docker interfaces
|
// default interfaces - all, except loopback
|
||||||
s.SetInterfaceFilter(func(name string) bool {
|
|
||||||
return name != "hassio" && name != "docker0"
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
s.SetInterfaceFilter(interfaceFilter)
|
||||||
|
|
||||||
|
var ipFilter func(ip net.IP) bool
|
||||||
if filters != nil && filters.IPs != nil {
|
if filters != nil && filters.IPs != nil {
|
||||||
s.SetIncludeLoopbackCandidate(true)
|
ipFilter = func(ip net.IP) bool {
|
||||||
s.SetIPFilter(func(ip net.IP) bool {
|
|
||||||
return core.Contains(filters.IPs, ip.String())
|
return core.Contains(filters.IPs, ip.String())
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// try filter all Docker-like interfaces
|
||||||
|
ipFilter = func(ip net.IP) bool {
|
||||||
|
return !xnet.Docker.Contains(ip)
|
||||||
|
}
|
||||||
|
// if there are no such interfaces - disable the filter
|
||||||
|
// the user will need to enable port forwarding
|
||||||
|
if nets, _ := xnet.IPNets(ipFilter); len(nets) == 0 {
|
||||||
|
ipFilter = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.SetIPFilter(ipFilter)
|
||||||
|
|
||||||
if filters != nil && filters.Networks != nil {
|
|
||||||
var networkTypes []webrtc.NetworkType
|
var networkTypes []webrtc.NetworkType
|
||||||
|
if filters != nil && filters.Networks != nil {
|
||||||
for _, s := range filters.Networks {
|
for _, s := range filters.Networks {
|
||||||
if networkType, err := webrtc.NewNetworkType(s); err == nil {
|
if networkType, err := webrtc.NewNetworkType(s); err == nil {
|
||||||
networkTypes = append(networkTypes, networkType)
|
networkTypes = append(networkTypes, networkType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.SetNetworkTypes(networkTypes)
|
|
||||||
} else {
|
} else {
|
||||||
s.SetNetworkTypes([]webrtc.NetworkType{
|
// default network types - all
|
||||||
|
networkTypes = []webrtc.NetworkType{
|
||||||
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6,
|
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6,
|
||||||
webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6,
|
webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6,
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
s.SetNetworkTypes(networkTypes)
|
||||||
|
|
||||||
if filters != nil && len(filters.UDPPorts) == 2 {
|
if filters != nil && len(filters.UDPPorts) == 2 {
|
||||||
_ = s.SetEphemeralUDPPortRange(filters.UDPPorts[0], filters.UDPPorts[1])
|
_ = s.SetEphemeralUDPPortRange(filters.UDPPorts[0], filters.UDPPorts[1])
|
||||||
@@ -100,10 +117,24 @@ func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
if network == "" || network == "udp" {
|
if network == "" || network == "udp" {
|
||||||
if ln, err := net.ListenPacket("udp", address); err == nil {
|
// UDPMuxDefault should not listening on unspecified address, use NewMultiUDPMuxFromPort instead
|
||||||
udpMux := webrtc.NewICEUDPMux(nil, ln)
|
var udpMux ice.UDPMux
|
||||||
s.SetICEUDPMux(udpMux)
|
if port := xnet.ParseUnspecifiedPort(address); port != 0 {
|
||||||
|
var networks []ice.NetworkType
|
||||||
|
for _, ntype := range networkTypes {
|
||||||
|
networks = append(networks, ice.NetworkType(ntype))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
udpMux, _ = ice.NewMultiUDPMuxFromPort(
|
||||||
|
port,
|
||||||
|
ice.UDPMuxFromPortWithInterfaceFilter(interfaceFilter),
|
||||||
|
ice.UDPMuxFromPortWithIPFilter(ipFilter),
|
||||||
|
ice.UDPMuxFromPortWithNetworks(networks...),
|
||||||
|
)
|
||||||
|
} else if ln, err := net.ListenPacket("udp", address); err == nil {
|
||||||
|
udpMux = ice.NewUDPMuxDefault(ice.UDPMuxParams{UDPConn: ln})
|
||||||
|
}
|
||||||
|
s.SetICEUDPMux(udpMux)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ func NewConn(pc *webrtc.PeerConnection) *Conn {
|
|||||||
}
|
}
|
||||||
pc.SCTP().Transport().ICETransport().OnSelectedCandidatePairChange(
|
pc.SCTP().Transport().ICETransport().OnSelectedCandidatePairChange(
|
||||||
func(pair *webrtc.ICECandidatePair) {
|
func(pair *webrtc.ICECandidatePair) {
|
||||||
|
// fix situation when candidate pair changes multiple times
|
||||||
|
if i := strings.IndexByte(c.Protocol, '+'); i > 0 {
|
||||||
|
c.Protocol = c.Protocol[:i]
|
||||||
|
}
|
||||||
c.Protocol += "+" + pair.Remote.Protocol.String()
|
c.Protocol += "+" + pair.Remote.Protocol.String()
|
||||||
c.RemoteAddr = fmt.Sprintf(
|
c.RemoteAddr = fmt.Sprintf(
|
||||||
"%s:%d %s", sanitizeIP6(pair.Remote.Address), pair.Remote.Port, pair.Remote.Typ,
|
"%s:%d %s", sanitizeIP6(pair.Remote.Address), pair.Remote.Port, pair.Remote.Typ,
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ package webtorrent
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package xnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Docker has common docker addresses (class B):
|
||||||
|
// https://en.wikipedia.org/wiki/Private_network
|
||||||
|
// - docker0 172.17.0.1/16
|
||||||
|
// - br-xxxx 172.18.0.1/16
|
||||||
|
// - hassio 172.30.32.1/23
|
||||||
|
var Docker = net.IPNet{
|
||||||
|
IP: []byte{172, 16, 0, 0},
|
||||||
|
Mask: []byte{255, 240, 0, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseUnspecifiedPort will return port if address is unspecified
|
||||||
|
// ex. ":8555" or "0.0.0.0:8555"
|
||||||
|
func ParseUnspecifiedPort(address string) int {
|
||||||
|
host, port, err := net.SplitHostPort(address)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if host != "" && host != "0.0.0.0" && host != "[::]" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
i, _ := strconv.Atoi(port)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func IPNets(ipFilter func(ip net.IP) bool) ([]*net.IPNet, error) {
|
||||||
|
ifaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var nets []*net.IPNet
|
||||||
|
|
||||||
|
for _, iface := range ifaces {
|
||||||
|
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs, _ := iface.Addrs() // range on nil slice is OK
|
||||||
|
for _, addr := range addrs {
|
||||||
|
switch v := addr.(type) {
|
||||||
|
case *net.IPNet:
|
||||||
|
ip := v.IP.To4()
|
||||||
|
if ip == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ipFilter != nil && !ipFilter(ip) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nets = append(nets, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nets, nil
|
||||||
|
}
|
||||||
+137
-111
@@ -23,149 +23,157 @@ func Encode(v any, indent int) ([]byte, error) {
|
|||||||
return b.Bytes(), nil
|
return b.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch - change key/value pair in YAML file without break formatting
|
func Patch(in []byte, path []string, value any) ([]byte, error) {
|
||||||
func Patch(src []byte, key string, value any, path ...string) ([]byte, error) {
|
out, err := patch(in, path, value)
|
||||||
nodeParent, err := FindParent(src, path...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var dst []byte
|
// validate
|
||||||
|
if err = yaml.Unmarshal(out, map[string]any{}); err != nil {
|
||||||
if nodeParent != nil {
|
|
||||||
dst, err = AddOrReplace(src, key, value, nodeParent)
|
|
||||||
} else {
|
|
||||||
dst, err = AddToEnd(src, key, value, path...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = yaml.Unmarshal(dst, map[string]any{}); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return dst, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindParent - return YAML Node from path of keys (tree)
|
func patch(in []byte, path []string, value any) ([]byte, error) {
|
||||||
func FindParent(src []byte, path ...string) (*yaml.Node, error) {
|
|
||||||
if len(src) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var root yaml.Node
|
var root yaml.Node
|
||||||
if err := yaml.Unmarshal(src, &root); err != nil {
|
if err := yaml.Unmarshal(in, &root); err != nil {
|
||||||
|
// invalid yaml
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if root.Content == nil {
|
// empty in
|
||||||
return nil, nil
|
if len(root.Content) != 1 {
|
||||||
|
return addToEnd(in, path, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
parent := root.Content[0] // yaml.DocumentNode
|
// yaml is not dict
|
||||||
for _, name := range path {
|
if root.Content[0].Kind != yaml.MappingNode {
|
||||||
if parent == nil {
|
return nil, errors.New("yaml: can't patch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// dict items list
|
||||||
|
nodes := root.Content[0].Content
|
||||||
|
|
||||||
|
n := len(path) - 1
|
||||||
|
|
||||||
|
// parent node key/value
|
||||||
|
pKey, pVal := findNode(nodes, path[:n])
|
||||||
|
if pKey == nil {
|
||||||
|
// no parent node
|
||||||
|
return addToEnd(in, path, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
var paste []byte
|
||||||
|
|
||||||
|
if value != nil {
|
||||||
|
// nil value means delete key
|
||||||
|
var err error
|
||||||
|
v := map[string]any{path[n]: value}
|
||||||
|
if paste, err = Encode(v, 2); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iKey, _ := findNode(pVal.Content, path[n:])
|
||||||
|
if iKey != nil {
|
||||||
|
// key item not nil (replace value)
|
||||||
|
paste = addIndent(paste, iKey.Column-1)
|
||||||
|
|
||||||
|
i0, i1 := nodeBounds(in, iKey)
|
||||||
|
return join(in[:i0], paste, in[i1:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if pVal.Content != nil {
|
||||||
|
// parent value not nil (use first child indent)
|
||||||
|
paste = addIndent(paste, pVal.Column-1)
|
||||||
|
} else {
|
||||||
|
// parent value is nil (use parent indent + 2)
|
||||||
|
paste = addIndent(paste, pKey.Column+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, i1 := nodeBounds(in, pKey)
|
||||||
|
return join(in[:i1], paste, in[i1:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findNode(nodes []*yaml.Node, keys []string) (key, value *yaml.Node) {
|
||||||
|
for i, name := range keys {
|
||||||
|
for j := 0; j < len(nodes); j += 2 {
|
||||||
|
if nodes[j].Value == name {
|
||||||
|
if i < len(keys)-1 {
|
||||||
|
nodes = nodes[j+1].Content
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
_, parent = FindChild(parent, name)
|
return nodes[j], nodes[j+1]
|
||||||
}
|
}
|
||||||
return parent, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindChild - search and return YAML key/value pair for current Node
|
|
||||||
func FindChild(node *yaml.Node, name string) (key, value *yaml.Node) {
|
|
||||||
for i, child := range node.Content {
|
|
||||||
if child.Value != name {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
return child, node.Content[i+1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FirstChild(node *yaml.Node) *yaml.Node {
|
func nodeBounds(in []byte, node *yaml.Node) (offset0, offset1 int) {
|
||||||
if node.Content == nil {
|
// start from next line after node
|
||||||
return node
|
offset0 = lineOffset(in, node.Line)
|
||||||
|
offset1 = lineOffset(in, node.Line+1)
|
||||||
|
|
||||||
|
if offset1 < 0 {
|
||||||
|
return offset0, len(in)
|
||||||
}
|
}
|
||||||
return node.Content[0]
|
|
||||||
|
for i := offset1; i < len(in); {
|
||||||
|
indent, length := parseLine(in[i:])
|
||||||
|
if indent+1 != length {
|
||||||
|
if node.Column < indent+1 {
|
||||||
|
offset1 = i + length
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += length
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func LastChild(node *yaml.Node) *yaml.Node {
|
func addToEnd(in []byte, path []string, value any) ([]byte, error) {
|
||||||
if node.Content == nil {
|
if len(path) != 2 || value == nil {
|
||||||
return node
|
return nil, errors.New("yaml: path not exist")
|
||||||
}
|
|
||||||
return LastChild(node.Content[len(node.Content)-1])
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddOrReplace(src []byte, key string, value any, nodeParent *yaml.Node) ([]byte, error) {
|
|
||||||
v := map[string]any{key: value}
|
|
||||||
put, err := Encode(v, 2)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if nodeKey, nodeValue := FindChild(nodeParent, key); nodeKey != nil {
|
|
||||||
put = AddIndent(put, nodeKey.Column-1)
|
|
||||||
|
|
||||||
i0 := LineOffset(src, nodeKey.Line)
|
|
||||||
i1 := LineOffset(src, LastChild(nodeValue).Line+1)
|
|
||||||
|
|
||||||
if i1 < 0 { // no new line on the end of file
|
|
||||||
if value != nil {
|
|
||||||
return append(src[:i0], put...), nil
|
|
||||||
}
|
|
||||||
return src[:i0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
dst := make([]byte, 0, len(src)+len(put))
|
|
||||||
dst = append(dst, src[:i0]...)
|
|
||||||
if value != nil {
|
|
||||||
dst = append(dst, put...)
|
|
||||||
}
|
|
||||||
return append(dst, src[i1:]...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
put = AddIndent(put, FirstChild(nodeParent).Column-1)
|
|
||||||
|
|
||||||
i := LineOffset(src, LastChild(nodeParent).Line+1)
|
|
||||||
|
|
||||||
if i < 0 { // no new line on the end of file
|
|
||||||
src = append(src, '\n')
|
|
||||||
if value != nil {
|
|
||||||
src = append(src, put...)
|
|
||||||
}
|
|
||||||
return src, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
dst := make([]byte, 0, len(src)+len(put))
|
|
||||||
dst = append(dst, src[:i]...)
|
|
||||||
if value != nil {
|
|
||||||
dst = append(dst, put...)
|
|
||||||
}
|
|
||||||
return append(dst, src[i:]...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddToEnd(src []byte, key string, value any, path ...string) ([]byte, error) {
|
|
||||||
if len(path) > 1 || value == nil {
|
|
||||||
return nil, errors.New("config: path not exist")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
v := map[string]map[string]any{
|
v := map[string]map[string]any{
|
||||||
path[0]: {key: value},
|
path[0]: {path[1]: value},
|
||||||
}
|
}
|
||||||
put, err := Encode(v, 2)
|
paste, err := Encode(v, 2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
dst := make([]byte, 0, len(src)+len(put)+10)
|
return join(in, paste), nil
|
||||||
dst = append(dst, src...)
|
|
||||||
if l := len(src); l > 0 && src[l-1] != '\n' {
|
|
||||||
dst = append(dst, '\n')
|
|
||||||
}
|
|
||||||
return append(dst, put...), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddPrefix(src, pre []byte) (dst []byte) {
|
func join(items ...[]byte) []byte {
|
||||||
|
n := len(items) - 1
|
||||||
|
for _, b := range items {
|
||||||
|
n += len(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 0, n)
|
||||||
|
for _, b := range items {
|
||||||
|
if len(b) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if n = len(buf); n > 0 && buf[n-1] != '\n' {
|
||||||
|
buf = append(buf, '\n')
|
||||||
|
}
|
||||||
|
buf = append(buf, b...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPrefix(src, pre []byte) (dst []byte) {
|
||||||
for len(src) > 0 {
|
for len(src) > 0 {
|
||||||
dst = append(dst, pre...)
|
dst = append(dst, pre...)
|
||||||
i := bytes.IndexByte(src, '\n') + 1
|
i := bytes.IndexByte(src, '\n') + 1
|
||||||
@@ -180,21 +188,21 @@ func AddPrefix(src, pre []byte) (dst []byte) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddIndent(src []byte, indent int) (dst []byte) {
|
func addIndent(in []byte, indent int) (dst []byte) {
|
||||||
pre := make([]byte, indent)
|
pre := make([]byte, indent)
|
||||||
for i := 0; i < indent; i++ {
|
for i := 0; i < indent; i++ {
|
||||||
pre[i] = ' '
|
pre[i] = ' '
|
||||||
}
|
}
|
||||||
return AddPrefix(src, pre)
|
return addPrefix(in, pre)
|
||||||
}
|
}
|
||||||
|
|
||||||
func LineOffset(b []byte, line int) (offset int) {
|
func lineOffset(in []byte, line int) (offset int) {
|
||||||
for l := 1; ; l++ {
|
for l := 1; ; l++ {
|
||||||
if l == line {
|
if l == line {
|
||||||
return offset
|
return offset
|
||||||
}
|
}
|
||||||
|
|
||||||
i := bytes.IndexByte(b[offset:], '\n') + 1
|
i := bytes.IndexByte(in[offset:], '\n') + 1
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -202,3 +210,21 @@ func LineOffset(b []byte, line int) (offset int) {
|
|||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseLine(b []byte) (indent int, length int) {
|
||||||
|
prefix := true
|
||||||
|
for ; length < len(b); length++ {
|
||||||
|
switch b[length] {
|
||||||
|
case ' ':
|
||||||
|
if prefix {
|
||||||
|
indent++
|
||||||
|
}
|
||||||
|
case '\n':
|
||||||
|
length++
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
prefix = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
+99
-136
@@ -7,140 +7,103 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestPatch(t *testing.T) {
|
func TestPatch(t *testing.T) {
|
||||||
b := []byte(`# prefix`)
|
tests := []struct {
|
||||||
|
name string
|
||||||
// 1. Add first
|
src string
|
||||||
b, err := Patch(b, "camera1", "url1", "streams")
|
path []string
|
||||||
require.Nil(t, err)
|
value any
|
||||||
|
expect string
|
||||||
require.Equal(t, `# prefix
|
}{
|
||||||
streams:
|
{
|
||||||
camera1: url1
|
name: "empty config",
|
||||||
`, string(b))
|
src: "",
|
||||||
|
path: []string{"streams", "camera1"},
|
||||||
// 2. Add second
|
value: "val1",
|
||||||
b, err = Patch(b, "camera2", []string{"url2", "url3"}, "streams")
|
expect: "streams:\n camera1: val1\n",
|
||||||
require.Nil(t, err)
|
},
|
||||||
|
{
|
||||||
require.Equal(t, `# prefix
|
name: "empty main key",
|
||||||
streams:
|
src: "#dummy",
|
||||||
camera1: url1
|
path: []string{"streams", "camera1"},
|
||||||
camera2:
|
value: "val1",
|
||||||
- url2
|
expect: "#dummy\nstreams:\n camera1: val1\n",
|
||||||
- url3
|
},
|
||||||
`, string(b))
|
{
|
||||||
|
name: "single line value",
|
||||||
// 3. Replace first
|
src: "streams:\n camera1: url1\n camera2: url2",
|
||||||
b, err = Patch(b, "camera1", "url4", "streams")
|
path: []string{"streams", "camera1"},
|
||||||
require.Nil(t, err)
|
value: "val1",
|
||||||
|
expect: "streams:\n camera1: val1\n camera2: url2",
|
||||||
require.Equal(t, `# prefix
|
},
|
||||||
streams:
|
{
|
||||||
camera1: url4
|
name: "next line value",
|
||||||
camera2:
|
src: "streams:\n camera1:\n url1\n camera2: url2",
|
||||||
- url2
|
path: []string{"streams", "camera1"},
|
||||||
- url3
|
value: "val1",
|
||||||
`, string(b))
|
expect: "streams:\n camera1: val1\n camera2: url2",
|
||||||
|
},
|
||||||
// 4. Replace second
|
{
|
||||||
b, err = Patch(b, "camera2", "url5", "streams")
|
name: "two lines value",
|
||||||
require.Nil(t, err)
|
src: "streams:\n camera1: url1\n url2\n camera2: url2",
|
||||||
|
path: []string{"streams", "camera1"},
|
||||||
require.Equal(t, `# prefix
|
value: "val1",
|
||||||
streams:
|
expect: "streams:\n camera1: val1\n camera2: url2",
|
||||||
camera1: url4
|
},
|
||||||
camera2: url5
|
{
|
||||||
`, string(b))
|
name: "next two lines value",
|
||||||
|
src: "streams:\n camera1:\n url1\n url2\n camera2: url2",
|
||||||
// 5. Delete first
|
path: []string{"streams", "camera1"},
|
||||||
b, err = Patch(b, "camera1", nil, "streams")
|
value: "val1",
|
||||||
require.Nil(t, err)
|
expect: "streams:\n camera1: val1\n camera2: url2",
|
||||||
|
},
|
||||||
require.Equal(t, `# prefix
|
{
|
||||||
streams:
|
name: "add array",
|
||||||
camera2: url5
|
src: "",
|
||||||
`, string(b))
|
path: []string{"streams", "camera1"},
|
||||||
}
|
value: []string{"val1", "val2"},
|
||||||
|
expect: "streams:\n camera1:\n - val1\n - val2\n",
|
||||||
func TestPatchParings(t *testing.T) {
|
},
|
||||||
b := []byte(`homekit:
|
{
|
||||||
camera1:
|
name: "remove value",
|
||||||
pin: 123-45-678
|
src: "streams:\n camera1: url1\n camera2: url2",
|
||||||
streams:
|
path: []string{"streams", "camera1"},
|
||||||
camera1: url1
|
value: nil,
|
||||||
`)
|
expect: "streams:\n camera2: url2",
|
||||||
|
},
|
||||||
// 1. Add new key
|
{
|
||||||
pairings := []string{"client1", "client2"}
|
name: "add pairings",
|
||||||
|
src: "homekit:\n camera1:\nstreams:\n camera1: url1",
|
||||||
b, err := Patch(b, "pairings", pairings, "homekit", "camera1")
|
path: []string{"homekit", "camera1", "pairings"},
|
||||||
require.Nil(t, err)
|
value: []string{"val1"},
|
||||||
|
expect: "homekit:\n camera1:\n pairings:\n - val1\nstreams:\n camera1: url1",
|
||||||
require.Equal(t, `homekit:
|
},
|
||||||
camera1:
|
{
|
||||||
pin: 123-45-678
|
name: "remove pairings",
|
||||||
pairings:
|
src: "homekit:\n camera1:\n pairings:\n - val1\nstreams:\n camera1: url1",
|
||||||
- client1
|
path: []string{"homekit", "camera1", "pairings"},
|
||||||
- client2
|
value: nil,
|
||||||
streams:
|
expect: "homekit:\n camera1:\nstreams:\n camera1: url1",
|
||||||
camera1: url1
|
},
|
||||||
`, string(b))
|
{
|
||||||
}
|
name: "no new line",
|
||||||
|
src: "streams:\n camera1: url1",
|
||||||
func TestPatch2(t *testing.T) {
|
path: []string{"streams", "camera1"},
|
||||||
b := []byte(`streams:
|
value: "val1",
|
||||||
camera1:
|
expect: "streams:\n camera1: val1\n",
|
||||||
- url1
|
},
|
||||||
- url2
|
{
|
||||||
`)
|
name: "no new line",
|
||||||
|
src: "streams:\n camera1: url1\nhomekit:\n camera1:\n name: dummy",
|
||||||
b, err := Patch(b, "camera2", "url3", "streams")
|
path: []string{"homekit", "camera1", "pairings"},
|
||||||
require.Nil(t, err)
|
value: []string{"val1"},
|
||||||
|
expect: "streams:\n camera1: url1\nhomekit:\n camera1:\n name: dummy\n pairings:\n - val1\n",
|
||||||
require.Equal(t, `streams:
|
},
|
||||||
camera1:
|
}
|
||||||
- url1
|
for _, tt := range tests {
|
||||||
- url2
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
camera2: url3
|
b, err := Patch([]byte(tt.src), tt.path, tt.value)
|
||||||
`, string(b))
|
require.NoError(t, err)
|
||||||
}
|
require.Equal(t, tt.expect, string(b))
|
||||||
|
})
|
||||||
func TestNoNewLineEnd1(t *testing.T) {
|
}
|
||||||
b := []byte(`streams:
|
|
||||||
camera1: url4
|
|
||||||
camera2:
|
|
||||||
- url2
|
|
||||||
- url3`)
|
|
||||||
|
|
||||||
b, err := Patch(b, "camera2", "url5", "streams")
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, `streams:
|
|
||||||
camera1: url4
|
|
||||||
camera2: url5
|
|
||||||
`, string(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNoNewLineEnd2(t *testing.T) {
|
|
||||||
b := []byte(`streams:
|
|
||||||
camera1: url1
|
|
||||||
homekit:
|
|
||||||
camera1:
|
|
||||||
pin: 123-45-678`)
|
|
||||||
|
|
||||||
// 1. Add new key
|
|
||||||
pairings := []string{"client1", "client2"}
|
|
||||||
|
|
||||||
b, err := Patch(b, "pairings", pairings, "homekit", "camera1")
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, `streams:
|
|
||||||
camera1: url1
|
|
||||||
homekit:
|
|
||||||
camera1:
|
|
||||||
pin: 123-45-678
|
|
||||||
pairings:
|
|
||||||
- client1
|
|
||||||
- client2
|
|
||||||
`, string(b))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ 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
|
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.
|
`win32` and `mac_amd64` binaries. All other binaries will use latest go version.
|
||||||
|
|
||||||
|
```
|
||||||
|
golang.org/x/crypto v0.33.0
|
||||||
|
golang.org/x/mod v0.20.0 // indirect
|
||||||
|
golang.org/x/tools v0.24.0 // indirect
|
||||||
|
```
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
- UPX-3.96 pack broken bin for `linux_mipsel`
|
- UPX-3.96 pack broken bin for `linux_mipsel`
|
||||||
|
|||||||
@@ -61,3 +61,13 @@ go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc
|
|||||||
@SET GOARCH=arm64
|
@SET GOARCH=arm64
|
||||||
@SET FILENAME=go2rtc_mac_arm64.zip
|
@SET FILENAME=go2rtc_mac_arm64.zip
|
||||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc
|
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc
|
||||||
|
|
||||||
|
@SET GOOS=freebsd
|
||||||
|
@SET GOARCH=amd64
|
||||||
|
@SET FILENAME=go2rtc_freebsd_amd64.zip
|
||||||
|
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc
|
||||||
|
|
||||||
|
@SET GOOS=freebsd
|
||||||
|
@SET GOARCH=arm64
|
||||||
|
@SET FILENAME=go2rtc_freebsd_arm64.zip
|
||||||
|
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc
|
||||||
|
|||||||
@@ -80,3 +80,15 @@ export GOOS=darwin
|
|||||||
export GOARCH=arm64
|
export GOARCH=arm64
|
||||||
FILENAME="go2rtc_mac_arm64.zip"
|
FILENAME="go2rtc_mac_arm64.zip"
|
||||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc
|
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc
|
||||||
|
|
||||||
|
# FreeBSD amd64
|
||||||
|
export GOOS=freebsd
|
||||||
|
export GOARCH=amd64
|
||||||
|
FILENAME="go2rtc_freebsd_amd64.zip"
|
||||||
|
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc
|
||||||
|
|
||||||
|
# FreeBSD arm64
|
||||||
|
export GOOS=freebsd
|
||||||
|
export GOARCH=arm64
|
||||||
|
FILENAME="go2rtc_freebsd_arm64.zip"
|
||||||
|
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
r = await fetch('api/config', {method: 'POST', body: editor.getValue()});
|
r = await fetch('api/config', {method: 'POST', body: editor.getValue()});
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
alert('OK');
|
alert('OK');
|
||||||
|
dump = editor.getValue();
|
||||||
await fetch('api/restart', {method: 'POST'});
|
await fetch('api/restart', {method: 'POST'});
|
||||||
} else {
|
} else {
|
||||||
alert(await r.text());
|
alert(await r.text());
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
const positions = network.getPositions();
|
const positions = network.getPositions();
|
||||||
const viewPosition = network.getViewPosition();
|
const viewPosition = network.getViewPosition();
|
||||||
const scale = network.getScale();
|
const scale = network.getScale();
|
||||||
|
const selectedNodes = network.getSelectedNodes();
|
||||||
|
|
||||||
network.setData(data);
|
network.setData(data);
|
||||||
|
|
||||||
@@ -65,6 +66,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
network.moveTo({position: viewPosition, scale: scale});
|
network.moveTo({position: viewPosition, scale: scale});
|
||||||
|
|
||||||
|
network.selectNodes(selectedNodes);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching or updating network data:', error);
|
console.error('Error fetching or updating network data:', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user