Compare commits

..

23 Commits

Author SHA1 Message Date
Alex X
a87dafbbec Update version to 1.8.4 2023-11-19 18:38:26 +03:00
Alex X
742cb7699b Improve magic producer about support mjpeg with trash on start 2023-11-18 17:08:57 +03:00
Alex X
43449e7b08 Fix api port for homekit module 2023-11-18 11:48:19 +03:00
Alex X
33512e73bd Add support ADTS to magic producer 2023-11-17 22:28:28 +03:00
Alex X
b367ffee6d Merge pull request #759 from russorat/ror/ngrok
fix: updating ngrok readme
2023-11-17 13:59:12 +03:00
Russ Savage
69447df6b3 fix: updating ngrok readme
Signed-off-by: Russ Savage <russorat@users.noreply.github.com>
2023-11-16 21:16:53 -08:00
Alex X
a6eac4ff02 Merge pull request #754 from inode64/master
Include support for Gentoo distribution
2023-11-15 21:20:46 +03:00
INODE64
1eaf879a76 Include support for Gentoo distribution 2023-11-15 17:45:36 +01:00
Alex X
c9ae6dcc03 Fix https source, again 2023-11-15 17:31:59 +03:00
Alex X
befa6bd356 Update version to 1.8.3 2023-11-15 12:20:47 +03:00
Alex X
100ab62ab4 Update dependencies 2023-11-15 12:16:34 +03:00
Alex X
a0f999d9c9 Add readme about gopro source 2023-11-15 12:10:16 +03:00
Alex X
9bda2f7e60 Add support gopro source 2023-11-15 11:41:42 +03:00
Alex X
54b19999c6 Fix support raw username/password for tapo source #748 2023-11-14 14:22:17 +03:00
Alex X
aa3c081352 Add support incoming H264 bitstream #745 2023-11-13 22:56:07 +03:00
Alex X
2d16ee8884 Code refactoring for mpegts input 2023-11-13 22:55:12 +03:00
Alex X
ec96a14807 Fix digest auth in some cases 2023-11-13 22:44:28 +03:00
Alex X
af72548a43 Fix panic for broken RTP with AAC #697 2023-11-13 21:56:35 +03:00
Alex X
6d85b36f47 Fix homekit source panic on stop producer #734 2023-11-13 21:51:52 +03:00
Alex X
28830a697d Add support unix socket for api module 2023-11-12 21:50:29 +03:00
Alex X
5d3953a948 Fix support Tapo C210 firmware v1.3.9 #733 2023-11-12 16:41:43 +03:00
Alex X
4d6432d38d Add about expr source to readme 2023-11-11 15:21:24 +03:00
Alex X
bcbebd5a36 Fix custom https client 2023-11-05 08:39:07 +03:00
23 changed files with 706 additions and 150 deletions

View File

