Merge branch 'master' of https://github.com/AlexxIT/go2rtc into update-readme

This commit is contained in:
Felipe Santos
2026-01-19 14:11:30 -03:00
169 changed files with 17332 additions and 2954 deletions
+224 -171
View File
@@ -19,7 +19,6 @@ Ultimate camera streaming application with support for RTSP, WebRTC, HomeKit, FF
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4), [HomeKit](#module-homekit) [HLS](#module-hls) or [MJPEG](#module-mjpeg)
- [publish](#publish-stream) any source to popular streaming services (YouTube, Telegram, etc.)
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
- support H265 for WebRTC in browser (Safari only, [read more](https://github.com/AlexxIT/Blog/issues/5))
- on-the-fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
- play audio files and live streams on some cameras with [speaker](#stream-to-camera)
- multi-source two-way [codecs negotiation](#codecs-negotiation)
@@ -28,6 +27,20 @@ Ultimate camera streaming application with support for RTSP, WebRTC, HomeKit, FF
- [two-way audio](#two-way-audio) for some cameras
- can be [integrated to](#module-api) any smart home platform or be used as [standalone app](#go2rtc-binary)
**Supported Formats** - describes the communication API: authorization, encryption, command set, structure of media packets
- devices: `alsa` (Linux audio), `v4l2` (Linux video)
- files: `adts`, `flv`, `h264`, `hevc`, `hls`, `mjpeg`, `mpegts`, `mp4`, `wav`
- network (public and well known): `mpjpeg`, `onvif`, `rtmp`, `rtp`, `rtsp`, `webrtc`, `yuv4mpegpipe`
- network (private and exclusive): `bubble`, `doorbird`, `dvrip`, `eseecloud`, `gopro`, `hass` (Home Assistant), `homekit` (Apple), `isapi` (Hikvision), `kasa` (TP-Link), `multitrans` (TP-Link), `nest` (Google), `ring`, `roborock`, `tapo` and `vigi` (TP-Link), `tuya`, `webtorrent`, `wyze`, `xiaomi` (Mi Home)
- webrtc related: `creality`, `kinesis` (Amazon), `openipc`, `switchbot`, `whep`, `whip`, `wyze`
- other: `ascii`, `echo`, `exec`, `expr`, `ffmpeg`
**Supported Protocols** - describes the transport for data transmission
- public: `http`, `pipe`, `rtmp`, `rtsp`, `tcp`, `udp`, `webrtc`, `ws` (WebSocket)
- private: `cs2` (PPPP), `hap` and `hds` (HomeKit), `tutk` (P2P)
**Inspired by:**
- series of streaming projects from [@deepch](https://github.com/deepch)
@@ -39,46 +52,51 @@ Ultimate camera streaming application with support for RTSP, WebRTC, HomeKit, FF
- creator of the project's logo [@v_novoseltsev](https://www.instagram.com/v_novoseltsev)
> [!CAUTION]
> There is NO existing website for go2rtc project other than this GitHub repository. The website go2rtc[.]com is in no way associated with the authors of this project.
> The official website of the project is this GitHub repository and go2rtc.org (hosted on GitHub Pages). The website go2rtc[.]com is in no way associated with the authors of this project.
---
- [Fast start](#fast-start)
- [go2rtc: Binary](#go2rtc-binary)
- [go2rtc: Docker](#go2rtc-docker)
- [go2rtc: Home Assistant Add-on](#go2rtc-home-assistant-add-on)
- [go2rtc: Home Assistant Integration](#go2rtc-home-assistant-integration)
- [go2rtc: Home Assistant add-on](#go2rtc-home-assistant-add-on)
- [go2rtc: Home Assistant integration](#go2rtc-home-assistant-integration)
- [go2rtc: Dev version](#go2rtc-dev-version)
- [Configuration](#configuration)
- [Module: Streams](#module-streams)
- [Two-way audio](#two-way-audio)
- [Source: RTSP](#source-rtsp)
- [Source: RTMP](#source-rtmp)
- [Source: HTTP](#source-http)
- [Source: ONVIF](#source-onvif)
- [Source: FFmpeg](#source-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)
- [Source: Nest](#source-nest)
- [Source: Ring](#source-ring)
- [Source: Roborock](#source-roborock)
- [Source: WebRTC](#source-webrtc)
- [Source: WebTorrent](#source-webtorrent)
- [Incoming sources](#incoming-sources)
- [Stream to camera](#stream-to-camera)
- [Publish stream](#publish-stream)
- [Preload stream](#preload-stream)
- [Two-way audio](#two-way-audio)
- [Source: RTSP](#source-rtsp)
- [Source: RTMP](#source-rtmp)
- [Source: HTTP](#source-http)
- [Source: ONVIF](#source-onvif)
- [Source: FFmpeg](#source-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: Multitrans](#source-multitrans)
- [Source: Tuya](#source-tuya)
- [Source: Xiaomi](#source-xiaomi)
- [Source: Wyze](#source-wyze)
- [Source: GoPro](#source-gopro)
- [Source: Ivideon](#source-ivideon)
- [Source: Hass](#source-hass)
- [Source: ISAPI](#source-isapi)
- [Source: Nest](#source-nest)
- [Source: Ring](#source-ring)
- [Source: Roborock](#source-roborock)
- [Source: Doorbird](#source-doorbird)
- [Source: WebRTC](#source-webrtc)
- [Source: WebTorrent](#source-webtorrent)
- [Incoming sources](#incoming-sources)
- [Stream to camera](#stream-to-camera)
- [Publish stream](#publish-stream)
- [Preload stream](#preload-stream)
- [Module: API](#module-api)
- [Module: RTSP](#module-rtsp)
- [Module: RTMP](#module-rtmp)
@@ -96,11 +114,10 @@ Ultimate camera streaming application with support for RTSP, WebRTC, HomeKit, FF
- [Codecs madness](#codecs-madness)
- [Codecs negotiation](#codecs-negotiation)
- [Projects using go2rtc](#projects-using-go2rtc)
- [Camera experience](#camera-experience)
- [Camera experience](#cameras-experience)
- [TIPS](#tips)
- [FAQ](#faq)
## Fast start
# Fast start
1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or Home Assistant [add-on](#go2rtc-home-assistant-add-on) or [Integration](#go2rtc-home-assistant-integration)
2. Open web interface: `http://localhost:1984/`
@@ -115,7 +132,7 @@ Ultimate camera streaming application with support for RTSP, WebRTC, HomeKit, FF
- write your own [web interface](#module-api)
- integrate [web api](#module-api) into your smart home platform
### go2rtc: Binary
## go2rtc: Binary
Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/):
@@ -135,11 +152,13 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
### go2rtc: Docker
PS. The application is compiled with the latest versions of the Go language for maximum speed and security. Therefore, the [minimum OS versions](https://go.dev/wiki/MinimumRequirements) depend on the Go language.
## go2rtc: Docker
The Docker container [`alexxit/go2rtc`](https://hub.docker.com/r/alexxit/go2rtc) supports multiple architectures including `amd64`, `386`, `arm64`, and `arm`. This container offers the same functionality as the [Home Assistant add-on](#go2rtc-home-assistant-add-on) but is designed to operate independently of Home Assistant. It comes preinstalled with [FFmpeg](#source-ffmpeg) and [Python](#source-echo).
### go2rtc: Home Assistant add-on
## go2rtc: Home Assistant add-on
[![](https://my.home-assistant.io/badges/supervisor_addon.svg)](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons)
@@ -148,11 +167,11 @@ The Docker container [`alexxit/go2rtc`](https://hub.docker.com/r/alexxit/go2rtc)
- go2rtc > Install > Start
2. Setup [Integration](#module-hass)
### go2rtc: Home Assistant Integration
## go2rtc: Home Assistant Integration
[WebRTC Camera](https://github.com/AlexxIT/WebRTC) custom component can be used on any [Home Assistant installation](https://www.home-assistant.io/installation/), including [HassWP](https://github.com/AlexxIT/HassWP) on Windows. It can automatically download and use the latest version of go2rtc. Or it can connect to an existing version of go2rtc. Addon installation in this case is optional.
### go2rtc: Dev version
## go2rtc: Dev version
Latest, but maybe unstable version:
@@ -160,7 +179,7 @@ Latest, but maybe unstable version:
- Docker: `alexxit/go2rtc:master` or `alexxit/go2rtc:master-hardware` versions
- Home Assistant add-on: `go2rtc master` or `go2rtc master hardware` versions
## Configuration
# Configuration
- by default go2rtc will search `go2rtc.yaml` in the current work directory
- `api` server will start on default **1984 port** (TCP)
@@ -184,7 +203,7 @@ Available modules:
- [hass](#module-hass) - Home Assistant integration
- [log](#module-log) - logs config
### Module: Streams
## Module: Streams
**go2rtc** supports different stream source types. You can config one or multiple links of any type as a stream source.
@@ -202,20 +221,25 @@ Available source types:
- [homekit](#source-homekit) - streaming from HomeKit Camera
- [bubble](#source-bubble) - streaming from ESeeCloud/dvr163 NVR
- [dvrip](#source-dvrip) - streaming from DVR-IP NVR
- [eseecloud](#source-eseecloud) - streaming from ESeeCloud/dvr163 NVR
- [tapo](#source-tapo) - TP-Link Tapo cameras with [two-way audio](#two-way-audio) support
- [ring](#source-ring) - Ring cameras with [two-way audio](#two-way-audio) support
- [tuya](#source-tuya) - Tuya cameras with [two-way audio](#two-way-audio) support
- [xiaomi](#source-xiaomi) - Xiaomi 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
- [roborock](#source-roborock) - Roborock vacuums with cameras
- [doorbird](#source-doorbird) - Doorbird cameras with [two-way audio](#two-way-audio) support
- [webrtc](#source-webrtc) - WebRTC/WHEP sources
- [webtorrent](#source-webtorrent) - WebTorrent source from another go2rtc
- [wyze](#source-wyze) - Wyze cameras with [two-way audio](#two-way-audio) support
Read more about [incoming sources](#incoming-sources)
#### Two-way audio
## Two-way audio
Supported sources:
@@ -224,15 +248,19 @@ Supported sources:
- [TP-Link Tapo](#source-tapo) cameras
- [Hikvision ISAPI](#source-isapi) cameras
- [Roborock vacuums](#source-roborock) models with cameras
- [Doorbird](#source-doorbird) cameras
- [Exec](#source-exec) audio on server
- [Ring](#source-ring) cameras
- [Tuya](#source-tuya) cameras
- [Wyze](#source-wyze) cameras
- [Xiaomi](#source-xiaomi) cameras
- [Any Browser](#incoming-browser) as IP-camera
Two-way audio can be used in browser with [WebRTC](#module-webrtc) technology. The browser will give access to the microphone only for HTTPS sites ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https)).
go2rtc also supports [play audio](#stream-to-camera) files and live streams on this cameras.
#### Source: RTSP
## Source: RTSP
```yaml
streams:
@@ -276,7 +304,7 @@ streams:
dahua-rtsp-ws: rtsp://user:pass@192.168.1.123/cam/realmonitor?channel=1&subtype=1&proto=Private3#transport=ws://192.168.1.123/rtspoverwebsocket
```
#### Source: RTMP
## Source: RTMP
You can get a stream from an RTMP server, for example [Nginx with nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module).
@@ -285,7 +313,7 @@ streams:
rtmp_stream: rtmp://192.168.1.123/live/camera1
```
#### Source: HTTP
## Source: HTTP
Support Content-Type:
@@ -316,7 +344,7 @@ streams:
**PS.** Dahua camera has a bug: if you select MJPEG codec for RTSP second stream, snapshot won't work.
#### Source: ONVIF
## Source: ONVIF
*[New in v1.5.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.5.0)*
@@ -331,7 +359,7 @@ streams:
tapo1: onvif://admin:password@192.168.1.123:2020
```
#### Source: FFmpeg
## Source: FFmpeg
You can get any stream, file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream.
@@ -361,16 +389,18 @@ streams:
rotate: ffmpeg:rtsp://12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
```
All transcoding formats have [built-in templates](./internal/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
All transcoding formats have [built-in templates](internal/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
But you can override them via YAML config. You can also add your own formats to the config and use them with source params.
```yaml
ffmpeg:
bin: ffmpeg # path to ffmpeg binary
global: "-hide_banner"
timeout: 5 # default timeout in seconds for rtsp inputs
h264: "-codec:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1"
mycodec: "-any args that supported by ffmpeg..."
myinput: "-fflags nobuffer -flags low_delay -timeout 5000000 -i {input}"
myinput: "-fflags nobuffer -flags low_delay -timeout {timeout} -i {input}"
myraw: "-ss 00:00:20"
```
@@ -380,16 +410,17 @@ ffmpeg:
- You can use `width` and/or `height` params, important with transcoding (ex. `#video=h264#width=1280`)
- You can use `drawtext` to add a timestamp (ex. `drawtext=x=2:y=2:fontsize=12:fontcolor=white:box=1:boxcolor=black`)
- This will greatly increase the CPU of the server, even with hardware acceleration
- You can use `timeout` param to set RTSP input timeout in seconds (ex. `#timeout=10`)
- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`)
- You can use `input` param to override default input template (ex. `#input=rtsp/udp` will change RTSP transport from TCP to UDP+TCP)
- You can use raw input value (ex. `#input=-timeout 5000000 -i {input}`)
- You can use raw input value (ex. `#input=-timeout {timeout} -i {input}`)
- You can add your own input templates
Read more about [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration).
**PS.** It is recommended to check the available hardware in the WebUI add page.
#### Source: FFmpeg Device
## Source: FFmpeg Device
You can get video from any USB camera or Webcam as RTSP or WebRTC stream. This is part of FFmpeg integration.
@@ -410,7 +441,7 @@ streams:
**PS.** It is recommended to check the available devices in the WebUI add page.
#### Source: Exec
## Source: Exec
Exec source can run any external application and expect data from it. Two transports are supported - **pipe** (*from [v1.5.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.5.0)*) and **RTSP**.
@@ -431,6 +462,7 @@ Pipe commands support parameters (format: `exec:{command}#{param1}#{param2}`):
- `killsignal` - signal which will be sent to stop the process (numeric form)
- `killtimeout` - time in seconds for forced termination with sigkill
- `backchannel` - enable backchannel for two-way audio
- `starttimeout` - time in seconds for waiting first byte from RTSP
```yaml
streams:
@@ -443,7 +475,7 @@ streams:
play_pcm48k: exec:ffplay -fflags nobuffer -f s16be -ar 48000 -i -#backchannel=1
```
#### Source: Echo
## Source: Echo
Some sources may have a dynamic link. And you will need to get it using a Bash or Python script. Your script should echo a link to the source. RTSP, FFmpeg or any of the [supported sources](#module-streams).
@@ -456,13 +488,15 @@ streams:
apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html
```
#### Source: Expr
## Source: Expr
*[New in v1.8.2](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.2)*
Like `echo` source, but uses the built-in [expr](https://github.com/antonmedv/expr) expression language ([read more](./internal/expr/README.md)).
Like `echo` source, but uses the built-in [expr](https://github.com/antonmedv/expr) expression language.
#### Source: HomeKit
*[read more](internal/expr/README.md)*
## Source: HomeKit
**Important:**
@@ -495,7 +529,7 @@ RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g
**This source is in active development!** Tested only with [Aqara Camera Hub G3](https://www.aqara.com/eu/product/camera-hub-g3) (both EU and CN versions).
#### Source: Bubble
## Source: Bubble
*[New in v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*
@@ -509,7 +543,7 @@ streams:
camera1: bubble://username:password@192.168.1.123:34567/bubble/live?ch=0&stream=0
```
#### Source: DVRIP
## Source: DVRIP
*[New in v1.2.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.2.0)*
@@ -529,7 +563,16 @@ streams:
- dvrip://username:password@192.168.1.123:34567?backchannel=1
```
#### Source: Tapo
## Source: EseeCloud
*[New in v1.9.10](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.10)*
```yaml
streams:
camera1: eseecloud://user:pass@192.168.1.123:80/livestream/12
```
## Source: Tapo
*[New in v1.2.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.2.0)*
@@ -559,7 +602,7 @@ echo -n "cloud password" | md5 | awk '{print toupper($0)}'
echo -n "cloud password" | shasum -a 256 | awk '{print toupper($0)}'
```
#### Source: Kasa
## Source: Kasa
*[New in v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*
@@ -575,13 +618,43 @@ streams:
Tested: KD110, KC200, KC401, KC420WS, EC71.
#### Source: GoPro
## Source: Multitrans
Two-way audio support for Chinese version of [TP-Link cameras](https://www.tp-link.com.cn/list_2549.html).
*[read more](internal/multitrans/README.md)*
## Source: Tuya
*[New in v1.9.13](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.13)*
[Tuya](https://www.tuya.com/) proprietary camera protocol with **two-way audio** support. Go2rtc supports `Tuya Smart API` and `Tuya Cloud API`.
*[read more](internal/tuya/README.md)*
## Source: Xiaomi
*[New in v1.9.13](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.13)*
This source allows you to view cameras from the [Xiaomi Mi Home](https://home.mi.com/) ecosystem.
*[read more](internal/xiaomi/README.md)*
## Source: Wyze
This source allows you to stream from [Wyze](https://wyze.com/) cameras using native P2P protocol - no `docker-wyze-bridge` required. Supports H.264/H.265 video, AAC/G.711 audio, and two-way audio.
*[read more](internal/wyze/README.md)*
## Source: GoPro
*[New in v1.8.3](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.3)*
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).
Support streaming from [GoPro](https://gopro.com/) cameras, connected via USB or Wi-Fi to Linux, Mac, Windows.
#### Source: Ivideon
*[read more](internal/gopro/README.md)*
## Source: Ivideon
Support public cameras from the service [Ivideon](https://tv.ivideon.com/).
@@ -590,7 +663,7 @@ streams:
quailcam: ivideon:100-tu5dkUPct39cTp9oNEN2B6/0
```
#### Source: Hass
## Source: Hass
Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files:
@@ -626,7 +699,7 @@ streams:
By default, the Home Assistant API does not allow you to get a dynamic RTSP link to a camera stream. So more cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), and possibly others, can also be imported using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-cameras-from-home-assistant-to-go2rtc-or-frigate).
#### Source: ISAPI
## Source: ISAPI
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
@@ -639,7 +712,7 @@ streams:
- isapi://admin:password@192.168.1.123:80/
```
#### Source: Nest
## Source: Nest
*[New in v1.6.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)*
@@ -652,7 +725,7 @@ streams:
nest-doorbell: nest:?client_id=***&client_secret=***&refresh_token=***&project_id=***&device_id=***
```
#### Source: Ring
## Source: Ring
This source type support Ring cameras with [two-way audio](#two-way-audio) support. If you have a `refresh_token` and `device_id` - you can use it in `go2rtc.yaml` config file. Otherwise, you can use the go2rtc interface and add your ring account (WebUI > Add > Ring). Once added, it will list all your Ring cameras.
@@ -662,7 +735,7 @@ streams:
ring_snapshot: ring:?device_id=XXX&refresh_token=XXX&snapshot
```
#### Source: Roborock
## Source: Roborock
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
@@ -676,7 +749,13 @@ Source supports loading Roborock credentials from Home Assistant [custom integra
If you have a graphic PIN for your vacuum, add it as a numeric PIN (lines: 123, 456, 789) to the end of the `roborock` link.
#### Source: WebRTC
## Source: Doorbird
This source type supports [Doorbird](https://www.doorbird.com/) devices including MJPEG stream, audio stream as well as two-way audio.
*[read more](internal/doorbird/README.md)*
## Source: WebRTC
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
@@ -694,9 +773,9 @@ This format is only supported in go2rtc. Unlike WHEP, it supports asynchronous W
Support connection to [OpenIPC](https://openipc.org/) cameras.
**wyze** (*from [v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*)
**wyze (via docker-wyze-bridge)** (*from [v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*)
Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC protocol. You can use the [docker-wyze-bridge](https://github.com/mrlt8/docker-wyze-bridge) project to get connection credentials.
Legacy method to connect to [Wyze](https://www.wyze.com/) cameras using WebRTC protocol via [docker-wyze-bridge](https://github.com/mrlt8/docker-wyze-bridge). For native P2P support without docker-wyze-bridge, see [Source: Wyze](#source-wyze).
**kinesis** (*from [v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*)
@@ -718,7 +797,7 @@ streams:
**PS.** For `kinesis` sources, you can use [echo](#source-echo) to get connection params using `bash`, `python` or any other script language.
#### Source: WebTorrent
## Source: WebTorrent
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
@@ -729,7 +808,7 @@ streams:
webtorrent1: webtorrent:?share=huofssuxaty00izc&pwd=k3l2j9djeg8v8r7e
```
#### Incoming sources
## Incoming sources
By default, go2rtc establishes a connection to the source when any client requests it. Go2rtc drops the connection to the source when it has no clients left.
@@ -765,7 +844,7 @@ By default, go2rtc establishes a connection to the source when any client reques
ffmpeg -re -i BigBuckBunny.mp4 -c copy -f mpegts http://localhost:1984/api/stream.ts?dst=camera1
```
#### Incoming: Browser
### Incoming: Browser
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
@@ -777,7 +856,7 @@ You can turn the browser of any PC or mobile into an IP camera with support for
4. Select `camera+microphone` or `display+speaker` option
5. Open `webrtc` local page (your go2rtc **should work over HTTPS!**) or `share link` via [WebTorrent](#module-webtorrent) technology (work over HTTPS by default)
#### Incoming: WebRTC/WHIP
### Incoming: WebRTC/WHIP
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
@@ -785,7 +864,7 @@ You can use **OBS Studio** or any other broadcast software with [WHIP](https://w
- Settings > Stream > Service: WHIP > `http://192.168.1.123:1984/api/webrtc?dst=camera1`
#### Stream to camera
## Stream to camera
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
@@ -807,7 +886,7 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
- you can stop active playback by calling the API with the empty `src` parameter
- you will see one active producer and one active consumer in go2rtc WebUI info page during streaming
### Publish stream
## Publish stream
*[New in v1.8.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.0)*
@@ -845,7 +924,7 @@ streams:
- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.
- **YouTube** > Create > Go live > Stream latency: Ultra low-latency > Copy: Stream URL + Stream key.
### Preload stream
## Preload stream
You can preload any stream on go2rtc start. This is useful for cameras that take a long time to start up.
@@ -865,13 +944,13 @@ streams:
- ffmpeg:camera3#video=h264#audio=opus#hardware
```
### Module: API
## Module: API
The HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.
**Important!** go2rtc passes requests from localhost and from Unix sockets without HTTP authorisation, even if you have it configured! It is your responsibility to set up secure external access to the API. If not properly configured, an attacker can gain access to your cameras and even your server.
[API description](https://github.com/AlexxIT/go2rtc/tree/master/api).
[API description](api/README.md).
**Module config**
@@ -886,6 +965,7 @@ api:
listen: ":1984" # default ":1984", HTTP API port ("" - disabled)
username: "admin" # default "", Basic auth for WebUI
password: "pass" # default "", Basic auth for WebUI
local_auth: true # default false, Enable auth check for localhost requests
base_path: "/rtc" # default "", API prefix for serving on suburl (/api => /rtc/api)
static_dir: "www" # default "", folder for static files (custom web interface)
origin: "*" # default "", allow CORS requests (only * supported)
@@ -906,7 +986,7 @@ api:
- MJPEG over WebSocket plays better than native MJPEG because Chrome [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=527446)
- MP4 over WebSocket was created only for Apple iOS because it doesn't support MSE and native MP4
### Module: RTSP
## Module: RTSP
You can get any stream as RTSP-stream: `rtsp://192.168.1.123:8554/{stream_name}`
@@ -929,7 +1009,7 @@ By default go2rtc provide RTSP-stream with only one first video and only one fir
Read more about [codecs filters](#codecs-filters).
### Module: RTMP
## Module: RTMP
*[New in v1.8.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.0)*
@@ -942,7 +1022,7 @@ rtmp:
listen: ":1935" # by default - disabled!
```
### Module: WebRTC
## Module: WebRTC
In most cases, [WebRTC](https://en.wikipedia.org/wiki/WebRTC) uses a direct peer-to-peer connection from your browser to go2rtc and sends media data via UDP.
It **can't pass** media data through your Nginx or Cloudflare or [Nabu Casa](https://www.nabucasa.com/) HTTP TCP connection!
@@ -1000,7 +1080,7 @@ webrtc:
credential: your_pass
```
### Module: HomeKit
## Module: HomeKit
*[New in v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*
@@ -1054,7 +1134,7 @@ homekit:
aqara1: # same stream ID from streams list
```
### Module: WebTorrent
## Module: WebTorrent
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
@@ -1078,13 +1158,15 @@ webtorrent:
src: rtsp-dahua1 # stream name from streams section
```
Link example: `https://alexxit.github.io/go2rtc/#share=02SNtgjKXY&pwd=wznEQqznxW&media=video+audio`
Link example: `https://go2rtc.org/webtorrent/#share=02SNtgjKXY&pwd=wznEQqznxW&media=video+audio`
### Module: ngrok
## Module: ngrok
With [ngrok](https://ngrok.com/) integration, you can get external access to your streams in situations when you have Internet with a private IP address ([read more](./internal/ngrok/README.md)).
With [ngrok](https://ngrok.com/) integration, you can get external access to your streams in situations when you have internet with a private IP address.
### Module: Hass
*[read more](internal/ngrok/README.md)*
## Module: Hass
While [go2rtc is used by default in Home Assistant](https://www.home-assistant.io/integrations/go2rtc/), the best and easiest way to have full control over it is to install the [WebRTC Camera](#go2rtc-home-assistant-integration) custom integration and card.
@@ -1115,7 +1197,7 @@ streams:
**PS.** There is also another nice card with two-way audio support through go2rtc - [Advanced Camera Card](https://github.com/dermotduffy/advanced-camera-card).
### Module: MP4
## Module: MP4
Provides several features:
@@ -1138,7 +1220,7 @@ Read more about [codecs filters](#codecs-filters).
**PS.** Rotate and scale params don't use transcoding and change video using metadata.
### Module: HLS
## Module: HLS
*[New in v1.1.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.1.0)*
@@ -1153,39 +1235,15 @@ API examples:
Read more about [codecs filters](#codecs-filters).
### Module: MJPEG
## Module: MJPEG
**Important.** For stream in MJPEG format, your source MUST contain the MJPEG codec. If your stream has an MJPEG codec, you can receive **MJPEG stream** or **JPEG snapshots** via API.
- This module can provide and receive streams in MJPEG format.
- This module is also responsible for receiving snapshots in JPEG format.
- This module also supports streaming to the server console (terminal) in the **animated ASCII art** format.
You can receive an MJPEG stream in several ways:
*[read more](internal/mjpeg/README.md)*
- some cameras support MJPEG codec inside [RTSP stream](#source-rtsp) (ex. second stream for Dahua cameras)
- some cameras have an HTTP link with [MJPEG stream](#source-http)
- some cameras have an HTTP link with snapshots - go2rtc can convert them to [MJPEG stream](#source-http)
- you can convert H264/H265 stream from your camera via [FFmpeg integraion](#source-ffmpeg)
With this example, your stream will have both H264 and MJPEG codecs:
```yaml
streams:
camera1:
- rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
- ffmpeg:camera1#video=mjpeg
```
API examples:
- MJPEG stream: `http://192.168.1.123:1984/api/stream.mjpeg?src=camera1`
- JPEG snapshots: `http://192.168.1.123:1984/api/frame.jpeg?src=camera1`
- You can use `width`/`w` and/or `height`/`h` params
- You can use `rotate` param with `90`, `180`, `270` or `-90` values
- You can use `hardware`/`hw` param [read more](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)
**PS.** This module also supports streaming to the server console (terminal) in the **animated ASCII art** format ([read more](./internal/mjpeg/README.md)):
[![](https://img.youtube.com/vi/sHj_3h_sX7M/mqdefault.jpg)](https://www.youtube.com/watch?v=sHj_3h_sX7M)
### Module: Log
## Module: Log
You can set different log levels for different modules.
@@ -1199,7 +1257,28 @@ log:
webrtc: fatal
```
## Security
# Security
> [!IMPORTANT]
> If an attacker gains access to the API, you are in danger. Through the API, an attacker can use insecure sources such as echo and exec. And get full access to your server.
For maximum (paranoid) security, go2rtc has special settings:
```yaml
app:
# use only allowed modules
modules: [api, rtsp, webrtc, exec, ffmpeg, mjpeg]
api:
# use only allowed API paths
allow_paths: [/api, /api/streams, /api/webrtc, /api/frame.jpeg]
# enable auth for localhost (used together with username and password)
local_auth: true
exec:
# use only allowed exec paths
allow_paths: [ffmpeg]
```
By default, `go2rtc` starts the Web interface on port `1984` and RTSP on port `8554`, as well as uses port `8555` for WebRTC connections. The three ports are accessible from your local network. So anyone on your local network can watch video from your cameras without authorization. The same rule applies to the Home Assistant add-on.
@@ -1225,7 +1304,7 @@ If you need web interface protection without the Home Assistant add-on, you need
PS. Additionally, WebRTC will try to use the 8555 UDP port to 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.
## Codecs filters
# Codecs filters
go2rtc can automatically detect which codecs your device supports for [WebRTC](#module-webrtc) and [MSE](#module-mp4) technologies.
@@ -1248,27 +1327,24 @@ Some examples:
- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=flac` - MP4 file with PCMA/PCMU/PCM audio support, won't work on old devices (ex. iOS 12)
- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=all` - MP4 file with non-standard audio codecs, won't work on some players
## Codecs madness
# Codecs madness
`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has many limitations in supporting different devices and browsers. It's all about patents and money; you can't do anything about it.
`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has many limitations in supporting different devices and browsers.
| Device | WebRTC | MSE | HTTP* | HLS |
|--------------------------------------------------------------------------|-----------------------------------------|-----------------------------------------|----------------------------------------------|-----------------------------|
| *latency* | best | medium | bad | bad |
| - Desktop Chrome 107+ <br/> - Desktop Edge <br/> - Android Chrome 107+ | H264 <br/> PCMU, PCMA <br/> OPUS | H264, H265* <br/> AAC, FLAC* <br/> OPUS | H264, H265* <br/> AAC, FLAC* <br/> OPUS, MP3 | no |
| Desktop Firefox | H264 <br/> PCMU, PCMA <br/> OPUS | H264 <br/> AAC, FLAC* <br/> OPUS | H264 <br/> AAC, FLAC* <br/> OPUS | no |
| - Desktop Safari 14+ <br/> - iPad Safari 14+ <br/> - iPhone Safari 17.1+ | H264, H265* <br/> PCMU, PCMA <br/> OPUS | H264, H265 <br/> AAC, FLAC* | **no!** | H264, H265 <br/> AAC, FLAC* |
| iPhone Safari 14+ | H264, H265* <br/> PCMU, PCMA <br/> OPUS | **no!** | **no!** | H264, H265 <br/> AAC, FLAC* |
| macOS [Home Assistant App][1] | no | no | no | H264, H265 <br/> AAC, FLAC* |
| Device | WebRTC | MSE | HTTP* | HLS |
|--------------------------------------------------------------------|-----------------------------------------|-----------------------------------------|----------------------------------------------|-----------------------------|
| *latency* | best | medium | bad | bad |
| Desktop Chrome 136+ <br/> Desktop Edge <br/> Android Chrome 136+ | H264, H265* <br/> PCMU, PCMA <br/> OPUS | H264, H265* <br/> AAC, FLAC* <br/> OPUS | H264, H265* <br/> AAC, FLAC* <br/> OPUS, MP3 | no |
| Desktop Firefox | H264 <br/> PCMU, PCMA <br/> OPUS | H264 <br/> AAC, FLAC* <br/> OPUS | H264 <br/> AAC, FLAC* <br/> OPUS | no |
| Desktop Safari 14+ <br/> iPad Safari 14+ <br/> iPhone Safari 17.1+ | H264, H265* <br/> PCMU, PCMA <br/> OPUS | H264, H265 <br/> AAC, FLAC* | **no!** | H264, H265 <br/> AAC, FLAC* |
| iPhone Safari 14+ | H264, H265* <br/> PCMU, PCMA <br/> OPUS | **no!** | **no!** | H264, H265 <br/> AAC, FLAC* |
| macOS [Hass App][1] | no | no | no | H264, H265 <br/> AAC, FLAC* |
[1]: https://apps.apple.com/app/home-assistant/id1099568401
`HTTP*` - HTTP Progressive Streaming, not related to [progressive download](https://en.wikipedia.org/wiki/Progressive_download), because the file has no size and no end
- Chrome H265: [read this](https://chromestatus.com/feature/5186511939567616) and [read this](https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding)
- Edge H265: [read this](https://www.reddit.com/r/MicrosoftEdge/comments/v9iw8k/enable_hevc_support_in_edge/)
- Desktop Safari H265: Menu > Develop > Experimental > WebRTC H265
- iOS Safari H265: Settings > Safari > Advanced > Experimental > WebRTC H265
- `HTTP*` - HTTP Progressive Streaming, not related to [progressive download](https://en.wikipedia.org/wiki/Progressive_download), because the file has no size and no end
- `WebRTC H265` - supported in [Chrome 136+](https://developer.chrome.com/release-notes/136), supported in [Safari 18+](https://developer.apple.com/documentation/safari-release-notes/safari-18-release-notes)
- `MSE iPhone` - supported in [iOS 17.1+](https://webkit.org/blog/14735/webkit-features-in-safari-17-1/)
**Audio**
@@ -1279,7 +1355,7 @@ Some examples:
**Apple devices**
- all Apple devices don't support HTTP progressive streaming
- iPhones don't support MSE technology because it competes with the HTTP Live Streaming (HLS) technology, invented by Apple
- old iPhone firmwares don't support MSE technology because it competes with the HTTP Live Streaming (HLS) technology, invented by Apple
- HLS is the worst technology for **live** streaming, it still exists only because of iPhones
**Codec names**
@@ -1292,7 +1368,7 @@ Some examples:
- AAC = MPEG4-GENERIC
- MP3 = MPEG-1 Audio Layer III or MPEG-2 Audio Layer III
## Built-in transcoding
# Built-in transcoding
There are no plans to embed complex transcoding algorithms inside go2rtc. [FFmpeg source](#source-ffmpeg) does a great job with this. Including [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration) support.
@@ -1321,7 +1397,7 @@ PCMU/xxx => PCMU/8000 => WebRTC
- FLAC codec not supported in an RTSP stream. If you are using Frigate or Home Assistant for recording MP4 files with PCMA/PCMU/PCM audio, you should set up transcoding to the AAC codec.
- PCMA and PCMU are VERY low-quality codecs. They support only 256! different sounds. Use them only when you have no other options.
## Codecs negotiation
# Codecs negotiation
For example, you want to watch RTSP-stream from [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your Chrome browser.
@@ -1350,9 +1426,10 @@ streams:
**PS.** You can select `PCMU` or `PCMA` codec in camera settings and not use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.
## Projects using go2rtc
# Projects using go2rtc
- [Frigate](https://frigate.video/) - open-source NVR built around real-time AI object detection
- [Home Assistant](https://www.home-assistant.io/) [2024.11+](https://www.home-assistant.io/integrations/go2rtc/) - top open-source smart home project
- [Frigate](https://frigate.video/) [0.12+](https://docs.frigate.video/guides/configuring_go2rtc/) - open-source NVR built around real-time AI object detection
- [Advanced Camera Card](https://github.com/dermotduffy/advanced-camera-card) - custom card for Home Assistant
- [OpenIPC](https://github.com/OpenIPC/firmware/tree/master/general/package/go2rtc) - alternative IP camera firmware from an open community
- [wz_mini_hacks](https://github.com/gtxaspec/wz_mini_hacks) - custom firmware for Wyze cameras
@@ -1373,7 +1450,7 @@ streams:
- [Synology NAS](https://synocommunity.com/package/go2rtc)
- [Unraid](https://unraid.net/community/apps?q=go2rtc)
## Camera experience
# Camera experience
- [Dahua](https://www.dahuasecurity.com/) - reference implementation streaming protocols, a lot of settings, high stream quality, multiple streaming clients
- [EZVIZ](https://www.ezviz.com/) - awful RTSP protocol implementation, many bugs in SDP
@@ -1383,7 +1460,7 @@ streams:
- [TP-Link](https://www.tp-link.com/) - few streaming clients, packet loss?
- Chinese cheap noname cameras, Wyze Cams, Xiaomi cameras with hacks (usually have `/live/ch00_1` in RTSP URL) - awful but usable RTSP protocol implementation, low stream quality, few settings, packet loss?
## Tips
# Tips
**Using apps for low RTSP delay**
@@ -1393,27 +1470,3 @@ streams:
**Snapshots to Telegram**
[read more](https://github.com/AlexxIT/go2rtc/wiki/Snapshot-to-Telegram)
## FAQ
**Q. What's the difference between go2rtc and WebRTC Camera?**
**go2rtc** is a new version of the server-side [WebRTC Camera](https://github.com/AlexxIT/WebRTC) integration, completely rewritten from scratch, with a number of fixes and a huge number of new features. [go2rtc is used by default in Home Assistant](https://www.home-assistant.io/integrations/go2rtc/).
**Q. Should I use the go2rtc add-on or WebRTC Camera integration?**
**go2rtc** is more than just viewing your stream online with WebRTC/MSE/HLS/etc. You can use it all the time for your various tasks. But every time Home Assistant is rebooted, all integrations are also rebooted. So your streams may be interrupted if you use them in additional tasks.
Basic users can use the **WebRTC Camera** integration. Advanced users can use the go2rtc add-on or the Frigate add-on.
**Q. Which RTSP link should I use inside Home Assistant?**
You can use a direct link to your cameras there (as you always do). **go2rtc** supports zero-config feature. You may leave `streams` config section empty. And your streams will be created on the fly on first start from Home Assistant. And your cameras will have multiple connections. Some from Home Assistant directly and one from **go2rtc**.
Also, you can specify your streams in **go2rtc** [config file](#configuration) and use RTSP links to this add-on with additional features: multi-source [codecs negotiation](#codecs-negotiation) or FFmpeg [transcoding](#source-ffmpeg) for unsupported codecs. Or use them as a source for Frigate. And your cameras will have one connection from **go2rtc**. And **go2rtc** will have multiple connections - some from Home Assistant via RTSP protocol, some from your browser via WebRTC/MSE/HLS protocols.
Use any config that you like.
**Q. What about a Home Assistant card with support for two-way audio?**
No built-in card supports two-way audio as of now in Home Assistant, but you can use [WebRTC Camera](https://github.com/AlexxIT/WebRTC/) or [Advanced Camera Card](https://github.com/dermotduffy/advanced-camera-card).
+1 -1
View File
@@ -4,7 +4,7 @@ Fill free to make any API design proposals.
## HTTP API
Interactive [OpenAPI](https://alexxit.github.io/go2rtc/api/).
Interactive [OpenAPI](https://go2rtc.org/api/).
`www/stream.html` - universal viewer with support params in URL:
+694 -82
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -47,6 +47,7 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver
COPY --from=build /build/go2rtc /usr/local/bin/
EXPOSE 1984 8554 8555 8555/udp
ENTRYPOINT ["/sbin/tini", "--"]
VOLUME /config
WORKDIR /config
+1
View File
@@ -49,6 +49,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,t
COPY --from=build /build/go2rtc /usr/local/bin/
EXPOSE 1984 8554 8555 8555/udp
ENTRYPOINT ["/usr/bin/tini", "--"]
VOLUME /config
WORKDIR /config
+1
View File
@@ -43,6 +43,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,t
COPY --from=build /build/go2rtc /usr/local/bin/
ADD --chmod=755 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/6.1-8-no_extra_dump/ffmpeg /usr/local/bin
EXPOSE 1984 8554 8555 8555/udp
ENTRYPOINT ["/usr/bin/tini", "--"]
VOLUME /config
WORKDIR /config
+9
View File
@@ -0,0 +1,9 @@
module pinggy
go 1.25
require (
github.com/Pinggy-io/pinggy-go/pinggy v0.6.9 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/sys v0.7.0 // indirect
)
+39
View File
@@ -0,0 +1,39 @@
github.com/Pinggy-io/pinggy-go/pinggy v0.6.9 h1:lzZ00JK6BUGQXnpkJZ+cVj8kIkXsmiVBUci9uEkSwEY=
github.com/Pinggy-io/pinggy-go/pinggy v0.6.9/go.mod h1:V1Sxb+4zyr36o9atZiqtT4XhsKtW1RSb2GvsbTbTJYw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
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/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"log"
"os"
"github.com/Pinggy-io/pinggy-go/pinggy"
)
func main() {
tunType := os.Args[1]
address := os.Args[2]
log.SetFlags(log.Llongfile | log.LstdFlags)
config := pinggy.Config{
Type: pinggy.TunnelType(tunType),
TcpForwardingAddr: address,
//SshOverSsl: true,
//Stdout: os.Stderr,
//Stderr: os.Stderr,
}
if tunType == "http" {
hman := pinggy.CreateHeaderManipulationAndAuthConfig()
//hman.SetReverseProxy(address)
//hman.SetPassPreflight(true)
//hman.SetNoReverseProxy()
config.HeaderManipulationAndAuth = hman
}
pl, err := pinggy.ConnectWithConfig(config)
if err != nil {
log.Panicln(err)
}
log.Println("Addrs: ", pl.RemoteUrls())
//err = pl.InitiateWebDebug("localhost:3424")
//log.Println(err)
pl.StartForwarding()
}
+5
View File
@@ -0,0 +1,5 @@
# tutk_decoder
1. Wireshark > Select any packet > Follow > UDP Stream
2. Wireshark > File > Export Packet Dissections > As JSON > Displayed, Values
3. `tutk_decoder wireshark.json decoded.txt`
+82
View File
@@ -0,0 +1,82 @@
package main
import (
"encoding/hex"
"encoding/json"
"fmt"
"log"
"os"
"strings"
"github.com/AlexxIT/go2rtc/pkg/tutk"
)
func main() {
if len(os.Args) != 3 {
fmt.Println("Usage: tutk_decoder wireshark.json decoded.txt")
return
}
src, err := os.Open(os.Args[1])
if err != nil {
log.Fatal(err)
}
defer src.Close()
dst, err := os.Create(os.Args[2])
if err != nil {
log.Fatal(err)
}
defer dst.Close()
var items []item
if err = json.NewDecoder(src).Decode(&items); err != nil {
log.Fatal(err)
}
var b []byte
for _, v := range items {
if v.Source.Layers.Data.DataData == "" {
continue
}
s := strings.ReplaceAll(v.Source.Layers.Data.DataData, ":", "")
b, err = hex.DecodeString(s)
if err != nil {
log.Fatal(err)
}
tutk.ReverseTransCodePartial(b, b)
ts := v.Source.Layers.Frame.FrameTimeRelative
_, _ = fmt.Fprintf(dst, "%8s: %s -> %s [%4d] %x\n",
ts[:len(ts)-6],
v.Source.Layers.Ip.IpSrc, v.Source.Layers.Ip.IpDst,
len(b), b)
}
}
type item struct {
Source struct {
Layers struct {
Frame struct {
FrameTimeRelative string `json:"frame.time_relative"`
FrameNumber string `json:"frame.number"`
} `json:"frame"`
Ip struct {
IpSrc string `json:"ip.src"`
IpDst string `json:"ip.dst"`
} `json:"ip"`
Udp struct {
UdpSrcport string `json:"udp.srcport"`
UdpDstport string `json:"udp.dstport"`
} `json:"udp"`
Data struct {
DataData string `json:"data.data"`
DataLen string `json:"data.len"`
} `json:"data"`
} `json:"layers"`
} `json:"_source"`
}
+26 -23
View File
@@ -3,47 +3,50 @@ module github.com/AlexxIT/go2rtc
go 1.24.0
require (
github.com/asticode/go-astits v1.13.0
github.com/expr-lang/expr v1.17.6
github.com/asticode/go-astits v1.14.0
github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/expr-lang/expr v1.17.7
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/mattn/go-isatty v0.0.20
github.com/miekg/dns v1.1.68
github.com/pion/ice/v4 v4.0.10
github.com/pion/interceptor v0.1.41
github.com/miekg/dns v1.1.70
github.com/pion/dtls/v3 v3.0.10
github.com/pion/ice/v4 v4.2.0
github.com/pion/interceptor v0.1.43
github.com/pion/rtcp v1.2.16
github.com/pion/rtp v1.8.24
github.com/pion/sdp/v3 v3.0.16
github.com/pion/srtp/v3 v3.0.8
github.com/pion/stun/v3 v3.0.0
github.com/pion/webrtc/v4 v4.1.6
github.com/pion/rtp v1.10.0
github.com/pion/sdp/v3 v3.0.17
github.com/pion/srtp/v3 v3.0.10
github.com/pion/stun/v3 v3.1.1
github.com/pion/webrtc/v4 v4.2.3
github.com/rs/zerolog v1.34.0
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
github.com/stretchr/testify v1.11.1
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
golang.org/x/crypto v0.43.0
golang.org/x/crypto v0.47.0
golang.org/x/net v0.49.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/asticode/go-astikit v0.56.0 // indirect
github.com/asticode/go-astikit v0.57.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v3 v3.0.7 // indirect
github.com/pion/datachannel v1.6.0 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/mdns/v2 v2.1.0 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.40 // indirect
github.com/pion/transport/v3 v3.0.8 // indirect
github.com/pion/turn/v4 v4.1.1 // indirect
github.com/pion/sctp v1.9.2 // indirect
github.com/pion/transport/v3 v3.1.1 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/pion/turn/v4 v4.1.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.41.0 // indirect
)
+88 -114
View File
@@ -1,22 +1,22 @@
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astikit v0.54.0 h1:uq9eurgisdkYwJU9vSWIQaPH4MH0cac82sQH00kmSNQ=
github.com/asticode/go-astikit v0.54.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
github.com/asticode/go-astikit v0.56.0 h1:DmD2p7YnvxiPdF0h+dRmos3bsejNEXbycENsY5JfBqw=
github.com/asticode/go-astikit v0.56.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
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-astikit v0.57.1 h1:fEykwH98Nny08kcRbk4uer+S8h0rKveCIpG9F6NVLuA=
github.com/asticode/go-astikit v0.57.1/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
github.com/asticode/go-astits v1.14.0 h1:zkgnZzipx2XX5mWycqsSBeEyDH58+i4HtyF4j2ROb00=
github.com/asticode/go-astits v1.14.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/expr-lang/expr v1.17.2 h1:o0A99O/Px+/DTjEnQiodAgOIK9PPxL8DtXhBRKC+Iso=
github.com/expr-lang/expr v1.17.2/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/expr-lang/expr v1.17.5 h1:i1WrMvcdLF249nSNlpQZN1S6NXuW9WaOfF5tPi3aw3k=
github.com/expr-lang/expr v1.17.5/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec=
github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
@@ -32,82 +32,66 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q=
github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8=
github.com/pion/ice/v4 v4.0.9 h1:VKgU4MwA2LUDVLq+WBkpEHTcAb8c5iCvFMECeuPOZNk=
github.com/pion/ice/v4 v4.0.9/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
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.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
github.com/pion/interceptor v0.1.41 h1:NpvX3HgWIukTf2yTBVjVGFXtpSpWgXjqz7IIpu7NsOw=
github.com/pion/interceptor v0.1.41/go.mod h1:nEt4187unvRXJFyjiw00GKo+kIuXMWQI9K89fsosDLY=
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/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk=
github.com/pion/dtls/v3 v3.0.9 h1:4AijfFRm8mAjd1gfdlB1wzJF3fjjR/VPIpJgkEtvYmM=
github.com/pion/dtls/v3 v3.0.9/go.mod h1:abApPjgadS/ra1wvUzHLc3o2HvoxppAh+NZkyApL4Os=
github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
github.com/pion/ice/v4 v4.1.0 h1:YlxIii2bTPWyC08/4hdmtYq4srbrY0T9xcTsTjldGqU=
github.com/pion/ice/v4 v4.1.0/go.mod h1:5gPbzYxqenvn05k7zKPIZFuSAufolygiy6P1U9HzvZ4=
github.com/pion/ice/v4 v4.2.0 h1:jJC8S+CvXCCvIQUgx+oNZnoUpt6zwc34FhjWwCU4nlw=
github.com/pion/ice/v4 v4.2.0/go.mod h1:EgjBGxDgmd8xB0OkYEVFlzQuEI7kWSCFu+mULqaisy4=
github.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ=
github.com/pion/interceptor v0.1.42/go.mod h1:g6XYTChs9XyolIQFhRHOOUS+bGVGLRfgTCUzH29EfVU=
github.com/pion/interceptor v0.1.43 h1:6hmRfnmjogSs300xfkR0JxYFZ9k5blTEvCD7wxEDuNQ=
github.com/pion/interceptor v0.1.43/go.mod h1:BSiC1qKIJt1XVr3l3xQ2GEmCFStk9tx8fwtCZxxgR7M=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=
github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=
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.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
github.com/pion/rtp v1.8.13 h1:8uSUPpjSL4OlwZI8Ygqu7+h2p9NPFB+yAZ461Xn5sNg=
github.com/pion/rtp v1.8.13/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
github.com/pion/rtp v1.8.20 h1:8zcyqohadZE8FCBeGdyEvHiclPIezcwRQH9zfapFyYI=
github.com/pion/rtp v1.8.20/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
github.com/pion/rtp v1.8.24 h1:+ICyZXUQDv95EsHN70RrA4XKJf5MGWyC6QQc1u6/ynI=
github.com/pion/rtp v1.8.24/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
github.com/pion/sctp v1.8.37 h1:ZDmGPtRPX9mKCiVXtMbTWybFw3z/hVKAZgU81wcOrqs=
github.com/pion/sctp v1.8.37/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
github.com/pion/sctp v1.8.40 h1:bqbgWYOrUhsYItEnRObUYZuzvOMsVplS3oNgzedBlG8=
github.com/pion/sctp v1.8.40/go.mod h1:SPBBUENXE6ThkEksN5ZavfAhFYll+h+66ZiG6IZQuzo=
github.com/pion/sdp/v3 v3.0.11 h1:VhgVSopdsBKwhCFoyyPmT1fKMeV9nLMrEKxNOdy3IVI=
github.com/pion/sdp/v3 v3.0.11/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
github.com/pion/sdp/v3 v3.0.14 h1:1h7gBr9FhOWH5GjWWY5lcw/U85MtdcibTyt/o6RxRUI=
github.com/pion/sdp/v3 v3.0.14/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc=
github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
github.com/pion/rtp v1.10.0 h1:XN/xca4ho6ZEcijpdF2VGFbwuHUfiIMf3ew8eAAE43w=
github.com/pion/rtp v1.10.0/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
github.com/pion/sctp v1.8.41 h1:20R4OHAno4Vky3/iE4xccInAScAa83X6nWUfyc65MIs=
github.com/pion/sctp v1.8.41/go.mod h1:2wO6HBycUH7iCssuGyc2e9+0giXVW0pyCv3ZuL8LiyY=
github.com/pion/sctp v1.9.2 h1:HxsOzEV9pWoeggv7T5kewVkstFNcGvhMPx0GvUOUQXo=
github.com/pion/sctp v1.9.2/go.mod h1:OTOlsQ5EDQ6mQ0z4MUGXt2CgQmKyafBEXhUVqLRB6G8=
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4=
github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY=
github.com/pion/srtp/v3 v3.0.8 h1:RjRrjcIeQsilPzxvdaElN0CpuQZdMvcl9VZ5UY9suUM=
github.com/pion/srtp/v3 v3.0.8/go.mod h1:2Sq6YnDH7/UDCvkSoHSDNDeyBcFgWL0sAVycVbAsXFg=
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/transport/v3 v3.0.8 h1:oI3myyYnTKUSTthu/NZZ8eu2I5sHbxbUNNFW62olaYc=
github.com/pion/transport/v3 v3.0.8/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
github.com/pion/webrtc/v4 v4.0.14 h1:nyds/sFRR+HvmWoBa6wrL46sSfpArE0qR883MBW96lg=
github.com/pion/webrtc/v4 v4.0.14/go.mod h1:R3+qTnQTS03UzwDarYecgioNf7DYgTsldxnCXB821Kk=
github.com/pion/webrtc/v4 v4.1.3 h1:YZ67Boj9X/hk190jJZ8+HFGQ6DqSZ/fYP3sLAZv7c3c=
github.com/pion/webrtc/v4 v4.1.3/go.mod h1:rsq+zQ82ryfR9vbb0L1umPJ6Ogq7zm8mcn9fcGnxomM=
github.com/pion/webrtc/v4 v4.1.6 h1:srHH2HwvCGwPba25EYJgUzgLqCQoXl1VCUnrGQMSzUw=
github.com/pion/webrtc/v4 v4.1.6/go.mod h1:wKecGRlkl3ox/As/MYghJL+b/cVXMEhoPMJWPuGQFhU=
github.com/pion/sdp/v3 v3.0.17 h1:9SfLAW/fF1XC8yRqQ3iWGzxkySxup4k4V7yN8Fs8nuo=
github.com/pion/sdp/v3 v3.0.17/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY=
github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8=
github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ=
github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M=
github.com/pion/stun/v3 v3.0.2 h1:BJuGEN2oLrJisiNEJtUTJC4BGbzbfp37LizfqswblFU=
github.com/pion/stun/v3 v3.0.2/go.mod h1:JFJKfIWvt178MCF5H/YIgZ4VX3LYE77vca4b9HP60SA=
github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=
github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pion/turn/v4 v4.1.3 h1:jVNW0iR05AS94ysEtvzsrk3gKs9Zqxf6HmnsLfRvlzA=
github.com/pion/turn/v4 v4.1.3/go.mod h1:TD/eiBUf5f5LwXbCJa35T7dPtTpCHRJ9oJWmyPLVT3A=
github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ=
github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ=
github.com/pion/webrtc/v4 v4.1.8 h1:ynkjfiURDQ1+8EcJsoa60yumHAmyeYjz08AaOuor+sk=
github.com/pion/webrtc/v4 v4.1.8/go.mod h1:KVaARG2RN0lZx0jc7AWTe38JpPv+1/KicOZ9jN52J/s=
github.com/pion/webrtc/v4 v4.2.3 h1:RtdWDnkenNQGxUrZqWa5gSkTm5ncsLg5d+zu0M4cXt4=
github.com/pion/webrtc/v4 v4.2.3/go.mod h1:7vsyFzRzaKP5IELUnj8zLcglPyIT6wWwqTppBZ1k6Kc=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
@@ -124,55 +108,45 @@ github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxI
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
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.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+17 -3
View File
@@ -7,6 +7,7 @@ import (
"net"
"net/http"
"os"
"slices"
"strconv"
"strings"
"sync"
@@ -23,6 +24,7 @@ func Init() {
Listen string `yaml:"listen"`
Username string `yaml:"username"`
Password string `yaml:"password"`
LocalAuth bool `yaml:"local_auth"`
BasePath string `yaml:"base_path"`
StaticDir string `yaml:"static_dir"`
Origin string `yaml:"origin"`
@@ -30,6 +32,8 @@ func Init() {
TLSCert string `yaml:"tls_cert"`
TLSKey string `yaml:"tls_key"`
UnixListen string `yaml:"unix_listen"`
AllowPaths []string `yaml:"allow_paths"`
} `yaml:"api"`
}
@@ -43,6 +47,7 @@ func Init() {
return
}
allowPaths = cfg.Mod.AllowPaths
basePath = cfg.Mod.BasePath
log = app.GetLogger("api")
@@ -61,7 +66,7 @@ func Init() {
}
if cfg.Mod.Username != "" {
Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, Handler) // 2nd
Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, cfg.Mod.LocalAuth, Handler) // 2nd
}
if log.Trace().Enabled() {
@@ -152,6 +157,10 @@ func HandleFunc(pattern string, handler http.HandlerFunc) {
if len(pattern) == 0 || pattern[0] != '/' {
pattern = basePath + "/" + pattern
}
if allowPaths != nil && !slices.Contains(allowPaths, pattern) {
log.Trace().Str("path", pattern).Msg("[api] ignore path not in allow_paths")
return
}
log.Trace().Str("path", pattern).Msg("[api] register path")
http.HandleFunc(pattern, handler)
}
@@ -185,6 +194,7 @@ func Response(w http.ResponseWriter, body any, contentType string) {
const StreamNotFound = "stream not found"
var allowPaths []string
var basePath string
var log zerolog.Logger
@@ -195,9 +205,13 @@ func middlewareLog(next http.Handler) http.Handler {
})
}
func middlewareAuth(username, password string, next http.Handler) http.Handler {
func isLoopback(remoteAddr string) bool {
return strings.HasPrefix(remoteAddr, "127.") || strings.HasPrefix(remoteAddr, "[::1]") || remoteAddr == "@"
}
func middlewareAuth(username, password string, localAuth bool, 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]") && r.RemoteAddr != "@" {
if localAuth || !isLoopback(r.RemoteAddr) {
user, pass, ok := r.BasicAuth()
if !ok || user != username || pass != password {
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
+22 -1
View File
@@ -11,6 +11,7 @@ import (
var (
Version string
Modules []string
UserAgent string
ConfigPath string
Info = make(map[string]any)
@@ -76,6 +77,16 @@ func Init() {
if ConfigPath != "" {
Logger.Info().Str("path", ConfigPath).Msg("config")
}
var cfg struct {
Mod struct {
Modules []string `yaml:"modules"`
} `yaml:"app"`
}
LoadConfig(&cfg)
Modules = cfg.Mod.Modules
}
func readRevisionTime() (revision, vcsTime string) {
@@ -92,10 +103,20 @@ func readRevisionTime() (revision, vcsTime string) {
vcsTime = setting.Value
case "vcs.modified":
if setting.Value == "true" {
revision = "mod." + revision
revision += ".dirty"
}
}
}
// Check version from -buildvcs info
// Format for tagged version : v1.9.13
// Format for modified code: v1.9.14-0.20251215184105-753d6617ab58+dirty
if info.Main.Version != "v"+Version {
// Format: 1.9.13+dev.753d661[.dirty]
// Compatible with "awesomeversion" and "packaging.version" from python.
// Version will be larger than the previous release, but smaller than the next release.
Version += "+dev." + revision
}
}
return
}
+2
View File
@@ -14,6 +14,8 @@ import (
var MemoryLog = newBuffer()
func GetLogger(module string) zerolog.Logger {
Logger.Trace().Str("module", module).Msgf("[log] init")
if s, ok := modules[module]; ok {
lvl, err := zerolog.ParseLevel(s)
if err == nil {
+21
View File
@@ -0,0 +1,21 @@
# Doorbird
*[added in v1.9.8](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.11)*
This source type supports [Doorbird](https://www.doorbird.com/) devices including MJPEG stream, audio stream as well as two-way audio.
It is recommended to create a sepearate user within your doorbird setup for go2rtc. Minimum permissions for the user are:
- Watch always
- API operator
## Configuration
```yaml
streams:
doorbird1:
- rtsp://admin:password@192.168.1.123:8557/mpeg/720p/media.amp # RTSP stream
- doorbird://admin:password@192.168.1.123?media=video # MJPEG stream
- doorbird://admin:password@192.168.1.123?media=audio # audio stream
- doorbird://admin:password@192.168.1.123 # two-way audio
```
+17
View File
@@ -2,7 +2,9 @@ package echo
import (
"bytes"
"errors"
"os/exec"
"slices"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
@@ -10,11 +12,25 @@ import (
)
func Init() {
var cfg struct {
Mod struct {
AllowPaths []string `yaml:"allow_paths"`
} `yaml:"echo"`
}
app.LoadConfig(&cfg)
allowPaths := cfg.Mod.AllowPaths
log := app.GetLogger("echo")
streams.RedirectFunc("echo", func(url string) (string, error) {
args := shell.QuoteSplit(url[5:])
if allowPaths != nil && !slices.Contains(allowPaths, args[0]) {
return "", errors.New("echo: bin not in allow_paths: " + args[0])
}
b, err := exec.Command(args[0], args[1:]...).Output()
if err != nil {
return "", err
@@ -26,4 +42,5 @@ func Init() {
return string(b), nil
})
streams.MarkInsecure("echo")
}
+31 -5
View File
@@ -9,6 +9,7 @@ import (
"io"
"net/url"
"os"
"slices"
"strings"
"sync"
"syscall"
@@ -26,6 +27,16 @@ import (
)
func Init() {
var cfg struct {
Mod struct {
AllowPaths []string `yaml:"allow_paths"`
} `yaml:"exec"`
}
app.LoadConfig(&cfg)
allowPaths = cfg.Mod.AllowPaths
rtsp.HandleFunc(func(conn *pkg.Conn) bool {
waitersMu.Lock()
waiter := waiters[conn.URL.Path]
@@ -45,10 +56,13 @@ func Init() {
})
streams.HandleFunc("exec", execHandle)
streams.MarkInsecure("exec")
log = app.GetLogger("exec")
}
var allowPaths []string
func execHandle(rawURL string) (prod core.Producer, err error) {
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
query := streams.ParseQuery(rawQuery)
@@ -73,6 +87,11 @@ func execHandle(rawURL string) (prod core.Producer, err error) {
debug: log.Debug().Enabled(),
}
if allowPaths != nil && !slices.Contains(allowPaths, cmd.Args[0]) {
_ = cmd.Close()
return nil, errors.New("exec: bin not in allow_paths: " + cmd.Args[0])
}
if s := query.Get("killsignal"); s != "" {
sig := syscall.Signal(core.Atoi(s))
cmd.Cancel = func() error {
@@ -89,10 +108,17 @@ func execHandle(rawURL string) (prod core.Producer, err error) {
return pcm.NewBackchannel(cmd, query.Get("audio"))
}
var timeout time.Duration
if s := query.Get("starttimeout"); s != "" {
timeout = time.Duration(core.Atoi(s)) * time.Second
} else {
timeout = 30 * time.Second
}
if path == "" {
prod, err = handlePipe(rawURL, cmd)
} else {
prod, err = handleRTSP(rawURL, cmd, path)
prod, err = handleRTSP(rawURL, cmd, path, timeout)
}
if err != nil {
@@ -141,7 +167,7 @@ func handlePipe(source string, cmd *shell.Command) (core.Producer, error) {
return prod, nil
}
func handleRTSP(source string, cmd *shell.Command, path string) (core.Producer, error) {
func handleRTSP(source string, cmd *shell.Command, path string, timeout time.Duration) (core.Producer, error) {
if log.Trace().Enabled() {
cmd.Stdout = os.Stdout
}
@@ -167,11 +193,11 @@ func handleRTSP(source string, cmd *shell.Command, path string) (core.Producer,
return nil, err
}
timeout := time.NewTimer(30 * time.Second)
defer timeout.Stop()
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-timeout.C:
case <-timer.C:
// haven't received data from app in timeout
log.Error().Str("source", source).Msg("[exec] timeout")
return nil, errors.New("exec: timeout")
+71 -11
View File
@@ -12,34 +12,94 @@
- `fetch` - JS-like HTTP requests
- `match` - JS-like RegExp queries
## Examples
## Fetch examples
Multiple fetch requests are executed within a single session. They share the same cookie.
**HTTP GET**
```js
var r = fetch('https://example.org/products.json');
```
**HTTP POST JSON**
```js
var r = fetch('https://example.org/post', {
method: 'POST',
// Content-Type: application/json will be set automatically
json: {username: 'example'}
});
```
**HTTP POST Form**
```js
var r = fetch('https://example.org/post', {
method: 'POST',
// Content-Type: application/x-www-form-urlencoded will be set automatically
data: {username: 'example', password: 'password'}
});
```
## Script 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" : ""
expr:
let host = 'admin:password@192.168.1.123';
var r = fetch('http://' + host + '/cgi-bin/configManager.cgi?action=setConfig&Encode[0].MainFormat[0].Audio.Compression=G.711A&Encode[0].MainFormat[0].Audio.Frequency=8000');
'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
You can get credentials from 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}
expr:
let camera = '***';
let token = '***';
let operator = '***';
fetch('https://myhome.proptech.ru/rest/v1/forpost/cameras/' + camera + '/video', {
headers: {
'Authorization': 'Bearer ' + token,
'User-Agent': 'Google sdkgphone64x8664 | Android 14 | erth | 8.26.0 (82600010) | 0 | 0 | 0',
'Operator': operator
}
}).json().data.URL
```
**dom.ufanet.ru**
```yaml
streams:
ufanet_ru: |
expr:
let username = '***';
let password = '***';
let cameraid = '***';
let r1 = fetch('https://ucams.ufanet.ru/api/internal/login/', {
method: 'POST',
data: {username: username, password: password}
});
let r2 = fetch('https://ucams.ufanet.ru/api/v0/cameras/this/?lang=ru', {
method: 'POST',
json: {'fields': ['token_l', 'server'], 'token_l_ttl': 3600, 'numbers': [cameraid]},
}).json().results[0];
'rtsp://' + r2.server.domain + '/' + r2.number + '?token=' + r2.token_l
```
**Parse HLS files from Apple**
Same example in two languages - python and expr.
+1
View File
@@ -25,4 +25,5 @@ func Init() {
return url, nil
})
streams.MarkInsecure("expr")
}
+14 -7
View File
@@ -58,15 +58,15 @@ func Init() {
}
var defaults = map[string]string{
"bin": "ffmpeg",
"global": "-hide_banner",
"bin": "ffmpeg",
"global": "-hide_banner",
"timeout": "5",
// inputs
"file": "-re -i {input}",
"http": "-fflags nobuffer -flags low_delay -i {input}",
"rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}",
"rtsp/udp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}",
"file": "-re -i {input}",
"http": "-fflags nobuffer -flags low_delay -i {input}",
"rtsp": "-fflags nobuffer -flags low_delay -timeout {timeout} -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}",
"rtsp/udp": "-fflags nobuffer -flags low_delay -timeout {timeout} -user_agent go2rtc/ffmpeg -i {input}",
// output
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
@@ -169,6 +169,13 @@ func inputTemplate(name, s string, query url.Values) string {
} else {
template = defaults[name]
}
if strings.Contains(template, "{timeout}") {
timeout := query.Get("timeout")
if timeout == "" {
timeout = defaults["timeout"]
}
template = strings.Replace(template, "{timeout}", timeout+"000000", 1)
}
return strings.Replace(template, "{input}", s, 1)
}
+5
View File
@@ -123,6 +123,11 @@ func TestParseArgsIpCam(t *testing.T) {
source: "rtmp://example.com#input=rtsp/udp",
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTSP] custom timeout",
source: "rtsp://example.com#timeout=10",
expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 10000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
+3 -2
View File
@@ -46,6 +46,7 @@ func NewProducer(url string) (core.Producer, error) {
{Name: core.CodecPCM, ClockRate: 16000},
{Name: core.CodecPCMA, ClockRate: 16000},
{Name: core.CodecPCMU, ClockRate: 16000},
{Name: core.CodecPCML, ClockRate: 8000},
{Name: core.CodecPCM, ClockRate: 8000},
{Name: core.CodecPCMA, ClockRate: 8000},
{Name: core.CodecPCMU, ClockRate: 8000},
@@ -95,11 +96,11 @@ func (p *Producer) newURL() string {
codec := receiver.Codec
switch codec.Name {
case core.CodecOpus:
s += "#audio=opus"
s += "#audio=opus/16000"
case core.CodecAAC:
s += "#audio=aac/16000"
case core.CodecPCML:
s += "#audio=pcml/16000"
s += "#audio=pcml/" + strconv.Itoa(int(codec.ClockRate))
case core.CodecPCM:
s += "#audio=pcm/" + strconv.Itoa(int(codec.ClockRate))
case core.CodecPCMA:
+2 -2
View File
@@ -30,10 +30,10 @@ func apiStream(w http.ResponseWriter, r *http.Request) {
// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}
// 2. static link to Hass camera
// 3. dynamic link to Hass camera
if streams.Patch(v.Name, v.Channels.First.Url) != nil {
if _, err := streams.Patch(v.Name, v.Channels.First.Url); err == nil {
apiOK(w, r)
} else {
http.Error(w, "", http.StatusBadRequest)
http.Error(w, err.Error(), http.StatusBadRequest)
}
// /stream/{id}/channel/0/webrtc
+8 -2
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"os"
"path"
"strings"
"sync"
"github.com/AlexxIT/go2rtc/internal/api"
@@ -37,8 +38,13 @@ func Init() {
api.HandleFunc("/streams", apiOK)
api.HandleFunc("/stream/", apiStream)
streams.RedirectFunc("hass", func(url string) (string, error) {
if location := entities[url[5:]]; location != "" {
streams.RedirectFunc("hass", func(rawURL string) (string, error) {
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
if location := entities[rawURL[5:]]; location != "" {
if rawQuery != "" {
return location + "#" + rawQuery, nil
}
return location, nil
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
)
func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
stream := streams.GetOrPatch(tr.Request.URL.Query())
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
+79 -37
View File
@@ -3,6 +3,7 @@ package homekit
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
@@ -14,56 +15,97 @@ import (
"github.com/AlexxIT/go2rtc/pkg/mdns"
)
func apiHandler(w http.ResponseWriter, r *http.Request) {
func apiDiscovery(w http.ResponseWriter, r *http.Request) {
sources, err := discovery()
if err != nil {
api.Error(w, err)
return
}
urls := findHomeKitURLs()
for id, u := range urls {
deviceID := u.Query().Get("device_id")
for _, source := range sources {
if strings.Contains(source.URL, deviceID) {
source.Location = id
break
}
}
}
for _, source := range sources {
if source.Location == "" {
source.Location = " "
}
}
api.ResponseSources(w, sources)
}
func apiHomekit(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
switch r.Method {
case "GET":
sources, err := discovery()
if err != nil {
api.Error(w, err)
return
}
urls := findHomeKitURLs()
for id, u := range urls {
deviceID := u.Query().Get("device_id")
for _, source := range sources {
if strings.Contains(source.URL, deviceID) {
source.Location = id
break
}
if id := r.Form.Get("id"); id != "" {
if srv := servers[id]; srv != nil {
api.ResponsePrettyJSON(w, srv)
} else {
http.Error(w, "server not found", http.StatusNotFound)
}
} else {
api.ResponsePrettyJSON(w, servers)
}
for _, source := range sources {
if source.Location == "" {
source.Location = " "
}
}
api.ResponseSources(w, sources)
case "POST":
if err := r.ParseMultipartForm(1024); err != nil {
api.Error(w, err)
return
}
if err := apiPair(r.Form.Get("id"), r.Form.Get("url")); err != nil {
api.Error(w, err)
id := r.Form.Get("id")
rawURL := r.Form.Get("src") + "&pin=" + r.Form.Get("pin")
if err := apiPair(id, rawURL); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
case "DELETE":
if err := r.ParseMultipartForm(1024); err != nil {
api.Error(w, err)
return
}
if err := apiUnpair(r.Form.Get("id")); err != nil {
api.Error(w, err)
id := r.Form.Get("id")
if err := apiUnpair(id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
func apiHomekitAccessories(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
stream := streams.Get(id)
if stream == nil {
http.Error(w, "", http.StatusNotFound)
return
}
rawURL := findHomeKitURL(stream.Sources())
if rawURL == "" {
http.Error(w, "", http.StatusBadRequest)
return
}
client, err := hap.Dial(rawURL)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer client.Close()
res, err := client.Get(hap.PathAccessories)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", api.MimeJSON)
_, _ = io.Copy(w, res.Body)
}
func discovery() ([]*api.Source, error) {
var sources []*api.Source
+33 -54
View File
@@ -2,8 +2,6 @@ package homekit
import (
"errors"
"io"
"net"
"net/http"
"strings"
@@ -26,6 +24,7 @@ func Init() {
Name string `yaml:"name"`
DeviceID string `yaml:"device_id"`
DevicePrivate string `yaml:"device_private"`
CategoryID string `yaml:"category_id"`
Pairings []string `yaml:"pairings"`
} `yaml:"homekit"`
}
@@ -35,12 +34,15 @@ func Init() {
streams.HandleFunc("homekit", streamHandler)
api.HandleFunc("api/homekit", apiHandler)
api.HandleFunc("api/homekit", apiHomekit)
api.HandleFunc("api/homekit/accessories", apiHomekitAccessories)
api.HandleFunc("api/discovery/homekit", apiDiscovery)
if cfg.Mod == nil {
return
}
hosts = map[string]*server{}
servers = map[string]*server{}
var entries []*mdns.ServiceEntry
@@ -63,36 +65,19 @@ func Init() {
deviceID := calcDeviceID(conf.DeviceID, id) // random MAC-address
name := calcName(conf.Name, deviceID)
setupID := calcSetupID(id)
srv := &server{
stream: id,
srtp: srtp.Server,
pairings: conf.Pairings,
setupID: setupID,
}
srv.hap = &hap.Server{
Pin: pin,
DeviceID: deviceID,
DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id),
GetPair: srv.GetPair,
AddPair: srv.AddPair,
Handler: homekit.ServerHandler(srv),
}
if url := findHomeKitURL(stream.Sources()); url != "" {
// 1. Act as transparent proxy for HomeKit camera
dial := func() (net.Conn, error) {
client, err := homekit.Dial(url, srtp.Server)
if err != nil {
return nil, err
}
return client.Conn(), nil
}
srv.hap.Handler = homekit.ProxyHandler(srv, dial)
} else {
// 2. Act as basic HomeKit camera
srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", app.Version)
srv.hap.Handler = homekit.ServerHandler(srv)
Pin: pin,
DeviceID: deviceID,
DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id),
GetClientPublic: srv.GetPair,
}
srv.mdns = &mdns.ServiceEntry{
@@ -106,23 +91,32 @@ func Init() {
hap.TXTProtoVersion: "1.1",
hap.TXTStateNumber: "1",
hap.TXTStatusFlags: hap.StatusNotPaired,
hap.TXTCategory: hap.CategoryCamera,
hap.TXTSetupHash: srv.hap.SetupHash(),
hap.TXTCategory: calcCategoryID(conf.CategoryID),
hap.TXTSetupHash: hap.SetupHash(setupID, deviceID),
},
}
entries = append(entries, srv.mdns)
srv.UpdateStatus()
if url := findHomeKitURL(stream.Sources()); url != "" {
// 1. Act as transparent proxy for HomeKit camera
srv.proxyURL = url
} else {
// 2. Act as basic HomeKit camera
srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", app.Version)
}
host := srv.mdns.Host(mdns.ServiceHAP)
servers[host] = srv
hosts[host] = srv
servers[id] = srv
log.Trace().Msgf("[homekit] new server: %s", srv.mdns)
}
api.HandleFunc(hap.PathPairSetup, hapHandler)
api.HandleFunc(hap.PathPairVerify, hapHandler)
log.Trace().Msgf("[homekit] mdns: %s", entries)
go func() {
if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil {
log.Error().Err(err).Caller().Send()
@@ -131,6 +125,7 @@ func Init() {
}
var log zerolog.Logger
var hosts map[string]*server
var servers map[string]*server
func streamHandler(rawURL string) (core.Producer, error) {
@@ -142,6 +137,8 @@ func streamHandler(rawURL string) (core.Producer, error) {
client, err := homekit.Dial(rawURL, srtp.Server)
if client != nil && rawQuery != "" {
query := streams.ParseQuery(rawQuery)
client.MaxWidth = core.Atoi(query.Get("maxwidth"))
client.MaxHeight = core.Atoi(query.Get("maxheight"))
client.Bitrate = parseBitrate(query.Get("bitrate"))
}
@@ -149,45 +146,27 @@ func streamHandler(rawURL string) (core.Producer, error) {
}
func resolve(host string) *server {
if len(servers) == 1 {
for _, srv := range servers {
if len(hosts) == 1 {
for _, srv := range hosts {
return srv
}
}
if srv, ok := servers[host]; ok {
if srv, ok := hosts[host]; ok {
return srv
}
return nil
}
func hapHandler(w http.ResponseWriter, r *http.Request) {
conn, rw, err := w.(http.Hijacker).Hijack()
if err != nil {
return
}
defer conn.Close()
// Can support multiple HomeKit cameras on single port ONLY for Apple devices.
// 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 {
log.Error().Msg("[homekit] unknown host: " + r.Host)
_ = hap.WriteBackoff(rw)
return
}
switch r.RequestURI {
case hap.PathPairSetup:
err = srv.hap.PairSetup(r, rw, conn)
case hap.PathPairVerify:
err = srv.hap.PairVerify(r, rw, conn)
}
if err != nil && err != io.EOF {
log.Error().Err(err).Caller().Send()
}
srv.Handle(w, r)
}
func findHomeKitURL(sources []string) string {
@@ -203,7 +182,7 @@ func findHomeKitURL(sources []string) string {
if strings.HasPrefix(url, "hass") {
location, _ := streams.Location(url)
if strings.HasPrefix(location, "homekit") {
return url
return location
}
}
+235 -95
View File
@@ -4,10 +4,16 @@ import (
"crypto/ed25519"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"slices"
"strings"
"sync"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
@@ -16,23 +22,142 @@ import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
"github.com/AlexxIT/go2rtc/pkg/hap/hds"
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/mdns"
"github.com/AlexxIT/go2rtc/pkg/srtp"
)
type server struct {
stream string // stream name from YAML
hap *hap.Server // server for HAP connection and encryption
mdns *mdns.ServiceEntry
srtp *srtp.Server
accessory *hap.Accessory // HAP accessory
pairings []string // pairings list
hap *hap.Server // server for HAP connection and encryption
mdns *mdns.ServiceEntry
streams map[string]*homekit.Consumer
consumer *homekit.Consumer
pairings []string // pairings list
conns []any
mu sync.Mutex
accessory *hap.Accessory // HAP accessory
consumer *homekit.Consumer
proxyURL string
setupID string
stream string // stream name from YAML
}
func (s *server) MarshalJSON() ([]byte, error) {
v := struct {
Name string `json:"name"`
DeviceID string `json:"device_id"`
Paired int `json:"paired,omitempty"`
CategoryID string `json:"category_id,omitempty"`
SetupCode string `json:"setup_code,omitempty"`
SetupID string `json:"setup_id,omitempty"`
Conns []any `json:"connections,omitempty"`
}{
Name: s.mdns.Name,
DeviceID: s.mdns.Info[hap.TXTDeviceID],
CategoryID: s.mdns.Info[hap.TXTCategory],
Paired: len(s.pairings),
Conns: s.conns,
}
if v.Paired == 0 {
v.SetupCode = s.hap.Pin
v.SetupID = s.setupID
}
return json.Marshal(v)
}
func (s *server) Handle(w http.ResponseWriter, r *http.Request) {
conn, rw, err := w.(http.Hijacker).Hijack()
if err != nil {
return
}
defer conn.Close()
// Fix reading from Body after Hijack.
r.Body = io.NopCloser(rw)
switch r.RequestURI {
case hap.PathPairSetup:
id, key, err := s.hap.PairSetup(r, rw)
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
s.AddPair(id, key, hap.PermissionAdmin)
case hap.PathPairVerify:
id, key, err := s.hap.PairVerify(r, rw)
if err != nil {
log.Debug().Err(err).Caller().Send()
return
}
log.Debug().Str("stream", s.stream).Str("client_id", id).Msgf("[homekit] %s: new conn", conn.RemoteAddr())
controller, err := hap.NewConn(conn, rw, key, false)
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
s.AddConn(controller)
defer s.DelConn(controller)
var handler homekit.HandlerFunc
switch {
case s.accessory != nil:
handler = homekit.ServerHandler(s)
case s.proxyURL != "":
client, err := hap.Dial(s.proxyURL)
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
handler = homekit.ProxyHandler(s, client.Conn)
}
// If your iPhone goes to sleep, it will be an EOF error.
if err = handler(controller); err != nil && !errors.Is(err, io.EOF) {
log.Error().Err(err).Caller().Send()
return
}
}
}
type logger struct {
v any
}
func (l logger) String() string {
switch v := l.v.(type) {
case *hap.Conn:
return "hap " + v.RemoteAddr().String()
case *hds.Conn:
return "hds " + v.RemoteAddr().String()
case *homekit.Consumer:
return "rtp " + v.RemoteAddr
}
return "unknown"
}
func (s *server) AddConn(v any) {
log.Trace().Str("stream", s.stream).Msgf("[homekit] add conn %s", logger{v})
s.mu.Lock()
s.conns = append(s.conns, v)
s.mu.Unlock()
}
func (s *server) DelConn(v any) {
log.Trace().Str("stream", s.stream).Msgf("[homekit] del conn %s", logger{v})
s.mu.Lock()
if i := slices.Index(s.conns, v); i >= 0 {
s.conns = slices.Delete(s.conns, i, i+1)
}
s.mu.Unlock()
}
func (s *server) UpdateStatus() {
@@ -44,12 +169,68 @@ func (s *server) UpdateStatus() {
}
}
func (s *server) pairIndex(id string) int {
id = "client_id=" + id
for i, pairing := range s.pairings {
if strings.HasPrefix(pairing, id) {
return i
}
}
return -1
}
func (s *server) GetPair(id string) []byte {
s.mu.Lock()
defer s.mu.Unlock()
if i := s.pairIndex(id); i >= 0 {
query, _ := url.ParseQuery(s.pairings[i])
b, _ := hex.DecodeString(query.Get("client_public"))
return b
}
return nil
}
func (s *server) AddPair(id string, public []byte, permissions byte) {
log.Debug().Str("stream", s.stream).Msgf("[homekit] add pair id=%s public=%x perm=%d", id, public, permissions)
s.mu.Lock()
if s.pairIndex(id) < 0 {
s.pairings = append(s.pairings, fmt.Sprintf(
"client_id=%s&client_public=%x&permissions=%d", id, public, permissions,
))
s.UpdateStatus()
s.PatchConfig()
}
s.mu.Unlock()
}
func (s *server) DelPair(id string) {
log.Debug().Str("stream", s.stream).Msgf("[homekit] del pair id=%s", id)
s.mu.Lock()
if i := s.pairIndex(id); i >= 0 {
s.pairings = append(s.pairings[:i], s.pairings[i+1:]...)
s.UpdateStatus()
s.PatchConfig()
}
s.mu.Unlock()
}
func (s *server) PatchConfig() {
if err := app.PatchConfig([]string{"homekit", s.stream, "pairings"}, s.pairings); err != nil {
log.Error().Err(err).Msgf(
"[homekit] can't save %s pairings=%v", s.stream, s.pairings,
)
}
}
func (s *server) GetAccessories(_ net.Conn) []*hap.Accessory {
return []*hap.Accessory{s.accessory}
}
func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any {
log.Trace().Msgf("[homekit] %s: get char aid=%d iid=0x%x", conn.RemoteAddr(), aid, iid)
log.Trace().Str("stream", s.stream).Msgf("[homekit] get char aid=%d iid=0x%x", aid, iid)
char := s.accessory.GetCharacterByID(iid)
if char == nil {
@@ -59,11 +240,12 @@ func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any {
switch char.Type {
case camera.TypeSetupEndpoints:
if s.consumer == nil {
consumer := s.consumer
if consumer == nil {
return nil
}
answer := s.consumer.GetAnswer()
answer := consumer.GetAnswer()
v, err := tlv8.MarshalBase64(answer)
if err != nil {
return nil
@@ -76,7 +258,7 @@ func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any {
}
func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value any) {
log.Trace().Msgf("[homekit] %s: set char aid=%d iid=0x%x value=%v", conn.RemoteAddr(), aid, iid, value)
log.Trace().Str("stream", s.stream).Msgf("[homekit] set char aid=%d iid=0x%x value=%v", aid, iid, value)
char := s.accessory.GetCharacterByID(iid)
if char == nil {
@@ -86,61 +268,64 @@ func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value a
switch char.Type {
case camera.TypeSetupEndpoints:
var offer camera.SetupEndpoints
var offer camera.SetupEndpointsRequest
if err := tlv8.UnmarshalBase64(value, &offer); err != nil {
return
}
s.consumer = homekit.NewConsumer(conn, srtp2.Server)
s.consumer.SetOffer(&offer)
consumer := homekit.NewConsumer(conn, srtp2.Server)
consumer.SetOffer(&offer)
s.consumer = consumer
case camera.TypeSelectedStreamConfiguration:
var conf camera.SelectedStreamConfig
var conf camera.SelectedStreamConfiguration
if err := tlv8.UnmarshalBase64(value, &conf); err != nil {
return
}
log.Trace().Msgf("[homekit] %s stream id=%x cmd=%d", conn.RemoteAddr(), conf.Control.SessionID, conf.Control.Command)
log.Trace().Str("stream", s.stream).Msgf("[homekit] stream id=%x cmd=%d", conf.Control.SessionID, conf.Control.Command)
switch conf.Control.Command {
case camera.SessionCommandEnd:
if consumer := s.streams[conf.Control.SessionID]; consumer != nil {
_ = consumer.Stop()
for _, consumer := range s.conns {
if consumer, ok := consumer.(*homekit.Consumer); ok {
if consumer.SessionID() == conf.Control.SessionID {
_ = consumer.Stop()
return
}
}
}
case camera.SessionCommandStart:
if s.consumer == nil {
consumer := s.consumer
if consumer == nil {
return
}
if !s.consumer.SetConfig(&conf) {
if !consumer.SetConfig(&conf) {
log.Warn().Msgf("[homekit] wrong config")
return
}
if s.streams == nil {
s.streams = map[string]*homekit.Consumer{}
}
s.streams[conf.Control.SessionID] = s.consumer
s.AddConn(consumer)
stream := streams.Get(s.stream)
if err := stream.AddConsumer(s.consumer); err != nil {
if err := stream.AddConsumer(consumer); err != nil {
return
}
go func() {
_, _ = s.consumer.WriteTo(nil)
stream.RemoveConsumer(s.consumer)
_, _ = consumer.WriteTo(nil)
stream.RemoveConsumer(consumer)
delete(s.streams, conf.Control.SessionID)
s.DelConn(consumer)
}()
}
}
}
func (s *server) GetImage(conn net.Conn, width, height int) []byte {
log.Trace().Msgf("[homekit] %s: get image width=%d height=%d", conn.RemoteAddr(), width, height)
log.Trace().Str("stream", s.stream).Msgf("[homekit] get image width=%d height=%d", width, height)
stream := streams.Get(s.stream)
cons := magic.NewKeyframe()
@@ -166,69 +351,6 @@ func (s *server) GetImage(conn net.Conn, width, height int) []byte {
return b
}
func (s *server) GetPair(conn net.Conn, id string) []byte {
log.Trace().Msgf("[homekit] %s: get pair id=%s", conn.RemoteAddr(), id)
for _, pairing := range s.pairings {
if !strings.Contains(pairing, id) {
continue
}
query, err := url.ParseQuery(pairing)
if err != nil {
continue
}
if query.Get("client_id") != id {
continue
}
s := query.Get("client_public")
b, _ := hex.DecodeString(s)
return b
}
return nil
}
func (s *server) AddPair(conn net.Conn, id string, public []byte, permissions byte) {
log.Trace().Msgf("[homekit] %s: add pair id=%s public=%x perm=%d", conn.RemoteAddr(), id, public, permissions)
query := url.Values{
"client_id": []string{id},
"client_public": []string{hex.EncodeToString(public)},
"permissions": []string{string('0' + permissions)},
}
if s.GetPair(conn, id) == nil {
s.pairings = append(s.pairings, query.Encode())
s.UpdateStatus()
s.PatchConfig()
}
}
func (s *server) DelPair(conn net.Conn, id string) {
log.Trace().Msgf("[homekit] %s: del pair id=%s", conn.RemoteAddr(), id)
id = "client_id=" + id
for i, pairing := range s.pairings {
if !strings.Contains(pairing, id) {
continue
}
s.pairings = append(s.pairings[:i], s.pairings[i+1:]...)
s.UpdateStatus()
s.PatchConfig()
break
}
}
func (s *server) PatchConfig() {
if err := app.PatchConfig([]string{"homekit", s.stream, "pairings"}, s.pairings); err != nil {
log.Error().Err(err).Msgf(
"[homekit] can't save %s pairings=%v", s.stream, s.pairings,
)
}
}
func calcName(name, seed string) string {
if name != "" {
return name
@@ -263,3 +385,21 @@ func calcDevicePrivate(private, seed string) []byte {
b := sha512.Sum512([]byte(seed))
return ed25519.NewKeyFromSeed(b[:ed25519.SeedSize])
}
func calcSetupID(seed string) string {
b := sha512.Sum512([]byte(seed))
return fmt.Sprintf("%02X%02X", b[44], b[46])
}
func calcCategoryID(categoryID string) string {
switch categoryID {
case "bridge":
return hap.CategoryBridge
case "doorbell":
return hap.CategoryDoorbell
}
if core.Atoi(categoryID) > 0 {
return categoryID
}
return hap.CategoryCamera
}
+42
View File
@@ -1,3 +1,45 @@
# MJPEG
**Important.** For stream in MJPEG format, your source MUST contain the MJPEG codec. If your stream has an MJPEG codec, you can receive **MJPEG stream** or **JPEG snapshots** via API.
You can receive an MJPEG stream in several ways:
- some cameras support MJPEG codec inside [RTSP stream](#source-rtsp) (ex. second stream for Dahua cameras)
- some cameras have an HTTP link with [MJPEG stream](#source-http)
- some cameras have an HTTP link with snapshots - go2rtc can convert them to [MJPEG stream](#source-http)
- you can convert H264/H265 stream from your camera via [FFmpeg integraion](#source-ffmpeg)
With this example, your stream will have both H264 and MJPEG codecs:
```yaml
streams:
camera1:
- rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
- ffmpeg:camera1#video=mjpeg
```
## API examples
**MJPEG stream**
```
http://192.168.1.123:1984/api/stream.mjpeg?src=camera1`
```
**JPEG snapshots**
```
http://192.168.1.123:1984/api/frame.jpeg?src=camera1
```
- You can use `width`/`w` and/or `height`/`h` params.
- You can use `rotate` param with `90`, `180`, `270` or `-90` values.
- You can use `hardware`/`hw` param [read more](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration).
- You can use `cache` param (`1m`, `10s`, etc.) to get a cached snapshot.
- The snapshot is cached only when requested with the `cache` parameter.
- A cached snapshot will be used if its time is not older than the time specified in the `cache` parameter.
- The `cache` parameter does not check the image sizes from the cache and those specified in the query.
## Stream as ASCII to Terminal
[![](https://img.youtube.com/vi/sHj_3h_sX7M/mqdefault.jpg)](https://www.youtube.com/watch?v=sHj_3h_sX7M)
@@ -6,6 +6,7 @@ import (
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
@@ -36,12 +37,44 @@ func Init() {
var log zerolog.Logger
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
stream := streams.GetOrPatch(r.URL.Query())
query := r.URL.Query()
stream, _ := streams.GetOrPatch(query)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
var b []byte
if s := query.Get("cache"); s != "" {
if timeout, err := time.ParseDuration(s); err == nil {
src := query.Get("src")
cacheMu.Lock()
entry, found := cache[src]
cacheMu.Unlock()
if found && time.Since(entry.timestamp) < timeout {
writeJPEGResponse(w, entry.payload)
return
}
defer func() {
if b == nil {
return
}
entry = cacheEntry{payload: b, timestamp: time.Now()}
cacheMu.Lock()
if cache == nil {
cache = map[string]cacheEntry{src: entry}
} else {
cache[src] = entry
}
cacheMu.Unlock()
}()
}
}
cons := magic.NewKeyframe()
cons.WithRequest(r)
@@ -52,7 +85,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
once := &core.OnceBuffer{} // init and first frame
_, _ = cons.WriteTo(once)
b := once.Buffer()
b = once.Buffer()
stream.RemoveConsumer(cons)
@@ -60,7 +93,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
case core.CodecH264, core.CodecH265:
ts := time.Now()
var err error
if b, err = ffmpeg.JPEGWithQuery(b, r.URL.Query()); err != nil {
if b, err = ffmpeg.JPEGWithQuery(b, query); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -69,6 +102,19 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
b = mjpeg.FixJPEG(b)
}
writeJPEGResponse(w, b)
}
var cache map[string]cacheEntry
var cacheMu sync.Mutex
// cacheEntry represents a cached keyframe with its timestamp
type cacheEntry struct {
payload []byte
timestamp time.Time
}
func writeJPEGResponse(w http.ResponseWriter, b []byte) {
h := w.Header()
h.Set("Content-Type", "image/jpeg")
h.Set("Content-Length", strconv.Itoa(len(b)))
@@ -145,7 +191,7 @@ func inputMjpeg(w http.ResponseWriter, r *http.Request) {
}
func handlerWS(tr *ws.Transport, _ *ws.Message) error {
stream := streams.GetOrPatch(tr.Request.URL.Query())
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
+1 -1
View File
@@ -91,7 +91,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
return
}
stream := streams.GetOrPatch(query)
stream, _ := streams.GetOrPatch(query)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
+2 -2
View File
@@ -11,7 +11,7 @@ import (
)
func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
stream := streams.GetOrPatch(tr.Request.URL.Query())
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
@@ -43,7 +43,7 @@ func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
}
func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error {
stream := streams.GetOrPatch(tr.Request.URL.Query())
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
+16
View File
@@ -0,0 +1,16 @@
# Multitrans
**added in v1.9.14** by [@forrestsocool](https://github.com/forrestsocool)
Two-way audio support for Chinese version of [TP-Link cameras](https://www.tp-link.com.cn/list_2549.html).
## Configuration
```yaml
streams:
tplink_cam:
# video use standard RTSP
- rtsp://admin:admin@192.168.1.202:554/stream1
# two-way audio use MULTITRANS schema
- multitrans://admin:admin@192.168.1.202:554
```
+10
View File
@@ -0,0 +1,10 @@
package multitrans
import (
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/multitrans"
)
func Init() {
streams.HandleFunc("multitrans", multitrans.Dial)
}
+26 -14
View File
@@ -7,6 +7,7 @@ import (
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
@@ -43,8 +44,17 @@ func streamOnvif(rawURL string) (core.Producer, error) {
return nil, err
}
// Append hash-based arguments to the retrieved URI
if i := strings.IndexByte(rawURL, '#'); i > 0 {
uri += rawURL[i:]
}
log.Debug().Msgf("[onvif] new uri=%s", uri)
if err = streams.Validate(uri); err != nil {
return nil, err
}
return streams.GetProducer(uri)
}
@@ -64,8 +74,10 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
log.Trace().Msgf("[onvif] server request %s %s:\n%s", r.Method, r.RequestURI, b)
switch operation {
case onvif.DeviceGetNetworkInterfaces, // important for Hass
case onvif.ServiceGetServiceCapabilities, // important for Hass
onvif.DeviceGetNetworkInterfaces, // important for Hass
onvif.DeviceGetSystemDateAndTime, // important for Hass
onvif.DeviceSetSystemDateAndTime, // return just OK
onvif.DeviceGetDiscoveryMode,
onvif.DeviceGetDNS,
onvif.DeviceGetHostname,
@@ -73,8 +85,10 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
onvif.DeviceGetNetworkProtocols,
onvif.DeviceGetNTP,
onvif.DeviceGetScopes,
onvif.MediaGetVideoEncoderConfiguration,
onvif.MediaGetVideoEncoderConfigurations,
onvif.MediaGetAudioEncoderConfigurations,
onvif.MediaGetVideoEncoderConfigurationOptions,
onvif.MediaGetAudioSources,
onvif.MediaGetAudioSourceConfigurations:
b = onvif.StaticResponse(operation)
@@ -90,11 +104,6 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
// important for Hass: SerialNumber (unique server ID)
b = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
case onvif.ServiceGetServiceCapabilities:
// important for Hass
// TODO: check path links to media
b = onvif.GetMediaServiceCapabilitiesResponse()
case onvif.DeviceSystemReboot:
b = onvif.StaticResponse(operation)
@@ -124,8 +133,7 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
case onvif.MediaGetStreamUri:
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
host = r.Host // in case of Host without port
}
uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken")
@@ -156,21 +164,21 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
var items []*api.Source
if src == "" {
urls, err := onvif.DiscoveryStreamingURLs()
devices, err := onvif.DiscoveryStreamingDevices()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, rawURL := range urls {
u, err := url.Parse(rawURL)
for _, device := range devices {
u, err := url.Parse(device.URL)
if err != nil {
log.Warn().Str("url", rawURL).Msg("[onvif] broken")
log.Warn().Str("url", device.URL).Msg("[onvif] broken")
continue
}
if u.Scheme != "http" {
log.Warn().Str("url", rawURL).Msg("[onvif] unsupported")
log.Warn().Str("url", device.URL).Msg("[onvif] unsupported")
continue
}
@@ -181,7 +189,11 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
u.Path = ""
}
items = append(items, &api.Source{Name: u.Host, URL: u.String()})
items = append(items, &api.Source{
Name: u.Host,
URL: u.String(),
Info: device.Name + " " + device.Hardware,
})
}
} else {
client, err := onvif.NewClient(src)
+54
View File
@@ -0,0 +1,54 @@
# Pinggy
[Pinggy](https://pinggy.io/) - nice service for public tunnels to your local services.
**Features:**
- A free account does not require registration.
- It does not require downloading third-party binaries and works over the SSH protocol.
- Works with HTTP, TCP and UDP protocols.
- Creates HTTPS for your HTTP services.
> [!IMPORTANT]
> A free account creates a tunnel with a random address that only works for an hour. It is suitable for testing purposes ONLY.
> [!CAUTION]
> Public access to go2rtc without authorization puts your entire home network at risk. Use with caution.
**Why:**
- It's easy to set up HTTPS for testing two-way audio.
- It's easy to check whether external access via WebRTC technology will work.
- It's easy to share direct access to your RTSP or HTTP camera with the go2rtc developer. If such access is necessary to debug your problem.
## Configuration
You will find public links in the go2rtc log after startup.
**Tunnel to go2rtc WebUI.**
```yaml
pinggy:
tunnel: http://localhost:1984
```
**Tunnel to RTSP camera.**
For example, you have camera: `rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0`
```yaml
pinggy:
tunnel: tcp://192.168.10.91:554
```
In go2rtc logs you will get similar output:
```
16:17:43.167 INF [pinggy] proxy url=tcp://abcde-123-123-123-123.a.free.pinggy.link:12345
```
Now you have working stream:
```
rtsp://admin:password@abcde-123-123-123-123.a.free.pinggy.link:12345/cam/realmonitor?channel=1&subtype=0
```
+60
View File
@@ -0,0 +1,60 @@
package pinggy
import (
"net/url"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/pkg/pinggy"
"github.com/rs/zerolog"
)
func Init() {
var cfg struct {
Mod struct {
Tunnel string `yaml:"tunnel"`
} `yaml:"pinggy"`
}
app.LoadConfig(&cfg)
if cfg.Mod.Tunnel == "" {
return
}
log = app.GetLogger("pinggy")
u, err := url.Parse(cfg.Mod.Tunnel)
if err != nil {
log.Error().Err(err).Send()
return
}
go proxy(u.Scheme, u.Host)
}
var log zerolog.Logger
func proxy(proto, address string) {
client, err := pinggy.NewClient(proto)
if err != nil {
log.Error().Err(err).Send()
return
}
defer client.Close()
urls, err := client.GetURLs()
if err != nil {
log.Error().Err(err).Send()
return
}
for _, s := range urls {
log.Info().Str("url", s).Msgf("[pinggy] proxy")
}
err = client.Proxy(address)
if err != nil {
log.Error().Err(err).Send()
return
}
}
+2
View File
@@ -198,6 +198,8 @@ func tcpHandler(conn *rtsp.Conn) {
{Name: core.CodecPCM, ClockRate: 8000},
{Name: core.CodecPCMA, ClockRate: 8000},
{Name: core.CodecPCMU, ClockRate: 8000},
{Name: core.CodecAAC, ClockRate: 8000},
{Name: core.CodecAAC, ClockRate: 16000},
},
})
}
+16 -13
View File
@@ -52,8 +52,8 @@ func apiStreams(w http.ResponseWriter, r *http.Request) {
name = src
}
if New(name, query["src"]...) == nil {
http.Error(w, "", http.StatusBadRequest)
if _, err := New(name, query["src"]...); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
@@ -69,8 +69,8 @@ func apiStreams(w http.ResponseWriter, r *http.Request) {
}
// support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass
if Patch(name, src) == nil {
http.Error(w, "", http.StatusBadRequest)
if _, err := Patch(name, src); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
case "POST":
@@ -130,16 +130,15 @@ func apiStreamsDOT(w http.ResponseWriter, r *http.Request) {
}
func apiPreload(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
src := query.Get("src")
// check if stream exists
stream := Get(src)
if stream == nil {
http.Error(w, "", http.StatusNotFound)
// GET - return all preloads
if r.Method == "GET" {
api.ResponseJSON(w, GetPreloads())
return
}
query := r.URL.Query()
src := query.Get("src")
switch r.Method {
case "PUT":
// it's safe to delete from map while iterating
@@ -153,7 +152,7 @@ func apiPreload(w http.ResponseWriter, r *http.Request) {
rawQuery := query.Encode()
if err := AddPreload(stream, rawQuery); err != nil {
if err := AddPreload(src, rawQuery); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -163,7 +162,7 @@ func apiPreload(w http.ResponseWriter, r *http.Request) {
}
case "DELETE":
if err := DelPreload(stream); err != nil {
if err := DelPreload(src); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -176,3 +175,7 @@ func apiPreload(w http.ResponseWriter, r *http.Request) {
http.Error(w, "", http.StatusMethodNotAllowed)
}
}
func apiSchemes(w http.ResponseWriter, r *http.Request) {
api.ResponseJSON(w, SupportedSchemes())
}
+66
View File
@@ -0,0 +1,66 @@
package streams
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/stretchr/testify/require"
)
func TestApiSchemes(t *testing.T) {
// Setup: Register some test handlers and redirects
HandleFunc("rtsp", func(url string) (core.Producer, error) { return nil, nil })
HandleFunc("rtmp", func(url string) (core.Producer, error) { return nil, nil })
RedirectFunc("http", func(url string) (string, error) { return "", nil })
t.Run("GET request returns schemes", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/schemes", nil)
w := httptest.NewRecorder()
apiSchemes(w, req)
require.Equal(t, http.StatusOK, w.Code)
require.Equal(t, "application/json", w.Header().Get("Content-Type"))
var schemes []string
err := json.Unmarshal(w.Body.Bytes(), &schemes)
require.NoError(t, err)
require.NotEmpty(t, schemes)
// Check that our test schemes are in the response
require.Contains(t, schemes, "rtsp")
require.Contains(t, schemes, "rtmp")
require.Contains(t, schemes, "http")
})
}
func TestApiSchemesNoDuplicates(t *testing.T) {
// Setup: Register a scheme in both handlers and redirects
HandleFunc("duplicate", func(url string) (core.Producer, error) { return nil, nil })
RedirectFunc("duplicate", func(url string) (string, error) { return "", nil })
req := httptest.NewRequest("GET", "/api/schemes", nil)
w := httptest.NewRecorder()
apiSchemes(w, req)
require.Equal(t, http.StatusOK, w.Code)
var schemes []string
err := json.Unmarshal(w.Body.Bytes(), &schemes)
require.NoError(t, err)
// Count occurrences of "duplicate"
count := 0
for _, scheme := range schemes {
if scheme == "duplicate" {
count++
}
}
// Should only appear once
require.Equal(t, 1, count, "scheme 'duplicate' should appear exactly once")
}
+37
View File
@@ -2,6 +2,7 @@ package streams
import (
"errors"
"regexp"
"strings"
"github.com/AlexxIT/go2rtc/pkg/core"
@@ -15,6 +16,21 @@ func HandleFunc(scheme string, handler Handler) {
handlers[scheme] = handler
}
func SupportedSchemes() []string {
uniqueKeys := make(map[string]struct{}, len(handlers)+len(redirects))
for scheme := range handlers {
uniqueKeys[scheme] = struct{}{}
}
for scheme := range redirects {
uniqueKeys[scheme] = struct{}{}
}
resultKeys := make([]string, 0, len(uniqueKeys))
for key := range uniqueKeys {
resultKeys = append(resultKeys, key)
}
return resultKeys
}
func HasProducer(url string) bool {
if i := strings.IndexByte(url, ':'); i > 0 {
scheme := url[:i]
@@ -95,3 +111,24 @@ func GetConsumer(url string) (core.Consumer, func(), error) {
return nil, nil, errors.New("streams: unsupported scheme: " + url)
}
var insecure = map[string]bool{}
func MarkInsecure(scheme string) {
insecure[scheme] = true
}
var sanitize = regexp.MustCompile(`\s`)
func Validate(source string) error {
// TODO: Review the entire logic of insecure sources
if i := strings.IndexByte(source, ':'); i > 0 {
if insecure[source[:i]] {
return errors.New("streams: source from insecure producer")
}
}
if sanitize.MatchString(source) {
return errors.New("streams: source with spaces may be insecure")
}
return nil
}
+28 -17
View File
@@ -1,23 +1,24 @@
package streams
import (
"errors"
"fmt"
"maps"
"net/url"
"sync"
"github.com/AlexxIT/go2rtc/pkg/probe"
)
var preloads = map[*Stream]*probe.Probe{}
var preloadsMu sync.Mutex
func Preload(stream *Stream, rawQuery string) {
if err := AddPreload(stream, rawQuery); err != nil {
log.Error().Err(err).Caller().Send()
}
type Preload struct {
stream *Stream // Don't output the stream to JSON to not worry about its secrets.
Cons *probe.Probe `json:"consumer"`
Query string `json:"query"`
}
func AddPreload(stream *Stream, rawQuery string) error {
var preloads = map[string]*Preload{}
var preloadsMu sync.Mutex
func AddPreload(name, rawQuery string) error {
if rawQuery == "" {
rawQuery = "video&audio"
}
@@ -30,29 +31,39 @@ func AddPreload(stream *Stream, rawQuery string) error {
preloadsMu.Lock()
defer preloadsMu.Unlock()
if cons := preloads[stream]; cons != nil {
stream.RemoveConsumer(cons)
if p := preloads[name]; p != nil {
p.stream.RemoveConsumer(p.Cons)
}
stream := Get(name)
if stream == nil {
return fmt.Errorf("streams: stream not found: %s", name)
}
cons := probe.Create("preload", query)
if err = stream.AddConsumer(cons); err != nil {
return err
}
preloads[stream] = cons
preloads[name] = &Preload{stream: stream, Cons: cons, Query: rawQuery}
return nil
}
func DelPreload(stream *Stream) error {
func DelPreload(name string) error {
preloadsMu.Lock()
defer preloadsMu.Unlock()
if cons := preloads[stream]; cons != nil {
stream.RemoveConsumer(cons)
delete(preloads, stream)
if p := preloads[name]; p != nil {
p.stream.RemoveConsumer(p.Cons)
delete(preloads, name)
return nil
}
return errors.New("streams: preload not found")
return fmt.Errorf("streams: preload not found: %s", name)
}
func GetPreloads() map[string]*Preload {
preloadsMu.Lock()
defer preloadsMu.Unlock()
return maps.Clone(preloads)
}
+22 -30
View File
@@ -3,7 +3,6 @@ package streams
import (
"errors"
"net/url"
"regexp"
"sync"
"time"
@@ -30,6 +29,7 @@ func Init() {
api.HandleFunc("api/streams", apiStreams)
api.HandleFunc("api/streams.dot", apiStreamsDOT)
api.HandleFunc("api/preload", apiPreload)
api.HandleFunc("api/schemes", apiSchemes)
if cfg.Publish == nil && cfg.Preload == nil {
return
@@ -43,27 +43,21 @@ func Init() {
}
}
for name, rawQuery := range cfg.Preload {
if stream := Get(name); stream != nil {
Preload(stream, rawQuery)
if err := AddPreload(name, rawQuery); err != nil {
log.Error().Err(err).Caller().Send()
}
}
})
}
var sanitize = regexp.MustCompile(`\s`)
// Validate - not allow creating dynamic streams with spaces in the source
func Validate(source string) error {
if sanitize.MatchString(source) {
return errors.New("streams: invalid dynamic source")
}
return nil
}
func New(name string, sources ...string) *Stream {
func New(name string, sources ...string) (*Stream, error) {
for _, source := range sources {
if Validate(source) != nil {
return nil
if !HasProducer(source) {
return nil, errors.New("streams: source not supported")
}
if err := Validate(source); err != nil {
return nil, err
}
}
@@ -73,10 +67,10 @@ func New(name string, sources ...string) *Stream {
streams[name] = stream
streamsMu.Unlock()
return stream
return stream, nil
}
func Patch(name string, source string) *Stream {
func Patch(name string, source string) (*Stream, error) {
streamsMu.Lock()
defer streamsMu.Unlock()
@@ -88,7 +82,7 @@ func Patch(name string, source string) *Stream {
// link (alias) streams[name] to streams[rtspName]
streams[name] = stream
}
return stream
return stream, nil
}
}
@@ -97,46 +91,44 @@ func Patch(name string, source string) *Stream {
// link (alias) streams[name] to streams[source]
streams[name] = stream
}
return stream
return stream, nil
}
// check if src has supported scheme
if !HasProducer(source) {
return nil
return nil, errors.New("streams: source not supported")
}
if Validate(source) != nil {
return nil
if err := Validate(source); err != nil {
return nil, err
}
// check an existing stream with this name
if stream, ok := streams[name]; ok {
stream.SetSource(source)
return stream
return stream, nil
}
// create new stream with this name
stream := NewStream(source)
streams[name] = stream
return stream
return stream, nil
}
func GetOrPatch(query url.Values) *Stream {
func GetOrPatch(query url.Values) (*Stream, error) {
// check if src param exists
source := query.Get("src")
if source == "" {
return nil
return nil, errors.New("streams: source empty")
}
// check if src is stream name
if stream := Get(source); stream != nil {
return stream
return stream, nil
}
// check if name param provided
if name := query.Get("name"); name != "" {
log.Info().Msgf("[streams] create new stream url=%s", source)
return Patch(name, source)
}
+39
View File
@@ -0,0 +1,39 @@
# Tuya
*[New in v1.9.13](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.13)*
[Tuya](https://www.tuya.com/) proprietary camera protocol with **two way audio** support. Go2rtc supports `Tuya Smart API` and `Tuya Cloud API`.
**Tuya Smart API (recommended)**:
- Cameras can be discovered through the go2rtc web interface via Tuya Smart account (Add > Tuya > Select region and fill in email and password > Login).
- **Smart Life accounts are not supported**, you need to create a Tuya Smart account. If the cameras are already added to the Smart Life app, you need to remove them and add them again to the Tuya Smart app.
**Tuya Cloud API**:
- Requires setting up a cloud project in the Tuya Developer Platform.
- Obtain `device_id`, `client_id`, `client_secret`, and `uid` from [Tuya IoT Platform](https://iot.tuya.com/). [Here's a guide](https://xzetsubou.github.io/hass-localtuya/cloud_api/).
- Please ensure that you have subscribed to the `IoT Video Live Stream` service (Free Trial) in the Tuya Developer Platform, otherwise the stream will not work (Tuya Developer Platform > Service API > Authorize > IoT Video Live Stream).
## Configuration
Use `resolution` parameter to select the stream (not all cameras support `hd` stream through WebRTC even if the camera has it):
- `hd` - HD stream (default)
- `sd` - SD stream
```yaml
streams:
# Tuya Smart API: WebRTC main stream (use Add > Tuya to discover the URL)
tuya_main:
- tuya://protect-us.ismartlife.me?device_id=XXX&email=XXX&password=XXX
# Tuya Smart API: WebRTC sub stream (use Add > Tuya to discover the URL)
tuya_sub:
- tuya://protect-us.ismartlife.me?device_id=XXX&email=XXX&password=XXX&resolution=sd
# Tuya Cloud API: WebRTC main stream
tuya_webrtc:
- tuya://openapi.tuyaus.com?device_id=XXX&uid=XXX&client_id=XXX&client_secret=XXX
# Tuya Cloud API: WebRTC sub stream
tuya_webrtc_sd:
- tuya://openapi.tuyaus.com?device_id=XXX&uid=XXX&client_id=XXX&client_secret=XXX&resolution=sd
```
+248
View File
@@ -0,0 +1,248 @@
package tuya
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/tuya"
)
func Init() {
streams.HandleFunc("tuya", func(source string) (core.Producer, error) {
return tuya.Dial(source)
})
api.HandleFunc("api/tuya", apiTuya)
}
func apiTuya(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
region := query.Get("region")
email := query.Get("email")
password := query.Get("password")
if email == "" || password == "" || region == "" {
http.Error(w, "email, password and region are required", http.StatusBadRequest)
return
}
var tuyaRegion *tuya.Region
for _, r := range tuya.AvailableRegions {
if r.Host == region {
tuyaRegion = &r
break
}
}
if tuyaRegion == nil {
http.Error(w, fmt.Sprintf("invalid region: %s", region), http.StatusBadRequest)
return
}
httpClient := tuya.CreateHTTPClientWithSession()
_, err := login(httpClient, tuyaRegion.Host, email, password, tuyaRegion.Continent)
if err != nil {
http.Error(w, fmt.Sprintf("login failed: %v", err), http.StatusInternalServerError)
return
}
tuyaAPI, err := tuya.NewTuyaSmartApiClient(
httpClient,
tuyaRegion.Host,
email,
password,
"",
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var devices []tuya.Device
homes, _ := tuyaAPI.GetHomeList()
if homes != nil && len(homes.Result) > 0 {
for _, home := range homes.Result {
roomList, err := tuyaAPI.GetRoomList(strconv.Itoa(home.Gid))
if err != nil {
continue
}
for _, room := range roomList.Result {
for _, device := range room.DeviceList {
if (device.Category == "sp" || device.Category == "dghsxj") && !containsDevice(devices, device.DeviceId) {
devices = append(devices, device)
}
}
}
}
}
sharedHomes, _ := tuyaAPI.GetSharedHomeList()
if sharedHomes != nil && len(sharedHomes.Result.SecurityWebCShareInfoList) > 0 {
for _, sharedHome := range sharedHomes.Result.SecurityWebCShareInfoList {
for _, device := range sharedHome.DeviceInfoList {
if (device.Category == "sp" || device.Category == "dghsxj") && !containsDevice(devices, device.DeviceId) {
devices = append(devices, device)
}
}
}
}
if len(devices) == 0 {
http.Error(w, "no cameras found", http.StatusNotFound)
return
}
var items []*api.Source
for _, device := range devices {
cleanQuery := url.Values{}
cleanQuery.Set("device_id", device.DeviceId)
cleanQuery.Set("email", email)
cleanQuery.Set("password", password)
url := fmt.Sprintf("tuya://%s?%s", tuyaRegion.Host, cleanQuery.Encode())
items = append(items, &api.Source{
Name: device.DeviceName,
URL: url,
})
}
api.ResponseSources(w, items)
}
func login(client *http.Client, serverHost, email, password, countryCode string) (*tuya.LoginResult, error) {
tokenResp, err := getLoginToken(client, serverHost, email, countryCode)
if err != nil {
return nil, err
}
encryptedPassword, err := tuya.EncryptPassword(password, tokenResp.Result.PbKey)
if err != nil {
return nil, fmt.Errorf("failed to encrypt password: %v", err)
}
var loginResp *tuya.PasswordLoginResponse
var url string
loginReq := tuya.PasswordLoginRequest{
CountryCode: countryCode,
Passwd: encryptedPassword,
Token: tokenResp.Result.Token,
IfEncrypt: 1,
Options: `{"group":1}`,
}
if tuya.IsEmailAddress(email) {
url = fmt.Sprintf("https://%s/api/private/email/login", serverHost)
loginReq.Email = email
} else {
url = fmt.Sprintf("https://%s/api/private/phone/login", serverHost)
loginReq.Mobile = email
}
loginResp, err = performLogin(client, url, loginReq, serverHost)
if err != nil {
return nil, err
}
if !loginResp.Success {
return nil, errors.New(loginResp.ErrorMsg)
}
return &loginResp.Result, nil
}
func getLoginToken(client *http.Client, serverHost, username, countryCode string) (*tuya.LoginTokenResponse, error) {
url := fmt.Sprintf("https://%s/api/login/token", serverHost)
tokenReq := tuya.LoginTokenRequest{
CountryCode: countryCode,
Username: username,
IsUid: false,
}
jsonData, err := json.Marshal(tokenReq)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Accept", "*/*")
req.Header.Set("Origin", fmt.Sprintf("https://%s", serverHost))
req.Header.Set("Referer", fmt.Sprintf("https://%s/login", serverHost))
req.Header.Set("X-Requested-With", "XMLHttpRequest")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var tokenResp tuya.LoginTokenResponse
if err = json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return nil, err
}
if !tokenResp.Success {
return nil, errors.New("tuya: " + tokenResp.Msg)
}
return &tokenResp, nil
}
func performLogin(client *http.Client, url string, loginReq tuya.PasswordLoginRequest, serverHost string) (*tuya.PasswordLoginResponse, error) {
jsonData, err := json.Marshal(loginReq)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Accept", "*/*")
req.Header.Set("Origin", fmt.Sprintf("https://%s", serverHost))
req.Header.Set("Referer", fmt.Sprintf("https://%s/login", serverHost))
req.Header.Set("X-Requested-With", "XMLHttpRequest")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var loginResp tuya.PasswordLoginResponse
if err := json.NewDecoder(resp.Body).Decode(&loginResp); err != nil {
return nil, err
}
return &loginResp, nil
}
func containsDevice(devices []tuya.Device, deviceID string) bool {
for _, device := range devices {
if device.DeviceId == deviceID {
return true
}
}
return false
}
+3 -1
View File
@@ -18,9 +18,11 @@ type Address struct {
Priority uint32
}
var stuns []string
func (a *Address) Host() string {
if a.host == "stun" {
ip, err := webrtc.GetCachedPublicIP()
ip, err := webrtc.GetCachedPublicIP(stuns...)
if err != nil {
return ""
}
+30 -5
View File
@@ -2,6 +2,7 @@ package webrtc
import (
"errors"
"net"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
@@ -26,24 +27,49 @@ func Init() {
cfg.Mod.Listen = ":8555"
cfg.Mod.IceServers = []pion.ICEServer{
{URLs: []string{"stun:stun.l.google.com:19302"}},
{URLs: []string{"stun:stun.cloudflare.com:3478", "stun:stun.l.google.com:19302"}},
}
app.LoadConfig(&cfg)
log = app.GetLogger("webrtc")
filters = cfg.Mod.Filters
if log.Debug().Enabled() {
itfs, _ := net.Interfaces()
for _, itf := range itfs {
addrs, _ := itf.Addrs()
log.Debug().Msgf("[webrtc] interface %+v addrs %v", itf, addrs)
}
}
address, network, _ := strings.Cut(cfg.Mod.Listen, "/")
for _, candidate := range cfg.Mod.Candidates {
AddCandidate(network, candidate)
if strings.HasPrefix(candidate, "stun:") && stuns == nil {
for _, ice := range cfg.Mod.IceServers {
for _, url := range ice.URLs {
if strings.HasPrefix(url, "stun:") {
stuns = append(stuns, url[5:])
}
}
}
}
}
webrtc.OnNewListener = func(ln any) {
switch ln := ln.(type) {
case *net.TCPListener:
log.Info().Stringer("addr", ln.Addr()).Msg("[webrtc] listen tcp")
case *net.UDPConn:
log.Info().Stringer("addr", ln.LocalAddr()).Msg("[webrtc] listen udp")
}
}
var err error
// create pionAPI with custom codecs list and custom network settings
serverAPI, err = webrtc.NewServerAPI(network, address, &filters)
serverAPI, err = webrtc.NewServerAPI(network, address, &cfg.Mod.Filters)
if err != nil {
log.Error().Err(err).Caller().Send()
return
@@ -53,7 +79,6 @@ func Init() {
clientAPI = serverAPI
if address != "" {
log.Info().Str("addr", cfg.Mod.Listen).Msg("[webrtc] listen")
clientAPI, _ = webrtc.NewAPI()
}
@@ -95,7 +120,7 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) (err error) {
query := tr.Request.URL.Query()
if name := query.Get("src"); name != "" {
stream = streams.GetOrPatch(query)
stream, _ = streams.GetOrPatch(query)
mode = core.ModePassiveConsumer
log.Debug().Str("src", name).Msg("[webrtc] new consumer")
} else if name = query.Get("dst"); name != "" {
+106
View File
@@ -0,0 +1,106 @@
# Wyze
This source allows you to stream from [Wyze](https://wyze.com/) cameras using native P2P protocol without the Wyze app or SDK.
**Important:**
1. **Requires Wyze account**. You need to login once via the WebUI to load your cameras.
2. **Requires firmware with DTLS**. Only cameras with DTLS-enabled firmware are supported.
3. Internet access is only needed when loading cameras from your account. After that, all streaming is local P2P.
4. Connection to the camera is local only (direct P2P to camera IP).
**Features:**
- H.264 and H.265 video codec support
- AAC, G.711, PCM, and Opus audio codec support
- Two-way audio (intercom) support
- Resolution switching (HD/SD)
## Setup
1. Get your API Key from [Wyze Developer Portal](https://support.wyze.com/hc/en-us/articles/16129834216731)
2. Go to go2rtc WebUI > Add > Wyze
3. Enter your API ID, API Key, email, and password
4. Select cameras to add - stream URLs are generated automatically
**Example Config**
```yaml
wyze:
user@email.com:
api_id: "your-api-id"
api_key: "your-api-key"
password: "yourpassword" # or MD5 triple-hash with "md5:" prefix
streams:
wyze_cam: wyze://192.168.1.123?uid=WYZEUID1234567890AB&enr=xxx&mac=AABBCCDDEEFF&model=HL_CAM4&dtls=true
```
## Stream URL Format
The stream URL is automatically generated when you add cameras via the WebUI:
```
wyze://[IP]?uid=[P2P_ID]&enr=[ENR]&mac=[MAC]&model=[MODEL]&subtype=[hd|sd]&dtls=true
```
| Parameter | Description |
|-----------|-------------|
| `IP` | Camera's local IP address |
| `uid` | P2P identifier (20 chars) |
| `enr` | Encryption key for DTLS |
| `mac` | Device MAC address |
| `model` | Camera model (e.g., HL_CAM4) |
| `dtls` | Enable DTLS encryption (default: true) |
| `subtype` | Camera resolution: `hd` or `sd` (default: `hd`) |
## Configuration
### Resolution
You can change the camera's resolution using the `subtype` parameter:
```yaml
streams:
wyze_hd: wyze://...&subtype=hd
wyze_sd: wyze://...&subtype=sd
```
### Two-Way Audio
Two-way audio (intercom) is supported automatically. When a consumer sends audio to the stream, it will be transmitted to the camera's speaker.
## Camera Compatibility
| Name | Model | Firmware | Protocol | Encryption | Codecs |
|------|-------|----------|----------|------------|--------|
| Wyze Cam v4 | HL_CAM4 | 4.52.9.4188 | TUTK | TransCode | h264, aac |
| | | 4.52.9.5332 | TUTK | HMAC-SHA1 | h264, aac |
| Wyze Cam v3 Pro | | | TUTK | | |
| Wyze Cam v3 | WYZE_CAKP2JFUS | 4.36.14.3497 | TUTK | TransCode | h264, pcm |
| Wyze Cam v2 | WYZEC1-JZ | 4.9.9.3006 | TUTK | TransCode | h264, pcmu |
| Wyze Cam v1 | | | TUTK | | |
| Wyze Cam Pan v4 | | | Gwell* | | |
| Wyze Cam Pan v3 | | | TUTK | | |
| Wyze Cam Pan v2 | | | TUTK | | |
| Wyze Cam Pan v1 | | | TUTK | | |
| Wyze Cam OG | | | Gwell* | | |
| Wyze Cam OG Telephoto | | | Gwell* | | |
| Wyze Cam OG (2025) | | | Gwell* | | |
| Wyze Cam Outdoor v2 | | | TUTK | | |
| Wyze Cam Outdoor v1 | | | TUTK | | |
| Wyze Cam Floodlight Pro | | | ? | | |
| Wyze Cam Floodlight v2 | | | TUTK | | |
| Wyze Cam Floodlight | | | TUTK | | |
| Wyze Video Doorbell v2 | HL_DB2 | 4.51.3.4992 | TUTK | TransCode | h264, pcm |
| Wyze Video Doorbell v1 | | | TUTK | | |
| Wyze Video Doorbell Pro | | | ? | | |
| Wyze Battery Video Doorbell | | | ? | | |
| Wyze Duo Cam Doorbell | | | ? | | |
| Wyze Battery Cam Pro | | | ? | | |
| Wyze Solar Cam Pan | | | ? | | |
| Wyze Duo Cam Pan | | | ? | | |
| Wyze Window Cam | | | ? | | |
| Wyze Bulb Cam | | | ? | | |
_* Gwell based protocols are not yet supported._
+202
View File
@@ -0,0 +1,202 @@
package wyze
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/wyze"
)
type AccountConfig struct {
APIKey string `yaml:"api_key"`
APIID string `yaml:"api_id"`
Password string `yaml:"password"`
}
var accounts map[string]AccountConfig
func Init() {
var v struct {
Cfg map[string]AccountConfig `yaml:"wyze"`
}
app.LoadConfig(&v)
accounts = v.Cfg
log := app.GetLogger("wyze")
streams.HandleFunc("wyze", func(rawURL string) (core.Producer, error) {
log.Debug().Msgf("wyze: dial %s", rawURL)
return wyze.NewProducer(rawURL)
})
api.HandleFunc("api/wyze", apiWyze)
}
func getCloud(email string) (*wyze.Cloud, error) {
cfg, ok := accounts[email]
if !ok {
return nil, fmt.Errorf("wyze: account not found: %s", email)
}
if cfg.APIKey == "" || cfg.APIID == "" {
return nil, fmt.Errorf("wyze: api_key and api_id required for account: %s", email)
}
cloud := wyze.NewCloud(cfg.APIKey, cfg.APIID)
if err := cloud.Login(email, cfg.Password); err != nil {
return nil, err
}
return cloud, nil
}
func apiWyze(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
apiDeviceList(w, r)
case "POST":
apiAuth(w, r)
}
}
func apiDeviceList(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
email := query.Get("id")
if email == "" {
accountList := make([]string, 0, len(accounts))
for id := range accounts {
accountList = append(accountList, id)
}
api.ResponseJSON(w, accountList)
return
}
err := func() error {
cloud, err := getCloud(email)
if err != nil {
return err
}
cameras, err := cloud.GetCameraList()
if err != nil {
return err
}
var items []*api.Source
for _, cam := range cameras {
items = append(items, &api.Source{
Name: cam.Nickname,
Info: fmt.Sprintf("%s | %s | %s", cam.ProductModel, cam.MAC, cam.IP),
URL: buildStreamURL(cam),
})
}
api.ResponseSources(w, items)
return nil
}()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func apiAuth(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
email := r.Form.Get("email")
password := r.Form.Get("password")
apiKey := r.Form.Get("api_key")
apiID := r.Form.Get("api_id")
if email == "" || password == "" || apiKey == "" || apiID == "" {
http.Error(w, "email, password, api_key and api_id required", http.StatusBadRequest)
return
}
// Try to login
cloud := wyze.NewCloud(apiKey, apiID)
if err := cloud.Login(email, password); err != nil {
// Check for MFA error
var authErr *wyze.AuthError
if ok := isAuthError(err, &authErr); ok {
w.Header().Set("Content-Type", api.MimeJSON)
w.WriteHeader(http.StatusUnauthorized)
_ = json.NewEncoder(w).Encode(authErr)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
cfg := map[string]string{
"password": password,
"api_key": apiKey,
"api_id": apiID,
}
if err := app.PatchConfig([]string{"wyze", email}, cfg); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if accounts == nil {
accounts = make(map[string]AccountConfig)
}
accounts[email] = AccountConfig{
APIKey: apiKey,
APIID: apiID,
Password: password,
}
cameras, err := cloud.GetCameraList()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var items []*api.Source
for _, cam := range cameras {
items = append(items, &api.Source{
Name: cam.Nickname,
Info: fmt.Sprintf("%s | %s | %s", cam.ProductModel, cam.MAC, cam.IP),
URL: buildStreamURL(cam),
})
}
api.ResponseSources(w, items)
}
func buildStreamURL(cam *wyze.Camera) string {
query := url.Values{}
query.Set("uid", cam.P2PID)
query.Set("enr", cam.ENR)
query.Set("mac", cam.MAC)
query.Set("model", cam.ProductModel)
if cam.DTLS == 1 {
query.Set("dtls", "true")
}
return fmt.Sprintf("wyze://%s?%s", cam.IP, query.Encode())
}
func isAuthError(err error, target **wyze.AuthError) bool {
if e, ok := err.(*wyze.AuthError); ok {
*target = e
return true
}
return false
}
+64
View File
@@ -0,0 +1,64 @@
# Xiaomi
**Added in v1.9.13. Improved in v1.9.14.**
This source allows you to view cameras from the [Xiaomi Mi Home](https://home.mi.com/) ecosystem.
Since 2020, Xiaomi has introduced a unified protocol for cameras called `miss`. I think it means **Mi Secure Streaming**. Until this point, the camera protocols were in chaos. Almost every model had different authorization, encryption, command lists, and media packet formats.
Go2rtc support two formats: `xiaomi/mess` and `xiaomi/legacy`.
And multiple P2P protocols: `cs2+udp`, `cs2+tcp`, several versions of `tutk+udp`.
Almost all cameras in the `xiaomi/mess` format and the `cs2` protocol work well.
Older `xiaomi/legacy` format cameras may have support issues.
The `tutk` protocol is the worst thing that's ever happened to the P2P world. It works terribly.
**Important:**
1. **Not all cameras are supported**. The list of supported cameras is collected in [this issue](https://github.com/AlexxIT/go2rtc/issues/1982).
2. Each time you connect to the camera, you need internet access to obtain encryption keys.
3. Connection to the camera is local only.
**Features:**
- Multiple Xiaomi accounts supported
- Cameras from multiple regions are supported for a single account
- Two-way audio is supported
- Cameras with multiple lenses are supported
## Setup
1. Goto go2rtc WebUI > Add > Xiaomi > Login with username and password
2. Receive verification code by email or phone if required.
3. Complete the captcha if required.
4. If everything is OK, your account will be added, and you can load cameras from it.
**Example**
```yaml
xiaomi:
1234567890: V1:***
streams:
xiaomi1: xiaomi://1234567890:cn@192.168.1.123?did=9876543210&model=isa.camera.hlc7
```
## Configuration
Quality in the `miss` protocol is specified by a number from 0 to 5. Usually 0 means auto, 1 - sd, 2 - hd.
Go2rtc by default sets quality to 2. But some new cameras have HD quality at number 3.
Old cameras may have broken codec settings at number 3, so this number should not be set for all cameras.
You can change camera's quality: `subtype=hd/sd/auto/0-5`.
```yaml
streams:
xiaomi1: xiaomi://***&subtype=sd
```
You can use second channel for Dual cameras: `channel=2`.
```yaml
streams:
xiaomi1: xiaomi://***&channel=2
```
+351
View File
@@ -0,0 +1,351 @@
package xiaomi
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/xiaomi"
"github.com/AlexxIT/go2rtc/pkg/xiaomi/crypto"
)
func Init() {
var v struct {
Cfg map[string]string `yaml:"xiaomi"`
}
app.LoadConfig(&v)
tokens = v.Cfg
log := app.GetLogger("xiaomi")
streams.HandleFunc("xiaomi", func(rawURL string) (core.Producer, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
if u.User != nil {
rawURL, err = getCameraURL(u)
if err != nil {
return nil, err
}
}
log.Debug().Msgf("xiaomi: dial %s", rawURL)
return xiaomi.Dial(rawURL)
})
api.HandleFunc("api/xiaomi", apiXiaomi)
}
var tokens map[string]string
var clouds map[string]*xiaomi.Cloud
var cloudsMu sync.Mutex
func getCloud(userID string) (*xiaomi.Cloud, error) {
cloudsMu.Lock()
defer cloudsMu.Unlock()
if cloud := clouds[userID]; cloud != nil {
return cloud, nil
}
cloud := xiaomi.NewCloud(AppXiaomiHome)
if err := cloud.LoginWithToken(userID, tokens[userID]); err != nil {
return nil, err
}
if clouds == nil {
clouds = map[string]*xiaomi.Cloud{userID: cloud}
} else {
clouds[userID] = cloud
}
return cloud, nil
}
func cloudRequest(userID, region, apiURL, params string) ([]byte, error) {
cloud, err := getCloud(userID)
if err != nil {
return nil, err
}
return cloud.Request(GetBaseURL(region), apiURL, params, nil)
}
func cloudUserRequest(user *url.Userinfo, apiURL, params string) ([]byte, error) {
userID := user.Username()
region, _ := user.Password()
return cloudRequest(userID, region, apiURL, params)
}
func getCameraURL(url *url.URL) (string, error) {
model := url.Query().Get("model")
// It is not known which models need to be awakened.
// Probably all the doorbells and all the battery cameras.
if strings.Contains(model, ".cateye.") {
_ = wakeUpCamera(url)
}
// The getMissURL request has a fallback to getP2PURL.
// But for known models we can save one request to the cloud.
if xiaomi.IsLegacy(model) {
return getLegacyURL(url)
}
return getMissURL(url)
}
func getLegacyURL(url *url.URL) (string, error) {
query := url.Query()
clientPublic, clientPrivate, err := crypto.GenerateKey()
if err != nil {
return "", err
}
params := fmt.Sprintf(`{"did":"%s","toSignAppData":"%x"}`, query.Get("did"), clientPublic)
userID := url.User.Username()
region, _ := url.User.Password()
res, err := cloudRequest(userID, region, "/device/devicepass", params)
if err != nil {
return "", err
}
var v struct {
UID string `json:"p2p_id"`
Password string `json:"password"`
PublicKey string `json:"p2p_dev_public_key"`
Sign string `json:"signForAppData"`
}
if err = json.Unmarshal(res, &v); err != nil {
return "", err
}
query.Set("uid", v.UID)
if v.Sign != "" {
query.Set("client_public", hex.EncodeToString(clientPublic))
query.Set("client_private", hex.EncodeToString(clientPrivate))
query.Set("device_public", v.PublicKey)
query.Set("sign", v.Sign)
} else {
query.Set("password", v.Password)
}
url.RawQuery = query.Encode()
return url.String(), nil
}
func getMissURL(url *url.URL) (string, error) {
clientPublic, clientPrivate, err := crypto.GenerateKey()
if err != nil {
return "", err
}
query := url.Query()
params := fmt.Sprintf(
`{"app_pubkey":"%x","did":"%s","support_vendors":"TUTK_CS2_MTP"}`,
clientPublic, query.Get("did"),
)
res, err := cloudUserRequest(url.User, "/v2/device/miss_get_vendor", params)
if err != nil {
if strings.Contains(err.Error(), "no available vendor support") {
return getLegacyURL(url)
}
return "", err
}
var v struct {
Vendor struct {
ID byte `json:"vendor"`
Params struct {
UID string `json:"p2p_id"`
} `json:"vendor_params"`
} `json:"vendor"`
PublicKey string `json:"public_key"`
Sign string `json:"sign"`
}
if err = json.Unmarshal(res, &v); err != nil {
return "", err
}
query.Set("client_public", hex.EncodeToString(clientPublic))
query.Set("client_private", hex.EncodeToString(clientPrivate))
query.Set("device_public", v.PublicKey)
query.Set("sign", v.Sign)
query.Set("vendor", getVendorName(v.Vendor.ID))
if v.Vendor.ID == 1 {
query.Set("uid", v.Vendor.Params.UID)
}
url.RawQuery = query.Encode()
return url.String(), nil
}
func getVendorName(i byte) string {
switch i {
case 1:
return "tutk"
case 3:
return "agora"
case 4:
return "cs2"
case 6:
return "mtp"
}
return fmt.Sprintf("%d", i)
}
func wakeUpCamera(url *url.URL) error {
const params = `{"id":1,"method":"wakeup","params":{"video":"1"}}`
did := url.Query().Get("did")
_, err := cloudUserRequest(url.User, "/home/rpc/"+did, params)
return err
}
func apiXiaomi(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
apiDeviceList(w, r)
case "POST":
apiAuth(w, r)
}
}
func apiDeviceList(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
user := query.Get("id")
if user == "" {
cloudsMu.Lock()
users := make([]string, 0, len(tokens))
for s := range tokens {
users = append(users, s)
}
cloudsMu.Unlock()
api.ResponseJSON(w, users)
return
}
err := func() error {
region := query.Get("region")
res, err := cloudRequest(user, region, "/v2/home/device_list_page", "{}")
if err != nil {
return err
}
var v struct {
List []*Device `json:"list"`
}
if err = json.Unmarshal(res, &v); err != nil {
return err
}
var items []*api.Source
for _, device := range v.List {
if !strings.Contains(device.Model, ".camera.") && !strings.Contains(device.Model, ".cateye.") {
continue
}
items = append(items, &api.Source{
Name: device.Name,
Info: fmt.Sprintf("ip: %s, mac: %s", device.IP, device.MAC),
URL: fmt.Sprintf("xiaomi://%s:%s@%s?did=%s&model=%s", user, region, device.IP, device.Did, device.Model),
})
}
api.ResponseSources(w, items)
return nil
}()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
type Device struct {
Did string `json:"did"`
Name string `json:"name"`
Model string `json:"model"`
MAC string `json:"mac"`
IP string `json:"localip"`
}
var auth *xiaomi.Cloud
func apiAuth(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
username := r.Form.Get("username")
password := r.Form.Get("password")
captcha := r.Form.Get("captcha")
verify := r.Form.Get("verify")
var err error
switch {
case username != "" || password != "":
auth = xiaomi.NewCloud(AppXiaomiHome)
err = auth.Login(username, password)
case captcha != "":
err = auth.LoginWithCaptcha(captcha)
case verify != "":
err = auth.LoginWithVerify(verify)
default:
http.Error(w, "wrong request", http.StatusBadRequest)
return
}
if err == nil {
userID, token := auth.UserToken()
auth = nil
cloudsMu.Lock()
if tokens == nil {
tokens = map[string]string{userID: token}
} else {
tokens[userID] = token
}
cloudsMu.Unlock()
err = app.PatchConfig([]string{"xiaomi", userID}, token)
}
if err != nil {
var login *xiaomi.LoginError
if errors.As(err, &login) {
w.Header().Set("Content-Type", api.MimeJSON)
w.WriteHeader(http.StatusUnauthorized)
_ = json.NewEncoder(w).Encode(err)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
const AppXiaomiHome = "xiaomiio"
func GetBaseURL(region string) string {
switch region {
case "de", "i2", "ru", "sg", "us":
return "https://" + region + ".api.io.mi.com/app"
}
return "https://api.io.mi.com/app"
}
+71 -59
View File
@@ -1,6 +1,8 @@
package main
import (
"slices"
"github.com/AlexxIT/go2rtc/internal/alsa"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
@@ -25,9 +27,11 @@ import (
"github.com/AlexxIT/go2rtc/internal/mjpeg"
"github.com/AlexxIT/go2rtc/internal/mp4"
"github.com/AlexxIT/go2rtc/internal/mpegts"
"github.com/AlexxIT/go2rtc/internal/multitrans"
"github.com/AlexxIT/go2rtc/internal/nest"
"github.com/AlexxIT/go2rtc/internal/ngrok"
"github.com/AlexxIT/go2rtc/internal/onvif"
"github.com/AlexxIT/go2rtc/internal/pinggy"
"github.com/AlexxIT/go2rtc/internal/ring"
"github.com/AlexxIT/go2rtc/internal/roborock"
"github.com/AlexxIT/go2rtc/internal/rtmp"
@@ -35,77 +39,85 @@ import (
"github.com/AlexxIT/go2rtc/internal/srtp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/tapo"
"github.com/AlexxIT/go2rtc/internal/tuya"
"github.com/AlexxIT/go2rtc/internal/v4l2"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"github.com/AlexxIT/go2rtc/internal/webtorrent"
"github.com/AlexxIT/go2rtc/internal/wyoming"
"github.com/AlexxIT/go2rtc/internal/wyze"
"github.com/AlexxIT/go2rtc/internal/xiaomi"
"github.com/AlexxIT/go2rtc/internal/yandex"
"github.com/AlexxIT/go2rtc/pkg/shell"
)
func main() {
app.Version = "1.9.11"
// version will be set later from -buildvcs info, this used only as fallback
app.Version = "1.9.14"
// 1. Core modules: app, api/ws, streams
type module struct {
name string
init func()
}
app.Init() // init config and logs
modules := []module{
{"", app.Init}, // init config and logs
{"api", api.Init}, // init API before all others
{"ws", ws.Init}, // init WS API endpoint
{"", streams.Init},
// Main sources and servers
{"http", http.Init}, // rtsp source, HTTP server
{"rtsp", rtsp.Init}, // rtsp source, RTSP server
{"webrtc", webrtc.Init}, // webrtc source, WebRTC server
// Main API
{"mp4", mp4.Init}, // MP4 API
{"hls", hls.Init}, // HLS API
{"mjpeg", mjpeg.Init}, // MJPEG API
// Other sources and servers
{"hass", hass.Init}, // hass source, Hass API server
{"homekit", homekit.Init}, // homekit source, HomeKit server
{"onvif", onvif.Init}, // onvif source, ONVIF API server
{"rtmp", rtmp.Init}, // rtmp source, RTMP server
{"webtorrent", webtorrent.Init}, // webtorrent source, WebTorrent module
{"wyoming", wyoming.Init},
// Exec and script sources
{"echo", echo.Init},
{"exec", exec.Init},
{"expr", expr.Init},
{"ffmpeg", ffmpeg.Init},
// Hardware sources
{"alsa", alsa.Init},
{"v4l2", v4l2.Init},
// Other sources
{"bubble", bubble.Init},
{"doorbird", doorbird.Init},
{"dvrip", dvrip.Init},
{"eseecloud", eseecloud.Init},
{"flussonic", flussonic.Init},
{"gopro", gopro.Init},
{"isapi", isapi.Init},
{"ivideon", ivideon.Init},
{"mpegts", mpegts.Init},
{"multitrans", multitrans.Init},
{"nest", nest.Init},
{"ring", ring.Init},
{"roborock", roborock.Init},
{"tapo", tapo.Init},
{"tuya", tuya.Init},
{"wyze", wyze.Init},
{"xiaomi", xiaomi.Init},
{"yandex", yandex.Init},
// Helper modules
{"debug", debug.Init},
{"ngrok", ngrok.Init},
{"pinggy", pinggy.Init},
{"srtp", srtp.Init},
}
api.Init() // init API before all others
ws.Init() // init WS API endpoint
streams.Init() // streams module
// 2. Main sources and servers
rtsp.Init() // rtsp source, RTSP server
webrtc.Init() // webrtc source, WebRTC server
// 3. Main API
mp4.Init() // MP4 API
hls.Init() // HLS API
mjpeg.Init() // MJPEG API
// 4. Other sources and servers
hass.Init() // hass source, Hass API server
onvif.Init() // onvif source, ONVIF API server
webtorrent.Init() // webtorrent source, WebTorrent module
wyoming.Init()
// 5. Other sources
rtmp.Init() // rtmp source
exec.Init() // exec source
ffmpeg.Init() // ffmpeg source
echo.Init() // echo source
ivideon.Init() // ivideon source
http.Init() // http/tcp source
dvrip.Init() // dvrip source
tapo.Init() // tapo source
isapi.Init() // isapi source
mpegts.Init() // mpegts passive source
roborock.Init() // roborock source
homekit.Init() // homekit source
ring.Init() // ring source
nest.Init() // nest source
bubble.Init() // bubble source
expr.Init() // expr source
gopro.Init() // gopro source
doorbird.Init() // doorbird source
v4l2.Init() // v4l2 source
alsa.Init() // alsa source
flussonic.Init()
eseecloud.Init()
yandex.Init()
// 6. Helper modules
ngrok.Init() // ngrok module
srtp.Init() // SRTP server
debug.Init() // debug API
// 7. Go
for _, m := range modules {
if app.Modules == nil || m.name == "" || slices.Contains(app.Modules, m.name) {
m.init()
}
}
shell.RunUntilSignal()
}
+60 -46
View File
@@ -10,35 +10,49 @@ Some formats and protocols go2rtc supports exclusively. They have no equivalent
- Codecs can be incoming - **Recevers codecs**
- Codecs can be outgoing (two way audio) - **Senders codecs**
| Format | Source protocols | Ingress protocols | Recevers codecs | Senders codecs | Example |
|--------------|------------------|-------------------|------------------------------|--------------------|---------------|
| adts | http,tcp,pipe | http | aac | | `http:` |
| alsa | pipe | | | pcm | `alsa:` |
| bubble | http | | h264,hevc,pcm_alaw | | `bubble:` |
| dvrip | tcp | | h264,hevc,pcm_alaw,pcm_mulaw | pcm_alaw | `dvrip:` |
| flv | http,tcp,pipe | http | h264,aac | | `http:` |
| gopro | http+udp | | TODO | | `gopro:` |
| hass/webrtc | ws+udp,tcp | | TODO | | `hass:` |
| hls/mpegts | http | | h264,h265,aac,opus | | `http:` |
| homekit | homekit+udp | | h264,eld* | | `homekit:` |
| isapi | http | | | pcm_alaw,pcm_mulaw | `isapi:` |
| ivideon | ws | | h264 | | `ivideon:` |
| kasa | http | | h264,pcm_mulaw | | `kasa:` |
| h264 | http,tcp,pipe | http | h264 | | `http:` |
| hevc | http,tcp,pipe | http | hevc | | `http:` |
| mjpeg | http,tcp,pipe | http | mjpeg | | `http:` |
| mpjpeg | http,tcp,pipe | http | mjpeg | | `http:` |
| mpegts | http,tcp,pipe | http | h264,hevc,aac,opus | | `http:` |
| nest/webrtc | http+udp | | TODO | | `nest:` |
| roborock | mqtt+udp | | h264,opus | opus | `roborock:` |
| rtmp | rtmp | rtmp | h264,aac | | `rtmp:` |
| rtsp | rtsp+tcp,ws | rtsp+tcp | h264,hevc,aac,pcm*,opus | pcm*,opus | `rtsp:` |
| stdin | pipe | | | pcm_alaw,pcm_mulaw | `stdin:` |
| tapo | http | | h264,pcma | pcm_alaw | `tapo:` |
| wav | http,tcp,pipe | http | pcm_alaw,pcm_mulaw | | `http:` |
| webrtc* | TODO | TODO | h264,pcm_alaw,pcm_mulaw,opus | pcm_alaw,pcm_mulaw | `webrtc:` |
| webtorrent | TODO | TODO | TODO | TODO | `webtorrent:` |
| yuv4mpegpipe | http,tcp,pipe | http | rawvideo | | `http:` |
| Group | Format | Protocols | Ingress | Recevers codecs | Senders codecs | Example |
|------------|--------------|-----------------|---------|---------------------------------|---------------------|---------------|
| Devices | alsa | pipe | | | pcm | `alsa:` |
| Devices | v4l2 | pipe | | | | `v4l2:` |
| Files | adts | http, tcp, pipe | http | aac | | `http:` |
| Files | flv | http, tcp, pipe | http | h264, aac | | `http:` |
| Files | h264 | http, tcp, pipe | http | h264 | | `http:` |
| Files | hevc | http, tcp, pipe | http | hevc | | `http:` |
| Files | hls | http | | h264, h265, aac, opus | | `http:` |
| Files | mjpeg | http, tcp, pipe | http | mjpeg | | `http:` |
| Files | mpegts | http, tcp, pipe | http | h264, hevc, aac, opus | | `http:` |
| Files | wav | http, tcp, pipe | http | pcm_alaw, pcm_mulaw | | `http:` |
| Net (pub) | mpjpeg | http, tcp, pipe | http | mjpeg | | `http:` |
| Net (pub) | onvif | rtsp | | | | `onvif:` |
| Net (pub) | rtmp | rtmp | rtmp | h264, aac | | `rtmp:` |
| Net (pub) | rtsp | rtsp, ws | rtsp | h264, hevc, aac, pcm*, opus | pcm*, opus | `rtsp:` |
| Net (pub) | webrtc* | webrtc | webrtc | h264, pcm_alaw, pcm_mulaw, opus | pcm_alaw, pcm_mulaw | `webrtc:` |
| Net (pub) | yuv4mpegpipe | http, tcp, pipe | http | rawvideo | | `http:` |
| Net (priv) | bubble | http | | h264, hevc, pcm_alaw | | `bubble:` |
| Net (priv) | doorbird | http | | | | `doorbird:` |
| Net (priv) | dvrip | tcp | | h264, hevc, pcm_alaw, pcm_mulaw | pcm_alaw | `dvrip:` |
| Net (priv) | eseecloud | http | | | | `eseecloud:` |
| Net (priv) | gopro | udp | | TODO | | `gopro:` |
| Net (priv) | hass | webrtc | | TODO | | `hass:` |
| Net (priv) | homekit | hap | | h264, eld* | | `homekit:` |
| Net (priv) | isapi | http | | | pcm_alaw, pcm_mulaw | `isapi:` |
| Net (priv) | kasa | http | | h264, pcm_mulaw | | `kasa:` |
| Net (priv) | nest | rtsp, webrtc | | TODO | | `nest:` |
| Net (priv) | ring | webrtc | | | | `ring:` |
| Net (priv) | roborock | webrtc | | h264, opus | opus | `roborock:` |
| Net (priv) | tapo | http | | h264, pcma | pcm_alaw | `tapo:` |
| Net (priv) | tuya | webrtc | | | | `tuya:` |
| Net (priv) | vigi | http | | | | `vigi:` |
| Net (priv) | webtorrent | webrtc | TODO | TODO | TODO | `webtorrent:` |
| Net (priv) | xiaomi* | cs2, tutk | | | | `xiaomi:` |
| Services | flussonic | ws | | | | `flussonic:` |
| Services | ivideon | ws | | h264 | | `ivideon:` |
| Services | yandex | webrtc | | | | `yandex:` |
| Other | echo | * | | | | `echo:` |
| Other | exec | pipe, rtsp | | | | `exec:` |
| Other | expr | * | | | | `expr:` |
| Other | ffmpeg | pipe, rtsp | | | | `ffmpeg:` |
| Other | stdin | pipe | | | pcm_alaw, pcm_mulaw | `stdin:` |
- **eld** - rare variant of aac codec
- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le
@@ -46,23 +60,23 @@ Some formats and protocols go2rtc supports exclusively. They have no equivalent
## Consumers (output)
| Format | Protocol | Send codecs | Recv codecs | Example |
|--------------|-------------|------------------------------|-------------------------|---------------------------------------|
| adts | http | aac | | `GET /api/stream.adts` |
| ascii | http | mjpeg | | `GET /api/stream.ascii` |
| flv | http | h264,aac | | `GET /api/stream.flv` |
| hls/mpegts | http | h264,hevc,aac | | `GET /api/stream.m3u8` |
| hls/fmp4 | http | h264,hevc,aac,pcm*,opus | | `GET /api/stream.m3u8?mp4` |
| homekit | homekit+udp | h264,opus | | Apple HomeKit app |
| mjpeg | ws | mjpeg | | `{"type":"mjpeg"}` -> `/api/ws` |
| mpjpeg | http | mjpeg | | `GET /api/stream.mjpeg` |
| mp4 | http | h264,hevc,aac,pcm*,opus | | `GET /api/stream.mp4` |
| mse/fmp4 | ws | h264,hevc,aac,pcm*,opus | | `{"type":"mse"}` -> `/api/ws` |
| mpegts | http | h264,hevc,aac | | `GET /api/stream.ts` |
| rtmp | rtmp | h264,aac | | `rtmp://localhost:1935/{stream_name}` |
| rtsp | rtsp+tcp | h264,hevc,aac,pcm*,opus | | `rtsp://localhost:8554/{stream_name}` |
| webrtc | TODO | h264,pcm_alaw,pcm_mulaw,opus | pcm_alaw,pcm_mulaw,opus | `{"type":"webrtc"}` -> `/api/ws` |
| yuv4mpegpipe | http | rawvideo | | `GET /api/stream.y4m` |
| Format | Protocol | Send codecs | Recv codecs | Example |
|--------------|----------|---------------------------------|---------------------------|---------------------------------------|
| adts | http | aac | | `GET /api/stream.adts` |
| ascii | http | mjpeg | | `GET /api/stream.ascii` |
| flv | http | h264, aac | | `GET /api/stream.flv` |
| hls/mpegts | http | h264, hevc, aac | | `GET /api/stream.m3u8` |
| hls/fmp4 | http | h264, hevc, aac, pcm*, opus | | `GET /api/stream.m3u8?mp4` |
| homekit | hap | h264, opus | | Apple HomeKit app |
| mjpeg | ws | mjpeg | | `{"type":"mjpeg"}` -> `/api/ws` |
| mpjpeg | http | mjpeg | | `GET /api/stream.mjpeg` |
| mp4 | http | h264, hevc, aac, pcm*, opus | | `GET /api/stream.mp4` |
| mse/fmp4 | ws | h264, hevc, aac, pcm*, opus | | `{"type":"mse"}` -> `/api/ws` |
| mpegts | http | h264, hevc, aac | | `GET /api/stream.ts` |
| rtmp | rtmp | h264, aac | | `rtmp://localhost:1935/{stream_name}` |
| rtsp | rtsp | h264, hevc, aac, pcm*, opus | | `rtsp://localhost:8554/{stream_name}` |
| webrtc | webrtc | h264, pcm_alaw, pcm_mulaw, opus | pcm_alaw, pcm_mulaw, opus | `{"type":"webrtc"}` -> `/api/ws` |
| yuv4mpegpipe | http | rawvideo | | `GET /api/stream.y4m` |
- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le
+20 -2
View File
@@ -8,8 +8,26 @@ import (
"github.com/pion/rtp"
)
const ADTSHeaderSize = 7
func ADTSHeaderLen(b []byte) int {
if HasCRC(b) {
return 9 // 7 bytes header + 2 bytes CRC
}
return ADTSHeaderSize
}
func IsADTS(b []byte) bool {
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF6 == 0xF0
// AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)
// A 12 Syncword, all bits must be set to 1.
// C 2 Layer, always set to 0.
return len(b) >= ADTSHeaderSize && b[0] == 0xFF && b[1]&0b1111_0110 == 0xF0
}
func HasCRC(b []byte) bool {
// AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)
// D 1 Protection absence, set to 1 if there is no CRC and 0 if there is CRC.
return b[1]&0b1 == 0
}
func ADTSToCodec(b []byte) *core.Codec {
@@ -58,7 +76,7 @@ func ADTSToCodec(b []byte) *core.Codec {
func ReadADTSSize(b []byte) uint16 {
// AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)
_ = b[5] // bounds
return uint16(b[3]&0x03)<<(8+3) | uint16(b[4])<<3 | uint16(b[5]>>5)
return uint16(b[3]&0b11)<<11 | uint16(b[4])<<3 | uint16(b[5]>>5)
}
func WriteADTSSize(b []byte, size uint16) {
+25 -11
View File
@@ -2,7 +2,7 @@ package aac
import (
"bufio"
"encoding/binary"
"errors"
"io"
"github.com/AlexxIT/go2rtc/pkg/core"
@@ -17,16 +17,22 @@ type Producer struct {
func Open(r io.Reader) (*Producer, error) {
rd := bufio.NewReader(r)
b, err := rd.Peek(8)
b, err := rd.Peek(ADTSHeaderSize)
if err != nil {
return nil, err
}
codec := ADTSToCodec(b)
if codec == nil {
return nil, errors.New("adts: wrong header")
}
codec.PayloadType = core.PayloadTypeRAW
medias := []*core.Media{
{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{ADTSToCodec(b)},
Codecs: []*core.Codec{codec},
},
}
return &Producer{
@@ -42,14 +48,25 @@ func Open(r io.Reader) (*Producer, error) {
func (c *Producer) Start() error {
for {
b, err := c.rd.Peek(6)
if err != nil {
// read ADTS header
adts := make([]byte, ADTSHeaderSize)
if _, err := io.ReadFull(c.rd, adts); err != nil {
return err
}
auSize := ReadADTSSize(b)
payload := make([]byte, 2+2+auSize)
if _, err = io.ReadFull(c.rd, payload[4:]); err != nil {
auSize := ReadADTSSize(adts) - ADTSHeaderSize
if HasCRC(adts) {
// skip CRC after header
if _, err := c.rd.Discard(2); err != nil {
return err
}
auSize -= 2
}
// read AAC payload after header
payload := make([]byte, auSize)
if _, err := io.ReadFull(c.rd, payload); err != nil {
return err
}
@@ -59,9 +76,6 @@ func (c *Producer) Start() error {
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,
+7 -4
View File
@@ -8,7 +8,6 @@ import (
)
const RTPPacketVersionAAC = 0
const ADTSHeaderSize = 7
func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
var timestamp uint32
@@ -65,7 +64,8 @@ func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
}
func RTPPay(handler core.HandlerFunc) core.HandlerFunc {
sequencer := rtp.NewRandomSequencer()
var seq uint16
var ts uint32
return func(packet *rtp.Packet) {
if packet.Version != RTPPacketVersionAAC {
@@ -85,12 +85,15 @@ func RTPPay(handler core.HandlerFunc) core.HandlerFunc {
Header: rtp.Header{
Version: 2,
Marker: true,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
SequenceNumber: seq,
Timestamp: ts,
},
Payload: payload,
}
handler(&clone)
seq++
ts += AUTime
}
}
+3 -3
View File
@@ -259,9 +259,9 @@ func ParseCodecString(s string) *Codec {
codec.Name = CodecPCM
case "pcm_s16le", "s16le", "pcml":
codec.Name = CodecPCML
case "pcm_alaw", "alaw", "pcma":
case "pcm_alaw", "alaw", "pcma", "g711a":
codec.Name = CodecPCMA
case "pcm_mulaw", "mulaw", "pcmu":
case "pcm_mulaw", "mulaw", "pcmu", "g711u":
codec.Name = CodecPCMU
case "aac", "mpeg4-generic":
codec.Name = CodecAAC
@@ -277,7 +277,7 @@ func ParseCodecString(s string) *Codec {
codec.ClockRate = uint32(Atoi(ss[1]))
}
if len(ss) >= 3 {
codec.Channels = uint8(Atoi(ss[1]))
codec.Channels = uint8(Atoi(ss[2]))
}
return &codec
+18 -3
View File
@@ -11,9 +11,9 @@ import (
const (
BufferSize = 64 * 1024 // 64K
ConnDialTimeout = time.Second * 3
ConnDeadline = time.Second * 5
ProbeTimeout = time.Second * 3
ConnDialTimeout = 5 * time.Second
ConnDeadline = 5 * time.Second
ProbeTimeout = 5 * time.Second
)
// Now90000 - timestamp for Video (clock rate = 90000 samples per second)
@@ -67,6 +67,21 @@ func Atoi(s string) (i int) {
return
}
// ParseByte - fast parsing string to byte function
func ParseByte(s string) (b byte) {
for i, ch := range []byte(s) {
ch -= '0'
if ch > 9 {
return 0
}
if i > 0 {
b *= 10
}
b += ch
}
return
}
func Assert(ok bool) {
if !ok {
_, file, line, _ := runtime.Caller(1)
+47
View File
@@ -0,0 +1,47 @@
package debug
import (
"bytes"
"math/rand"
"net"
)
type badConn struct {
net.Conn
delay int
buf []byte
}
func NewBadConn(conn net.Conn) net.Conn {
return &badConn{Conn: conn}
}
const (
missChance = 0.05
delayChance = 0.1
)
func (c *badConn) Read(b []byte) (n int, err error) {
if rand.Float32() < missChance {
if _, err = c.Conn.Read(b); err != nil {
return
}
//log.Printf("bad conn: miss")
}
if c.delay > 0 {
if c.delay--; c.delay == 0 {
n = copy(b, c.buf)
return
}
} else if rand.Float32() < delayChance {
if n, err = c.Conn.Read(b); err != nil {
return
}
c.delay = 1 + rand.Intn(5)
c.buf = bytes.Clone(b[:n])
//log.Printf("bad conn: delay %d", c.delay)
}
return c.Conn.Read(b)
}
+108 -73
View File
@@ -1,40 +1,78 @@
package expr
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"regexp"
"strings"
"time"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
)
func newRequest(method, url string, headers map[string]any, body string) (*http.Request, error) {
func newRequest(rawURL string, options map[string]any) (*http.Request, error) {
var method, contentType string
var rd io.Reader
if method == "" {
// method from js fetch
if s, ok := options["method"].(string); ok {
method = s
} else {
method = "GET"
}
if body != "" {
rd = strings.NewReader(body)
// params key from python requests
if kv, ok := options["params"].(map[string]any); ok {
rawURL += "?" + url.Values(kvToString(kv)).Encode()
}
req, err := http.NewRequest(method, url, rd)
// json key from python requests
// data key from python requests
// body key from js fetch
if v, ok := options["json"]; ok {
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
contentType = "application/json"
rd = bytes.NewReader(b)
} else if kv, ok := options["data"].(map[string]any); ok {
contentType = "application/x-www-form-urlencoded"
rd = strings.NewReader(url.Values(kvToString(kv)).Encode())
} else if s, ok := options["body"].(string); ok {
rd = strings.NewReader(s)
}
req, err := http.NewRequest(method, rawURL, rd)
if err != nil {
return nil, err
}
for k, v := range headers {
req.Header.Set(k, fmt.Sprintf("%v", v))
if kv, ok := options["headers"].(map[string]any); ok {
req.Header = kvToString(kv)
}
if contentType != "" && req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", contentType)
}
return req, nil
}
func kvToString(kv map[string]any) map[string][]string {
dst := make(map[string][]string, len(kv))
for k, v := range kv {
dst[k] = []string{fmt.Sprintf("%v", v)}
}
return dst
}
func regExp(params ...any) (*regexp.Regexp, error) {
exp := params[0].(string)
if len(params) >= 2 {
@@ -49,72 +87,69 @@ func regExp(params ...any) (*regexp.Regexp, error) {
return regexp.Compile(exp)
}
var Options = []expr.Option{
expr.Function(
"fetch",
func(params ...any) (any, error) {
var req *http.Request
var err error
url := params[0].(string)
if len(params) == 2 {
options := params[1].(map[string]any)
method, _ := options["method"].(string)
headers, _ := options["headers"].(map[string]any)
body, _ := options["body"].(string)
req, err = newRequest(method, url, headers, body)
} else {
req, err = http.NewRequest("GET", url, nil)
}
if err != nil {
return nil, err
}
res, err := tcp.Do(req)
if err != nil {
return nil, err
}
b, _ := io.ReadAll(res.Body)
return map[string]any{
"ok": res.StatusCode < 400,
"status": res.Status,
"text": string(b),
"json": func() (v any) {
_ = json.Unmarshal(b, &v)
return
},
}, nil
},
//new(func(url string) map[string]any),
//new(func(url string, options map[string]any) map[string]any),
),
expr.Function(
"match",
func(params ...any) (any, error) {
re, err := regExp(params[1:]...)
if err != nil {
return nil, err
}
str := params[0].(string)
return re.FindStringSubmatch(str), nil
},
//new(func(str, expr string) []string),
//new(func(str, expr, flags string) []string),
),
expr.Function(
"RegExp",
func(params ...any) (any, error) {
return regExp(params)
},
),
}
func Compile(input string) (*vm.Program, error) {
return expr.Compile(input, Options...)
// support http sessions
jar, _ := cookiejar.New(nil)
client := http.Client{
Jar: jar,
Timeout: 5 * time.Second,
}
return expr.Compile(
input,
expr.Function(
"fetch",
func(params ...any) (any, error) {
var req *http.Request
var err error
rawURL := params[0].(string)
if len(params) == 2 {
options := params[1].(map[string]any)
req, err = newRequest(rawURL, options)
} else {
req, err = http.NewRequest("GET", rawURL, nil)
}
if err != nil {
return nil, err
}
res, err := client.Do(req)
if err != nil {
return nil, err
}
b, _ := io.ReadAll(res.Body)
return map[string]any{
"ok": res.StatusCode < 400,
"status": res.Status,
"text": string(b),
"json": func() (v any) {
_ = json.Unmarshal(b, &v)
return
},
}, nil
},
//new(func(url string) map[string]any),
//new(func(url string, options map[string]any) map[string]any),
),
expr.Function(
"match",
func(params ...any) (any, error) {
re, err := regExp(params[1:]...)
if err != nil {
return nil, err
}
str := params[0].(string)
return re.FindStringSubmatch(str), nil
},
//new(func(str, expr string) []string),
//new(func(str, expr, flags string) []string),
),
)
}
func Eval(input string, env any) (any, error) {
+21
View File
@@ -0,0 +1,21 @@
package flv
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestTimeToRTP(t *testing.T) {
// Reolink camera has 20 FPS
// Video timestamp increases by 50ms, SampleRate 90000, RTP timestamp increases by 4500
// Audio timestamp increases by 64ms, SampleRate 16000, RTP timestamp increases by 1024
frameN := 1
for i := 0; i < 32; i++ {
// 1000ms/(90000/4500) = 50ms
require.Equal(t, uint32(frameN*4500), TimeToRTP(uint32(frameN*50), 90000))
// 1000ms/(16000/1024) = 64ms
require.Equal(t, uint32(frameN*1024), TimeToRTP(uint32(frameN*64), 16000))
frameN *= 2
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ func (m *Muxer) GetInit() []byte {
switch codec.Name {
case core.CodecH264:
b[4] |= FlagsVideo
obj["videocodecid"] = CodecAVC
obj["videocodecid"] = CodecH264
case core.CodecAAC:
b[4] |= FlagsAudio
+17 -8
View File
@@ -44,7 +44,9 @@ const (
TagData = 18
CodecAAC = 10
CodecAVC = 7
CodecH264 = 7
CodecHEVC = 12
)
const (
@@ -207,15 +209,18 @@ func (c *Producer) probe() error {
} else {
_ = pkt.Payload[0] >> 4 // FrameType
if codecID := pkt.Payload[0] & 0b1111; codecID != CodecAVC {
continue
}
if packetType := pkt.Payload[1]; packetType != PacketTypeAVCHeader { // check if header
continue
}
codec = h264.ConfigToCodec(pkt.Payload[5:])
switch codecID := pkt.Payload[0] & 0b1111; codecID {
case CodecH264:
codec = h264.ConfigToCodec(pkt.Payload[5:])
case CodecHEVC:
codec = h265.ConfigToCodec(pkt.Payload[5:])
default:
continue
}
}
media := &core.Media{
@@ -294,8 +299,12 @@ func (c *Producer) readPacket() (*rtp.Packet, error) {
return pkt, nil
}
func TimeToRTP(timeMS uint32, clockRate uint32) uint32 {
return timeMS * clockRate / 1000
// TimeToRTP convert time in milliseconds to RTP time
func TimeToRTP(timeMS, clockRate uint32) uint32 {
// for clockRates 90000, 16000, 8000, etc. - we can use:
// return timeMS * (clockRate / 1000)
// but for clockRates 44100, 22050, 11025 - we should use:
return uint32(uint64(timeMS) * uint64(clockRate) / 1000)
}
func isExHeader(data []byte) bool {
+11
View File
@@ -17,3 +17,14 @@ func TestDecodeSPS(t *testing.T) {
require.Equal(t, uint16(5120), sps.Width())
require.Equal(t, uint16(1440), sps.Height())
}
func TestDecodeSPS2(t *testing.T) {
s := "QgEBIUAAAAMAkAAAAwAAAwCWoAUCAWlnpbkShc1AQIC4QAAAAwBAAAAFFEn/eEAOpgAV+V8IBBA="
b, err := base64.StdEncoding.DecodeString(s)
require.Nil(t, err)
sps := DecodeSPS(b)
require.NotNil(t, sps)
require.Equal(t, uint16(640), sps.Width())
require.Equal(t, uint16(360), sps.Height())
}
+3
View File
@@ -0,0 +1,3 @@
## Useful links
- https://github.com/bauer-andreas/secure-video-specification
+13 -13
View File
@@ -49,17 +49,17 @@ func ServiceCameraRTPStreamManagement() *hap.Service {
val120, _ := tlv8.MarshalBase64(StreamingStatus{
Status: StreamingStatusAvailable,
})
val114, _ := tlv8.MarshalBase64(SupportedVideoStreamConfig{
Codecs: []VideoCodec{
val114, _ := tlv8.MarshalBase64(SupportedVideoStreamConfiguration{
Codecs: []VideoCodecConfiguration{
{
CodecType: VideoCodecTypeH264,
CodecParams: []VideoParams{
CodecParams: []VideoCodecParameters{
{
ProfileID: []byte{VideoCodecProfileMain},
Level: []byte{VideoCodecLevel31, VideoCodecLevel40},
},
},
VideoAttrs: []VideoAttrs{
VideoAttrs: []VideoCodecAttributes{
{Width: 1920, Height: 1080, Framerate: 30},
{Width: 1280, Height: 720, Framerate: 30}, // important for iPhones
{Width: 320, Height: 240, Framerate: 15}, // apple watch
@@ -67,23 +67,23 @@ func ServiceCameraRTPStreamManagement() *hap.Service {
},
},
})
val115, _ := tlv8.MarshalBase64(SupportedAudioStreamConfig{
Codecs: []AudioCodec{
val115, _ := tlv8.MarshalBase64(SupportedAudioStreamConfiguration{
Codecs: []AudioCodecConfiguration{
{
CodecType: AudioCodecTypeOpus,
CodecParams: []AudioParams{
CodecParams: []AudioCodecParameters{
{
Channels: 1,
Bitrate: AudioCodecBitrateVariable,
SampleRate: []byte{AudioCodecSampleRate16Khz},
Channels: 1,
BitrateMode: AudioCodecBitrateVariable,
SampleRate: []byte{AudioCodecSampleRate16Khz},
},
},
},
},
ComfortNoise: 0,
ComfortNoiseSupport: 0,
})
val116, _ := tlv8.MarshalBase64(SupportedRTPConfig{
CryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80},
val116, _ := tlv8.MarshalBase64(SupportedRTPConfiguration{
SRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80},
})
service := &hap.Service{
+39 -39
View File
@@ -63,19 +63,19 @@ func TestAqaraG3(t *testing.T) {
{
name: "114",
value: "AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==",
actual: &SupportedVideoStreamConfig{},
expect: &SupportedVideoStreamConfig{
Codecs: []VideoCodec{
actual: &SupportedVideoStreamConfiguration{},
expect: &SupportedVideoStreamConfiguration{
Codecs: []VideoCodecConfiguration{
{
CodecType: VideoCodecTypeH264,
CodecParams: []VideoParams{
CodecParams: []VideoCodecParameters{
{
ProfileID: []byte{VideoCodecProfileMain},
Level: []byte{VideoCodecLevel31, VideoCodecLevel40},
CVOEnabled: []byte{0},
},
},
VideoAttrs: []VideoAttrs{
VideoAttrs: []VideoCodecAttributes{
{Width: 1920, Height: 1080, Framerate: 30},
{Width: 1280, Height: 720, Framerate: 30},
{Width: 640, Height: 360, Framerate: 30},
@@ -94,29 +94,29 @@ func TestAqaraG3(t *testing.T) {
{
name: "115",
value: "AQ4BAQICCQEBAQIBAAMBAQIBAA==",
actual: &SupportedAudioStreamConfig{},
expect: &SupportedAudioStreamConfig{
Codecs: []AudioCodec{
actual: &SupportedAudioStreamConfiguration{},
expect: &SupportedAudioStreamConfiguration{
Codecs: []AudioCodecConfiguration{
{
CodecType: AudioCodecTypeAACELD,
CodecParams: []AudioParams{
CodecParams: []AudioCodecParameters{
{
Channels: 1,
Bitrate: AudioCodecBitrateVariable,
SampleRate: []byte{AudioCodecSampleRate16Khz},
Channels: 1,
BitrateMode: AudioCodecBitrateVariable,
SampleRate: []byte{AudioCodecSampleRate16Khz},
},
},
},
},
ComfortNoise: 0,
ComfortNoiseSupport: 0,
},
},
{
name: "116",
value: "AgEAAAACAQEAAAIBAg==",
actual: &SupportedRTPConfig{},
expect: &SupportedRTPConfig{
CryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoAES_CM_256_HMAC_SHA1_80, CryptoNone},
actual: &SupportedRTPConfiguration{},
expect: &SupportedRTPConfiguration{
SRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoAES_CM_256_HMAC_SHA1_80, CryptoDisabled},
},
},
}
@@ -130,18 +130,18 @@ func TestHomebridge(t *testing.T) {
{
name: "114",
value: "AcUBAQACHQEBAAAAAQEBAAABAQICAQAAAAIBAQAAAgECAwEAAwsBAkABAgK0AAMBHgAAAwsBAkABAgLwAAMBDwAAAwsBAkABAgLwAAMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAoAHAgI4BAMBHgAAAwsBAkAGAgKwBAMBHg==",
actual: &SupportedVideoStreamConfig{},
expect: &SupportedVideoStreamConfig{
Codecs: []VideoCodec{
actual: &SupportedVideoStreamConfiguration{},
expect: &SupportedVideoStreamConfiguration{
Codecs: []VideoCodecConfiguration{
{
CodecType: VideoCodecTypeH264,
CodecParams: []VideoParams{
CodecParams: []VideoCodecParameters{
{
ProfileID: []byte{VideoCodecProfileConstrainedBaseline, VideoCodecProfileMain, VideoCodecProfileHigh},
Level: []byte{VideoCodecLevel31, VideoCodecLevel32, VideoCodecLevel40},
},
},
VideoAttrs: []VideoAttrs{
VideoAttrs: []VideoCodecAttributes{
{Width: 320, Height: 180, Framerate: 30},
{Width: 320, Height: 240, Framerate: 15},
@@ -162,9 +162,9 @@ func TestHomebridge(t *testing.T) {
{
name: "116",
value: "AgEA",
actual: &SupportedRTPConfig{},
expect: &SupportedRTPConfig{
CryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80},
actual: &SupportedRTPConfiguration{},
expect: &SupportedRTPConfiguration{
SRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80},
},
},
}
@@ -178,18 +178,18 @@ func TestScrypted(t *testing.T) {
{
name: "114",
value: "AVIBAQACEwEBAQIBAAAAAgEBAAACAQIDAQADCwECAA8CAnAIAwEeAAADCwECgAcCAjgEAwEeAAADCwECAAUCAtACAwEeAAADCwECQAECAvAAAwEP",
actual: &SupportedVideoStreamConfig{},
expect: &SupportedVideoStreamConfig{
Codecs: []VideoCodec{
actual: &SupportedVideoStreamConfiguration{},
expect: &SupportedVideoStreamConfiguration{
Codecs: []VideoCodecConfiguration{
{
CodecType: VideoCodecTypeH264,
CodecParams: []VideoParams{
CodecParams: []VideoCodecParameters{
{
ProfileID: []byte{VideoCodecProfileMain},
Level: []byte{VideoCodecLevel31, VideoCodecLevel32, VideoCodecLevel40},
},
},
VideoAttrs: []VideoAttrs{
VideoAttrs: []VideoCodecAttributes{
{Width: 3840, Height: 2160, Framerate: 30},
{Width: 1920, Height: 1080, Framerate: 30},
{Width: 1280, Height: 720, Framerate: 30},
@@ -202,15 +202,15 @@ func TestScrypted(t *testing.T) {
{
name: "115",
value: "AScBAQMCIgEBAQIBAAMBAAAAAwEAAAADAQEAAAMBAQAAAwECAAADAQICAQA=",
actual: &SupportedAudioStreamConfig{},
expect: &SupportedAudioStreamConfig{
Codecs: []AudioCodec{
actual: &SupportedAudioStreamConfiguration{},
expect: &SupportedAudioStreamConfiguration{
Codecs: []AudioCodecConfiguration{
{
CodecType: AudioCodecTypeOpus,
CodecParams: []AudioParams{
CodecParams: []AudioCodecParameters{
{
Channels: 1,
Bitrate: AudioCodecBitrateVariable,
Channels: 1,
BitrateMode: AudioCodecBitrateVariable,
SampleRate: []byte{
AudioCodecSampleRate8Khz, AudioCodecSampleRate8Khz,
AudioCodecSampleRate16Khz, AudioCodecSampleRate16Khz,
@@ -220,15 +220,15 @@ func TestScrypted(t *testing.T) {
},
},
},
ComfortNoise: 0,
ComfortNoiseSupport: 0,
},
},
{
name: "116",
value: "AgEAAAACAQI=",
actual: &SupportedRTPConfig{},
expect: &SupportedRTPConfig{
CryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoNone},
actual: &SupportedRTPConfiguration{},
expect: &SupportedRTPConfiguration{
SRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoDisabled},
},
},
}
+10 -10
View File
@@ -2,15 +2,15 @@ package camera
const TypeSupportedVideoStreamConfiguration = "114"
type SupportedVideoStreamConfig struct {
Codecs []VideoCodec `tlv8:"1"`
type SupportedVideoStreamConfiguration struct {
Codecs []VideoCodecConfiguration `tlv8:"1"`
}
type VideoCodec struct {
CodecType byte `tlv8:"1"`
CodecParams []VideoParams `tlv8:"2"`
VideoAttrs []VideoAttrs `tlv8:"3"`
RTPParams []RTPParams `tlv8:"4"`
type VideoCodecConfiguration struct {
CodecType byte `tlv8:"1"`
CodecParams []VideoCodecParameters `tlv8:"2"`
VideoAttrs []VideoCodecAttributes `tlv8:"3"`
RTPParams []RTPParams `tlv8:"4"`
}
//goland:noinspection ALL
@@ -31,15 +31,15 @@ const (
VideoCodecCvoSuppported = 1
)
type VideoParams struct {
type VideoCodecParameters struct {
ProfileID []byte `tlv8:"1"` // 0 - baseline, 1 - main, 2 - high
Level []byte `tlv8:"2"` // 0 - 3.1, 1 - 3.2, 2 - 4.0
PacketizationMode byte `tlv8:"3"` // only 0 - non interleaved
CVOEnabled []byte `tlv8:"4"` // 0 - not supported, 1 - supported
CVOID []byte `tlv8:"5"` // ???
CVOID []byte `tlv8:"5"` // ID for CVO RTP extensio
}
type VideoAttrs struct {
type VideoCodecAttributes struct {
Width uint16 `tlv8:"1"`
Height uint16 `tlv8:"2"`
Framerate uint8 `tlv8:"3"`
+13 -13
View File
@@ -2,9 +2,9 @@ package camera
const TypeSupportedAudioStreamConfiguration = "115"
type SupportedAudioStreamConfig struct {
Codecs []AudioCodec `tlv8:"1"`
ComfortNoise byte `tlv8:"2"`
type SupportedAudioStreamConfiguration struct {
Codecs []AudioCodecConfiguration `tlv8:"1"`
ComfortNoiseSupport byte `tlv8:"2"`
}
//goland:noinspection ALL
@@ -31,16 +31,16 @@ const (
RTPTimeAACLD24 = 40 // 24000/1000*40=960
)
type AudioCodec struct {
CodecType byte `tlv8:"1"`
CodecParams []AudioParams `tlv8:"2"`
RTPParams []RTPParams `tlv8:"3"`
ComfortNoise []byte `tlv8:"4"`
type AudioCodecConfiguration struct {
CodecType byte `tlv8:"1"`
CodecParams []AudioCodecParameters `tlv8:"2"`
RTPParams []RTPParams `tlv8:"3"`
ComfortNoise []byte `tlv8:"4"`
}
type AudioParams struct {
Channels uint8 `tlv8:"1"`
Bitrate byte `tlv8:"2"` // 0 - variable, 1 - constant
SampleRate []byte `tlv8:"3"` // 0 - 8000, 1 - 16000, 2 - 24000
RTPTime []uint8 `tlv8:"4"` // 20, 30, 40, 60
type AudioCodecParameters struct {
Channels uint8 `tlv8:"1"`
BitrateMode byte `tlv8:"2"` // 0 - variable, 1 - constant
SampleRate []byte `tlv8:"3"` // 0 - 8000, 1 - 16000, 2 - 24000
RTPTime []uint8 `tlv8:"4"` // 20, 30, 40, 60
}
@@ -6,9 +6,9 @@ const TypeSupportedRTPConfiguration = "116"
const (
CryptoAES_CM_128_HMAC_SHA1_80 = 0
CryptoAES_CM_256_HMAC_SHA1_80 = 1
CryptoNone = 2
CryptoDisabled = 2
)
type SupportedRTPConfig struct {
CryptoType []byte `tlv8:"2"`
type SupportedRTPConfiguration struct {
SRTPCryptoType []byte `tlv8:"2"`
}
+4 -4
View File
@@ -2,10 +2,10 @@ package camera
const TypeSelectedStreamConfiguration = "117"
type SelectedStreamConfig struct {
Control SessionControl `tlv8:"1"`
VideoCodec VideoCodec `tlv8:"2"`
AudioCodec AudioCodec `tlv8:"3"`
type SelectedStreamConfiguration struct {
Control SessionControl `tlv8:"1"`
VideoCodec VideoCodecConfiguration `tlv8:"2"`
AudioCodec AudioCodecConfiguration `tlv8:"3"`
}
//goland:noinspection ALL
+20 -13
View File
@@ -2,25 +2,32 @@ package camera
const TypeSetupEndpoints = "118"
type SetupEndpoints struct {
SessionID string `tlv8:"1"`
Status []byte `tlv8:"2"`
Address Addr `tlv8:"3"`
VideoCrypto CryptoSuite `tlv8:"4"`
AudioCrypto CryptoSuite `tlv8:"5"`
VideoSSRC []uint32 `tlv8:"6"`
AudioSSRC []uint32 `tlv8:"7"`
type SetupEndpointsRequest struct {
SessionID string `tlv8:"1"`
Address Address `tlv8:"3"`
VideoCrypto SRTPCryptoSuite `tlv8:"4"`
AudioCrypto SRTPCryptoSuite `tlv8:"5"`
}
type Addr struct {
type SetupEndpointsResponse struct {
SessionID string `tlv8:"1"`
Status byte `tlv8:"2"`
Address Address `tlv8:"3"`
VideoCrypto SRTPCryptoSuite `tlv8:"4"`
AudioCrypto SRTPCryptoSuite `tlv8:"5"`
VideoSSRC uint32 `tlv8:"6"`
AudioSSRC uint32 `tlv8:"7"`
}
type Address struct {
IPVersion byte `tlv8:"1"`
IPAddr string `tlv8:"2"`
VideoRTPPort uint16 `tlv8:"3"`
AudioRTPPort uint16 `tlv8:"4"`
}
type CryptoSuite struct {
CryptoType byte `tlv8:"1"`
MasterKey string `tlv8:"2"` // 16 (AES_CM_128) or 32 (AES_256_CM)
MasterSalt string `tlv8:"3"` // 14 byte
type SRTPCryptoSuite struct {
CryptoSuite byte `tlv8:"1"`
MasterKey string `tlv8:"2"` // 16 (AES_CM_128) or 32 (AES_256_CM)
MasterSalt string `tlv8:"3"` // 14 byte
}
+1 -1
View File
@@ -9,6 +9,6 @@ type StreamingStatus struct {
//goland:noinspection ALL
const (
StreamingStatusAvailable = 0
StreamingStatusBusy = 1
StreamingStatusInUse = 1
StreamingStatusUnavailable = 2
)
@@ -0,0 +1,11 @@
package camera
const TypeSupportedDataStreamTransportConfiguration = "130"
type SupportedDataStreamTransportConfiguration struct {
Configs []TransferTransportConfiguration `tlv8:"1"`
}
type TransferTransportConfiguration struct {
TransportType byte `tlv8:"1"`
}
+2 -2
View File
@@ -2,13 +2,13 @@ package camera
const TypeSetupDataStreamTransport = "131"
type SetupDataStreamRequest struct {
type SetupDataStreamTransportRequest struct {
SessionCommandType byte `tlv8:"1"`
TransportType byte `tlv8:"2"`
ControllerKeySalt string `tlv8:"3"`
}
type SetupDataStreamResponse struct {
type SetupDataStreamTransportResponse struct {
Status byte `tlv8:"1"`
TransportTypeSessionParameters struct {
TCPListeningPort uint16 `tlv8:"1"`
+18
View File
@@ -0,0 +1,18 @@
package camera
const TypeSupportedCameraRecordingConfiguration = "205"
type SupportedCameraRecordingConfiguration struct {
PrebufferLength uint32 `tlv8:"1"`
EventTriggerOptions uint64 `tlv8:"2"`
MediaContainerConfigurations `tlv8:"3"`
}
type MediaContainerConfigurations struct {
MediaContainerType uint8 `tlv8:"1"`
MediaContainerParameters `tlv8:"2"`
}
type MediaContainerParameters struct {
FragmentLength uint32 `tlv8:"1"`
}
+20
View File
@@ -0,0 +1,20 @@
package camera
const TypeSupportedVideoRecordingConfiguration = "206"
type SupportedVideoRecordingConfiguration struct {
CodecConfigs []VideoRecordingCodecConfiguration `tlv8:"1"`
}
type VideoRecordingCodecConfiguration struct {
CodecType uint8 `tlv8:"1"`
CodecParams VideoRecordingCodecParameters `tlv8:"2"`
CodecAttrs VideoCodecAttributes `tlv8:"3"`
}
type VideoRecordingCodecParameters struct {
ProfileID uint8 `tlv8:"1"`
Level uint8 `tlv8:"2"`
Bitrate uint32 `tlv8:"3"`
IFrameInterval uint32 `tlv8:"4"`
}
+19
View File
@@ -0,0 +1,19 @@
package camera
const TypeSupportedAudioRecordingConfiguration = "207"
type SupportedAudioRecordingConfiguration struct {
CodecConfigs []AudioRecordingCodecConfiguration `tlv8:"1"`
}
type AudioRecordingCodecConfiguration struct {
CodecType byte `tlv8:"1"`
CodecParams []AudioRecordingCodecParameters `tlv8:"2"`
}
type AudioRecordingCodecParameters struct {
Channels uint8 `tlv8:"1"`
BitrateMode []byte `tlv8:"2"`
SampleRate []byte `tlv8:"3"`
MaxAudioBitrate []uint32 `tlv8:"4"`
}
+9
View File
@@ -0,0 +1,9 @@
package camera
const TypeSelectedCameraRecordingConfiguration = "209"
type SelectedCameraRecordingConfiguration struct {
GeneralConfig SupportedCameraRecordingConfiguration `tlv8:"1"`
VideoConfig SupportedVideoRecordingConfiguration `tlv8:"2"`
AudioConfig SupportedAudioRecordingConfiguration `tlv8:"3"`
}
+11 -11
View File
@@ -15,7 +15,7 @@ type Stream struct {
}
func NewStream(
client *hap.Client, videoCodec *VideoCodec, audioCodec *AudioCodec,
client *hap.Client, videoCodec *VideoCodecConfiguration, audioCodec *AudioCodecConfiguration,
videoSession, audioSession *srtp.Session, bitrate int,
) (*Stream, error) {
stream := &Stream{
@@ -58,7 +58,7 @@ func NewStream(
}
audioCodec.ComfortNoise = []byte{0}
config := &SelectedStreamConfig{
config := &SelectedStreamConfiguration{
Control: SessionControl{
SessionID: stream.id,
Command: SessionCommandStart,
@@ -103,19 +103,19 @@ func (s *Stream) GetFreeStream() error {
}
func (s *Stream) ExchangeEndpoints(videoSession, audioSession *srtp.Session) error {
req := SetupEndpoints{
req := SetupEndpointsRequest{
SessionID: s.id,
Address: Addr{
Address: Address{
IPVersion: 0,
IPAddr: videoSession.Local.Addr,
VideoRTPPort: videoSession.Local.Port,
AudioRTPPort: audioSession.Local.Port,
},
VideoCrypto: CryptoSuite{
VideoCrypto: SRTPCryptoSuite{
MasterKey: string(videoSession.Local.MasterKey),
MasterSalt: string(videoSession.Local.MasterSalt),
},
AudioCrypto: CryptoSuite{
AudioCrypto: SRTPCryptoSuite{
MasterKey: string(audioSession.Local.MasterKey),
MasterSalt: string(audioSession.Local.MasterSalt),
},
@@ -129,7 +129,7 @@ func (s *Stream) ExchangeEndpoints(videoSession, audioSession *srtp.Session) err
return err
}
var res SetupEndpoints
var res SetupEndpointsResponse
if err := s.client.GetCharacter(char); err != nil {
return err
}
@@ -142,7 +142,7 @@ func (s *Stream) ExchangeEndpoints(videoSession, audioSession *srtp.Session) err
Port: res.Address.VideoRTPPort,
MasterKey: []byte(res.VideoCrypto.MasterKey),
MasterSalt: []byte(res.VideoCrypto.MasterSalt),
SSRC: res.VideoSSRC[0],
SSRC: res.VideoSSRC,
}
audioSession.Remote = &srtp.Endpoint{
@@ -150,13 +150,13 @@ func (s *Stream) ExchangeEndpoints(videoSession, audioSession *srtp.Session) err
Port: res.Address.AudioRTPPort,
MasterKey: []byte(res.AudioCrypto.MasterKey),
MasterSalt: []byte(res.AudioCrypto.MasterSalt),
SSRC: res.AudioSSRC[0],
SSRC: res.AudioSSRC,
}
return nil
}
func (s *Stream) SetStreamConfig(config *SelectedStreamConfig) error {
func (s *Stream) SetStreamConfig(config *SelectedStreamConfiguration) error {
char := s.service.GetCharacter(TypeSelectedStreamConfiguration)
if err := char.Write(config); err != nil {
return err
@@ -169,7 +169,7 @@ func (s *Stream) SetStreamConfig(config *SelectedStreamConfig) error {
}
func (s *Stream) Close() error {
config := &SelectedStreamConfig{
config := &SelectedStreamConfiguration{
Control: SessionControl{
SessionID: s.id,
Command: SessionCommandEnd,
+11 -5
View File
@@ -18,7 +18,6 @@ import (
"github.com/AlexxIT/go2rtc/pkg/hap/curve25519"
"github.com/AlexxIT/go2rtc/pkg/hap/ed25519"
"github.com/AlexxIT/go2rtc/pkg/hap/hkdf"
"github.com/AlexxIT/go2rtc/pkg/hap/secure"
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
"github.com/AlexxIT/go2rtc/pkg/mdns"
)
@@ -46,7 +45,7 @@ type Client struct {
err error
}
func NewClient(rawURL string) (*Client, error) {
func Dial(rawURL string) (*Client, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
@@ -61,6 +60,10 @@ func NewClient(rawURL string) (*Client, error) {
ClientPrivate: DecodeKey(query.Get("client_private")),
}
if err = c.Dial(); err != nil {
return nil, err
}
return c, nil
}
@@ -96,6 +99,7 @@ func (c *Client) Dial() (err error) {
return false
})
// TODO: close conn on error
if c.Conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout); err != nil {
return
}
@@ -124,7 +128,7 @@ func (c *Client) Dial() (err error) {
EncryptedData string `tlv8:"5"`
State byte `tlv8:"6"`
}
if err = tlv8.UnmarshalReader(res.Body, &cipherM2); err != nil {
if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &cipherM2); err != nil {
return err
}
if cipherM2.State != StateM2 {
@@ -209,15 +213,17 @@ func (c *Client) Dial() (err error) {
var plainM4 struct {
State byte `tlv8:"6"`
}
if err = tlv8.UnmarshalReader(res.Body, &plainM4); err != nil {
if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM4); err != nil {
return
}
if plainM4.State != StateM4 {
return newResponseError(cipherM3, plainM4)
}
rw := bufio.NewReadWriter(c.reader, bufio.NewWriter(c.Conn))
// like tls.Client wrapper over net.Conn
if c.Conn, err = secure.Client(c.Conn, sessionShared, true); err != nil {
if c.Conn, err = NewConn(c.Conn, rw, sessionShared, true); err != nil {
return
}
// new reader for new conn
+17
View File
@@ -82,3 +82,20 @@ func ReadResponse(r *bufio.Reader, req *http.Request) (*http.Response, error) {
return res, nil
}
func WriteEvent(w io.Writer, res *http.Response) error {
return res.Write(&eventWriter{w: w})
}
type eventWriter struct {
w io.Writer
done bool
}
func (e *eventWriter) Write(p []byte) (n int, err error) {
if !e.done {
p = append([]byte("EVENT/1.0"), p[8:]...)
e.done = true
}
return e.w.Write(p)
}
+8 -9
View File
@@ -107,7 +107,7 @@ func (c *Client) Pair(feature, pin string) (err error) {
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}
if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil {
if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil {
return
}
if plainM2.State != StateM2 {
@@ -121,9 +121,7 @@ func (c *Client) Pair(feature, pin string) (err error) {
username := []byte("Pair-Setup")
// Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE)
pake, err := srp.NewSRP(
"rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username),
)
pake, err := srp.NewSRP("rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username))
if err != nil {
return
}
@@ -132,6 +130,7 @@ func (c *Client) Pair(feature, pin string) (err error) {
// username: "Pair-Setup", password: PIN (with dashes)
session := pake.NewClientSession(username, []byte(pin))
sessionShared, err := session.ComputeKey([]byte(plainM2.Salt), []byte(plainM2.SessionKey))
if err != nil {
return
@@ -159,7 +158,7 @@ func (c *Client) Pair(feature, pin string) (err error) {
EncryptedData string `tlv8:"5"` // skip EncryptedData validation (for MFi devices)
}
if err = tlv8.UnmarshalReader(res.Body, &plainM4); err != nil {
if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM4); err != nil {
return
}
if plainM4.State != StateM4 {
@@ -232,7 +231,7 @@ func (c *Client) Pair(feature, pin string) (err error) {
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
if err = tlv8.UnmarshalReader(res.Body, &cipherM6); err != nil {
if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &cipherM6); err != nil {
return
}
if cipherM6.State != StateM6 || cipherM6.Error != 0 {
@@ -296,7 +295,7 @@ func (c *Client) ListPairings() error {
State byte `tlv8:"6"`
Permission byte `tlv8:"11"`
}
if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil {
if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil {
return err
}
@@ -329,7 +328,7 @@ func (c *Client) PairingsAdd(clientID string, clientPublic []byte, admin bool) e
State byte `tlv8:"6"`
Unknown byte `tlv8:"7"`
}
if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil {
if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil {
return err
}
@@ -354,7 +353,7 @@ func (c *Client) DeletePairing(id string) error {
var plainM2 struct {
State byte `tlv8:"6"`
}
if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil {
if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil {
return err
}
if plainM2.State != StateM2 {
+44 -20
View File
@@ -1,32 +1,50 @@
package secure
package hap
import (
"bufio"
"encoding/binary"
"encoding/json"
"errors"
"io"
"net"
"sync"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
"github.com/AlexxIT/go2rtc/pkg/hap/hkdf"
)
type Conn struct {
conn net.Conn
rd *bufio.Reader
wr *bufio.Writer
rw *bufio.ReadWriter
wmu sync.Mutex
encryptKey []byte
decryptKey []byte
encryptCnt uint64
decryptCnt uint64
//ClientID string
SharedKey []byte
recv int
send int
}
func Client(conn net.Conn, sharedKey []byte, isClient bool) (net.Conn, error) {
func (c *Conn) MarshalJSON() ([]byte, error) {
conn := core.Connection{
ID: core.ID(c),
FormatName: "homekit",
Protocol: "hap",
RemoteAddr: c.conn.RemoteAddr().String(),
Recv: c.recv,
Send: c.send,
}
return json.Marshal(conn)
}
func NewConn(conn net.Conn, rw *bufio.ReadWriter, sharedKey []byte, isClient bool) (*Conn, error) {
key1, err := hkdf.Sha512(sharedKey, "Control-Salt", "Control-Read-Encryption-Key")
if err != nil {
return nil, err
@@ -39,8 +57,7 @@ func Client(conn net.Conn, sharedKey []byte, isClient bool) (net.Conn, error) {
c := &Conn{
conn: conn,
rd: bufio.NewReaderSize(conn, 32*1024),
wr: bufio.NewWriterSize(conn, 32*1024),
rw: rw,
SharedKey: sharedKey,
}
@@ -55,8 +72,8 @@ func Client(conn net.Conn, sharedKey []byte, isClient bool) (net.Conn, error) {
}
const (
// PacketSizeMax is the max length of encrypted packets
PacketSizeMax = 0x400
// packetSizeMax is the max length of encrypted packets
packetSizeMax = 0x400
VerifySize = 2
NonceSize = 8
@@ -64,19 +81,19 @@ const (
)
func (c *Conn) Read(b []byte) (n int, err error) {
if cap(b) < PacketSizeMax {
if cap(b) < packetSizeMax {
return 0, errors.New("hap: read buffer is too small")
}
verify := make([]byte, 2) // verify = plain message size
if _, err = io.ReadFull(c.rd, verify); err != nil {
verify := make([]byte, VerifySize) // verify = plain message size
if _, err = io.ReadFull(c.rw, verify); err != nil {
return
}
n = int(binary.LittleEndian.Uint16(verify))
ciphertext := make([]byte, n+Overhead)
if _, err = io.ReadFull(c.rd, ciphertext); err != nil {
ciphertext := make([]byte, n+Overhead)
if _, err = io.ReadFull(c.rw, ciphertext); err != nil {
return
}
@@ -85,22 +102,27 @@ func (c *Conn) Read(b []byte) (n int, err error) {
c.decryptCnt++
_, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, b[:0], nonce, ciphertext, verify)
c.recv += n
return
}
func (c *Conn) Write(b []byte) (n int, err error) {
buf := make([]byte, 0, PacketSizeMax+Overhead)
c.wmu.Lock()
defer c.wmu.Unlock()
buf := make([]byte, 0, packetSizeMax+Overhead)
nonce := make([]byte, NonceSize)
verify := make([]byte, VerifySize)
for len(b) > 0 {
size := len(b)
if size > PacketSizeMax {
size = PacketSizeMax
if size > packetSizeMax {
size = packetSizeMax
}
binary.LittleEndian.PutUint16(verify, uint16(size))
if _, err = c.wr.Write(verify); err != nil {
if _, err = c.rw.Write(verify); err != nil {
return
}
@@ -112,7 +134,7 @@ func (c *Conn) Write(b []byte) (n int, err error) {
return
}
if _, err = c.wr.Write(buf[:size+Overhead]); err != nil {
if _, err = c.rw.Write(buf[:size+Overhead]); err != nil {
return
}
@@ -120,7 +142,9 @@ func (c *Conn) Write(b []byte) (n int, err error) {
n += size
}
err = c.wr.Flush()
err = c.rw.Flush()
c.send += n
return
}
+62 -9
View File
@@ -4,16 +4,19 @@ package hds
import (
"bufio"
"encoding/binary"
"encoding/json"
"errors"
"io"
"net"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
"github.com/AlexxIT/go2rtc/pkg/hap/hkdf"
"github.com/AlexxIT/go2rtc/pkg/hap/secure"
)
func Client(conn net.Conn, key []byte, salt string, controller bool) (*Conn, error) {
func NewConn(conn net.Conn, key []byte, salt string, controller bool) (*Conn, error) {
writeKey, err := hkdf.Sha512(key, salt, "HDS-Write-Encryption-Key")
if err != nil {
return nil, err
@@ -49,43 +52,91 @@ type Conn struct {
encryptKey []byte
decryptCnt uint64
encryptCnt uint64
recv int
send int
}
func (c *Conn) Read(p []byte) (n int, err error) {
func (c *Conn) MarshalJSON() ([]byte, error) {
conn := core.Connection{
ID: core.ID(c),
FormatName: "homekit",
Protocol: "hds",
RemoteAddr: c.conn.RemoteAddr().String(),
Recv: c.recv,
Send: c.send,
}
return json.Marshal(conn)
}
func (c *Conn) read() (b []byte, err error) {
verify := make([]byte, 4)
if _, err = io.ReadFull(c.rd, verify); err != nil {
return
}
n = int(binary.BigEndian.Uint32(verify) & 0xFFFFFF)
n := int(binary.BigEndian.Uint32(verify) & 0xFFFFFF)
ciphertext := make([]byte, n+secure.Overhead)
ciphertext := make([]byte, n+hap.Overhead)
if _, err = io.ReadFull(c.rd, ciphertext); err != nil {
return
}
nonce := make([]byte, secure.NonceSize)
nonce := make([]byte, hap.NonceSize)
binary.LittleEndian.PutUint64(nonce, c.decryptCnt)
c.decryptCnt++
_, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, p[:0], nonce, ciphertext, verify)
c.recv += n
return chacha20poly1305.DecryptAndVerify(c.decryptKey, ciphertext[:0], nonce, ciphertext, verify)
}
func (c *Conn) Read(p []byte) (n int, err error) {
b, err := c.read()
if err != nil {
return 0, err
}
n = copy(p, b)
if len(b) > n {
err = errors.New("hds: read buffer too small")
}
return
}
func (c *Conn) WriteTo(w io.Writer) (int64, error) {
var total int64
for {
b, err := c.read()
if err != nil {
return total, err
}
n, err := w.Write(b)
total += int64(n)
if err != nil {
return total, err
}
}
}
func (c *Conn) Write(b []byte) (n int, err error) {
n = len(b)
if n > 0xFFFFFF {
return 0, errors.New("hds: write buffer too big")
}
verify := make([]byte, 4)
binary.BigEndian.PutUint32(verify, 0x01000000|uint32(n))
if _, err = c.wr.Write(verify); err != nil {
return
}
nonce := make([]byte, secure.NonceSize)
nonce := make([]byte, hap.NonceSize)
binary.LittleEndian.PutUint64(nonce, c.encryptCnt)
c.encryptCnt++
buf := make([]byte, n+secure.Overhead)
buf := make([]byte, n+hap.Overhead)
if _, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf[:0], nonce, b, verify); err != nil {
return
}
@@ -95,6 +146,8 @@ func (c *Conn) Write(b []byte) (n int, err error) {
}
err = c.wr.Flush()
c.send += n
return
}
+8
View File
@@ -3,6 +3,8 @@ package hap
import (
"crypto/ed25519"
"crypto/rand"
"crypto/sha512"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
@@ -99,6 +101,12 @@ func GenerateUUID() string {
return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:]
}
func SetupHash(setupID, deviceID string) string {
// should be setup_id (random 4 alphanum) + device_id (mac address)
b := sha512.Sum512([]byte(setupID + deviceID))
return base64.StdEncoding.EncodeToString(b[:4])
}
func Append(items ...any) (b []byte) {
for _, item := range items {
switch v := item.(type) {
+273 -52
View File
@@ -3,32 +3,25 @@ package hap
import (
"bufio"
"crypto/sha512"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/http"
"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
"github.com/AlexxIT/go2rtc/pkg/hap/curve25519"
"github.com/AlexxIT/go2rtc/pkg/hap/ed25519"
"github.com/AlexxIT/go2rtc/pkg/hap/hkdf"
"github.com/AlexxIT/go2rtc/pkg/hap/secure"
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
"github.com/tadglines/go-pkgs/crypto/srp"
)
type HandlerFunc func(net.Conn) error
type Server struct {
Pin string
DeviceID string
DevicePrivate []byte
GetPair func(conn net.Conn, id string) []byte
AddPair func(conn net.Conn, id string, public []byte, permissions byte)
Handler HandlerFunc
// GetClientPublic may be nil, so client validation will be disabled
GetClientPublic func(id string) []byte
}
func (s *Server) ServerPublic() []byte {
@@ -42,44 +35,240 @@ func (s *Server) ServerPublic() []byte {
// return StatusPaired
//}
func (s *Server) SetupHash() string {
// should be setup_id (random 4 alphanum) + device_id (mac address)
// but device_id is random, so OK
b := sha512.Sum512([]byte(s.DeviceID))
return base64.StdEncoding.EncodeToString(b[:4])
}
func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Conn) error {
// Request from iPhone
func (s *Server) PairSetup(req *http.Request, rw *bufio.ReadWriter) (id string, publicKey []byte, err error) {
// STEP 1. Request from iPhone
var plainM1 struct {
PublicKey string `tlv8:"3"`
State byte `tlv8:"6"`
State byte `tlv8:"6"`
Method byte `tlv8:"0"`
Flags uint32 `tlv8:"19"`
}
if err := tlv8.UnmarshalReader(io.LimitReader(rw, req.ContentLength), &plainM1); err != nil {
return err
if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil {
return
}
if plainM1.State != StateM1 {
return newRequestError(plainM1)
err = newRequestError(plainM1)
return
}
username := []byte("Pair-Setup")
// Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE)
pake, err := srp.NewSRP("rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username))
if err != nil {
return
}
pake.SaltLength = 16
salt, verifier, err := pake.ComputeVerifier([]byte(s.Pin))
if err != nil {
return
}
session := pake.NewServerSession(username, salt, verifier)
// STEP 2. Response to iPhone
plainM2 := struct {
State byte `tlv8:"6"`
PublicKey string `tlv8:"3"`
Salt string `tlv8:"2"`
}{
State: StateM2,
PublicKey: string(session.GetB()),
Salt: string(salt),
}
body, err := tlv8.Marshal(plainM2)
if err != nil {
return
}
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
return
}
// STEP 3. Request from iPhone
if req, err = http.ReadRequest(rw.Reader); err != nil {
return
}
var plainM3 struct {
State byte `tlv8:"6"`
PublicKey string `tlv8:"3"`
Proof string `tlv8:"4"`
}
if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM3); err != nil {
return
}
if plainM3.State != StateM3 {
err = newRequestError(plainM3)
return
}
// important to compute key before verify client
sessionShared, err := session.ComputeKey([]byte(plainM3.PublicKey))
if err != nil {
return
}
if !session.VerifyClientAuthenticator([]byte(plainM3.Proof)) {
err = errors.New("hap: VerifyClientAuthenticator")
return
}
proof := session.ComputeAuthenticator([]byte(plainM3.Proof)) // server proof
// STEP 4. Response to iPhone
payloadM4 := struct {
State byte `tlv8:"6"`
Proof string `tlv8:"4"`
}{
State: StateM4,
Proof: string(proof),
}
if body, err = tlv8.Marshal(payloadM4); err != nil {
return
}
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
return
}
// STEP 5. Request from iPhone
if req, err = http.ReadRequest(rw.Reader); err != nil {
return
}
var cipherM5 struct {
State byte `tlv8:"6"`
EncryptedData string `tlv8:"5"`
}
if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &cipherM5); err != nil {
return
}
if cipherM5.State != StateM5 {
err = newRequestError(cipherM5)
return
}
// decrypt message using session shared
encryptKey, err := hkdf.Sha512(sessionShared, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info")
if err != nil {
return
}
b, err := chacha20poly1305.Decrypt(encryptKey, "PS-Msg05", []byte(cipherM5.EncryptedData))
if err != nil {
return
}
// unpack message from TLV8
var plainM5 struct {
Identifier string `tlv8:"1"`
PublicKey string `tlv8:"3"`
Signature string `tlv8:"10"`
}
if err = tlv8.Unmarshal(b, &plainM5); err != nil {
return
}
// 3. verify client ID and Public
remoteSign, err := hkdf.Sha512(
sessionShared, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info",
)
if err != nil {
return
}
b = Append(remoteSign, plainM5.Identifier, plainM5.PublicKey)
if !ed25519.ValidateSignature([]byte(plainM5.PublicKey), b, []byte(plainM5.Signature)) {
err = errors.New("hap: ValidateSignature")
return
}
// 4. generate signature to our ID and Public
localSign, err := hkdf.Sha512(
sessionShared, "Pair-Setup-Accessory-Sign-Salt", "Pair-Setup-Accessory-Sign-Info",
)
if err != nil {
return
}
b = Append(localSign, s.DeviceID, s.ServerPublic()) // ServerPublic
signature, err := ed25519.Signature(s.DevicePrivate, b)
if err != nil {
return
}
// 5. pack our ID and Public
plainM6 := struct {
Identifier string `tlv8:"1"`
PublicKey string `tlv8:"3"`
Signature string `tlv8:"10"`
}{
Identifier: s.DeviceID,
PublicKey: string(s.ServerPublic()),
Signature: string(signature),
}
if b, err = tlv8.Marshal(plainM6); err != nil {
return
}
// 6. encrypt message
b, err = chacha20poly1305.Encrypt(encryptKey, "PS-Msg06", b)
if err != nil {
return
}
// STEP 6. Response to iPhone
cipherM6 := struct {
State byte `tlv8:"6"`
EncryptedData string `tlv8:"5"`
}{
State: StateM6,
EncryptedData: string(b),
}
if body, err = tlv8.Marshal(cipherM6); err != nil {
return
}
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
return
}
id = plainM5.Identifier
publicKey = []byte(plainM5.PublicKey)
return
}
func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter) (id string, sessionKey []byte, err error) {
// Request from iPhone
var plainM1 struct {
State byte `tlv8:"6"`
PublicKey string `tlv8:"3"`
}
if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil {
return
}
if plainM1.State != StateM1 {
err = newRequestError(plainM1)
return
}
// Generate the key pair
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
sessionShared, err := curve25519.SharedSecret(sessionPrivate, []byte(plainM1.PublicKey))
if err != nil {
return err
return
}
encryptKey, err := hkdf.Sha512(
sessionShared, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info",
)
if err != nil {
return err
return
}
b := Append(sessionPublic, s.DeviceID, plainM1.PublicKey)
signature, err := ed25519.Signature(s.DevicePrivate, b)
if err != nil {
return err
return
}
// STEP M2. Response to iPhone
@@ -91,12 +280,12 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co
Signature: string(signature),
}
if b, err = tlv8.Marshal(plainM2); err != nil {
return err
return
}
b, err = chacha20poly1305.Encrypt(encryptKey, "PV-Msg02", b)
if err != nil {
return err
return
}
cipherM2 := struct {
@@ -110,30 +299,32 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co
}
body, err := tlv8.Marshal(cipherM2)
if err != nil {
return err
return
}
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
return err
return
}
// STEP M3. Request from iPhone
if req, err = http.ReadRequest(rw.Reader); err != nil {
return err
return
}
var cipherM3 struct {
EncryptedData string `tlv8:"5"`
State byte `tlv8:"6"`
EncryptedData string `tlv8:"5"`
}
if err = tlv8.UnmarshalReader(req.Body, &cipherM3); err != nil {
return err
if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &cipherM3); err != nil {
return
}
if cipherM3.State != StateM3 {
return newRequestError(cipherM3)
err = newRequestError(cipherM3)
return
}
if b, err = chacha20poly1305.Decrypt(encryptKey, "PV-Msg03", []byte(cipherM3.EncryptedData)); err != nil {
return err
b, err = chacha20poly1305.Decrypt(encryptKey, "PV-Msg03", []byte(cipherM3.EncryptedData))
if err != nil {
return
}
var plainM3 struct {
@@ -141,17 +332,21 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co
Signature string `tlv8:"10"`
}
if err = tlv8.Unmarshal(b, &plainM3); err != nil {
return err
return
}
clientPublic := s.GetPair(conn, plainM3.Identifier)
if clientPublic == nil {
return fmt.Errorf("hap: PairVerify from: %s, with unknown client_id: %s", conn.RemoteAddr(), plainM3.Identifier)
}
if s.GetClientPublic != nil {
clientPublic := s.GetClientPublic(plainM3.Identifier)
if clientPublic == nil {
err = errors.New("hap: PairVerify with unknown client_id: " + plainM3.Identifier)
return
}
b = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic)
if !ed25519.ValidateSignature(clientPublic, b, []byte(plainM3.Signature)) {
return errors.New("new: ValidateSignature")
b = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic)
if !ed25519.ValidateSignature(clientPublic, b, []byte(plainM3.Signature)) {
err = errors.New("hap: ValidateSignature")
return
}
}
// STEP M4. Response to iPhone
@@ -161,15 +356,41 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co
State: StateM4,
}
if body, err = tlv8.Marshal(payloadM4); err != nil {
return err
return
}
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
return err
return
}
if conn, err = secure.Client(conn, sessionShared, false); err != nil {
return err
}
id = plainM3.Identifier
sessionKey = sessionShared
return s.Handler(conn)
return
}
func WriteResponse(w *bufio.Writer, statusCode int, contentType string, body []byte) error {
header := fmt.Sprintf(
"HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %d\r\n\r\n",
statusCode, http.StatusText(statusCode), contentType, len(body),
)
body = append([]byte(header), body...)
if _, err := w.Write(body); err != nil {
return err
}
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)
//}
-252
View File
@@ -1,252 +0,0 @@
package hap
import (
"bufio"
"crypto/sha512"
"errors"
"fmt"
"io"
"net"
"net/http"
"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
"github.com/AlexxIT/go2rtc/pkg/hap/ed25519"
"github.com/AlexxIT/go2rtc/pkg/hap/hkdf"
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
"github.com/tadglines/go-pkgs/crypto/srp"
)
const (
PairMethodSetup = iota
PairMethodSetupWithAuth
PairMethodVerify
PairMethodAdd
PairMethodRemove
PairMethodList
)
func (s *Server) PairSetup(req *http.Request, rw *bufio.ReadWriter, conn net.Conn) error {
if req.Header.Get("Content-Type") != MimeTLV8 {
return errors.New("hap: wrong content type")
}
// STEP 1. Request from iPhone
var plainM1 struct {
Method byte `tlv8:"0"`
State byte `tlv8:"6"`
Flags uint32 `tlv8:"19"`
}
if err := tlv8.UnmarshalReader(io.LimitReader(rw, req.ContentLength), &plainM1); err != nil {
return err
}
if plainM1.State != StateM1 {
return newRequestError(plainM1)
}
username := []byte("Pair-Setup")
// Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE)
pake, err := srp.NewSRP(
"rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username),
)
if err != nil {
return err
}
pake.SaltLength = 16
salt, verifier, err := pake.ComputeVerifier([]byte(s.Pin))
session := pake.NewServerSession(username, salt, verifier)
// STEP 2. Response to iPhone
plainM2 := struct {
Salt string `tlv8:"2"`
PublicKey string `tlv8:"3"`
State byte `tlv8:"6"`
}{
State: StateM2,
PublicKey: string(session.GetB()),
Salt: string(salt),
}
body, err := tlv8.Marshal(plainM2)
if err != nil {
return err
}
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
return err
}
// STEP 3. Request from iPhone
if req, err = http.ReadRequest(rw.Reader); err != nil {
return err
}
var plainM3 struct {
SessionKey string `tlv8:"3"`
Proof string `tlv8:"4"`
State byte `tlv8:"6"`
}
if err = tlv8.UnmarshalReader(req.Body, &plainM3); err != nil {
return err
}
if plainM3.State != StateM3 {
return newRequestError(plainM3)
}
// important to compute key before verify client
sessionShared, err := session.ComputeKey([]byte(plainM3.SessionKey))
if err != nil {
return err
}
if !session.VerifyClientAuthenticator([]byte(plainM3.Proof)) {
return errors.New("hap: VerifyClientAuthenticator")
}
proof := session.ComputeAuthenticator([]byte(plainM3.Proof)) // server proof
// STEP 4. Response to iPhone
payloadM4 := struct {
Proof string `tlv8:"4"`
State byte `tlv8:"6"`
}{
Proof: string(proof),
State: StateM4,
}
if body, err = tlv8.Marshal(payloadM4); err != nil {
return err
}
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
return err
}
// STEP 5. Request from iPhone
if req, err = http.ReadRequest(rw.Reader); err != nil {
return err
}
var cipherM5 struct {
EncryptedData string `tlv8:"5"`
State byte `tlv8:"6"`
}
if err = tlv8.UnmarshalReader(req.Body, &cipherM5); err != nil {
return err
}
if cipherM5.State != StateM5 {
return newRequestError(cipherM5)
}
// decrypt message using session shared
encryptKey, err := hkdf.Sha512(sessionShared, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info")
if err != nil {
return err
}
b, err := chacha20poly1305.Decrypt(encryptKey, "PS-Msg05", []byte(cipherM5.EncryptedData))
if err != nil {
return err
}
// unpack message from TLV8
var plainM5 struct {
Identifier string `tlv8:"1"`
PublicKey string `tlv8:"3"`
Signature string `tlv8:"10"`
}
if err = tlv8.Unmarshal(b, &plainM5); err != nil {
return err
}
// 3. verify client ID and Public
remoteSign, err := hkdf.Sha512(
sessionShared, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info",
)
if err != nil {
return err
}
b = Append(remoteSign, plainM5.Identifier, plainM5.PublicKey)
if !ed25519.ValidateSignature([]byte(plainM5.PublicKey), b, []byte(plainM5.Signature)) {
return errors.New("hap: ValidateSignature")
}
// 4. generate signature to our ID and Public
localSign, err := hkdf.Sha512(
sessionShared, "Pair-Setup-Accessory-Sign-Salt", "Pair-Setup-Accessory-Sign-Info",
)
if err != nil {
return err
}
b = Append(localSign, s.DeviceID, s.ServerPublic()) // ServerPublic
signature, err := ed25519.Signature(s.DevicePrivate, b)
if err != nil {
return err
}
// 5. pack our ID and Public
plainM6 := struct {
Identifier string `tlv8:"1"`
PublicKey string `tlv8:"3"`
Signature string `tlv8:"10"`
}{
Identifier: s.DeviceID,
PublicKey: string(s.ServerPublic()),
Signature: string(signature),
}
if b, err = tlv8.Marshal(plainM6); err != nil {
return err
}
// 6. encrypt message
b, err = chacha20poly1305.Encrypt(encryptKey, "PS-Msg06", b)
if err != nil {
return err
}
// STEP 6. Response to iPhone
cipherM6 := struct {
EncryptedData string `tlv8:"5"`
State byte `tlv8:"6"`
}{
State: StateM6,
EncryptedData: string(b),
}
if body, err = tlv8.Marshal(cipherM6); err != nil {
return err
}
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
return err
}
s.AddPair(conn, plainM5.Identifier, []byte(plainM5.PublicKey), PermissionAdmin)
return nil
}
func WriteResponse(w *bufio.Writer, statusCode int, contentType string, body []byte) error {
header := fmt.Sprintf(
"HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %d\r\n\r\n",
statusCode, http.StatusText(statusCode), contentType, len(body),
)
body = append([]byte(header), body...)
if _, err := w.Write(body); err != nil {
return err
}
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)
}
+32
View File
@@ -0,0 +1,32 @@
package setup
import (
"strconv"
"strings"
)
const (
FlagNFC = 1
FlagIP = 2
FlagBLE = 4
FlagWAC = 8 // Wireless Accessory Configuration (WAC)/Apples MFi
)
func GenerateSetupURI(category, pin, setupID string) string {
c, _ := strconv.Atoi(category)
p, _ := strconv.Atoi(strings.ReplaceAll(pin, "-", ""))
payload := int64(c&0xFF)<<31 | int64(FlagIP&0xF)<<27 | int64(p&0x7FFFFFF)
return "X-HM://" + FormatInt36(payload, 9) + setupID
}
// FormatInt36 equal to strings.ToUpper(fmt.Sprintf("%0"+strconv.Itoa(n)+"s", strconv.FormatInt(value, 36)))
func FormatInt36(value int64, n int) string {
b := make([]byte, n)
for i := n - 1; 0 <= i; i-- {
b[i] = digits[value%36]
value /= 36
}
return string(b)
}
const digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+18
View File
@@ -0,0 +1,18 @@
package setup
import (
"fmt"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestFormatAlphaNum(t *testing.T) {
value := int64(999)
n := 5
s1 := strings.ToUpper(fmt.Sprintf("%0"+strconv.Itoa(n)+"s", strconv.FormatInt(value, 36)))
s2 := FormatInt36(value, n)
require.Equal(t, s1, s2)
}
+21 -2
View File
@@ -112,6 +112,10 @@ func appendValue(b []byte, tag byte, value reflect.Value) ([]byte, error) {
v := value.Uint()
return append(b, tag, 4, byte(v), byte(v>>8), byte(v>>16), byte(v>>24)), nil
case reflect.Uint64:
v := value.Uint()
return binary.LittleEndian.AppendUint64(append(b, tag, 8), v), nil
case reflect.Float32:
v := math.Float32bits(float32(value.Float()))
return append(b, tag, 4, byte(v), byte(v>>8), byte(v>>16), byte(v>>24)), nil
@@ -170,11 +174,20 @@ func UnmarshalBase64(in any, out any) error {
return Unmarshal(data, out)
}
func UnmarshalReader(r io.Reader, v any) error {
data, err := io.ReadAll(r)
func UnmarshalReader(r io.Reader, n int64, v any) error {
var data []byte
var err error
if n > 0 {
data = make([]byte, n)
_, err = io.ReadFull(r, data)
} else {
data, err = io.ReadAll(r)
}
if err != nil {
return err
}
return Unmarshal(data, v)
}
@@ -301,6 +314,12 @@ func unmarshalValue(v []byte, value reflect.Value) error {
}
value.SetUint(uint64(v[0]) | uint64(v[1])<<8 | uint64(v[2])<<16 | uint64(v[3])<<24)
case reflect.Uint64:
if len(v) != 8 {
return errors.New("tlv8: wrong size: " + value.Type().Name())
}
value.SetUint(binary.LittleEndian.Uint64(v))
case reflect.Float32:
f := math.Float32frombits(binary.LittleEndian.Uint32(v))
value.SetFloat(float64(f))
+15 -11
View File
@@ -49,7 +49,7 @@ func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer {
Connection: core.Connection{
ID: core.NewID(),
FormatName: "homekit",
Protocol: "udp",
Protocol: "rtp",
RemoteAddr: conn.RemoteAddr().String(),
Medias: medias,
Transport: conn,
@@ -59,7 +59,11 @@ func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer {
}
}
func (c *Consumer) SetOffer(offer *camera.SetupEndpoints) {
func (c *Consumer) SessionID() string {
return c.sessionID
}
func (c *Consumer) SetOffer(offer *camera.SetupEndpointsRequest) {
c.sessionID = offer.SessionID
c.videoSession = &srtp.Session{
Remote: &srtp.Endpoint{
@@ -79,32 +83,32 @@ func (c *Consumer) SetOffer(offer *camera.SetupEndpoints) {
}
}
func (c *Consumer) GetAnswer() *camera.SetupEndpoints {
func (c *Consumer) GetAnswer() *camera.SetupEndpointsResponse {
c.videoSession.Local = c.srtpEndpoint()
c.audioSession.Local = c.srtpEndpoint()
return &camera.SetupEndpoints{
return &camera.SetupEndpointsResponse{
SessionID: c.sessionID,
Status: []byte{0},
Address: camera.Addr{
Status: camera.StreamingStatusAvailable,
Address: camera.Address{
IPAddr: c.videoSession.Local.Addr,
VideoRTPPort: c.videoSession.Local.Port,
AudioRTPPort: c.audioSession.Local.Port,
},
VideoCrypto: camera.CryptoSuite{
VideoCrypto: camera.SRTPCryptoSuite{
MasterKey: string(c.videoSession.Local.MasterKey),
MasterSalt: string(c.videoSession.Local.MasterSalt),
},
AudioCrypto: camera.CryptoSuite{
AudioCrypto: camera.SRTPCryptoSuite{
MasterKey: string(c.audioSession.Local.MasterKey),
MasterSalt: string(c.audioSession.Local.MasterSalt),
},
VideoSSRC: []uint32{c.videoSession.Local.SSRC},
AudioSSRC: []uint32{c.audioSession.Local.SSRC},
VideoSSRC: c.videoSession.Local.SSRC,
AudioSSRC: c.audioSession.Local.SSRC,
}
}
func (c *Consumer) SetConfig(conf *camera.SelectedStreamConfig) bool {
func (c *Consumer) SetConfig(conf *camera.SelectedStreamConfiguration) bool {
if c.sessionID != conf.Control.SessionID {
return false
}
+13 -10
View File
@@ -13,7 +13,7 @@ var videoCodecs = [...]string{core.CodecH264}
var videoProfiles = [...]string{"4200", "4D00", "6400"}
var videoLevels = [...]string{"1F", "20", "28"}
func videoToMedia(codecs []camera.VideoCodec) *core.Media {
func videoToMedia(codecs []camera.VideoCodecConfiguration) *core.Media {
media := &core.Media{
Kind: core.KindVideo, Direction: core.DirectionRecvonly,
}
@@ -39,7 +39,7 @@ func videoToMedia(codecs []camera.VideoCodec) *core.Media {
var audioCodecs = [...]string{core.CodecPCMU, core.CodecPCMA, core.CodecELD, core.CodecOpus}
var audioSampleRates = [...]uint32{8000, 16000, 24000}
func audioToMedia(codecs []camera.AudioCodec) *core.Media {
func audioToMedia(codecs []camera.AudioCodecConfiguration) *core.Media {
media := &core.Media{
Kind: core.KindAudio, Direction: core.DirectionRecvonly,
}
@@ -67,10 +67,10 @@ func audioToMedia(codecs []camera.AudioCodec) *core.Media {
return media
}
func trackToVideo(track *core.Receiver, video0 *camera.VideoCodec) *camera.VideoCodec {
func trackToVideo(track *core.Receiver, video0 *camera.VideoCodecConfiguration, maxWidth, maxHeight int) *camera.VideoCodecConfiguration {
profileID := video0.CodecParams[0].ProfileID[0]
level := video0.CodecParams[0].Level[0]
attrs := video0.VideoAttrs[0]
var attrs camera.VideoCodecAttributes
if track != nil {
profile := h264.GetProfileLevelID(track.Codec.FmtpLine)
@@ -90,25 +90,28 @@ func trackToVideo(track *core.Receiver, video0 *camera.VideoCodec) *camera.Video
}
for _, s := range video0.VideoAttrs {
if (maxWidth > 0 && int(s.Width) > maxWidth) || (maxHeight > 0 && int(s.Height) > maxHeight) {
continue
}
if s.Width > attrs.Width || s.Height > attrs.Height {
attrs = s
}
}
}
return &camera.VideoCodec{
return &camera.VideoCodecConfiguration{
CodecType: video0.CodecType,
CodecParams: []camera.VideoParams{
CodecParams: []camera.VideoCodecParameters{
{
ProfileID: []byte{profileID},
Level: []byte{level},
},
},
VideoAttrs: []camera.VideoAttrs{attrs},
VideoAttrs: []camera.VideoCodecAttributes{attrs},
}
}
func trackToAudio(track *core.Receiver, audio0 *camera.AudioCodec) *camera.AudioCodec {
func trackToAudio(track *core.Receiver, audio0 *camera.AudioCodecConfiguration) *camera.AudioCodecConfiguration {
codecType := audio0.CodecType
channels := audio0.CodecParams[0].Channels
sampleRate := audio0.CodecParams[0].SampleRate[0]
@@ -131,9 +134,9 @@ func trackToAudio(track *core.Receiver, audio0 *camera.AudioCodec) *camera.Audio
}
}
return &camera.AudioCodec{
return &camera.AudioCodecConfiguration{
CodecType: codecType,
CodecParams: []camera.AudioParams{
CodecParams: []camera.AudioCodecParameters{
{
Channels: channels,
SampleRate: []byte{sampleRate},
+45
View File
@@ -0,0 +1,45 @@
package log
import (
"bytes"
"io"
"log"
"net/http"
)
func Debug(v any) {
switch v := v.(type) {
case *http.Request:
if v == nil {
return
}
if v.ContentLength != 0 {
b, err := io.ReadAll(v.Body)
if err != nil {
panic(err)
}
v.Body = io.NopCloser(bytes.NewReader(b))
log.Printf("[homekit] request: %s %s\n%s", v.Method, v.RequestURI, b)
} else {
log.Printf("[homekit] request: %s %s <nobody>", v.Method, v.RequestURI)
}
case *http.Response:
if v == nil {
return
}
if v.Header.Get("Content-Type") == "image/jpeg" {
log.Printf("[homekit] response: %d <jpeg>", v.StatusCode)
return
}
if v.ContentLength != 0 {
b, err := io.ReadAll(v.Body)
if err != nil {
panic(err)
}
v.Body = io.NopCloser(bytes.NewReader(b))
log.Printf("[homekit] response: %s %d\n%s", v.Proto, v.StatusCode, b)
} else {
log.Printf("[homekit] response: %s %d <nobody>", v.Proto, v.StatusCode)
}
}
}
+7 -19
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"math/rand"
"net"
"net/url"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
@@ -22,36 +21,25 @@ type Client struct {
hap *hap.Client
srtp *srtp.Server
videoConfig camera.SupportedVideoStreamConfig
audioConfig camera.SupportedAudioStreamConfig
videoConfig camera.SupportedVideoStreamConfiguration
audioConfig camera.SupportedAudioStreamConfiguration
videoSession *srtp.Session
audioSession *srtp.Session
stream *camera.Stream
Bitrate int // in bits/s
MaxWidth int `json:"-"`
MaxHeight int `json:"-"`
Bitrate int `json:"-"` // in bits/s
}
func Dial(rawURL string, server *srtp.Server) (*Client, error) {
u, err := url.Parse(rawURL)
conn, err := hap.Dial(rawURL)
if err != nil {
return nil, err
}
query := u.Query()
conn := &hap.Client{
DeviceAddress: u.Host,
DeviceID: query.Get("device_id"),
DevicePublic: hap.DecodeKey(query.Get("device_public")),
ClientID: query.Get("client_id"),
ClientPrivate: hap.DecodeKey(query.Get("client_private")),
}
if err = conn.Dial(); err != nil {
return nil, err
}
client := &Client{
Connection: core.Connection{
ID: core.NewID(),
@@ -129,7 +117,7 @@ func (c *Client) Start() error {
}
videoTrack := c.trackByKind(core.KindVideo)
videoCodec := trackToVideo(videoTrack, &c.videoConfig.Codecs[0])
videoCodec := trackToVideo(videoTrack, &c.videoConfig.Codecs[0], c.MaxWidth, c.MaxHeight)
audioTrack := c.trackByKind(core.KindAudio)
audioCodec := trackToAudio(audioTrack, &c.audioConfig.Codecs[0])
+33 -27
View File
@@ -4,31 +4,30 @@ import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"time"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
"github.com/AlexxIT/go2rtc/pkg/hap/hds"
"github.com/AlexxIT/go2rtc/pkg/hap/secure"
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
)
func ProxyHandler(pair ServerPair, dial func() (net.Conn, error)) hap.HandlerFunc {
type ServerProxy interface {
ServerPair
AddConn(conn any)
DelConn(conn any)
}
func ProxyHandler(srv ServerProxy, acc net.Conn) HandlerFunc {
return func(con net.Conn) error {
defer con.Close()
acc, err := dial()
if err != nil {
return err
}
defer acc.Close()
pr := &Proxy{
con: con.(*secure.Conn),
acc: acc.(*secure.Conn),
con: con.(*hap.Conn),
acc: acc.(*hap.Conn),
res: make(chan *http.Response),
}
@@ -36,17 +35,17 @@ func ProxyHandler(pair ServerPair, dial func() (net.Conn, error)) hap.HandlerFun
go pr.handleAcc()
// controller => accessory
return pr.handleCon(pair)
return pr.handleCon(srv)
}
}
type Proxy struct {
con *secure.Conn
acc *secure.Conn
con *hap.Conn
acc *hap.Conn
res chan *http.Response
}
func (p *Proxy) handleCon(pair ServerPair) error {
func (p *Proxy) handleCon(srv ServerProxy) error {
var hdsCharIID uint64
rd := bufio.NewReader(p.con)
@@ -61,7 +60,7 @@ func (p *Proxy) handleCon(pair ServerPair) error {
switch {
case req.Method == "POST" && req.URL.Path == hap.PathPairings:
var res *http.Response
if res, err = handlePairings(p.con, req, pair); err != nil {
if res, err = handlePairings(req, srv); err != nil {
return err
}
if err = res.Write(p.con); err != nil {
@@ -74,7 +73,7 @@ func (p *Proxy) handleCon(pair ServerPair) error {
_ = json.Unmarshal(body, &v)
for _, char := range v.Value {
if char.IID == hdsCharIID {
var hdsReq camera.SetupDataStreamRequest
var hdsReq camera.SetupDataStreamTransportRequest
_ = tlv8.UnmarshalBase64(char.Value, &hdsReq)
hdsConSalt = hdsReq.ControllerKeySalt
break
@@ -110,14 +109,14 @@ func (p *Proxy) handleCon(pair ServerPair) error {
_ = json.Unmarshal(body, &v)
for i, char := range v.Value {
if char.IID == hdsCharIID {
var hdsRes camera.SetupDataStreamResponse
var hdsRes camera.SetupDataStreamTransportResponse
_ = tlv8.UnmarshalBase64(char.Value, &hdsRes)
hdsAccSalt := hdsRes.AccessoryKeySalt
hdsPort := int(hdsRes.TransportTypeSessionParameters.TCPListeningPort)
// swtich accPort to conPort
hdsPort, err = p.listenHDS(hdsPort, hdsConSalt+hdsAccSalt)
hdsPort, err = p.listenHDS(srv, hdsPort, hdsConSalt+hdsAccSalt)
if err != nil {
return err
}
@@ -149,7 +148,7 @@ func (p *Proxy) handleAcc() error {
}
if res.Proto == hap.ProtoEvent {
if err = res.Write(p.con); err != nil {
if err = hap.WriteEvent(p.con, res); err != nil {
return err
}
continue
@@ -166,7 +165,8 @@ func (p *Proxy) handleAcc() error {
}
}
func (p *Proxy) listenHDS(accPort int, salt string) (int, error) {
func (p *Proxy) listenHDS(srv ServerProxy, accPort int, salt string) (int, error) {
// The TCP port range for HDS must be >= 32768.
ln, err := net.ListenTCP("tcp", nil)
if err != nil {
return 0, err
@@ -175,30 +175,36 @@ func (p *Proxy) listenHDS(accPort int, salt string) (int, error) {
go func() {
defer ln.Close()
_ = ln.SetDeadline(time.Now().Add(30 * time.Second))
// raw controller conn
con, err := ln.Accept()
conn1, err := ln.Accept()
if err != nil {
return
}
defer con.Close()
defer conn1.Close()
// secured controller conn (controlle=false because we are accessory)
con, err = hds.Client(con, p.con.SharedKey, salt, false)
con, err := hds.NewConn(conn1, p.con.SharedKey, salt, false)
if err != nil {
return
}
srv.AddConn(con)
defer srv.DelConn(con)
accIP := p.acc.RemoteAddr().(*net.TCPAddr).IP
// raw accessory conn
acc, err := net.Dial("tcp", fmt.Sprintf("%s:%d", accIP, accPort))
conn2, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: accIP, Port: accPort})
if err != nil {
return
}
defer acc.Close()
defer conn2.Close()
// secured accessory conn (controller=true because we are controller)
acc, err = hds.Client(acc, p.acc.SharedKey, salt, true)
acc, err := hds.NewConn(conn2, p.acc.SharedKey, salt, true)
if err != nil {
return
}
+12 -47
View File
@@ -15,15 +15,17 @@ import (
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
)
type HandlerFunc func(net.Conn) error
type Server interface {
ServerPair
ServerAccessory
}
type ServerPair interface {
GetPair(conn net.Conn, id string) []byte
AddPair(conn net.Conn, id string, public []byte, permissions byte)
DelPair(conn net.Conn, id string)
GetPair(id string) []byte
AddPair(id string, public []byte, permissions byte)
DelPair(id string)
}
type ServerAccessory interface {
@@ -33,11 +35,11 @@ type ServerAccessory interface {
GetImage(conn net.Conn, width, height int) []byte
}
func ServerHandler(server Server) hap.HandlerFunc {
func ServerHandler(server Server) HandlerFunc {
return handleRequest(func(conn net.Conn, req *http.Request) (*http.Response, error) {
switch req.URL.Path {
case hap.PathPairings:
return handlePairings(conn, req, server)
return handlePairings(req, server)
case hap.PathAccessories:
body := hap.JSONAccessories{Value: server.GetAccessories(conn)}
@@ -103,7 +105,7 @@ func ServerHandler(server Server) hap.HandlerFunc {
})
}
func handleRequest(handle func(conn net.Conn, req *http.Request) (*http.Response, error)) hap.HandlerFunc {
func handleRequest(handle func(conn net.Conn, req *http.Request) (*http.Response, error)) HandlerFunc {
return func(conn net.Conn) error {
rw := bufio.NewReaderSize(conn, 16*1024)
wr := bufio.NewWriterSize(conn, 16*1024)
@@ -130,7 +132,7 @@ func handleRequest(handle func(conn net.Conn, req *http.Request) (*http.Response
}
}
func handlePairings(conn net.Conn, req *http.Request, pair ServerPair) (*http.Response, error) {
func handlePairings(req *http.Request, srv ServerPair) (*http.Response, error) {
cmd := struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
@@ -139,15 +141,15 @@ func handlePairings(conn net.Conn, req *http.Request, pair ServerPair) (*http.Re
Permissions byte `tlv8:"11"`
}{}
if err := tlv8.UnmarshalReader(req.Body, &cmd); err != nil {
if err := tlv8.UnmarshalReader(req.Body, req.ContentLength, &cmd); err != nil {
return nil, err
}
switch cmd.Method {
case 3: // add
pair.AddPair(conn, cmd.Identifier, []byte(cmd.PublicKey), cmd.Permissions)
srv.AddPair(cmd.Identifier, []byte(cmd.PublicKey), cmd.Permissions)
case 4: // delete
pair.DelPair(conn, cmd.Identifier)
srv.DelPair(cmd.Identifier)
}
body := struct {
@@ -190,40 +192,3 @@ func makeResponse(mime string, v any) (*http.Response, error) {
}
return res, nil
}
//func debug(v any) {
// switch v := v.(type) {
// case *http.Request:
// if v == nil {
// return
// }
// if v.ContentLength != 0 {
// b, err := io.ReadAll(v.Body)
// if err != nil {
// panic(err)
// }
// v.Body = io.NopCloser(bytes.NewReader(b))
// log.Printf("[homekit] request: %s %s\n%s", v.Method, v.RequestURI, b)
// } else {
// log.Printf("[homekit] request: %s %s <nobody>", v.Method, v.RequestURI)
// }
// case *http.Response:
// if v == nil {
// return
// }
// if v.Header.Get("Content-Type") == "image/jpeg" {
// log.Printf("[homekit] response: %d <jpeg>", v.StatusCode)
// return
// }
// if v.ContentLength != 0 {
// b, err := io.ReadAll(v.Body)
// if err != nil {
// panic(err)
// }
// v.Body = io.NopCloser(bytes.NewReader(b))
// log.Printf("[homekit] response: %d\n%s", v.StatusCode, b)
// } else {
// log.Printf("[homekit] response: %d <nobody>", v.StatusCode)
// }
// }
//}
+2 -1
View File
@@ -330,6 +330,7 @@ const (
StreamTypeH264 = 0x1B
StreamTypeH265 = 0x24
StreamTypePCMATapo = 0x90
StreamTypePCMUTapo = 0x91
StreamTypePrivateOPUS = 0xEB
)
@@ -392,7 +393,7 @@ func (p *PES) GetPacket() (pkt *rtp.Packet) {
//p.Timestamp += aac.RTPTimeSize(pkt.Payload) // update next timestamp!
case StreamTypePCMATapo:
case StreamTypePCMATapo, StreamTypePCMUTapo:
p.Sequence++
pkt = &rtp.Packet{

Some files were not shown because too many files have changed in this diff Show More