Compare commits
287 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfe47559d1 | |||
| eabd7d60cd | |||
| dfda4b11ff | |||
| 353262307b | |||
| d734140eaf | |||
| 2409bb56d7 | |||
| df484cc904 | |||
| 7e0c7a8173 | |||
| 7eaa4a1b55 | |||
| 03941a5691 | |||
| 28821c41e0 | |||
| 8636e96379 | |||
| 7119384184 | |||
| 57b0ace802 | |||
| b0f46bc919 | |||
| a4d4598a13 | |||
| 17c1f69f66 | |||
| fb31a251b8 | |||
| c5277daa45 | |||
| 76a5e160c2 | |||
| 494daed937 | |||
| a86e10446a | |||
| 209b73a0f1 | |||
| 54473ff1de | |||
| 7eb5fe0355 | |||
| fbd5215669 | |||
| 3ebc115cc5 | |||
| 6d1a95a4e3 | |||
| 4ec2849008 | |||
| 4ef6a147a6 | |||
| 94df080bf7 | |||
| 86edd814c9 | |||
| 5a259993d8 | |||
| b6fe8871df | |||
| b47a2ba73c | |||
| 4934fa4cc1 | |||
| 61f74820bc | |||
| 248fc7a11a | |||
| aa0ece2d1e | |||
| 68036b68c1 | |||
| 319dbf2c63 | |||
| 4b419309a8 | |||
| 42e7a03534 | |||
| 290e8fcfda | |||
| 6c78b5cb53 | |||
| c72b205d87 | |||
| 2cd009646a | |||
| 8f5fce4d73 | |||
| b705cadc04 | |||
| 2523a5ac38 | |||
| e246e2e756 | |||
| 2dc0d58ba7 | |||
| cb22ae7833 | |||
| c98b0a83c4 | |||
| 0bae158e41 | |||
| e2b63a4f6c | |||
| 3897f10a4d | |||
| ac80f1470e | |||
| 1fe602679e | |||
| e2c7d06730 | |||
| 2133f5323c | |||
| c10a06d199 | |||
| d053d88ce9 | |||
| 2ce38b4486 | |||
| 44d59b1696 | |||
| 15ec995ecc | |||
| 231cab36b2 | |||
| 640db3029e | |||
| 2836fdae13 | |||
| 964bb225fa | |||
| 5cc32197b8 | |||
| bc1a4ac8e4 | |||
| 158f9d3a08 | |||
| 81cfcf877a | |||
| 96919bf9e3 | |||
| e4359ac217 | |||
| 56d7a6fee4 | |||
| 292b32af99 | |||
| 33e4527042 | |||
| 62a9046f01 | |||
| 25e7ac531e | |||
| 0f27bb1124 | |||
| 0ff3bf67e1 | |||
| 6d3d45e337 | |||
| c6940eb0f3 | |||
| 8142d2fc43 | |||
| 721ed98afb | |||
| fb8c6e1b1b | |||
| 80ab32379c | |||
| 863174839c | |||
| ff18283d11 | |||
| 994e0dc526 | |||
| 7254bd4fbc | |||
| 9f407a754d | |||
| cc97bc33c4 | |||
| bd8e4fa298 | |||
| 6db4dda535 | |||
| be80eb1ac9 | |||
| a8d2312cb0 | |||
| 5bbc2aaf2e | |||
| 7abc963a50 | |||
| f8c88cfbe0 | |||
| cf4acd5a8d | |||
| 19226df7d6 | |||
| c415c8f2af | |||
| 2751f41afb | |||
| d59cb99f0d | |||
| 007e8dbc75 | |||
| dae396a1ed | |||
| d894483166 | |||
| 60ef52f44b | |||
| 240e1960f8 | |||
| a107d13e61 | |||
| f911936d79 | |||
| ea23957f2a | |||
| f1971cec7c | |||
| 7291c03cea | |||
| fdb3116027 | |||
| c87885be48 | |||
| 98f88d037e | |||
| fde1fdc592 | |||
| cca216ace5 | |||
| e953e949ef | |||
| fe2cc4b525 | |||
| 6a67fc3712 | |||
| 94b7c33485 | |||
| 887f0f4890 | |||
| ec08dfee9c | |||
| 54b95dced4 | |||
| 37d7409e82 | |||
| 4dd1f73a18 | |||
| 22cc8ac2c4 | |||
| a667acad07 | |||
| ee5e31d3b3 | |||
| c196b82a72 | |||
| 670370056c | |||
| 0c5a2bf02b | |||
| 8f9e78be0c | |||
| 50d5fa93b6 | |||
| 3a0e4078fd | |||
| 549da0257e | |||
| 2b5f9429a8 | |||
| c7119f4403 | |||
| 7d9862202a | |||
| dd7130d2d4 | |||
| d697bdcf05 | |||
| abd61919cf | |||
| ad2383de80 | |||
| 9f3ff98951 | |||
| d286980548 | |||
| 87533ac5a1 | |||
| d05451416d | |||
| 5c01cbad9e | |||
| f2242e31c8 | |||
| 975a43d392 | |||
| 3d38e5e567 | |||
| 7d2ad92c4b | |||
| b82023bc32 | |||
| a92e04b6e0 | |||
| 56e61a85ee | |||
| 7d83b0d6c8 | |||
| 708277230a | |||
| 06e6e31443 | |||
| 8474f2f571 | |||
| 9ddea7d9d6 | |||
| c2fbd372b3 | |||
| e9611769be | |||
| c5dfa84ff2 | |||
| da68101c09 | |||
| 30c418542c | |||
| b58c1a7ed6 | |||
| 47e87281a1 | |||
| 60b6b93ff8 | |||
| 3149b6f750 | |||
| e44f1ad53e | |||
| c38c8a7fce | |||
| 9efc717633 | |||
| a2d422f5cb | |||
| 3036dd7cfe | |||
| 5be5d9247c | |||
| 42b7eea852 | |||
| 7ee3f6e4f7 | |||
| fbd8d995ed | |||
| 998c85d6f5 | |||
| 67dfc942a0 | |||
| 1cc8b373de | |||
| f045f3fccd | |||
| 691f6d9cdd | |||
| 8a8fb66eeb | |||
| e7044a93f6 | |||
| a9bcb46f38 | |||
| 16a812c8b8 | |||
| 27fe2622ec | |||
| 3d222136f9 | |||
| 524cdb7176 | |||
| 499dc10390 | |||
| e7bd3d401f | |||
| 5ec942cb5e | |||
| 6c255cd2f2 | |||
| 4f969d750a | |||
| bd2cbe20e0 | |||
| 6d8d6a91ef | |||
| 05c12b34e5 | |||
| 43b7a662c1 | |||
| a7e76db464 | |||
| b797a2fcd1 | |||
| 6e35f1a389 | |||
| 30d48e139c | |||
| e74fc6f198 | |||
| e00d211619 | |||
| c68e3cafe4 | |||
| 7bb0f0d2e6 | |||
| 96d7066085 | |||
| ae49946740 | |||
| fcc91c9b8a | |||
| 994fd41826 | |||
| 3282b38900 | |||
| 647b2acf48 | |||
| ef318f663e | |||
| 5771454400 | |||
| 6732e726d5 | |||
| 45503aa8c5 | |||
| b6579122d1 | |||
| 42a67f8ad5 | |||
| 91eeefec68 | |||
| 8ab7aeb8b2 | |||
| 493fa1ef6a | |||
| 020549ef60 | |||
| dfc1f45f97 | |||
| 641e65ee95 | |||
| 24ca87e00d | |||
| 859cd1cbe6 | |||
| 79656d1344 | |||
| 759f979182 | |||
| 7c17e64090 | |||
| bf45f64a7e | |||
| 72890d5983 | |||
| e8e798d955 | |||
| 8a8b379bfc | |||
| ca491def83 | |||
| 5a597277a9 | |||
| c90fcd1ce1 | |||
| e0687db9e2 | |||
| 24310e2f7a | |||
| a1f0b86ab3 | |||
| 7f87c6e478 | |||
| a0145b4b24 | |||
| 2fcbb1d836 | |||
| a2beea1bbd | |||
| e5e55b7a50 | |||
| 0830d8342e | |||
| adb1b21e81 | |||
| edfa09bb9f | |||
| 2eef7bdbd3 | |||
| 124556f4db | |||
| d528e167db | |||
| f151969fe1 | |||
| 251686608a | |||
| 993aa613fd | |||
| 2fdfec6f21 | |||
| a0d8f3ae81 | |||
| fd5746a954 | |||
| 2c3813deb9 | |||
| a8b51ad619 | |||
| 49c4d45731 | |||
| 1282b23c57 | |||
| 54afd0b50b | |||
| 6ee748a87a | |||
| 0ebda76cc8 | |||
| 68b3dc6f37 | |||
| 3c83102e91 | |||
| dd77c3e1f0 | |||
| e5e1f6bd05 | |||
| ac96b64c64 | |||
| fa8fd60ac6 | |||
| 9c9393e0cf | |||
| 8b26f9309f | |||
| 4027809f32 | |||
| b28ffa9543 | |||
| 7836f2e47f | |||
| 648873978c | |||
| c51c13b4b8 | |||
| 9a7c7d2a4b | |||
| 5f17474ff4 | |||
| 3376bf8b99 | |||
| ad1bea088e | |||
| d0ac99fc69 |
+10
-10
@@ -124,7 +124,7 @@ jobs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ github.repository }}
|
||||
name=${{ github.repository }},enable=${{ github.event.repository.fork == false }}
|
||||
ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
@@ -138,14 +138,14 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name == 'push' && github.event.repository.fork == false
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ github.repository }}
|
||||
name=${{ github.repository }},enable=${{ github.event.repository.fork == false }}
|
||||
ghcr.io/${{ github.repository }}
|
||||
flavor: |
|
||||
suffix=-hardware,onlatest=true
|
||||
@@ -198,14 +198,14 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name == 'push' && github.event.repository.fork == false
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -236,7 +236,7 @@ jobs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ github.repository }}
|
||||
name=${{ github.repository }},enable=${{ github.event.repository.fork == false }}
|
||||
ghcr.io/${{ github.repository }}
|
||||
flavor: |
|
||||
suffix=-rockchip,onlatest=true
|
||||
@@ -253,14 +253,14 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name == 'push' && github.event.repository.fork == false
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
|
||||
@@ -9,6 +9,9 @@ go2rtc_linux*
|
||||
go2rtc_mac*
|
||||
go2rtc_win*
|
||||
|
||||
/go2rtc
|
||||
/go2rtc.exe
|
||||
|
||||
0_test.go
|
||||
|
||||
.DS_Store
|
||||
|
||||
@@ -19,14 +19,12 @@ 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 2-way [codecs negotiation](#codecs-negotiation)
|
||||
- multi-source two-way [codecs negotiation](#codecs-negotiation)
|
||||
- mixing tracks from different sources to single stream
|
||||
- auto-match client-supported codecs
|
||||
- [2-way audio](#two-way-audio) for some cameras
|
||||
- streaming from private networks via [ngrok](#module-ngrok)
|
||||
- [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)
|
||||
|
||||
**Inspired by:**
|
||||
@@ -39,6 +37,9 @@ Ultimate camera streaming application with support for RTSP, WebRTC, HomeKit, FF
|
||||
- HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap)
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
* [Fast start](#fast-start)
|
||||
@@ -64,17 +65,22 @@ Ultimate camera streaming application with support for RTSP, WebRTC, HomeKit, FF
|
||||
* [Source: DVRIP](#source-dvrip)
|
||||
* [Source: Tapo](#source-tapo)
|
||||
* [Source: Kasa](#source-kasa)
|
||||
* [Source: Tuya](#source-tuya)
|
||||
* [Source: Xiaomi](#source-xiaomi)
|
||||
* [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)
|
||||
@@ -133,7 +139,7 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
||||
|
||||
### 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), [ngrok](#module-ngrok), and [Python](#source-echo).
|
||||
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
|
||||
|
||||
@@ -198,13 +204,18 @@ 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
|
||||
|
||||
@@ -219,7 +230,11 @@ 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
|
||||
- [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)).
|
||||
@@ -523,6 +538,15 @@ streams:
|
||||
- dvrip://username:password@192.168.1.123:34567?backchannel=1
|
||||
```
|
||||
|
||||
#### 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)*
|
||||
@@ -569,6 +593,18 @@ streams:
|
||||
|
||||
Tested: KD110, KC200, KC401, KC420WS, EC71.
|
||||
|
||||
#### 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](https://github.com/AlexxIT/go2rtc/blob/master/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](https://github.com/AlexxIT/go2rtc/blob/master/internal/xiaomi/README.md).
|
||||
|
||||
#### Source: GoPro
|
||||
|
||||
*[New in v1.8.3](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.3)*
|
||||
@@ -646,6 +682,16 @@ streams:
|
||||
nest-doorbell: nest:?client_id=***&client_secret=***&refresh_token=***&project_id=***&device_id=***
|
||||
```
|
||||
|
||||
#### 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.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
ring: ring:?device_id=XXX&refresh_token=XXX
|
||||
ring_snapshot: ring:?device_id=XXX&refresh_token=XXX&snapshot
|
||||
```
|
||||
|
||||
#### Source: Roborock
|
||||
|
||||
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||
@@ -660,6 +706,21 @@ 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: Doorbird
|
||||
|
||||
*[New in v1.9.11](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.11)*
|
||||
|
||||
This source type supports Doorbird devices including MJPEG stream, audio stream as well as two-way audio.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
#### Source: WebRTC
|
||||
|
||||
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||
@@ -688,7 +749,7 @@ Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-str
|
||||
|
||||
**switchbot**
|
||||
|
||||
Support connection to [SwitchBot](https://us.switch-bot.com/) cameras that are based on Kinesis Video Streams. Specifically, this includes [Pan/Tilt Cam Plus 2K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-2k) and [Pan/Tilt Cam Plus 3K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-3k). `Outdoor Spotlight Cam 1080P`, `Outdoor Spotlight Cam 2K`, `Pan/Tilt Cam`, `Pan/Tilt Cam 2K`, `Indoor Cam` are based on Tuya, so this feature is not available.
|
||||
Support connection to [SwitchBot](https://us.switch-bot.com/) cameras that are based on Kinesis Video Streams. Specifically, this includes [Pan/Tilt Cam Plus 2K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-2k) and [Pan/Tilt Cam Plus 3K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-3k) and [Smart Video Doorbell](https://www.switchbot.jp/products/switchbot-smart-video-doorbell). `Outdoor Spotlight Cam 1080P`, `Outdoor Spotlight Cam 2K`, `Pan/Tilt Cam`, `Pan/Tilt Cam 2K`, `Indoor Cam` are based on Tuya, so this feature is not available.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
@@ -697,7 +758,7 @@ streams:
|
||||
webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}]
|
||||
webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze
|
||||
webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}]
|
||||
webrtc-switchbot: webrtc:wss://...amazonaws.com/?...#format=switchbot#resolution=hd#client_id=...#ice_servers=[{...},{...}]
|
||||
webrtc-switchbot: webrtc:wss://...amazonaws.com/?...#format=switchbot#resolution=hd#play_type=0#client_id=...#ice_servers=[{...},{...}]
|
||||
```
|
||||
|
||||
**PS.** For `kinesis` sources, you can use [echo](#source-echo) to get connection params using `bash`, `python` or any other script language.
|
||||
@@ -822,6 +883,26 @@ 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
|
||||
|
||||
You can preload any stream on go2rtc start. This is useful for cameras that take a long time to start up.
|
||||
|
||||
```yaml
|
||||
preload:
|
||||
camera1: # default: video&audio = ANY
|
||||
camera2: "video" # preload only video track
|
||||
camera3: "video=h264&audio=opus" # preload H264 video and OPUS audio
|
||||
|
||||
streams:
|
||||
camera1:
|
||||
- rtsp://192.168.1.100/stream
|
||||
camera2:
|
||||
- rtsp://192.168.1.101/stream
|
||||
camera3:
|
||||
- rtsp://192.168.1.102/h265stream
|
||||
- ffmpeg:camera3#video=h264#audio=opus#hardware
|
||||
```
|
||||
|
||||
### Module: API
|
||||
|
||||
The HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.
|
||||
@@ -843,6 +924,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)
|
||||
@@ -940,15 +1022,6 @@ webrtc:
|
||||
- stun:8555 # if you have a dynamic public IP address
|
||||
```
|
||||
|
||||
**Private IP**
|
||||
|
||||
- setup integration with [ngrok service](#module-ngrok)
|
||||
|
||||
```yaml
|
||||
ngrok:
|
||||
command: ...
|
||||
```
|
||||
|
||||
**Hard tech way 1. Own TCP-tunnel**
|
||||
|
||||
If you have a personal [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), you can create a TCP tunnel and setup in the same way as "Static public IP". But use your VPS IP address in the YAML config.
|
||||
@@ -1046,63 +1119,9 @@ webtorrent:
|
||||
|
||||
Link example: https://alexxit.github.io/go2rtc/#share=02SNtgjKXY&pwd=wznEQqznxW&media=video+audio
|
||||
|
||||
TODO: article on how it works...
|
||||
|
||||
### Module: ngrok
|
||||
|
||||
With ngrok integration, you can get external access to your streams in situations when you have Internet with a private IP address.
|
||||
|
||||
- ngrok is pre-installed for **Docker** and **Hass add-on** users
|
||||
- you may need external access for two different things:
|
||||
- WebRTC stream, so you need a tunnel WebRTC TCP port (ex. 8555)
|
||||
- go2rtc web interface, so you need a tunnel API HTTP port (ex. 1984)
|
||||
- ngrok supports authorization for your web interface
|
||||
- ngrok automatically adds HTTPS to your web interface
|
||||
|
||||
The ngrok free subscription has the following limitations:
|
||||
|
||||
- You can reserve a free domain for serving the web interface, but the TCP address you get will always be random and change with each restart of the ngrok agent (not a problem for WebRTC stream)
|
||||
- You can forward multiple ports from a single agent, but you can only run one ngrok agent on the free plan
|
||||
|
||||
go2rtc will automatically get your external TCP address (if you enable it in ngrok config) and use it with WebRTC connection (if you enable it in webrtc config).
|
||||
|
||||
You need to manually download the [ngrok agent app](https://ngrok.com/download) for your OS and register with the [ngrok service](https://ngrok.com/signup).
|
||||
|
||||
**Tunnel for only WebRTC Stream**
|
||||
|
||||
You need to add your [ngrok authtoken](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
|
||||
|
||||
```yaml
|
||||
ngrok:
|
||||
command: ngrok tcp 8555 --authtoken eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
|
||||
```
|
||||
|
||||
**Tunnel for WebRTC and Web interface**
|
||||
|
||||
You need to create `ngrok.yaml` config file and add it to the go2rtc config:
|
||||
|
||||
```yaml
|
||||
ngrok:
|
||||
command: ngrok start --all --config ngrok.yaml
|
||||
```
|
||||
|
||||
ngrok config example:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
authtoken: eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
|
||||
tunnels:
|
||||
api:
|
||||
addr: 1984 # use the same port as in the go2rtc config
|
||||
proto: http
|
||||
basic_auth:
|
||||
- admin:password # you can set login/pass for your web interface
|
||||
webrtc:
|
||||
addr: 8555 # use the same port as in the go2rtc config
|
||||
proto: tcp
|
||||
```
|
||||
|
||||
See the [ngrok agent documentation](https://ngrok.com/docs/agent/config/) for more details on the ngrok configuration file.
|
||||
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](https://github.com/AlexxIT/go2rtc/blob/master/internal/ngrok/README.md)).
|
||||
|
||||
### Module: Hass
|
||||
|
||||
@@ -1221,7 +1240,6 @@ log:
|
||||
level: info # default level
|
||||
api: trace
|
||||
exec: debug
|
||||
ngrok: info
|
||||
rtsp: warn
|
||||
streams: error
|
||||
webrtc: fatal
|
||||
@@ -1229,6 +1247,27 @@ log:
|
||||
|
||||
## 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.
|
||||
|
||||
This is not a problem if you trust your local network as much as I do. But you can change this behaviour with a `go2rtc.yaml` config:
|
||||
@@ -1249,7 +1288,7 @@ webrtc:
|
||||
- external access to WebRTC TCP port is not a problem, because it is used only for transmitting encrypted media data
|
||||
- anyway you need to open this port to your local network and to the Internet for WebRTC to work
|
||||
|
||||
If you need web interface protection without the Home Assistant add-on, you need to use a reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [ngrok](https://ngrok.com/), etc.
|
||||
If you need web interface protection without the Home Assistant add-on, you need to use a reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), etc.
|
||||
|
||||
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.
|
||||
|
||||
@@ -1278,25 +1317,22 @@ Some examples:
|
||||
|
||||
## 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 [Hass 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**
|
||||
|
||||
@@ -1307,7 +1343,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**
|
||||
@@ -1380,7 +1416,8 @@ streams:
|
||||
|
||||
## Projects using go2rtc
|
||||
|
||||
- [Frigate](https://frigate.video/) 0.12+ - 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
|
||||
- [Frigate Lovelace Card](https://github.com/dermotduffy/frigate-hass-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
|
||||
@@ -1421,27 +1458,3 @@ streams:
|
||||
**Snapshots to Telegram**
|
||||
|
||||
[read more](https://github.com/AlexxIT/go2rtc/wiki/Snapshot-to-Telegram)
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q. What's the difference between go2rtc, WebRTC Camera and RTSPtoWebRTC?**
|
||||
|
||||
**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. It is compatible with native Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration. So you [can use](#module-hass) default Lovelace Picture Entity or Picture Glance.
|
||||
|
||||
**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 Hass 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 0.12+ add-on.
|
||||
|
||||
**Q. Which RTSP link should I use inside Hass?**
|
||||
|
||||
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 Hass. And your cameras will have multiple connections. Some from Hass 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 Hass via RTSP protocol, some from your browser via WebRTC/MSE/HLS protocols.
|
||||
|
||||
Use any config that you like.
|
||||
|
||||
**Q. What about Lovelace card with support for two-way audio?**
|
||||
|
||||
At this moment, I am focused on improving stability and adding new features to **go2rtc**. Maybe someone could write such a card themselves. It's not difficult, I have [some sketches](https://github.com/AlexxIT/go2rtc/blob/master/www/webrtc.html).
|
||||
|
||||
@@ -237,6 +237,62 @@ paths:
|
||||
|
||||
|
||||
|
||||
/api/preload:
|
||||
get:
|
||||
summary: Get all preloaded streams
|
||||
tags: [ Streams list ]
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
content:
|
||||
application/json: { example: { camera1: "video&audio", camera2: "video" } }
|
||||
put:
|
||||
summary: Preload new stream
|
||||
tags: [ Streams list ]
|
||||
parameters:
|
||||
- name: src
|
||||
in: query
|
||||
description: Stream source (name)
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: "camera1"
|
||||
- name: video
|
||||
in: query
|
||||
description: Video codecs filter
|
||||
required: false
|
||||
schema: { type: string }
|
||||
example: all,h264,h265,...
|
||||
- name: audio
|
||||
in: query
|
||||
description: Audio codecs filter
|
||||
required: false
|
||||
schema: { type: string }
|
||||
example: all,aac,opus,...
|
||||
- name: microphone
|
||||
in: query
|
||||
description: Microphone codecs filter
|
||||
required: false
|
||||
schema: { type: string }
|
||||
example: all,aac,opus,...
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
delete:
|
||||
summary: Delete preloaded stream
|
||||
tags: [ Streams list ]
|
||||
parameters:
|
||||
- name: src
|
||||
in: query
|
||||
description: Stream source (name)
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: "camera1"
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
/api/streams?src={src}:
|
||||
get:
|
||||
summary: Get stream info in JSON format
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:labs
|
||||
|
||||
# 0. Prepare images
|
||||
ARG PYTHON_VERSION="3.11"
|
||||
ARG PYTHON_VERSION="3.13"
|
||||
ARG GO_VERSION="1.25"
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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()
|
||||
}
|
||||
@@ -1,49 +1,50 @@
|
||||
module github.com/AlexxIT/go2rtc
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/asticode/go-astits v1.13.0
|
||||
github.com/expr-lang/expr v1.17.5
|
||||
github.com/asticode/go-astits v1.14.0
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1
|
||||
github.com/expr-lang/expr v1.17.6
|
||||
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.66
|
||||
github.com/pion/ice/v4 v4.0.10
|
||||
github.com/pion/interceptor v0.1.40
|
||||
github.com/pion/rtcp v1.2.15
|
||||
github.com/pion/rtp v1.8.20
|
||||
github.com/pion/sdp/v3 v3.0.14
|
||||
github.com/pion/srtp/v3 v3.0.6
|
||||
github.com/pion/stun/v3 v3.0.0
|
||||
github.com/pion/webrtc/v4 v4.1.3
|
||||
github.com/miekg/dns v1.1.69
|
||||
github.com/pion/ice/v4 v4.1.0
|
||||
github.com/pion/interceptor v0.1.42
|
||||
github.com/pion/rtcp v1.2.16
|
||||
github.com/pion/rtp v1.8.26
|
||||
github.com/pion/sdp/v3 v3.0.16
|
||||
github.com/pion/srtp/v3 v3.0.9
|
||||
github.com/pion/stun/v3 v3.0.2
|
||||
github.com/pion/webrtc/v4 v4.1.8
|
||||
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.10.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/net v0.48.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.6 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.9 // 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.39 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v4 v4.0.2 // indirect
|
||||
github.com/pion/sctp v1.8.41 // indirect
|
||||
github.com/pion/transport/v3 v3.1.1 // indirect
|
||||
github.com/pion/turn/v4 v4.1.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
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/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=
|
||||
@@ -30,60 +30,40 @@ 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.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||
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/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/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/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/ice/v4 v4.1.0 h1:YlxIii2bTPWyC08/4hdmtYq4srbrY0T9xcTsTjldGqU=
|
||||
github.com/pion/ice/v4 v4.1.0/go.mod h1:5gPbzYxqenvn05k7zKPIZFuSAufolygiy6P1U9HzvZ4=
|
||||
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/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/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/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/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/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/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/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/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/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
|
||||
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
|
||||
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/sctp v1.8.41 h1:20R4OHAno4Vky3/iE4xccInAScAa83X6nWUfyc65MIs=
|
||||
github.com/pion/sctp v1.8.41/go.mod h1:2wO6HBycUH7iCssuGyc2e9+0giXVW0pyCv3ZuL8LiyY=
|
||||
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.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY=
|
||||
github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8=
|
||||
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/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/turn/v4 v4.1.3 h1:jVNW0iR05AS94ysEtvzsrk3gKs9Zqxf6HmnsLfRvlzA=
|
||||
github.com/pion/turn/v4 v4.1.3/go.mod h1:TD/eiBUf5f5LwXbCJa35T7dPtTpCHRJ9oJWmyPLVT3A=
|
||||
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/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=
|
||||
@@ -100,41 +80,32 @@ 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/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/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/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/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
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/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
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/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/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.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/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
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
@@ -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"`)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/AlexxIT/go2rtc/pkg/creds"
|
||||
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
||||
)
|
||||
|
||||
@@ -71,13 +71,15 @@ func initConfig(confs flagConfig) {
|
||||
// config as file
|
||||
if ConfigPath == "" {
|
||||
ConfigPath = conf
|
||||
initStorage()
|
||||
}
|
||||
|
||||
if data, _ = os.ReadFile(conf); data == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
data = []byte(shell.ReplaceEnvVars(string(data)))
|
||||
loadEnv(data)
|
||||
data = creds.ReplaceVars(data)
|
||||
configs = append(configs, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/creds"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
@@ -88,6 +89,8 @@ func initLogger() {
|
||||
writer = MemoryLog
|
||||
}
|
||||
|
||||
writer = creds.SecretWriter(writer)
|
||||
|
||||
lvl, _ := zerolog.ParseLevel(modules["level"])
|
||||
Logger = zerolog.New(writer).Level(lvl)
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/creds"
|
||||
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
||||
)
|
||||
|
||||
func initStorage() {
|
||||
storage = &envStorage{data: make(map[string]string)}
|
||||
creds.SetStorage(storage)
|
||||
}
|
||||
|
||||
func loadEnv(data []byte) {
|
||||
var cfg struct {
|
||||
Env map[string]string `yaml:"env"`
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
storage.mu.Lock()
|
||||
for name, value := range cfg.Env {
|
||||
storage.data[name] = value
|
||||
creds.AddSecret(value)
|
||||
}
|
||||
storage.mu.Unlock()
|
||||
}
|
||||
|
||||
var storage *envStorage
|
||||
|
||||
type envStorage struct {
|
||||
data map[string]string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (s *envStorage) SetValue(name, value string) error {
|
||||
if err := PatchConfig([]string{"env", name}, value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.data[name] = value
|
||||
s.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *envStorage) GetValue(name string) (value string, ok bool) {
|
||||
s.mu.Lock()
|
||||
value, ok = s.data[name]
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
@@ -29,8 +29,8 @@ var stackSkip = [][]byte{
|
||||
[]byte("created by github.com/AlexxIT/go2rtc/internal/homekit.Init"),
|
||||
|
||||
// webrtc/api.go
|
||||
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),
|
||||
[]byte("created by github.com/pion/ice/v2.NewUDPMuxDefault"),
|
||||
[]byte("created by github.com/pion/ice/v4.NewTCPMuxDefault"),
|
||||
[]byte("created by github.com/pion/ice/v4.NewUDPMuxDefault"),
|
||||
}
|
||||
|
||||
func stackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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,10 @@ func execHandle(rawURL string) (prod core.Producer, err error) {
|
||||
debug: log.Debug().Enabled(),
|
||||
}
|
||||
|
||||
if allowPaths != nil && !slices.Contains(allowPaths, cmd.Args[0]) {
|
||||
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 {
|
||||
|
||||
+71
-11
@@ -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.
|
||||
|
||||
@@ -25,4 +25,5 @@ func Init() {
|
||||
|
||||
return url, nil
|
||||
})
|
||||
streams.MarkInsecure("expr")
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ var defaults = map[string]string{
|
||||
// `-profile high -level 4.1` - most used streaming profile
|
||||
// `-pix_fmt:v yuv420p` - important for Telegram
|
||||
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
|
||||
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
|
||||
"h265": "-c:v libx265 -g 50 -profile:v main -x265-params level=5.1:high-tier=0 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
|
||||
"mjpeg": "-c:v mjpeg",
|
||||
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func Init() {
|
||||
var log zerolog.Logger
|
||||
|
||||
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
stream := streams.GetOrPatch(r.URL.Query())
|
||||
stream, _ := streams.GetOrPatch(r.URL.Query())
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
@@ -145,7 +145,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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
With ngrok integration, you can get external access to your streams in situations when you have Internet with a private IP address.
|
||||
|
||||
- you may need external access for two different things:
|
||||
- WebRTC stream, so you need a tunnel WebRTC TCP port (ex. 8555)
|
||||
- go2rtc web interface, so you need a tunnel API HTTP port (ex. 1984)
|
||||
- ngrok supports authorization for your web interface
|
||||
- ngrok automatically adds HTTPS to your web interface
|
||||
|
||||
The ngrok free subscription has the following limitations:
|
||||
|
||||
- You can reserve a free domain for serving the web interface, but the TCP address you get will always be random and change with each restart of the ngrok agent (not a problem for WebRTC stream)
|
||||
- You can forward multiple ports from a single agent, but you can only run one ngrok agent on the free plan
|
||||
|
||||
go2rtc will automatically get your external TCP address (if you enable it in ngrok config) and use it with WebRTC connection (if you enable it in webrtc config).
|
||||
|
||||
You need to manually download the [ngrok agent app](https://ngrok.com/download) for your OS and register with the [ngrok service](https://ngrok.com/signup).
|
||||
|
||||
**Tunnel for only WebRTC Stream**
|
||||
|
||||
You need to add your [ngrok authtoken](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
|
||||
|
||||
```yaml
|
||||
ngrok:
|
||||
command: ngrok tcp 8555 --authtoken eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
|
||||
```
|
||||
|
||||
**Tunnel for WebRTC and Web interface**
|
||||
|
||||
You need to create `ngrok.yaml` config file and add it to the go2rtc config:
|
||||
|
||||
```yaml
|
||||
ngrok:
|
||||
command: ngrok start --all --config ngrok.yaml
|
||||
```
|
||||
|
||||
ngrok config example:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
authtoken: eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
|
||||
tunnels:
|
||||
api:
|
||||
addr: 1984 # use the same port as in the go2rtc config
|
||||
proto: http
|
||||
basic_auth:
|
||||
- admin:password # you can set login/pass for your web interface
|
||||
webrtc:
|
||||
addr: 8555 # use the same port as in the go2rtc config
|
||||
proto: tcp
|
||||
```
|
||||
|
||||
See the [ngrok agent documentation](https://ngrok.com/docs/agent/config/) for more details on the ngrok configuration file.
|
||||
@@ -45,6 +45,10 @@ func streamOnvif(rawURL string) (core.Producer, error) {
|
||||
|
||||
log.Debug().Msgf("[onvif] new uri=%s", uri)
|
||||
|
||||
if err = streams.Validate(uri); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return streams.GetProducer(uri)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+14
-10
@@ -1,10 +1,11 @@
|
||||
package ring
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
@@ -21,8 +22,7 @@ func Init() {
|
||||
|
||||
func apiRing(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
var ringAPI *ring.RingRestClient
|
||||
var err error
|
||||
var ringAPI *ring.RingApi
|
||||
|
||||
// Check auth method
|
||||
if email := query.Get("email"); email != "" {
|
||||
@@ -30,7 +30,8 @@ func apiRing(w http.ResponseWriter, r *http.Request) {
|
||||
password := query.Get("password")
|
||||
code := query.Get("code")
|
||||
|
||||
ringAPI, err = ring.NewRingRestClient(ring.EmailAuth{
|
||||
var err error
|
||||
ringAPI, err = ring.NewRestClient(ring.EmailAuth{
|
||||
Email: email,
|
||||
Password: password,
|
||||
}, nil)
|
||||
@@ -44,7 +45,7 @@ func apiRing(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err = ringAPI.GetAuth(code); err != nil {
|
||||
if ringAPI.Using2FA {
|
||||
// Return 2FA prompt
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
api.ResponseJSON(w, map[string]interface{}{
|
||||
"needs_2fa": true,
|
||||
"prompt": ringAPI.PromptFor2FA,
|
||||
})
|
||||
@@ -53,36 +54,39 @@ func apiRing(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
} else if refreshToken := query.Get("refresh_token"); refreshToken != "" {
|
||||
// Refresh Token Flow
|
||||
refreshToken := query.Get("refresh_token")
|
||||
if refreshToken == "" {
|
||||
http.Error(w, "either email/password or refresh_token is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ringAPI, err = ring.NewRingRestClient(ring.RefreshTokenAuth{
|
||||
var err error
|
||||
ringAPI, err = ring.NewRestClient(ring.RefreshTokenAuth{
|
||||
RefreshToken: refreshToken,
|
||||
}, nil)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "either email/password or refresh token is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch devices
|
||||
devices, err := ringAPI.FetchRingDevices()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Create clean query with only required parameters
|
||||
cleanQuery := url.Values{}
|
||||
cleanQuery.Set("refresh_token", ringAPI.RefreshToken)
|
||||
|
||||
var items []*api.Source
|
||||
for _, camera := range devices.AllCameras {
|
||||
cleanQuery.Set("camera_id", fmt.Sprint(camera.ID))
|
||||
cleanQuery.Set("device_id", camera.DeviceID)
|
||||
|
||||
// Stream source
|
||||
|
||||
+62
-5
@@ -5,10 +5,14 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/creds"
|
||||
"github.com/AlexxIT/go2rtc/pkg/probe"
|
||||
)
|
||||
|
||||
func apiStreams(w http.ResponseWriter, r *http.Request) {
|
||||
w = creds.SecretResponse(w)
|
||||
|
||||
query := r.URL.Query()
|
||||
src := query.Get("src")
|
||||
|
||||
@@ -27,7 +31,7 @@ func apiStreams(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
cons := probe.NewProbe(query)
|
||||
cons := probe.Create("probe", query)
|
||||
if len(cons.Medias) != 0 {
|
||||
cons.WithRequest(r)
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
@@ -48,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
|
||||
}
|
||||
|
||||
@@ -65,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":
|
||||
@@ -120,5 +124,58 @@ func apiStreamsDOT(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
dot = append(dot, '}')
|
||||
|
||||
dot = []byte(creds.SecretString(string(dot)))
|
||||
|
||||
api.Response(w, dot, "text/vnd.graphviz")
|
||||
}
|
||||
|
||||
func apiPreload(w http.ResponseWriter, r *http.Request) {
|
||||
// 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
|
||||
for k := range query {
|
||||
switch k {
|
||||
case core.KindVideo, core.KindAudio, "microphone":
|
||||
default:
|
||||
delete(query, k)
|
||||
}
|
||||
}
|
||||
|
||||
rawQuery := query.Encode()
|
||||
|
||||
if err := AddPreload(src, rawQuery); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := app.PatchConfig([]string{"preload", src}, rawQuery); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
case "DELETE":
|
||||
if err := DelPreload(src); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := app.PatchConfig([]string{"preload", src}, nil); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
default:
|
||||
http.Error(w, "", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func apiSchemes(w http.ResponseWriter, r *http.Request) {
|
||||
api.ResponseJSON(w, SupportedSchemes())
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/probe"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
var preloads = map[string]*Preload{}
|
||||
var preloadsMu sync.Mutex
|
||||
|
||||
func AddPreload(name, rawQuery string) error {
|
||||
if rawQuery == "" {
|
||||
rawQuery = "video&audio"
|
||||
}
|
||||
|
||||
query, err := url.ParseQuery(rawQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
preloadsMu.Lock()
|
||||
defer preloadsMu.Unlock()
|
||||
|
||||
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[name] = &Preload{stream: stream, Cons: cons, Query: rawQuery}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DelPreload(name string) error {
|
||||
preloadsMu.Lock()
|
||||
defer preloadsMu.Unlock()
|
||||
|
||||
if p := preloads[name]; p != nil {
|
||||
p.stream.RemoveConsumer(p.Cons)
|
||||
delete(preloads, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("streams: preload not found: %s", name)
|
||||
}
|
||||
|
||||
func GetPreloads() map[string]*Preload {
|
||||
preloadsMu.Lock()
|
||||
defer preloadsMu.Unlock()
|
||||
return maps.Clone(preloads)
|
||||
}
|
||||
+31
-31
@@ -3,7 +3,6 @@ package streams
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -14,8 +13,9 @@ import (
|
||||
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Streams map[string]any `yaml:"streams"`
|
||||
Publish map[string]any `yaml:"publish"`
|
||||
Streams map[string]any `yaml:"streams"`
|
||||
Publish map[string]any `yaml:"publish"`
|
||||
Preload map[string]string `yaml:"preload"`
|
||||
}
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
@@ -28,34 +28,36 @@ 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 {
|
||||
if cfg.Publish == nil && cfg.Preload == nil {
|
||||
return
|
||||
}
|
||||
|
||||
time.AfterFunc(time.Second, func() {
|
||||
// range for nil map is OK
|
||||
for name, dst := range cfg.Publish {
|
||||
if stream := Get(name); stream != nil {
|
||||
Publish(stream, dst)
|
||||
}
|
||||
}
|
||||
for name, rawQuery := range cfg.Preload {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,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()
|
||||
|
||||
@@ -80,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -33,8 +33,12 @@ func switchbotClient(rawURL string, query url.Values) (core.Producer, error) {
|
||||
v.Resolution = 0
|
||||
case "sd":
|
||||
v.Resolution = 1
|
||||
case "auto":
|
||||
v.Resolution = 2
|
||||
}
|
||||
|
||||
v.PlayType = core.Atoi(query.Get("play_type")) // zero by default
|
||||
|
||||
return v, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ 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)
|
||||
@@ -38,6 +38,16 @@ func Init() {
|
||||
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:])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
@@ -95,7 +105,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 != "" {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Xiaomi
|
||||
|
||||
This source allows you to view cameras from the [Xiaomi Mi Home](https://home.mi.com/) ecosystem.
|
||||
|
||||
**Important:**
|
||||
|
||||
1. **Not all cameras are supported**. There are several P2P protocol vendors in the Xiaomi ecosystem.
|
||||
Currently, the **CS2** vendor is supported. However, the **TUTK** vendor is not supported.
|
||||
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
|
||||
|
||||
You can change camera's quality: `subtype=hd/sd/auto`
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
xiaomi1: xiaomi://***&subtype=sd
|
||||
```
|
||||
|
||||
You can use second channel for Dual cameras: `channel=1`
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
xiaomi1: xiaomi://***&channel=1
|
||||
```
|
||||
@@ -0,0 +1,267 @@
|
||||
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/miss"
|
||||
)
|
||||
|
||||
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 tokensMu sync.Mutex
|
||||
|
||||
func getCloud(userID string) (*xiaomi.Cloud, error) {
|
||||
tokensMu.Lock()
|
||||
defer tokensMu.Unlock()
|
||||
|
||||
token := tokens[userID]
|
||||
cloud := xiaomi.NewCloud(AppXiaomiHome)
|
||||
if err := cloud.LoginWithToken(userID, token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cloud, nil
|
||||
}
|
||||
|
||||
func getCameraURL(url *url.URL) (string, error) {
|
||||
clientPublic, clientPrivate, err := miss.GenerateKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
query := url.Query()
|
||||
|
||||
params := fmt.Sprintf(
|
||||
`{"app_pubkey":"%x","did":"%s","support_vendors":"CS2"}`,
|
||||
clientPublic, query.Get("did"),
|
||||
)
|
||||
|
||||
cloud, err := getCloud(url.User.Username())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
region, _ := url.User.Password()
|
||||
|
||||
res, err := cloud.Request(GetBaseURL(region), "/v2/device/miss_get_vendor", params, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var v struct {
|
||||
Vendor struct {
|
||||
VendorID byte `json:"vendor"`
|
||||
} `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.VendorID))
|
||||
|
||||
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 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 == "" {
|
||||
tokensMu.Lock()
|
||||
users := make([]string, 0, len(tokens))
|
||||
for s := range tokens {
|
||||
users = append(users, s)
|
||||
}
|
||||
tokensMu.Unlock()
|
||||
|
||||
api.ResponseJSON(w, users)
|
||||
return
|
||||
}
|
||||
|
||||
err := func() error {
|
||||
cloud, err := getCloud(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
region := query.Get("region")
|
||||
|
||||
res, err := cloud.Request(GetBaseURL(region), "/v2/home/device_list_page", "{}", nil)
|
||||
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.") {
|
||||
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
|
||||
|
||||
tokensMu.Lock()
|
||||
if tokens == nil {
|
||||
tokens = map[string]string{userID: token}
|
||||
} else {
|
||||
tokens[userID] = token
|
||||
}
|
||||
tokensMu.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"
|
||||
}
|
||||
@@ -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"
|
||||
@@ -28,6 +30,7 @@ import (
|
||||
"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 +38,81 @@ 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/xiaomi"
|
||||
"github.com/AlexxIT/go2rtc/internal/yandex"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Version = "1.9.10"
|
||||
app.Version = "1.9.13"
|
||||
|
||||
// 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},
|
||||
{"nest", nest.Init},
|
||||
{"ring", ring.Init},
|
||||
{"roborock", roborock.Init},
|
||||
{"tapo", tapo.Init},
|
||||
{"tuya", tuya.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()
|
||||
}
|
||||
|
||||
+13
-2
@@ -8,8 +8,19 @@ import (
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
const ADTSHeaderSize = 7
|
||||
|
||||
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 +69,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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+18
-3
@@ -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)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Credentials
|
||||
|
||||
This module allows you to get variables:
|
||||
|
||||
- from custom storage (ex. config file)
|
||||
- from [credential files](https://systemd.io/CREDENTIALS/)
|
||||
- from environment variables
|
||||
@@ -0,0 +1,79 @@
|
||||
package creds
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Storage interface {
|
||||
SetValue(name, value string) error
|
||||
GetValue(name string) (string, bool)
|
||||
}
|
||||
|
||||
var storage Storage
|
||||
|
||||
func SetStorage(s Storage) {
|
||||
storage = s
|
||||
}
|
||||
|
||||
func SetValue(name, value string) error {
|
||||
if storage == nil {
|
||||
return errors.New("credentials: storage not initialized")
|
||||
}
|
||||
if err := storage.SetValue(name, value); err != nil {
|
||||
return err
|
||||
}
|
||||
AddSecret(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetValue(name string) (value string, ok bool) {
|
||||
value, ok = getValue(name)
|
||||
AddSecret(value)
|
||||
return
|
||||
}
|
||||
|
||||
func getValue(name string) (string, bool) {
|
||||
if storage != nil {
|
||||
if value, ok := storage.GetValue(name); ok {
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
|
||||
if dir, ok := os.LookupEnv("CREDENTIALS_DIRECTORY"); ok {
|
||||
if value, _ := os.ReadFile(filepath.Join(dir, name)); value != nil {
|
||||
return strings.TrimSpace(string(value)), true
|
||||
}
|
||||
}
|
||||
|
||||
return os.LookupEnv(name)
|
||||
}
|
||||
|
||||
// ReplaceVars - support format ${CAMERA_PASSWORD} and ${RTSP_USER:admin}
|
||||
func ReplaceVars(data []byte) []byte {
|
||||
re := regexp.MustCompile(`\${([^}{]+)}`)
|
||||
return re.ReplaceAllFunc(data, func(match []byte) []byte {
|
||||
key := string(match[2 : len(match)-1])
|
||||
|
||||
var def string
|
||||
var defok bool
|
||||
|
||||
if i := strings.IndexByte(key, ':'); i > 0 {
|
||||
key, def = key[:i], key[i+1:]
|
||||
defok = true
|
||||
}
|
||||
|
||||
if value, ok := GetValue(key); ok {
|
||||
return []byte(value)
|
||||
}
|
||||
|
||||
if defok {
|
||||
return []byte(def)
|
||||
}
|
||||
|
||||
return match
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package creds
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func AddSecret(value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
|
||||
secretsMu.Lock()
|
||||
defer secretsMu.Unlock()
|
||||
|
||||
if slices.Contains(secrets, value) {
|
||||
return
|
||||
}
|
||||
|
||||
secrets = append(secrets, value)
|
||||
secretsReplacer = nil
|
||||
}
|
||||
|
||||
var secrets []string
|
||||
var secretsMu sync.Mutex
|
||||
var secretsReplacer *strings.Replacer
|
||||
|
||||
func getReplacer() *strings.Replacer {
|
||||
secretsMu.Lock()
|
||||
defer secretsMu.Unlock()
|
||||
|
||||
if secretsReplacer == nil {
|
||||
oldnew := make([]string, 0, 2*len(secrets))
|
||||
for _, s := range secrets {
|
||||
oldnew = append(oldnew, s, "***")
|
||||
}
|
||||
secretsReplacer = strings.NewReplacer(oldnew...)
|
||||
}
|
||||
|
||||
return secretsReplacer
|
||||
}
|
||||
|
||||
func SecretString(s string) string {
|
||||
re := getReplacer()
|
||||
return re.Replace(s)
|
||||
}
|
||||
|
||||
func SecretWriter(w io.Writer) io.Writer {
|
||||
return &secretWriter{w}
|
||||
}
|
||||
|
||||
type secretWriter struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (s *secretWriter) Write(b []byte) (int, error) {
|
||||
re := getReplacer()
|
||||
return re.WriteString(s.w, string(b))
|
||||
}
|
||||
|
||||
type secretResponse struct {
|
||||
w http.ResponseWriter
|
||||
}
|
||||
|
||||
func (s *secretResponse) Header() http.Header {
|
||||
return s.w.Header()
|
||||
}
|
||||
|
||||
func (s *secretResponse) Write(b []byte) (int, error) {
|
||||
re := getReplacer()
|
||||
return re.WriteString(s.w, string(b))
|
||||
}
|
||||
|
||||
func (s *secretResponse) WriteHeader(statusCode int) {
|
||||
s.w.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func SecretResponse(w http.ResponseWriter) http.ResponseWriter {
|
||||
return &secretResponse{w}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package creds
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
AddSecret("admin")
|
||||
AddSecret("pa$$word")
|
||||
|
||||
s := SecretString("rtsp://admin:pa$$word@192.168.1.123/stream1")
|
||||
require.Equal(t, "rtsp://***:***@192.168.1.123/stream1", s)
|
||||
}
|
||||
@@ -88,6 +88,8 @@ func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Rece
|
||||
}
|
||||
|
||||
func (c *Client) Start() (err error) {
|
||||
_, err = c.conn.Read(nil)
|
||||
// just block until c.conn closed
|
||||
b := make([]byte, 1)
|
||||
_, err = c.conn.Read(b)
|
||||
return
|
||||
}
|
||||
|
||||
+108
-73
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
+38
-12
@@ -9,11 +9,12 @@ import (
|
||||
)
|
||||
|
||||
func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
//vps, sps, pps := GetParameterSet(codec.FmtpLine)
|
||||
//ps := h264.EncodeAVC(vps, sps, pps)
|
||||
vps, sps, pps := GetParameterSet(codec.FmtpLine)
|
||||
ps := h264.JoinNALU(vps, sps, pps)
|
||||
|
||||
buf := make([]byte, 0, 512*1024) // 512K
|
||||
var nuStart int
|
||||
var seqNum uint16
|
||||
|
||||
return func(packet *rtp.Packet) {
|
||||
data := packet.Payload
|
||||
@@ -34,28 +35,55 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// when we collect data into one buffer, we need to make sure
|
||||
// that all of it falls into the same sequence
|
||||
if len(buf) > 0 && packet.SequenceNumber-seqNum != 1 {
|
||||
//log.Printf("broken H265 sequence")
|
||||
buf = buf[:0] // drop data
|
||||
return
|
||||
}
|
||||
|
||||
seqNum = packet.SequenceNumber
|
||||
|
||||
if nuType == NALUTypeFU {
|
||||
switch data[2] >> 6 {
|
||||
case 2: // begin
|
||||
case 0b10: // begin
|
||||
nuType = data[2] & 0x3F
|
||||
|
||||
// push PS data before keyframe
|
||||
//if len(buf) == 0 && nuType >= 19 && nuType <= 21 {
|
||||
// buf = append(buf, ps...)
|
||||
//}
|
||||
if len(buf) == 0 && nuType >= 19 && nuType <= 21 {
|
||||
buf = append(buf, ps...)
|
||||
}
|
||||
|
||||
nuStart = len(buf)
|
||||
buf = append(buf, 0, 0, 0, 0) // NAL unit size
|
||||
buf = append(buf, (data[0]&0x81)|(nuType<<1), data[1])
|
||||
buf = append(buf, data[3:]...)
|
||||
return
|
||||
case 0: // continue
|
||||
case 0b00: // continue
|
||||
if len(buf) == 0 {
|
||||
//log.Printf("broken H265 fragment")
|
||||
return
|
||||
}
|
||||
|
||||
buf = append(buf, data[3:]...)
|
||||
return
|
||||
case 1: // end
|
||||
case 0b01: // end
|
||||
if len(buf) == 0 {
|
||||
//log.Printf("broken H265 fragment")
|
||||
return
|
||||
}
|
||||
|
||||
buf = append(buf, data[3:]...)
|
||||
|
||||
if nuStart > len(buf)+4 {
|
||||
//log.Printf("broken H265 fragment")
|
||||
buf = buf[:0] // drop data
|
||||
return
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4))
|
||||
case 3: // wrong RFC 7798 realisation from OpenIPC project
|
||||
case 0b11: // wrong RFC 7798 realisation from OpenIPC project
|
||||
// A non-fragmented NAL unit MUST NOT be transmitted in one FU; i.e.,
|
||||
// the Start bit and End bit must not both be set to 1 in the same FU
|
||||
// header.
|
||||
@@ -65,10 +93,8 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
buf = append(buf, data[3:]...)
|
||||
}
|
||||
} else {
|
||||
nuStart = len(buf)
|
||||
buf = append(buf, 0, 0, 0, 0) // NAL unit size
|
||||
buf = binary.BigEndian.AppendUint32(buf, uint32(len(data))) // NAL unit size
|
||||
buf = append(buf, data...)
|
||||
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(data)))
|
||||
}
|
||||
|
||||
// collect all NAL Units for Access Unit
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
## Useful links
|
||||
|
||||
- https://github.com/bauer-andreas/secure-video-specification
|
||||
+13
-13
@@ -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{
|
||||
|
||||
@@ -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},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,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"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
//}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
@@ -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
@@ -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},
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -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{
|
||||
|
||||
+10
-13
@@ -31,19 +31,18 @@ func NewClient(rawURL string) (*Client, error) {
|
||||
baseURL := "http://" + u.Host
|
||||
|
||||
client := &Client{url: u}
|
||||
if u.Path == "" {
|
||||
client.deviceURL = baseURL + PathDevice
|
||||
} else {
|
||||
client.deviceURL = baseURL + u.Path
|
||||
}
|
||||
client.deviceURL = baseURL + GetPath(u.Path, PathDevice)
|
||||
|
||||
b, err := client.DeviceRequest(DeviceGetCapabilities)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.mediaURL = FindTagValue(b, "Media.+?XAddr")
|
||||
client.imaginURL = FindTagValue(b, "Imaging.+?XAddr")
|
||||
s := FindTagValue(b, "Media.+?XAddr")
|
||||
client.mediaURL = baseURL + GetPath(s, "/onvif/media_service")
|
||||
|
||||
s = FindTagValue(b, "Imaging.+?XAddr")
|
||||
client.imaginURL = baseURL + GetPath(s, "/onvif/imaging_service")
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -188,13 +187,11 @@ func (c *Client) Request(url, body string) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
// need to close body with eny response status
|
||||
b, err := io.ReadAll(res.Body)
|
||||
|
||||
if err == nil && res.StatusCode != http.StatusOK {
|
||||
err = errors.New("onvif: " + res.Status + " for " + url)
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("onvif: wrong response " + res.Status)
|
||||
}
|
||||
|
||||
return b, err
|
||||
return io.ReadAll(res.Body)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package onvif
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -129,3 +130,14 @@ func GetPosixTZ(current time.Time) string {
|
||||
|
||||
return prefix + fmt.Sprintf("%02d:%02d", offset/60, offset%60)
|
||||
}
|
||||
|
||||
func GetPath(urlOrPath, defPath string) string {
|
||||
if urlOrPath == "" || urlOrPath[0] == '/' {
|
||||
return defPath
|
||||
}
|
||||
u, err := url.Parse(urlOrPath)
|
||||
if err != nil {
|
||||
return defPath
|
||||
}
|
||||
return GetPath(u.Path, defPath)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user