@@ -23,7 +23,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- mixing tracks from different sources to single stream
- auto match client supported codecs
- [2-way audio](#two-way-audio) for some cameras
- streaming from private networks via [Ngrok](#module-ngrok)
- streaming from private networks via [ngrok](#module-ngrok)
- can be [integrated to](#module-api) any smart home platform or be used as [standalone app](#go2rtc-binary)
**Inspired by:**
@@ -54,11 +54,13 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
* [Source: FFmpeg Device](#source-ffmpeg-device)
* [Source: Exec](#source-exec)
* [Source: Echo](#source-echo)
* [Source: Expr](#source-expr)
* [Source: HomeKit](#source-homekit)
* [Source: Bubble](#source-bubble)
* [Source: DVRIP](#source-dvrip)
* [Source: Tapo](#source-tapo)
* [Source: Kasa](#source-kasa)
* [Source: GoPro](#source-gopro)
* [Source: Ivideon](#source-ivideon)
* [Source: Hass](#source-hass)
* [Source: ISAPI](#source-isapi)
@@ -75,7 +77,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
* [Module: WebRTC](#module-webrtc)
* [Module: HomeKit](#module-homekit)
* [Module: WebTorrent](#module-webtorrent)
* [Module: Ngrok](#module-ngrok)
* [Module: ngrok](#module-ngrok)
* [Module: Hass](#module-hass)
* [Module: MP4](#module-mp4)
* [Module: HLS](#module-hls)
@@ -125,7 +127,7 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
### go2rtc: Docker
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [Ngrok](#module-ngrok) and [Python](#source-echo).
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [ngrok](#module-ngrok) and [Python](#source-echo).
### go2rtc: Home Assistant Add-on
@@ -168,7 +170,7 @@ Available modules:
- [hls](#module-hls) - HLS TS or fMP4 stream Server
- [mjpeg](#module-mjpeg) - MJPEG Server
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
- [ngrok](#module-ngrok) - ngrok integration (external access for private network)
- [hass](#module-hass) - Home Assistant integration
- [log](#module-log) - logs config
@@ -186,11 +188,13 @@ Available source types:
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
- [exec](#source-exec) - get media from external app output
- [echo](#source-echo) - get stream link from bash or python
- [expr](#source-expr) - get stream link via built-in expression language
- [homekit](#source-homekit) - streaming from HomeKit Camera
- [bubble](#source-bubble) - streaming from ESeeCloud/dvr163 NVR
- [dvrip](#source-dvrip) - streaming from DVR-IP NVR
- [tapo](#source-tapo) - TP-Link Tapo cameras with [two way audio](#two-way-audio) support
- [kasa](#source-tapo) - TP-Link Kasa cameras
- [gopro](#source-gopro) - GoPro cameras
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
- [hass](#source-hass) - Home Assistant integration
- [isapi](#source-isapi) - two way audio for Hikvision (ISAPI) cameras
@@ -426,6 +430,10 @@ streams:
apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html
```
#### Source: Expr
Like `echo` source, but uses the built-in [expr](https://github.com/antonmedv/expr) expression language ([read more](https://github.com/AlexxIT/go2rtc/blob/master/internal/expr/README.md)).
#### Source: HomeKit
**Important:**
@@ -514,6 +522,10 @@ streams:
kasa: kasa://user:pass@192.168.1.123:19443/https/stream/mixed
```
#### Source: GoPro
Support streaming from [GoPro](https://gopro.com/) cameras, connected via USB or Wi-Fi to Linux, Mac, Windows. [Read more](https://github.com/AlexxIT/go2rtc/tree/master/internal/gopro).
#### Source: Ivideon
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
@@ -849,7 +861,7 @@ webrtc:
**Private IP**
- setup integration with [Ngrok service](#module-ngrok)
- setup integration with [ngrok service](#module-ngrok)
```yaml
ngrok:
@@ -951,29 +963,29 @@ Link example: https://alexxit.github.io/go2rtc/#share=02SNtgjKXY&pwd=wznEQqznxW&
TODO: article how it works...
### Module: Ngrok
### Module: ngrok
With Ngrok integration you can get external access to your streams in situation when you have Internet with private IP-address.
With ngrok integration you can get external access to your streams in situations when you have Internet with private IP-address.
- Ngrok preistalled for **Docker** and **Hass Add-on** users
- ngrok is pre-installed for **Docker** and **Hass Add-on** users
- you may need external access for two different things:
- WebRTC stream, so you need tunnel WebRTC TCP port (ex. 8555)
- go2rtc web interface, so you need tunnel API HTTP port (ex. 1984)
- Ngrok support authorization for your web interface
- Ngrok automatically adds HTTPS to your web interface
- ngrok support authorization for your web interface
- ngrok automatically adds HTTPS to your web interface
Ngrok free subscription limitations:
The ngrok free subscription has the following limitations:
- you will always get random external address (not a problem for webrtc stream)
- you can forward multiple ports but use only one Ngrok app
- You can reserve a free domain for serving the web interface, but the TCP address you get will always be random and change with each restart of the ngrok agent (not a problem for webrtc stream)
- You can forward multiple ports from a single agent, but you can only run one ngrok agent on the free plan
go2rtc will automatically get your external TCP address (if you enable it in ngrok config) and use it with WebRTC connection (if you enable it in webrtc config).
You need manually download [Ngrok agent app](https://ngrok.com/download) for your OS and register in [Ngrok service](https://ngrok.com/).
You need to manually download the [ngrok agent app](https://ngrok.com/download) for your OS and register with the [ngrok service](https://ngrok.com/signup).
**Tunnel for only WebRTC Stream**
You need to add your [Ngrok token](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
You need to add your [ngrok authtoken](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
```yaml
ngrok:
@@ -989,7 +1001,7 @@ ngrok:
command: ngrok start --all --config ngrok.yaml
```
Ngrok config example:
ngrok config example:
```yaml
version: "2"
@@ -1005,6 +1017,8 @@ tunnels:
proto: tcp
```
See the [ngrok agent documentation](https://ngrok.com/docs/agent/config/) for more details on the ngrok configuration file.
### Module: Hass
The best and easiest way to use go2rtc inside the Home Assistant is to install the custom integration [WebRTC Camera](#go2rtc-home-assistant-integration) and custom lovelace card.
@@ -1144,7 +1158,7 @@ webrtc:
- external access to WebRTC TCP port is not a problem, because it used only for transmit encrypted media data
- anyway you need to open this port to your local network and to the Internet in order for WebRTC to work
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [Ngrok](https://ngrok.com/), etc.
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [ngrok](https://ngrok.com/), etc.
PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted media. It works without problems on the local network. And sometimes also works for external access, even if you haven't opened this port on your router ([read more](https://en.wikipedia.org/wiki/UDP_hole_punching)). But for stable external WebRTC access, you need to open the 8555 port on your router for both TCP and UDP.
@@ -1286,6 +1300,7 @@ streams:
**Distributions**
- [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=go2rtc)
- [Gentoo](https://github.com/inode64/inode64-overlay/tree/main/media-video/go2rtc)
- [NixOS](https://search.nixos.org/packages?query=go2rtc)
- [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/)
- [QNAP](https://www.myqnap.org/product/go2rtc/)

30
go.mod
View File

@@ -4,33 +4,33 @@ go 1.21
require (
github.com/antonmedv/expr v1.15.3
github.com/gorilla/websocket v1.5.0
github.com/miekg/dns v1.1.56
github.com/gorilla/websocket v1.5.1
github.com/miekg/dns v1.1.57
github.com/pion/ice/v2 v2.3.11
github.com/pion/interceptor v0.1.22
github.com/pion/rtcp v1.2.10
github.com/pion/rtp v1.8.2
github.com/pion/interceptor v0.1.25
github.com/pion/rtcp v1.2.12
github.com/pion/rtp v1.8.3
github.com/pion/sdp/v3 v3.0.6
github.com/pion/srtp/v2 v2.0.17
github.com/pion/srtp/v2 v2.0.18
github.com/pion/stun v0.6.1
github.com/pion/webrtc/v3 v3.2.21
github.com/pion/webrtc/v3 v3.2.22
github.com/rs/zerolog v1.31.0
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
github.com/stretchr/testify v1.8.4
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
golang.org/x/crypto v0.14.0
golang.org/x/crypto v0.15.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pion/datachannel v1.5.5 // indirect
github.com/pion/dtls/v2 v2.2.7 // indirect
github.com/pion/dtls/v2 v2.2.8 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.9 // indirect
github.com/pion/randutil v0.1.0 // indirect
@@ -38,8 +38,8 @@ require (
github.com/pion/transport/v2 v2.2.4 // indirect
github.com/pion/turn/v2 v2.1.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/mod v0.13.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/tools v0.14.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/tools v0.15.0 // indirect
)

31
go.sum
View File

@@ -23,8 +23,12 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
@@ -37,8 +41,12 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE=
github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -52,11 +60,15 @@ github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/dtls/v2 v2.2.8 h1:BUroldfiIbV9jSnC6cKOMnyiORRWrWWpV11JUyEu5OA=
github.com/pion/dtls/v2 v2.2.8/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/ice/v2 v2.3.11 h1:rZjVmUwyT55cmN8ySMpL7rsS8KYsJERsrxJLLxpKhdw=
github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
github.com/pion/interceptor v0.1.18/go.mod h1:tpvvF4cPM6NGxFA1DUMbhabzQBxdWMATDGEUYOR9x6I=
github.com/pion/interceptor v0.1.22 h1:khhimAF0/VmGaIfeE+bA3X1jm0lD8C8HOGcU7vpWcPA=
github.com/pion/interceptor v0.1.22/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc=
github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
@@ -66,9 +78,13 @@ github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
github.com/pion/rtcp v1.2.12 h1:bKWiX93XKgDZENEXCijvHRU/wRifm6JV5DGcH6twtSM=
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtp v1.8.1/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/rtp v1.8.2 h1:oKMM0K1/QYQ5b5qH+ikqDSZRipP5mIxPJcgcvw5sH0w=
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
@@ -77,6 +93,8 @@ github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/srtp/v2 v2.0.17 h1:ECuOk+7uIpY6HUlTb0nXhfvu4REG2hjtC4ronYFCZE4=
github.com/pion/srtp/v2 v2.0.17/go.mod h1:y5WSHcJY4YfNB/5r7ca5YjHeIr1H3LM1rKArGGs8jMc=
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
@@ -93,6 +111,8 @@ github.com/pion/turn/v2 v2.1.4 h1:2xn8rduI5W6sCZQkEnIUDAkrBQNl2eYIBCHMZ3QMmP8=
github.com/pion/turn/v2 v2.1.4/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
github.com/pion/webrtc/v3 v3.2.21 h1:c8fy5JcqJkAQBwwy3Sk9huQLTBUSqaggyRlv9Lnh2zY=
github.com/pion/webrtc/v3 v3.2.21/go.mod h1:vVURQTBOG5BpWKOJz3nlr23NfTDeyKVmubRNqzQp+Tg=
github.com/pion/webrtc/v3 v3.2.22 h1:Hno262T7+V56MgUO30O0ZirZmVSvbXtnau31SB0WSpc=
github.com/pion/webrtc/v3 v3.2.22/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -129,11 +149,15 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -152,6 +176,8 @@ golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -159,6 +185,7 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -186,6 +213,8 @@ 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.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -214,6 +243,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -10,6 +10,7 @@ import (
"strconv"
"strings"
"sync"
"syscall"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/pkg/shell"
@@ -28,6 +29,7 @@ func Init() {
TLSListen string `yaml:"tls_listen"`
TLSCert string `yaml:"tls_cert"`
TLSKey string `yaml:"tls_key"`
UnixListen string `yaml:"unix_listen"`
} `yaml:"api"`
}
@@ -37,7 +39,7 @@ func Init() {
// load config from YAML
app.LoadConfig(&cfg)
if cfg.Mod.Listen == "" {
if cfg.Mod.Listen == "" && cfg.Mod.UnixListen == "" && cfg.Mod.TLSListen == "" {
return
}
@@ -51,16 +53,6 @@ func Init() {
HandleFunc("api/exit", exitHandler)
HandleFunc("api/restart", restartHandler)
// ensure we can listen without errors
var err error
ln, err = net.Listen("tcp", cfg.Mod.Listen)
if err != nil {
log.Fatal().Err(err).Msg("[api] listen")
return
}
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
Handler = http.DefaultServeMux // 4th
if cfg.Mod.Origin == "*" {
@@ -75,58 +67,73 @@ func Init() {
Handler = middlewareLog(Handler) // 1st
}
go func() {
s := http.Server{}
s.Handler = Handler
if err = s.Serve(ln); err != nil {
log.Fatal().Err(err).Msg("[api] serve")
if cfg.Mod.Listen != "" {
go listen("tcp", cfg.Mod.Listen)
}
if cfg.Mod.UnixListen != "" {
_ = syscall.Unlink(cfg.Mod.UnixListen)
go listen("unix", cfg.Mod.UnixListen)
}
}()
// Initialize the HTTPS server
if cfg.Mod.TLSListen != "" && cfg.Mod.TLSCert != "" && cfg.Mod.TLSKey != "" {
go tlsListen("tcp", cfg.Mod.TLSListen, cfg.Mod.TLSCert, cfg.Mod.TLSKey)
}
}
func listen(network, address string) {
ln, err := net.Listen(network, address)
if err != nil {
log.Error().Err(err).Msg("[api] listen")
return
}
log.Info().Str("addr", address).Msg("[api] listen")
if network == "tcp" {
Port = ln.Addr().(*net.TCPAddr).Port
}
server := http.Server{Handler: Handler}
if err = server.Serve(ln); err != nil {
log.Fatal().Err(err).Msg("[api] serve")
}
}
func tlsListen(network, address, certFile, keyFile string) {
var cert tls.Certificate
if strings.IndexByte(cfg.Mod.TLSCert, '\n') < 0 && strings.IndexByte(cfg.Mod.TLSKey, '\n') < 0 {
var err error
if strings.IndexByte(certFile, '\n') < 0 && strings.IndexByte(keyFile, '\n') < 0 {
// check if file path
cert, err = tls.LoadX509KeyPair(cfg.Mod.TLSCert, cfg.Mod.TLSKey)
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
} else {
// if text file content
cert, err = tls.X509KeyPair([]byte(cfg.Mod.TLSCert), []byte(cfg.Mod.TLSKey))
cert, err = tls.X509KeyPair([]byte(certFile), []byte(keyFile))
}
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
tlsListener, err := net.Listen("tcp", cfg.Mod.TLSListen)
ln, err := net.Listen(network, address)
if err != nil {
log.Fatal().Err(err).Caller().Send()
log.Error().Err(err).Msg("[api] tls listen")
return
}
log.Info().Str("addr", cfg.Mod.TLSListen).Msg("[api] tls listen")
log.Info().Str("addr", address).Msg("[api] tls listen")
tlsServer := &http.Server{
server := &http.Server{
Handler: Handler,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
},
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
}
go func() {
if err := tlsServer.ServeTLS(tlsListener, "", ""); err != nil {
if err = server.ServeTLS(ln, "", ""); err != nil {
log.Fatal().Err(err).Msg("[api] tls serve")
}
}()
}
}
func Port() int {
if ln == nil {
return 0
}
return ln.Addr().(*net.TCPAddr).Port
}
var Port int
const (
MimeJSON = "application/json"
@@ -187,7 +194,7 @@ func middlewareLog(next http.Handler) http.Handler {
func middlewareAuth(username, password string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") {
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") && r.RemoteAddr != "@" {
user, pass, ok := r.BasicAuth()
if !ok || user != username || pass != password {
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
@@ -209,7 +216,6 @@ func middlewareCORS(next http.Handler) http.Handler {
})
}
var ln net.Listener
var mu sync.Mutex
func apiHandler(w http.ResponseWriter, r *http.Request) {

View File

@@ -17,7 +17,7 @@ import (
"github.com/rs/zerolog/log"
)
var Version = "1.8.2"
var Version = "1.8.4"
var UserAgent = "go2rtc/" + Version
var ConfigPath string

View File

@@ -1,12 +1,91 @@
# Expr
[Expr](https://github.com/antonmedv/expr) - expression language and expression evaluation for Go.
- [language definition](https://expr.medv.io/docs/Language-Definition) - takes best from JS, Python, Jinja2 syntax
- your expression should return a link of any supported source
- expression supports multiple operation, but:
- all operations must be separated by a semicolon
- all operations, except the last one, must declare a new variable (`let s = "abc";`)
- the last operation should return a string
- go2rtc supports additional functions:
- `fetch` - JS-like HTTP requests
- `match` - JS-like RegExp queries
## Examples
**Two way audio for Dahua VTO**
```yaml
streams:
dahua_vto: |
expr: let host = "admin:password@192.168.1.123";
fetch("http://"+host+"/cgi-bin/configManager.cgi?action=setConfig&Encode[0].MainFormat[0].Audio.Compression=G.711A&Encode[0].MainFormat[0].Audio.Frequency=8000").ok
? "rtsp://"+host+"/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif" : ""
```
**dom.ru**
You can get credentials via:
- https://github.com/alexmorbo/domru (file `/share/domru/accounts`)
- https://github.com/ad/domru
```yaml
streams:
dom_ru: |
expr: let camera = "99999999"; let token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; let operator = 99;
fetch("https://myhome.novotelecom.ru/rest/v1/forpost/cameras/"+camera+"/video", {
headers: {Authorization: "Bearer "+token, Operator: operator}
}).json().data.URL
```
**Parse HLS files from Apple**
Same example in two languages - python and expr.
```yaml
streams:
example_python: |
echo:python -c 'from urllib.request import urlopen; import re
# url1 = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
html1 = urlopen("https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html").read().decode("utf-8")
url1 = re.search(r"https.+?m3u8", html1)[0]
# url2 = "gear1/prog_index.m3u8"
html2 = urlopen(url1).read().decode("utf-8")
url2 = re.search(r"^[a-z0-1/_]+\.m3u8$", html2, flags=re.MULTILINE)[0]
# url3 = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear1/prog_index.m3u8"
url3 = url1[:url1.rindex("/")+1] + url2
print("ffmpeg:" + url3 + "#video=copy")'
example_expr: |
expr:
let html1 = fetch("https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html").text;
let url1 = match(html1, "https.+?m3u8")[0];
let html2 = fetch(url1).text;
let url2 = match(html2, "^[a-z0-1/_]+\\.m3u8$", "m")[0];
let url3 = url1[:lastIndexOf(url1, "/")+1] + url2;
"ffmpeg:" + url3 + "#video=copy"
```
## Comparsion
| expr | python | js |
|--------------------------|--------------------------|--------------------------------|
|------------------------------|----------------------------|--------------------------------|
| let x = 1; | x = 1 | let x = 1 |
| {a: 1, b: 2} | {"a": 1, "b": 2} | {a: 1, b: 2} |
| r = fetch(url, {method}) | r = request(method, url) | r = await fetch(url, {method}) |
| let r = fetch(url, {method}) | r = request(method, url) | r = await fetch(url, {method}) |
| r.ok | r.ok | r.ok |
| r.status | r.status_code | r.status |
| r.text | r.text | await r.text() |
| r.json() | r.json() | await r.json() |
| r.headers | r.headers | r.headers |
| let m = match(text, "abc") | m = re.search("abc", text) | let m = text.match(/abc/) |

25
internal/gopro/README.md Normal file
View File

@@ -0,0 +1,25 @@
# GoPro
Supported models: HERO9, HERO10, HERO11, HERO12.
Supported OS: Linux, Mac, Windows, [HassOS](https://www.home-assistant.io/installation/)
The other camera models have different APIs. I will try to add them in the next versions.
## Config
- USB-connected cameras create a new network interface in the system
- Linux users do not need to install anything
- Windows users should install the [network driver](https://community.gopro.com/s/article/GoPro-Webcam)
- if the camera is detected but the stream does not start - you need to disable firewall
1. Discover camera address: WebUI > Add > GoPro
2. Add camera to config
```yaml
streams:
hero12: gopro://172.20.100.51
```
## Useful links
- https://gopro.github.io/OpenGoPro/

30
internal/gopro/gopro.go Normal file
View File

@@ -0,0 +1,30 @@
package gopro
import (
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/gopro"
)
func Init() {
streams.HandleFunc("gopro", handleGoPro)
api.HandleFunc("api/gopro", apiGoPro)
}
func handleGoPro(rawURL string) (core.Producer, error) {
return gopro.Dial(rawURL)
}
func apiGoPro(w http.ResponseWriter, r *http.Request) {
var items []*api.Source
for _, host := range gopro.Discovery() {
items = append(items, &api.Source{Name: host, URL: "gopro://" + host})
}
api.ResponseSources(w, items)
}

View File

@@ -98,7 +98,7 @@ func Init() {
srv.mdns = &mdns.ServiceEntry{
Name: name,
Port: uint16(api.Port()),
Port: uint16(api.Port),
Info: map[string]string{
hap.TXTConfigNumber: "1",
hap.TXTFeatureFlags: "0",

View File

@@ -7,6 +7,7 @@ import (
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hls"
@@ -22,6 +23,8 @@ func Init() {
streams.HandleFunc("httpx", handleHTTP)
streams.HandleFunc("tcp", handleTCP)
api.HandleFunc("api/stream", apiStream)
}
func handleHTTP(rawURL string) (core.Producer, error) {
@@ -89,3 +92,26 @@ func handleTCP(rawURL string) (core.Producer, error) {
return magic.Open(conn)
}
func apiStream(w http.ResponseWriter, r *http.Request) {
dst := r.URL.Query().Get("dst")
stream := streams.Get(dst)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
client, err := magic.Open(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stream.AddProducer(client)
defer stream.RemoveProducer(client)
if err = client.Start(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

View File

@@ -56,19 +56,17 @@ func inputMpegTS(w http.ResponseWriter, r *http.Request) {
return
}
res := &http.Response{Body: r.Body, Request: r}
client, err := mpegts.Open(res.Body)
client, err := mpegts.Open(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stream.AddProducer(client)
defer stream.RemoveProducer(client)
if err = client.Start(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stream.RemoveProducer(client)
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/AlexxIT/go2rtc/internal/exec"
"github.com/AlexxIT/go2rtc/internal/expr"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
"github.com/AlexxIT/go2rtc/internal/gopro"
"github.com/AlexxIT/go2rtc/internal/hass"
"github.com/AlexxIT/go2rtc/internal/hls"
"github.com/AlexxIT/go2rtc/internal/homekit"
@@ -78,10 +79,11 @@ func main() {
nest.Init() // nest source
bubble.Init() // bubble source
expr.Init() // expr source
gopro.Init() // gopro source
// 6. Helper modules
ngrok.Init() // Ngrok module
ngrok.Init() // ngrok module
srtp.Init() // SRTP server
debug.Init() // debug API

View File

@@ -10,7 +10,7 @@ import (
func IsADTS(b []byte) bool {
_ = b[1]
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF0 == 0xF0
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF6 == 0xF0
}
func ADTSToCodec(b []byte) *core.Codec {

73
pkg/aac/producer.go Normal file
View File

@@ -0,0 +1,73 @@
package aac
import (
"bufio"
"encoding/binary"
"io"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
)
type Producer struct {
core.SuperProducer
rd *bufio.Reader
cl io.Closer
}
func Open(r io.Reader) (*Producer, error) {
rd := bufio.NewReader(r)
b, err := rd.Peek(8)
if err != nil {
return nil, err
}
codec := ADTSToCodec(b)
prod := &Producer{rd: rd, cl: r.(io.Closer)}
prod.Type = "ADTS producer"
prod.Medias = []*core.Media{
{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
},
}
return prod, nil
}
func (c *Producer) Start() error {
for {
b, err := c.rd.Peek(6)
if err != nil {
return err
}
auSize := ReadADTSSize(b)
payload := make([]byte, 2+2+auSize)
if _, err = io.ReadFull(c.rd, payload[4:]); err != nil {
return err
}
c.Recv += int(auSize)
if len(c.Receivers) == 0 {
continue
}
payload[1] = 16 // header size in bits
binary.BigEndian.PutUint16(payload[2:], auSize<<3)
pkt := &rtp.Packet{
Header: rtp.Header{Timestamp: core.Now90000()},
Payload: payload,
}
c.Receivers[0].WriteRTP(pkt)
}
}
func (c *Producer) Stop() error {
_ = c.SuperProducer.Close()
return c.cl.Close()
}

View File

@@ -28,9 +28,13 @@ func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
headers := packet.Payload[2 : 2+headersSize]
units := packet.Payload[2+headersSize:]
for len(headers) > 0 {
for len(headers) >= 2 {
unitSize := binary.BigEndian.Uint16(headers) >> 3
if len(units) < int(unitSize) {
return
}
unit := units[:unitSize]
headers = headers[2:]

43
pkg/gopro/discovery.go Normal file
View File

@@ -0,0 +1,43 @@
package gopro
import (
"net"
"net/http"
"regexp"
)
func Discovery() (urls []string) {
ints, err := net.Interfaces()
if err != nil {
return nil
}
// The socket address for USB connections is 172.2X.1YZ.51:8080
// https://gopro.github.io/OpenGoPro/http_2_0#socket-address
re := regexp.MustCompile(`^172\.2\d\.1\d\d\.`)
for _, itf := range ints {
addrs, err := itf.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
host := addr.String()
if !re.MatchString(host) {
continue
}
host = host[:11] + "51" // 172.2x.1xx.xxx
res, err := http.Get("http://" + host + ":8080/gopro/webcam/status")
if err != nil {
continue
}
_ = res.Body.Close()
urls = append(urls, host)
}
}
return
}

117
pkg/gopro/gopro.go Normal file
View File

@@ -0,0 +1,117 @@
package gopro
import (
"errors"
"io"
"net"
"net/http"
"net/url"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
)
func Dial(rawURL string) (core.Producer, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
r := &listener{host: u.Host}
if err = r.command("/gopro/webcam/stop"); err != nil {
return nil, err
}
if err = r.listen(); err != nil {
return nil, err
}
if err = r.command("/gopro/webcam/start"); err != nil {
return nil, err
}
return mpegts.Open(r)
}
type listener struct {
conn net.PacketConn
host string
packet []byte
packets chan []byte
}
func (r *listener) Read(p []byte) (n int, err error) {
if r.packet == nil {
var ok bool
if r.packet, ok = <-r.packets; !ok {
return 0, io.EOF // channel closed
}
}
n = copy(p, r.packet)
if n < len(r.packet) {
r.packet = r.packet[n:]
} else {
r.packet = nil
}
return
}
func (r *listener) Close() error {
return r.conn.Close()
}
func (r *listener) command(api string) error {
client := &http.Client{Timeout: 5 * time.Second}
res, err := client.Get("http://" + r.host + ":8080" + api)
if err != nil {
return err
}
_ = res.Body.Close()
if res.StatusCode != http.StatusOK {
return errors.New("gopro: wrong response: " + res.Status)
}
return nil
}
func (r *listener) listen() (err error) {
if r.conn, err = net.ListenPacket("udp4", ":8554"); err != nil {
return
}
r.packets = make(chan []byte, 1024)
go r.worker()
return
}
func (r *listener) worker() {
b := make([]byte, 1500)
for {
if err := r.conn.SetReadDeadline(time.Now().Add(3 * time.Second)); err != nil {
break
}
n, _, err := r.conn.ReadFrom(b)
if err != nil {
break
}
packet := make([]byte, n)
copy(packet, b)
r.packets <- packet
}
close(r.packets)
_ = r.command("/gopro/webcam/stop")
}

View File

@@ -70,6 +70,7 @@ func (c *Producer) Start() error {
break
}
if len(c.Receivers) > 0 {
pkt := &rtp.Packet{
Header: rtp.Header{Timestamp: core.Now90000()},
Payload: annexb.EncodeToAVCC(buf[:i], true),
@@ -77,6 +78,7 @@ func (c *Producer) Start() error {
c.Receivers[0].WriteRTP(pkt)
//log.Printf("[AVC] %v, len: %d", h264.Types(pkt.Payload), len(pkt.Payload))
}
buf = buf[i:]
}

View File

@@ -6,6 +6,7 @@ import (
"errors"
"io"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/flv"
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
@@ -33,6 +34,9 @@ func Open(r io.Reader) (core.Producer, error) {
case bytes.HasPrefix(b, []byte(flv.Signature)):
return flv.Open(rd)
case bytes.HasPrefix(b, []byte{0xFF, 0xF1}):
return aac.Open(rd)
case bytes.HasPrefix(b, []byte("--")):
return multipart.Open(rd)
@@ -40,5 +44,16 @@ func Open(r io.Reader) (core.Producer, error) {
return mpegts.Open(rd)
}
return nil, errors.New("magic: unsupported header: " + hex.EncodeToString(b))
// support MJPEG with trash on start
// https://github.com/AlexxIT/go2rtc/issues/747
if b, err = rd.Peek(4096); err != nil {
return nil, err
}
if i := bytes.Index(b, []byte{0xFF, 0xD8, 0xFF, 0xDB}); i > 0 {
_, _ = io.ReadFull(rd, make([]byte, i))
return mjpeg.Open(rd)
}
return nil, errors.New("magic: unsupported header: " + hex.EncodeToString(b[:4]))
}

View File

@@ -57,7 +57,8 @@ func (s *Server) DelSession(session *Session) {
delete(s.sessions, session.Remote.SSRC)
if len(s.sessions) == 0 {
// check s.conn for https://github.com/AlexxIT/go2rtc/issues/734
if len(s.sessions) == 0 && s.conn != nil {
_ = s.conn.Close()
}

View File

@@ -1,10 +1,12 @@
package tapo
import (
"bufio"
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
@@ -14,6 +16,7 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
@@ -62,33 +65,19 @@ func (c *Client) newConn() (net.Conn, error) {
return nil, err
}
// support raw username/password
username := u.User.Username()
password, _ := u.User.Password()
// or cloud password in place of username
if password == "" {
password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
username = "admin"
u.User = url.UserPassword(username, password)
}
u.Scheme = "http"
u.Path = "/stream"
if u.Port() == "" {
u.Host += ":8800"
}
// TODO: fix closing connection
ctx, pconn := tcp.WithConn()
req, err := http.NewRequestWithContext(ctx, "POST", u.String(), nil)
req, err := http.NewRequest("POST", "http://"+u.Host+"/stream", nil)
if err != nil {
return nil, err
}
req.URL.User = u.User
req.Header.Set("Content-Type", "multipart/mixed; boundary=--client-stream-boundary--")
res, err := tcp.Do(req)
conn, res, err := dial(req)
if err != nil {
return nil, err
}
@@ -98,13 +87,16 @@ func (c *Client) newConn() (net.Conn, error) {
}
if c.decrypt == nil {
c.newDectypter(res, username, password)
c.newDectypter(res)
}
return *pconn, nil
return conn, nil
}
func (c *Client) newDectypter(res *http.Response, username, password string) {
func (c *Client) newDectypter(res *http.Response) {
username := res.Request.URL.User.Username()
password, _ := res.Request.URL.User.Password()
// extract nonce from response
// cipher="AES_128_CBC" username="admin" padding="PKCS7_16" algorithm="MD5" nonce="***"
nonce := res.Header.Get("Key-Exchange")
@@ -244,3 +236,70 @@ func (c *Client) Request(conn net.Conn, body []byte) (string, error) {
return v.Params.SessionID, nil
}
}
func dial(req *http.Request) (net.Conn, *http.Response, error) {
conn, err := net.DialTimeout("tcp", req.URL.Host, core.ConnDialTimeout)
if err != nil {
return nil, nil, err
}
username := req.URL.User.Username()
password, _ := req.URL.User.Password()
req.URL.User = nil
if err = req.Write(conn); err != nil {
return nil, nil, err
}
r := bufio.NewReader(conn)
res, err := http.ReadResponse(r, req)
if err != nil {
return nil, nil, err
}
auth := res.Header.Get("WWW-Authenticate")
if res.StatusCode != http.StatusUnauthorized || !strings.HasPrefix(auth, "Digest") {
return nil, nil, err
}
if password == "" {
// support cloud password in place of username
if strings.Contains(auth, `encrypt_type="3"`) {
password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username)))
} else {
password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
}
username = "admin"
}
realm := tcp.Between(auth, `realm="`, `"`)
nonce := tcp.Between(auth, `nonce="`, `"`)
qop := tcp.Between(auth, `qop="`, `"`)
uri := req.URL.RequestURI()
ha1 := tcp.HexMD5(username, realm, password)
ha2 := tcp.HexMD5(req.Method, uri)
nc := "00000001"
cnonce := "00000001"
response := tcp.HexMD5(ha1, nonce, nc, cnonce, qop, ha2)
header := fmt.Sprintf(
`Digest username="%s", realm="%s", nonce="%s", uri="%s", qop=%s, nc=%s, cnonce="%s", response="%s"`,
username, realm, nonce, uri, qop, nc, cnonce, response,
)
req.Header.Set("Authorization", header)
if err = req.Write(conn); err != nil {
return nil, nil, err
}
if res, err = http.ReadResponse(r, req); err != nil {
return nil, nil, err
}
req.URL.User = url.UserPassword(username, password)
return conn, res, nil
}

View File

@@ -22,11 +22,14 @@ func Do(req *http.Request) (*http.Response, error) {
case "https":
if hostname := req.URL.Hostname(); IsIP(hostname) {
secure = &tls.Config{InsecureSkipVerify: true}
} else {
secure = &tls.Config{ServerName: hostname}
}
}
if secure != nil {
ctx := context.WithValue(req.Context(), secureKey, secure)
req = req.WithContext(ctx)
}
if client == nil {
transport := http.DefaultTransport.(*http.Transport).Clone()
@@ -43,10 +46,21 @@ func Do(req *http.Request) (*http.Response, error) {
if err != nil {
return nil, err
}
tlsConn := tls.Client(conn, secure)
var conf *tls.Config
if v, ok := ctx.Value(secureKey).(*tls.Config); ok {
conf = v
} else if host, _, err := net.SplitHostPort(addr); err != nil {
conf = &tls.Config{ServerName: addr}
} else {
conf = &tls.Config{ServerName: host}
}
tlsConn := tls.Client(conn, conf)
if err = tlsConn.Handshake(); err != nil {
return nil, err
}
if pconn, ok := ctx.Value(connKey).(*net.Conn); ok {
*pconn = tlsConn
}
@@ -97,7 +111,7 @@ func Do(req *http.Request) (*http.Response, error) {
response := HexMD5(ha1, nonce, ha2)
header = fmt.Sprintf(
`Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s"`,
user, realm, nonce, uri, response,
username, realm, nonce, uri, response,
)
case "auth":
nc := "00000001"
@@ -122,7 +136,11 @@ func Do(req *http.Request) (*http.Response, error) {
}
var client *http.Client
var connKey struct{}
type key string
var connKey = key("conn")
var secureKey = key("secure")
func WithConn() (context.Context, *net.Conn) {
pconn := new(net.Conn)

View File

@@ -246,6 +246,18 @@
</script>
<button id="gopro">GoPro</button>
<div class="module">
<table id="gopro-table"></table>
</div>
<script>
document.getElementById('gopro').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
await getSources('gopro-table', 'api/gopro');
});
</script>
<button id="hass">Home Assistant</button>
<div class="module">
<table id="hass-table"></table>