Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ccec41a10f | |||
| 9feb98db3f | |||
| a724c5f3ce | |||
| c60767c8b0 | |||
| ae13a72fde | |||
| 458d5e7d0d | |||
| 89e15d9b57 | |||
| 0d2292c311 | |||
| 62343af009 | |||
| c8c3b22d19 | |||
| 853e98879b | |||
| bf5cb33385 | |||
| 7ad4d350f8 | |||
| c63fc6a2ad | |||
| 7036d196be | |||
| d3bc18c369 | |||
| 1f3a32023f | |||
| a46bad0522 | |||
| d0dfa1d3dd | |||
| fc5b36acd3 | |||
| 0a8ab9bbd1 | |||
| b60000ac34 | |||
| 39d87625d7 | |||
| 0da8b46148 | |||
| 8d9f87061c | |||
| 4bdfa62039 | |||
| 67ea2d9d02 | |||
| 39b614fb0f | |||
| 84469dcd25 | |||
| eceb4a476f | |||
| 051a4eabd7 | |||
| e68a304698 | |||
| 2e6c6b1d41 | |||
| 0def6f8de9 | |||
| 7ac5b4f114 | |||
| ab47d5718f | |||
| 94aced0fc0 | |||
| 66a4c3d06e | |||
| 8d382afa0f | |||
| 051c5ff913 | |||
| a87dafbbec | |||
| 742cb7699b | |||
| 43449e7b08 | |||
| 33512e73bd | |||
| b367ffee6d | |||
| 69447df6b3 | |||
| a6eac4ff02 | |||
| 1eaf879a76 | |||
| c9ae6dcc03 | |||
| befa6bd356 | |||
| 100ab62ab4 | |||
| a0f999d9c9 | |||
| 9bda2f7e60 | |||
| 54b19999c6 | |||
| aa3c081352 | |||
| 2d16ee8884 | |||
| ec96a14807 | |||
| af72548a43 | |||
| 6d85b36f47 | |||
| 28830a697d | |||
| 5d3953a948 | |||
| 4d6432d38d | |||
| bcbebd5a36 | |||
| 50e2a626a6 | |||
| f4fe8c3769 | |||
| e42085a237 | |||
| a060b3447c | |||
| d7784b24c6 | |||
| 39645cb3d8 | |||
| 36166caccc | |||
| 0f1dc73d55 | |||
| 6b29c37433 | |||
| 535bacf9d6 | |||
| e6fb4081f7 | |||
| eb04fafaa4 | |||
| b4ed738d17 | |||
| 6a9ae93fa1 | |||
| 2dd47654e6 | |||
| c27e735c17 | |||
| 8bc65e4c91 | |||
| 0a476a74b3 | |||
| b5be4ce03b | |||
| f291f1d827 | |||
| 041ce885c7 | |||
| df16f28825 | |||
| a8867bc3cb | |||
| b2b115ec9c | |||
| 95de3a1f3e | |||
| dd4376cd37 | |||
| 20d45bff92 | |||
| 4ad67e9f6f | |||
| e367940bd9 |
@@ -23,7 +23,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
- mixing tracks from different sources to single stream
|
||||
- auto match client supported codecs
|
||||
- [2-way audio](#two-way-audio) for some cameras
|
||||
- streaming from private networks via [Ngrok](#module-ngrok)
|
||||
- streaming from private networks via [ngrok](#module-ngrok)
|
||||
- can be [integrated to](#module-api) any smart home platform or be used as [standalone app](#go2rtc-binary)
|
||||
|
||||
**Inspired by:**
|
||||
@@ -54,11 +54,13 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
* [Source: FFmpeg Device](#source-ffmpeg-device)
|
||||
* [Source: Exec](#source-exec)
|
||||
* [Source: Echo](#source-echo)
|
||||
* [Source: Expr](#source-expr)
|
||||
* [Source: HomeKit](#source-homekit)
|
||||
* [Source: Bubble](#source-bubble)
|
||||
* [Source: DVRIP](#source-dvrip)
|
||||
* [Source: Tapo](#source-tapo)
|
||||
* [Source: Kasa](#source-kasa)
|
||||
* [Source: GoPro](#source-gopro)
|
||||
* [Source: Ivideon](#source-ivideon)
|
||||
* [Source: Hass](#source-hass)
|
||||
* [Source: ISAPI](#source-isapi)
|
||||
@@ -75,7 +77,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
* [Module: WebRTC](#module-webrtc)
|
||||
* [Module: HomeKit](#module-homekit)
|
||||
* [Module: WebTorrent](#module-webtorrent)
|
||||
* [Module: Ngrok](#module-ngrok)
|
||||
* [Module: ngrok](#module-ngrok)
|
||||
* [Module: Hass](#module-hass)
|
||||
* [Module: MP4](#module-mp4)
|
||||
* [Module: HLS](#module-hls)
|
||||
@@ -125,7 +127,7 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
||||
|
||||
### go2rtc: Docker
|
||||
|
||||
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [Ngrok](#module-ngrok) and [Python](#source-echo).
|
||||
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [ngrok](#module-ngrok) and [Python](#source-echo).
|
||||
|
||||
### go2rtc: Home Assistant Add-on
|
||||
|
||||
@@ -168,7 +170,7 @@ Available modules:
|
||||
- [hls](#module-hls) - HLS TS or fMP4 stream Server
|
||||
- [mjpeg](#module-mjpeg) - MJPEG Server
|
||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
|
||||
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
|
||||
- [ngrok](#module-ngrok) - ngrok integration (external access for private network)
|
||||
- [hass](#module-hass) - Home Assistant integration
|
||||
- [log](#module-log) - logs config
|
||||
|
||||
@@ -186,11 +188,13 @@ Available source types:
|
||||
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
|
||||
- [exec](#source-exec) - get media from external app output
|
||||
- [echo](#source-echo) - get stream link from bash or python
|
||||
- [expr](#source-expr) - get stream link via built-in expression language
|
||||
- [homekit](#source-homekit) - streaming from HomeKit Camera
|
||||
- [bubble](#source-bubble) - streaming from ESeeCloud/dvr163 NVR
|
||||
- [dvrip](#source-dvrip) - streaming from DVR-IP NVR
|
||||
- [tapo](#source-tapo) - TP-Link Tapo cameras with [two way audio](#two-way-audio) support
|
||||
- [kasa](#source-tapo) - TP-Link Kasa cameras
|
||||
- [gopro](#source-gopro) - GoPro cameras
|
||||
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
|
||||
- [hass](#source-hass) - Home Assistant integration
|
||||
- [isapi](#source-isapi) - two way audio for Hikvision (ISAPI) cameras
|
||||
@@ -301,6 +305,8 @@ streams:
|
||||
|
||||
#### Source: ONVIF
|
||||
|
||||
*[New in v1.5.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.5.0)*
|
||||
|
||||
The source is not very useful if you already know RTSP and snapshot links for your camera. But it can be useful if you don't.
|
||||
|
||||
**WebUI > Add** webpage support ONVIF autodiscovery. Your server must be on the same subnet as the camera. If you use docker, you must use "network host".
|
||||
@@ -393,7 +399,7 @@ streams:
|
||||
|
||||
#### Source: Exec
|
||||
|
||||
Exec source can run any external application and expect data from it. Two transports are supported - **pipe** and **RTSP**.
|
||||
Exec source can run any external application and expect data from it. Two transports are supported - **pipe** (*from [v1.5.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.5.0)*) and **RTSP**.
|
||||
|
||||
If you want to use **RTSP** transport - the command must contain the `{output}` argument in any place. On launch, it will be replaced by the local address of the RTSP server.
|
||||
|
||||
@@ -426,6 +432,12 @@ streams:
|
||||
apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html
|
||||
```
|
||||
|
||||
#### Source: Expr
|
||||
|
||||
*[New in v1.8.2](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.2)*
|
||||
|
||||
Like `echo` source, but uses the built-in [expr](https://github.com/antonmedv/expr) expression language ([read more](https://github.com/AlexxIT/go2rtc/blob/master/internal/expr/README.md)).
|
||||
|
||||
#### Source: HomeKit
|
||||
|
||||
**Important:**
|
||||
@@ -461,6 +473,8 @@ RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g
|
||||
|
||||
#### Source: Bubble
|
||||
|
||||
*[New in v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*
|
||||
|
||||
Other names: [ESeeCloud](http://www.eseecloud.com/), [dvr163](http://help.dvr163.com/).
|
||||
|
||||
- you can skip `username`, `password`, `port`, `ch` and `stream` if they are default
|
||||
@@ -473,6 +487,8 @@ streams:
|
||||
|
||||
#### Source: DVRIP
|
||||
|
||||
*[New in v1.2.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.2.0)*
|
||||
|
||||
Other names: DVR-IP, NetSurveillance, Sofia protocol (NETsurveillance ActiveX plugin XMeye SDK).
|
||||
|
||||
- you can skip `username`, `password`, `port`, `channel` and `subtype` if they are default
|
||||
@@ -491,22 +507,34 @@ streams:
|
||||
|
||||
#### Source: Tapo
|
||||
|
||||
*[New in v1.2.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.2.0)*
|
||||
|
||||
[TP-Link Tapo](https://www.tapo.com/) proprietary camera protocol with **two way audio** support.
|
||||
|
||||
- stream quality is the same as [RTSP protocol](https://www.tapo.com/en/faq/34/)
|
||||
- use the **cloud password**, this is not the RTSP password! you do not need to add a login!
|
||||
- you can also use UPPERCASE MD5 hash from your cloud password with `admin` username
|
||||
- some new camera firmwares requires SHA256 instead of MD5
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
# cloud password without username
|
||||
camera1: tapo://cloud-password@192.168.1.123
|
||||
# admin username and UPPERCASE MD5 cloud-password hash
|
||||
camera2: tapo://admin:MD5-PASSWORD-HASH@192.168.1.123
|
||||
camera2: tapo://admin:UPPERCASE-MD5@192.168.1.123
|
||||
# admin username and UPPERCASE SHA256 cloud-password hash
|
||||
camera3: tapo://admin:UPPERCASE-SHA256@192.168.1.123
|
||||
```
|
||||
|
||||
```bash
|
||||
echo -n "cloud password" | md5 | awk '{print toupper($0)}'
|
||||
echo -n "cloud password" | shasum -a 256 | awk '{print toupper($0)}'
|
||||
```
|
||||
|
||||
#### Source: Kasa
|
||||
|
||||
*[New in v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*
|
||||
|
||||
[TP-Link Kasa](https://www.kasasmart.com/) non-standard protocol [more info](https://medium.com/@hu3vjeen/reverse-engineering-tp-link-kc100-bac4641bf1cd).
|
||||
|
||||
```yaml
|
||||
@@ -514,6 +542,12 @@ streams:
|
||||
kasa: kasa://user:pass@192.168.1.123:19443/https/stream/mixed
|
||||
```
|
||||
|
||||
#### Source: GoPro
|
||||
|
||||
*[New in v1.8.3](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.3)*
|
||||
|
||||
Support streaming from [GoPro](https://gopro.com/) cameras, connected via USB or Wi-Fi to Linux, Mac, Windows. [Read more](https://github.com/AlexxIT/go2rtc/tree/master/internal/gopro).
|
||||
|
||||
#### Source: Ivideon
|
||||
|
||||
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
|
||||
@@ -541,7 +575,7 @@ streams:
|
||||
aqara_g3: hass:Camera-Hub-G3-AB12
|
||||
```
|
||||
|
||||
**WebRTC Cameras**
|
||||
**WebRTC Cameras** (*from [v1.6.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)*)
|
||||
|
||||
Any cameras in WebRTC format are supported. But at the moment Home Assistant only supports some [Nest](https://www.home-assistant.io/integrations/nest/) cameras in this fomat.
|
||||
|
||||
@@ -561,6 +595,8 @@ By default, the Home Assistant API does not allow you to get dynamic RTSP link t
|
||||
|
||||
#### Source: ISAPI
|
||||
|
||||
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||
|
||||
This source type support only backchannel audio for Hikvision ISAPI protocol. So it should be used as second source in addition to the RTSP protocol.
|
||||
|
||||
```yaml
|
||||
@@ -572,6 +608,8 @@ streams:
|
||||
|
||||
#### Source: Nest
|
||||
|
||||
*[New in v1.6.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)*
|
||||
|
||||
Currently only WebRTC cameras are supported. Stream reconnects every 5 minutes.
|
||||
|
||||
For simplicity, it is recommended to connect the Nest/WebRTC camera to the [Home Assistant](#source-hass). But if you can somehow get the below parameters - Nest/WebRTC source will work without Hass.
|
||||
@@ -583,6 +621,8 @@ streams:
|
||||
|
||||
#### Source: Roborock
|
||||
|
||||
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||
|
||||
This source type support Roborock vacuums with cameras. Known working models:
|
||||
|
||||
- Roborock S6 MaxV - only video (the vacuum has no microphone)
|
||||
@@ -594,6 +634,8 @@ If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456
|
||||
|
||||
#### Source: WebRTC
|
||||
|
||||
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||
|
||||
This source type support four connection formats.
|
||||
|
||||
**whep**
|
||||
@@ -604,15 +646,15 @@ This source type support four connection formats.
|
||||
|
||||
This format is only supported in go2rtc. Unlike WHEP it supports asynchronous WebRTC connection and two way audio.
|
||||
|
||||
**openipc**
|
||||
**openipc** (*from [v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*)
|
||||
|
||||
Support connection to [OpenIPC](https://openipc.org/) cameras.
|
||||
|
||||
**wyze**
|
||||
**wyze** (*from [v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*)
|
||||
|
||||
Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC protocol. You can use [docker-wyze-bridge](https://github.com/mrlt8/docker-wyze-bridge) project to get connection credentials.
|
||||
|
||||
**kinesis**
|
||||
**kinesis** (*from [v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*)
|
||||
|
||||
Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-streams/), using WebRTC protocol. You need to specify signalling WebSocket URL with all credentials in query params, `client_id` and `ice_servers` list in [JSON format](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer).
|
||||
|
||||
@@ -629,6 +671,8 @@ streams:
|
||||
|
||||
#### Source: WebTorrent
|
||||
|
||||
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||
|
||||
This source can get a stream from another go2rtc via [WebTorrent](#module-webtorrent) protocol.
|
||||
|
||||
```yaml
|
||||
@@ -667,6 +711,8 @@ By default, go2rtc establishes a connection to the source when any client reques
|
||||
|
||||
#### Incoming: Browser
|
||||
|
||||
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||
|
||||
You can turn the browser of any PC or mobile into an IP-camera with support video and two way audio. Or even broadcast your PC screen:
|
||||
|
||||
1. Create empty stream in the `go2rtc.yaml`
|
||||
@@ -677,12 +723,16 @@ You can turn the browser of any PC or mobile into an IP-camera with support vide
|
||||
|
||||
#### Incoming: WebRTC/WHIP
|
||||
|
||||
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||
|
||||
You can use **OBS Studio** or any other broadcast software with [WHIP](https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html) protocol support. This standard has not yet been approved. But you can download OBS Studio [dev version](https://github.com/obsproject/obs-studio/actions/runs/3969201209):
|
||||
|
||||
- Settings > Stream > Service: WHIP > http://192.168.1.123:1984/api/webrtc?dst=camera1
|
||||
|
||||
#### Stream to camera
|
||||
|
||||
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||
|
||||
go2rtc support play audio files (ex. music or [TTS](https://www.home-assistant.io/integrations/#text-to-speech)) and live streams (ex. radio) on cameras with [two way audio](#two-way-audio) support (RTSP/ONVIF cameras, TP-Link Tapo, Hikvision ISAPI, Roborock vacuums, any Browser).
|
||||
|
||||
API example:
|
||||
@@ -703,6 +753,8 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
|
||||
|
||||
### Publish stream
|
||||
|
||||
*[New in v1.8.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.0)*
|
||||
|
||||
You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:
|
||||
|
||||
- Supported codecs: H264 for video and AAC for audio
|
||||
@@ -728,7 +780,7 @@ publish:
|
||||
|
||||
streams:
|
||||
# for TP-Link cameras it's important to use transcoding because of wrong pixel format
|
||||
tplink_tapo: ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware=vaapi#audio=aac
|
||||
tplink_tapo: ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware#audio=aac
|
||||
```
|
||||
|
||||
- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.
|
||||
@@ -797,6 +849,8 @@ Read more about [codecs filters](#codecs-filters).
|
||||
|
||||
### Module: RTMP
|
||||
|
||||
*[New in v1.8.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.0)*
|
||||
|
||||
You can get any stream as RTMP-stream: `rtmp://192.168.1.123/{stream_name}`. Only H264/AAC codecs supported right now.
|
||||
|
||||
[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has differnt problems with this format.
|
||||
@@ -849,7 +903,7 @@ webrtc:
|
||||
|
||||
**Private IP**
|
||||
|
||||
- setup integration with [Ngrok service](#module-ngrok)
|
||||
- setup integration with [ngrok service](#module-ngrok)
|
||||
|
||||
```yaml
|
||||
ngrok:
|
||||
@@ -875,6 +929,8 @@ webrtc:
|
||||
|
||||
### Module: HomeKit
|
||||
|
||||
*[New in v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*
|
||||
|
||||
HomeKit module can work in two modes:
|
||||
|
||||
- export any H264 camera to Apple HomeKit
|
||||
@@ -927,6 +983,8 @@ homekit:
|
||||
|
||||
### Module: WebTorrent
|
||||
|
||||
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||
|
||||
This module support:
|
||||
|
||||
- Share any local stream via [WebTorrent](https://webtorrent.io/) technology
|
||||
@@ -951,29 +1009,29 @@ Link example: https://alexxit.github.io/go2rtc/#share=02SNtgjKXY&pwd=wznEQqznxW&
|
||||
|
||||
TODO: article how it works...
|
||||
|
||||
### Module: Ngrok
|
||||
### Module: ngrok
|
||||
|
||||
With Ngrok integration you can get external access to your streams in situation when you have Internet with private IP-address.
|
||||
With ngrok integration you can get external access to your streams in situations when you have Internet with private IP-address.
|
||||
|
||||
- Ngrok preistalled for **Docker** and **Hass Add-on** users
|
||||
- ngrok is pre-installed for **Docker** and **Hass Add-on** users
|
||||
- you may need external access for two different things:
|
||||
- WebRTC stream, so you need tunnel WebRTC TCP port (ex. 8555)
|
||||
- go2rtc web interface, so you need tunnel API HTTP port (ex. 1984)
|
||||
- Ngrok support authorization for your web interface
|
||||
- Ngrok automatically adds HTTPS to your web interface
|
||||
- ngrok support authorization for your web interface
|
||||
- ngrok automatically adds HTTPS to your web interface
|
||||
|
||||
Ngrok free subscription limitations:
|
||||
The ngrok free subscription has the following limitations:
|
||||
|
||||
- you will always get random external address (not a problem for webrtc stream)
|
||||
- you can forward multiple ports but use only one Ngrok app
|
||||
- You can reserve a free domain for serving the web interface, but the TCP address you get will always be random and change with each restart of the ngrok agent (not a problem for webrtc stream)
|
||||
- You can forward multiple ports from a single agent, but you can only run one ngrok agent on the free plan
|
||||
|
||||
go2rtc will automatically get your external TCP address (if you enable it in ngrok config) and use it with WebRTC connection (if you enable it in webrtc config).
|
||||
|
||||
You need manually download [Ngrok agent app](https://ngrok.com/download) for your OS and register in [Ngrok service](https://ngrok.com/).
|
||||
You need to manually download the [ngrok agent app](https://ngrok.com/download) for your OS and register with the [ngrok service](https://ngrok.com/signup).
|
||||
|
||||
**Tunnel for only WebRTC Stream**
|
||||
|
||||
You need to add your [Ngrok token](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
|
||||
You need to add your [ngrok authtoken](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
|
||||
|
||||
```yaml
|
||||
ngrok:
|
||||
@@ -989,7 +1047,7 @@ ngrok:
|
||||
command: ngrok start --all --config ngrok.yaml
|
||||
```
|
||||
|
||||
Ngrok config example:
|
||||
ngrok config example:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
@@ -1005,6 +1063,8 @@ tunnels:
|
||||
proto: tcp
|
||||
```
|
||||
|
||||
See the [ngrok agent documentation](https://ngrok.com/docs/agent/config/) for more details on the ngrok configuration file.
|
||||
|
||||
### Module: Hass
|
||||
|
||||
The best and easiest way to use go2rtc inside the Home Assistant is to install the custom integration [WebRTC Camera](#go2rtc-home-assistant-integration) and custom lovelace card.
|
||||
@@ -1068,6 +1128,8 @@ Read more about [codecs filters](#codecs-filters).
|
||||
|
||||
### Module: HLS
|
||||
|
||||
*[New in v1.1.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.1.0)*
|
||||
|
||||
[HLS](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) is the worst technology for real-time streaming. It can only be useful on devices that do not support more modern technology, like [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4).
|
||||
|
||||
The go2rtc implementation differs from the standards and may not work with all players.
|
||||
@@ -1144,7 +1206,7 @@ webrtc:
|
||||
- external access to WebRTC TCP port is not a problem, because it used only for transmit encrypted media data
|
||||
- anyway you need to open this port to your local network and to the Internet in order for WebRTC to work
|
||||
|
||||
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [Ngrok](https://ngrok.com/), etc.
|
||||
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [ngrok](https://ngrok.com/), etc.
|
||||
|
||||
PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted media. It works without problems on the local network. And sometimes also works for external access, even if you haven't opened this port on your router ([read more](https://en.wikipedia.org/wiki/UDP_hole_punching)). But for stable external WebRTC access, you need to open the 8555 port on your router for both TCP and UDP.
|
||||
|
||||
@@ -1175,17 +1237,14 @@ Some examples:
|
||||
|
||||
`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
|
||||
|
||||
| Device | WebRTC | MSE | HTTP | HLS |
|
||||
|---------------------|-------------------------------|-------------------------------|------------------------------------|------------------------|
|
||||
| *latency* | best | medium | bad | bad |
|
||||
| Desktop Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
|
||||
| Desktop Edge | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
|
||||
| Android Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
|
||||
| Desktop Firefox | H264, OPUS, PCMU, PCMA | H264, AAC, FLAC*, OPUS | H264, AAC, FLAC*, OPUS | no |
|
||||
| Desktop Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** | H264, H265, AAC, FLAC* |
|
||||
| iPad Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** | H264, H265, AAC, FLAC* |
|
||||
| iPhone Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | **no!** | **no!** | H264, H265, AAC, FLAC* |
|
||||
| macOS [Hass App][1] | no | no | no | H264, H265, AAC, FLAC* |
|
||||
| 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* |
|
||||
|
||||
[1]: https://apps.apple.com/app/home-assistant/id1099568401
|
||||
|
||||
@@ -1212,8 +1271,8 @@ Some examples:
|
||||
|
||||
- H264 = H.264 = AVC (Advanced Video Coding)
|
||||
- H265 = H.265 = HEVC (High Efficiency Video Coding)
|
||||
- PCMU = G.711 PCM (A-law) = PCM A-law (`alaw`)
|
||||
- PCMA = G.711 PCM (µ-law) = PCM mu-law (`mulaw`)
|
||||
- PCMA = G.711 PCM (A-law) = PCM A-law (`alaw`)
|
||||
- PCMU = G.711 PCM (µ-law) = PCM mu-law (`mulaw`)
|
||||
- PCM = L16 = PCM signed 16-bit big-endian (`s16be`)
|
||||
- AAC = MPEG4-GENERIC
|
||||
- MP3 = MPEG-1 Audio Layer III or MPEG-2 Audio Layer III
|
||||
@@ -1280,14 +1339,21 @@ streams:
|
||||
|
||||
- [Frigate 12+](https://frigate.video/) - 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
|
||||
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge
|
||||
- [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
|
||||
- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - A small project that provides a Video/Audio Stream from Eufy cameras that don't directly support RTSP
|
||||
- [ioBroker.euSec](https://github.com/bropat/ioBroker.eusec) - [ioBroker](https://www.iobroker.net/) adapter for control Eufy security devices
|
||||
- [wz_mini_hacks](https://github.com/gtxaspec/wz_mini_hacks) - Custom firmware for Wyze cameras
|
||||
- [MMM-go2rtc](https://github.com/Anonym-tsk/MMM-go2rtc) - MagicMirror² Module
|
||||
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge
|
||||
|
||||
**Distributions**
|
||||
|
||||
- [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=go2rtc)
|
||||
- [Gentoo](https://github.com/inode64/inode64-overlay/tree/main/media-video/go2rtc)
|
||||
- [NixOS](https://search.nixos.org/packages?query=go2rtc)
|
||||
- [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/)
|
||||
- [QNAP](https://www.myqnap.org/product/go2rtc/)
|
||||
- [Synology NAS](https://synocommunity.com/package/go2rtc)
|
||||
- [Unraid](https://unraid.net/community/apps?q=go2rtc)
|
||||
|
||||
## Cameras experience
|
||||
|
||||
+8
-1
@@ -115,7 +115,14 @@ paths:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
/api/restart:
|
||||
post:
|
||||
summary: Restart Daemon
|
||||
description: Restarts the daemon.
|
||||
tags: [ Application ]
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
/api/config:
|
||||
get:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 154 KiB |
@@ -3,42 +3,45 @@ module github.com/AlexxIT/go2rtc
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/miekg/dns v1.1.55
|
||||
github.com/asticode/go-astits v1.13.0
|
||||
github.com/expr-lang/expr v1.15.7
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/miekg/dns v1.1.57
|
||||
github.com/pion/ice/v2 v2.3.11
|
||||
github.com/pion/interceptor v0.1.19
|
||||
github.com/pion/rtcp v1.2.10
|
||||
github.com/pion/rtp v1.8.1
|
||||
github.com/pion/interceptor v0.1.25
|
||||
github.com/pion/rtcp v1.2.13
|
||||
github.com/pion/rtp v1.8.3
|
||||
github.com/pion/sdp/v3 v3.0.6
|
||||
github.com/pion/srtp/v2 v2.0.17
|
||||
github.com/pion/srtp/v2 v2.0.18
|
||||
github.com/pion/stun v0.6.1
|
||||
github.com/pion/webrtc/v3 v3.2.19
|
||||
github.com/rs/zerolog v1.30.0
|
||||
github.com/pion/webrtc/v3 v3.2.24
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||
golang.org/x/crypto v0.13.0
|
||||
golang.org/x/crypto v0.17.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/asticode/go-astikit v0.30.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/kr/pretty v0.2.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pion/datachannel v1.5.5 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.7 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.8 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/mdns v0.0.9 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/sctp v1.8.9 // indirect
|
||||
github.com/pion/transport/v2 v2.2.4 // indirect
|
||||
github.com/pion/turn/v2 v2.1.3 // indirect
|
||||
github.com/pion/turn/v2 v2.1.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.15.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/tools v0.16.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
|
||||
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
|
||||
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/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.15.7 h1:BK0JcWUkoW6nrbLBo6xCKhz4BvH5DSOOu1Gx5lucyZo=
|
||||
github.com/expr-lang/expr v1.15.7/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
@@ -19,10 +25,11 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
@@ -30,15 +37,14 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
|
||||
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
||||
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.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
|
||||
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -50,13 +56,13 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.8 h1:BUroldfiIbV9jSnC6cKOMnyiORRWrWWpV11JUyEu5OA=
|
||||
github.com/pion/dtls/v2 v2.2.8/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/ice/v2 v2.3.11 h1:rZjVmUwyT55cmN8ySMpL7rsS8KYsJERsrxJLLxpKhdw=
|
||||
github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
|
||||
github.com/pion/interceptor v0.1.18/go.mod h1:tpvvF4cPM6NGxFA1DUMbhabzQBxdWMATDGEUYOR9x6I=
|
||||
github.com/pion/interceptor v0.1.19 h1:tq0TGBzuZQqipyBhaC1mVUCfCh8XjDKUuibq9rIl5t4=
|
||||
github.com/pion/interceptor v0.1.19/go.mod h1:VANhFxdJezB8mwToMMmrmyHyP9gym6xLqIUch31xryg=
|
||||
github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc=
|
||||
github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
|
||||
@@ -64,18 +70,21 @@ github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
|
||||
github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
|
||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||
github.com/pion/rtp v1.8.1 h1:26OxTc6lKg/qLSGir5agLyj0QKaOv8OP5wps2SFnVNQ=
|
||||
github.com/pion/rtp v1.8.1/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.13 h1:+EQijuisKwm/8VBs8nWllr0bIndR7Lf7cZG200mpbNo=
|
||||
github.com/pion/rtcp v1.2.13/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
|
||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
|
||||
github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
|
||||
github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
|
||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp/v2 v2.0.17 h1:ECuOk+7uIpY6HUlTb0nXhfvu4REG2hjtC4ronYFCZE4=
|
||||
github.com/pion/srtp/v2 v2.0.17/go.mod h1:y5WSHcJY4YfNB/5r7ca5YjHeIr1H3LM1rKArGGs8jMc=
|
||||
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
|
||||
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
||||
@@ -87,16 +96,18 @@ github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QA
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA=
|
||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.2.19 h1:XNu5e62mkzafw1qYuKtQ+Dviw4JpbzC/SLx3zZt49JY=
|
||||
github.com/pion/webrtc/v3 v3.2.19/go.mod h1:vVURQTBOG5BpWKOJz3nlr23NfTDeyKVmubRNqzQp+Tg=
|
||||
github.com/pion/turn/v2 v2.1.4 h1:2xn8rduI5W6sCZQkEnIUDAkrBQNl2eYIBCHMZ3QMmP8=
|
||||
github.com/pion/turn/v2 v2.1.4/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.2.24 h1:MiFL5DMo2bDaaIFWr0DDpwiV/L4EGbLZb+xoRvfEo1Y=
|
||||
github.com/pion/webrtc/v3 v3.2.24/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
|
||||
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
|
||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
||||
@@ -105,6 +116,7 @@ github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDf
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
@@ -124,13 +136,14 @@ golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -146,15 +159,16 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -167,8 +181,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -181,8 +193,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -209,8 +222,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
|
||||
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
+97
-69
@@ -10,6 +10,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
@@ -19,25 +20,26 @@ import (
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Mod struct {
|
||||
Listen string `yaml:"listen"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
BasePath string `yaml:"base_path"`
|
||||
StaticDir string `yaml:"static_dir"`
|
||||
Origin string `yaml:"origin"`
|
||||
TLSListen string `yaml:"tls_listen"`
|
||||
TLSCert string `yaml:"tls_cert"`
|
||||
TLSKey string `yaml:"tls_key"`
|
||||
Listen string `yaml:"listen"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
BasePath string `yaml:"base_path"`
|
||||
StaticDir string `yaml:"static_dir"`
|
||||
Origin string `yaml:"origin"`
|
||||
TLSListen string `yaml:"tls_listen"`
|
||||
TLSCert string `yaml:"tls_cert"`
|
||||
TLSKey string `yaml:"tls_key"`
|
||||
UnixListen string `yaml:"unix_listen"`
|
||||
} `yaml:"api"`
|
||||
}
|
||||
|
||||
// default config
|
||||
cfg.Mod.Listen = "0.0.0.0:1984"
|
||||
cfg.Mod.Listen = ":1984"
|
||||
|
||||
// load config from YAML
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
if cfg.Mod.Listen == "" {
|
||||
if cfg.Mod.Listen == "" && cfg.Mod.UnixListen == "" && cfg.Mod.TLSListen == "" {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -50,16 +52,7 @@ func Init() {
|
||||
HandleFunc("api/config", configHandler)
|
||||
HandleFunc("api/exit", exitHandler)
|
||||
HandleFunc("api/restart", restartHandler)
|
||||
|
||||
// ensure we can listen without errors
|
||||
var err error
|
||||
ln, err = net.Listen("tcp", cfg.Mod.Listen)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("[api] listen")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
|
||||
HandleFunc("api/log", logHandler)
|
||||
|
||||
Handler = http.DefaultServeMux // 4th
|
||||
|
||||
@@ -75,59 +68,74 @@ func Init() {
|
||||
Handler = middlewareLog(Handler) // 1st
|
||||
}
|
||||
|
||||
go func() {
|
||||
s := http.Server{}
|
||||
s.Handler = Handler
|
||||
if err = s.Serve(ln); err != nil {
|
||||
log.Fatal().Err(err).Msg("[api] serve")
|
||||
}
|
||||
}()
|
||||
if cfg.Mod.Listen != "" {
|
||||
go listen("tcp", cfg.Mod.Listen)
|
||||
}
|
||||
|
||||
if cfg.Mod.UnixListen != "" {
|
||||
_ = syscall.Unlink(cfg.Mod.UnixListen)
|
||||
go listen("unix", cfg.Mod.UnixListen)
|
||||
}
|
||||
|
||||
// Initialize the HTTPS server
|
||||
if cfg.Mod.TLSListen != "" && cfg.Mod.TLSCert != "" && cfg.Mod.TLSKey != "" {
|
||||
var cert tls.Certificate
|
||||
if strings.IndexByte(cfg.Mod.TLSCert, '\n') < 0 && strings.IndexByte(cfg.Mod.TLSKey, '\n') < 0 {
|
||||
// check if file path
|
||||
cert, err = tls.LoadX509KeyPair(cfg.Mod.TLSCert, cfg.Mod.TLSKey)
|
||||
} else {
|
||||
// if text file content
|
||||
cert, err = tls.X509KeyPair([]byte(cfg.Mod.TLSCert), []byte(cfg.Mod.TLSKey))
|
||||
}
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
tlsListener, err := net.Listen("tcp", cfg.Mod.TLSListen)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Str("addr", cfg.Mod.TLSListen).Msg("[api] tls listen")
|
||||
|
||||
tlsServer := &http.Server{
|
||||
Handler: Handler,
|
||||
TLSConfig: &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
},
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := tlsServer.ServeTLS(tlsListener, "", ""); err != nil {
|
||||
log.Fatal().Err(err).Msg("[api] tls serve")
|
||||
}
|
||||
}()
|
||||
go tlsListen("tcp", cfg.Mod.TLSListen, cfg.Mod.TLSCert, cfg.Mod.TLSKey)
|
||||
}
|
||||
}
|
||||
|
||||
func Port() int {
|
||||
if ln == nil {
|
||||
return 0
|
||||
func listen(network, address string) {
|
||||
ln, err := net.Listen(network, address)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api] listen")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Str("addr", address).Msg("[api] listen")
|
||||
|
||||
if network == "tcp" {
|
||||
Port = ln.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
server := http.Server{Handler: Handler}
|
||||
if err = server.Serve(ln); err != nil {
|
||||
log.Fatal().Err(err).Msg("[api] serve")
|
||||
}
|
||||
return ln.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
func tlsListen(network, address, certFile, keyFile string) {
|
||||
var cert tls.Certificate
|
||||
var err error
|
||||
if strings.IndexByte(certFile, '\n') < 0 && strings.IndexByte(keyFile, '\n') < 0 {
|
||||
// check if file path
|
||||
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
} else {
|
||||
// if text file content
|
||||
cert, err = tls.X509KeyPair([]byte(certFile), []byte(keyFile))
|
||||
}
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
ln, err := net.Listen(network, address)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api] tls listen")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Str("addr", address).Msg("[api] tls listen")
|
||||
|
||||
server := &http.Server{
|
||||
Handler: Handler,
|
||||
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
|
||||
}
|
||||
if err = server.ServeTLS(ln, "", ""); err != nil {
|
||||
log.Fatal().Err(err).Msg("[api] tls serve")
|
||||
}
|
||||
}
|
||||
|
||||
var Port int
|
||||
|
||||
const (
|
||||
MimeJSON = "application/json"
|
||||
MimeText = "text/plain"
|
||||
@@ -187,7 +195,7 @@ func middlewareLog(next http.Handler) http.Handler {
|
||||
|
||||
func middlewareAuth(username, password string, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") {
|
||||
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") && r.RemoteAddr != "@" {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok || user != username || pass != password {
|
||||
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
|
||||
@@ -204,12 +212,11 @@ func middlewareCORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
var ln net.Listener
|
||||
var mu sync.Mutex
|
||||
|
||||
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -227,7 +234,14 @@ func exitHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
s := r.URL.Query().Get("code")
|
||||
code, _ := strconv.Atoi(s)
|
||||
code, err := strconv.Atoi(s)
|
||||
|
||||
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02
|
||||
if err != nil || code < 0 || code > 125 {
|
||||
http.Error(w, "Code must be in the range [0, 125]", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
@@ -240,6 +254,20 @@ func restartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
go shell.Restart()
|
||||
}
|
||||
|
||||
func logHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
// Send current state of the log file immediately
|
||||
w.Header().Set("Content-Type", "application/jsonlines")
|
||||
_, _ = app.MemoryLog.WriteTo(w)
|
||||
case "DELETE":
|
||||
app.MemoryLog.Reset()
|
||||
Response(w, "OK", "text/plain")
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
+1
-39
@@ -4,20 +4,17 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var Version = ""
|
||||
var Version = "1.8.5"
|
||||
var UserAgent = "go2rtc/" + Version
|
||||
|
||||
var ConfigPath string
|
||||
@@ -86,26 +83,6 @@ func Init() {
|
||||
migrateStore()
|
||||
}
|
||||
|
||||
func NewLogger(format string, level string) zerolog.Logger {
|
||||
var writer io.Writer = os.Stdout
|
||||
|
||||
if format != "json" {
|
||||
writer = zerolog.ConsoleWriter{
|
||||
Out: writer, TimeFormat: "15:04:05.000",
|
||||
NoColor: writer != os.Stdout || format == "text",
|
||||
}
|
||||
}
|
||||
|
||||
zerolog.TimeFieldFormat = time.RFC3339Nano
|
||||
|
||||
lvl, err := zerolog.ParseLevel(level)
|
||||
if err != nil || lvl == zerolog.NoLevel {
|
||||
lvl = zerolog.InfoLevel
|
||||
}
|
||||
|
||||
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
|
||||
}
|
||||
|
||||
func LoadConfig(v any) {
|
||||
for _, data := range configs {
|
||||
if err := yaml.Unmarshal(data, v); err != nil {
|
||||
@@ -114,18 +91,6 @@ func LoadConfig(v any) {
|
||||
}
|
||||
}
|
||||
|
||||
func GetLogger(module string) zerolog.Logger {
|
||||
if s, ok := modules[module]; ok {
|
||||
lvl, err := zerolog.ParseLevel(s)
|
||||
if err == nil {
|
||||
return log.Level(lvl)
|
||||
}
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
}
|
||||
|
||||
return log.Logger
|
||||
}
|
||||
|
||||
func PatchConfig(key string, value any, path ...string) error {
|
||||
if ConfigPath == "" {
|
||||
return errors.New("config file disabled")
|
||||
@@ -156,6 +121,3 @@ func (c *Config) Set(value string) error {
|
||||
}
|
||||
|
||||
var configs [][]byte
|
||||
|
||||
// modules log levels
|
||||
var modules map[string]string
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var MemoryLog *circularBuffer
|
||||
|
||||
func NewLogger(format string, level string) zerolog.Logger {
|
||||
var writer io.Writer = os.Stdout
|
||||
|
||||
if format != "json" {
|
||||
writer = zerolog.ConsoleWriter{
|
||||
Out: writer, TimeFormat: "15:04:05.000", NoColor: format == "text",
|
||||
}
|
||||
}
|
||||
|
||||
MemoryLog = newBuffer(16)
|
||||
|
||||
writer = zerolog.MultiLevelWriter(writer, MemoryLog)
|
||||
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
||||
|
||||
lvl, err := zerolog.ParseLevel(level)
|
||||
if err != nil || lvl == zerolog.NoLevel {
|
||||
lvl = zerolog.InfoLevel
|
||||
}
|
||||
|
||||
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
|
||||
}
|
||||
|
||||
func GetLogger(module string) zerolog.Logger {
|
||||
if s, ok := modules[module]; ok {
|
||||
lvl, err := zerolog.ParseLevel(s)
|
||||
if err == nil {
|
||||
return log.Level(lvl)
|
||||
}
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
}
|
||||
|
||||
return log.Logger
|
||||
}
|
||||
|
||||
// modules log levels
|
||||
var modules map[string]string
|
||||
|
||||
const chunkSize = 1 << 16
|
||||
|
||||
type circularBuffer struct {
|
||||
chunks [][]byte
|
||||
r, w int
|
||||
}
|
||||
|
||||
func newBuffer(chunks int) *circularBuffer {
|
||||
b := &circularBuffer{chunks: make([][]byte, 0, chunks)}
|
||||
// create first chunk
|
||||
b.chunks = append(b.chunks, make([]byte, 0, chunkSize))
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *circularBuffer) Write(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
|
||||
// check if chunk has size
|
||||
if len(b.chunks[b.w])+n > chunkSize {
|
||||
// increase write chunk index
|
||||
if b.w++; b.w == cap(b.chunks) {
|
||||
b.w = 0
|
||||
}
|
||||
// check overflow
|
||||
if b.r == b.w {
|
||||
// increase read chunk index
|
||||
if b.r++; b.r == cap(b.chunks) {
|
||||
b.r = 0
|
||||
}
|
||||
}
|
||||
// check if current chunk exists
|
||||
if b.w == len(b.chunks) {
|
||||
// allocate new chunk
|
||||
b.chunks = append(b.chunks, make([]byte, 0, chunkSize))
|
||||
} else {
|
||||
// reset len of current chunk
|
||||
b.chunks[b.w] = b.chunks[b.w][:0]
|
||||
}
|
||||
}
|
||||
|
||||
b.chunks[b.w] = append(b.chunks[b.w], p...)
|
||||
return
|
||||
}
|
||||
|
||||
func (b *circularBuffer) WriteTo(w io.Writer) (n int64, err error) {
|
||||
for i := b.r; ; {
|
||||
var nn int
|
||||
if nn, err = w.Write(b.chunks[i]); err != nil {
|
||||
return
|
||||
}
|
||||
n += int64(nn)
|
||||
|
||||
if i == b.w {
|
||||
break
|
||||
}
|
||||
if i++; i == cap(b.chunks) {
|
||||
i = 0
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *circularBuffer) Reset() {
|
||||
b.chunks[0] = b.chunks[0][:0]
|
||||
b.r = 0
|
||||
b.w = 0
|
||||
}
|
||||
@@ -83,7 +83,12 @@ func handlePipe(url string, cmd *exec.Cmd) (core.Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return magic.Open(r)
|
||||
prod, err := magic.Open(r)
|
||||
if err != nil {
|
||||
_ = r.Close()
|
||||
}
|
||||
|
||||
return prod, err
|
||||
}
|
||||
|
||||
func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# Expr
|
||||
|
||||
[Expr](https://github.com/antonmedv/expr) - expression language and expression evaluation for Go.
|
||||
|
||||
- [language definition](https://expr.medv.io/docs/Language-Definition) - takes best from JS, Python, Jinja2 syntax
|
||||
- your expression should return a link of any supported source
|
||||
- expression supports multiple operation, but:
|
||||
- all operations must be separated by a semicolon
|
||||
- all operations, except the last one, must declare a new variable (`let s = "abc";`)
|
||||
- the last operation should return a string
|
||||
- go2rtc supports additional functions:
|
||||
- `fetch` - JS-like HTTP requests
|
||||
- `match` - JS-like RegExp queries
|
||||
|
||||
## Examples
|
||||
|
||||
**Two way audio for Dahua VTO**
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
dahua_vto: |
|
||||
expr: let host = "admin:password@192.168.1.123";
|
||||
fetch("http://"+host+"/cgi-bin/configManager.cgi?action=setConfig&Encode[0].MainFormat[0].Audio.Compression=G.711A&Encode[0].MainFormat[0].Audio.Frequency=8000").ok
|
||||
? "rtsp://"+host+"/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif" : ""
|
||||
```
|
||||
|
||||
**dom.ru**
|
||||
|
||||
You can get credentials via:
|
||||
|
||||
- https://github.com/alexmorbo/domru (file `/share/domru/accounts`)
|
||||
- https://github.com/ad/domru
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
dom_ru: |
|
||||
expr: let camera = "99999999"; let token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; let operator = 99;
|
||||
fetch("https://myhome.novotelecom.ru/rest/v1/forpost/cameras/"+camera+"/video", {
|
||||
headers: {Authorization: "Bearer "+token, Operator: operator}
|
||||
}).json().data.URL
|
||||
```
|
||||
|
||||
**Parse HLS files from Apple**
|
||||
|
||||
Same example in two languages - python and expr.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
example_python: |
|
||||
echo:python -c 'from urllib.request import urlopen; import re
|
||||
|
||||
# url1 = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
|
||||
html1 = urlopen("https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html").read().decode("utf-8")
|
||||
url1 = re.search(r"https.+?m3u8", html1)[0]
|
||||
|
||||
# url2 = "gear1/prog_index.m3u8"
|
||||
html2 = urlopen(url1).read().decode("utf-8")
|
||||
url2 = re.search(r"^[a-z0-1/_]+\.m3u8$", html2, flags=re.MULTILINE)[0]
|
||||
|
||||
# url3 = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear1/prog_index.m3u8"
|
||||
url3 = url1[:url1.rindex("/")+1] + url2
|
||||
|
||||
print("ffmpeg:" + url3 + "#video=copy")'
|
||||
|
||||
example_expr: |
|
||||
expr:
|
||||
|
||||
let html1 = fetch("https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html").text;
|
||||
let url1 = match(html1, "https.+?m3u8")[0];
|
||||
|
||||
let html2 = fetch(url1).text;
|
||||
let url2 = match(html2, "^[a-z0-1/_]+\\.m3u8$", "m")[0];
|
||||
|
||||
let url3 = url1[:lastIndexOf(url1, "/")+1] + url2;
|
||||
|
||||
"ffmpeg:" + url3 + "#video=copy"
|
||||
```
|
||||
|
||||
## Comparsion
|
||||
|
||||
| expr | python | js |
|
||||
|------------------------------|----------------------------|--------------------------------|
|
||||
| let x = 1; | x = 1 | let x = 1 |
|
||||
| {a: 1, b: 2} | {"a": 1, "b": 2} | {a: 1, b: 2} |
|
||||
| let r = fetch(url, {method}) | r = request(method, url) | r = await fetch(url, {method}) |
|
||||
| r.ok | r.ok | r.ok |
|
||||
| r.status | r.status_code | r.status |
|
||||
| r.text | r.text | await r.text() |
|
||||
| r.json() | r.json() | await r.json() |
|
||||
| r.headers | r.headers | r.headers |
|
||||
| let m = match(text, "abc") | m = re.search("abc", text) | let m = text.match(/abc/) |
|
||||
@@ -0,0 +1,28 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/expr"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
log := app.GetLogger("expr")
|
||||
|
||||
streams.RedirectFunc("expr", func(url string) (string, error) {
|
||||
v, err := expr.Run(url[5:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Debug().Msgf("[expr] url=%s", url)
|
||||
|
||||
if url = v.(string); url == "" {
|
||||
return "", errors.New("expr: result is empty")
|
||||
}
|
||||
|
||||
return url, nil
|
||||
})
|
||||
}
|
||||
@@ -45,30 +45,20 @@ func queryToInput(query url.Values) string {
|
||||
}
|
||||
|
||||
if video != "" {
|
||||
input += ` -i video="` + video + `"`
|
||||
input += ` -i "video=` + video
|
||||
|
||||
if audio != "" {
|
||||
input += `:audio="` + audio + `"`
|
||||
input += `:audio=` + audio
|
||||
}
|
||||
|
||||
input += `"`
|
||||
} else {
|
||||
input += ` -i audio="` + audio + `"`
|
||||
input += ` -i "audio=` + audio + `"`
|
||||
}
|
||||
|
||||
return input
|
||||
}
|
||||
|
||||
func deviceInputSuffix(video, audio string) string {
|
||||
switch {
|
||||
case video != "" && audio != "":
|
||||
return `video="` + video + `":audio=` + audio + `"`
|
||||
case video != "":
|
||||
return `video="` + video + `"`
|
||||
case audio != "":
|
||||
return `audio="` + audio + `"`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func initDevices() {
|
||||
cmd := exec.Command(
|
||||
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
|
||||
|
||||
@@ -61,47 +61,58 @@ var defaults = map[string]string{
|
||||
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
|
||||
// https://github.com/pion/webrtc/issues/1514
|
||||
// https://ffmpeg.org/ffmpeg-resampler.html
|
||||
// `-async 1` or `-min_comp 0` - force frame_size=960, important for WebRTC audio quality
|
||||
"opus": "-c:a libopus -application:a lowdelay -frame_duration 20 -min_comp 0",
|
||||
// `-async 1` or `-min_comp 0` - force resampling for static timestamp inc, important for WebRTC audio quality
|
||||
"opus": "-c:a libopus -application:a lowdelay -min_comp 0",
|
||||
"opus/16000": "-c:a libopus -application:a lowdelay -min_comp 0 -ar:a 16000 -ac:a 1",
|
||||
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
|
||||
"pcmu/8000": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
|
||||
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
|
||||
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
|
||||
"pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
|
||||
"pcma/8000": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
|
||||
"pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
|
||||
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
|
||||
"aac": "-c:a aac", // keep sample rate and channels
|
||||
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
|
||||
"mp3": "-c:a libmp3lame -q:a 8",
|
||||
"pcm": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
|
||||
"pcm/8000": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
|
||||
"pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
|
||||
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
|
||||
"pcml": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
|
||||
"pcml/8000": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
|
||||
"pcml/44100": "-c:a pcm_s16le -ar:a 44100 -ac:a 1",
|
||||
|
||||
// hardware Intel and AMD on Linux
|
||||
// better not to set `-async_depth:v 1` like for QSV, because framedrops
|
||||
// `-bf 0` - disable B-frames is very important
|
||||
"h264/vaapi": "-c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0",
|
||||
"h265/vaapi": "-c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0",
|
||||
"h265/vaapi": "-c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0",
|
||||
"mjpeg/vaapi": "-c:v mjpeg_vaapi",
|
||||
|
||||
// hardware Raspberry
|
||||
"h264/v4l2m2m": "-c:v h264_v4l2m2m -g 50 -bf 0",
|
||||
"h265/v4l2m2m": "-c:v hevc_v4l2m2m -g 50 -bf 0",
|
||||
|
||||
// hardware Rockchip
|
||||
// important to use custom ffmpeg https://github.com/AlexxIT/go2rtc/issues/768
|
||||
// hevc - doesn't have a profile setting
|
||||
"h264/rkmpp": "-c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1",
|
||||
"h265/rkmpp": "-c:v hevc_rkmpp_encoder -g 50 -bf 0 -level:v 5.1",
|
||||
|
||||
// hardware NVidia on Linux and Windows
|
||||
// preset=p2 - faster, tune=ll - low latency
|
||||
"h264/cuda": "-c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
|
||||
"h265/cuda": "-c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto",
|
||||
"h265/cuda": "-c:v hevc_nvenc -g 50 -bf 0 -profile:v main -level:v auto",
|
||||
|
||||
// hardware Intel on Windows
|
||||
"h264/dxva2": "-c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1",
|
||||
"h265/dxva2": "-c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1",
|
||||
"mjpeg/dxva2": "-c:v mjpeg_qsv -profile:v high -level:v 5.1",
|
||||
"h265/dxva2": "-c:v hevc_qsv -g 50 -bf 0 -profile:v main -level:v 5.1 -async_depth:v 1",
|
||||
"mjpeg/dxva2": "-c:v mjpeg_qsv",
|
||||
|
||||
// hardware macOS
|
||||
"h264/videotoolbox": "-c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1",
|
||||
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1",
|
||||
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v main -level:v 5.1",
|
||||
}
|
||||
|
||||
// configTemplate - return template from config (defaults) if exist or return raw template
|
||||
|
||||
@@ -35,12 +35,15 @@ func TestParseArgsFile(t *testing.T) {
|
||||
func TestParseArgsDevice(t *testing.T) {
|
||||
// [DEVICE] video will be output for MJPEG to pipe, with size 1920x1080
|
||||
args := parseArgs("device?video=0&video_size=1920x1080")
|
||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i "video=0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped
|
||||
//args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma")
|
||||
args = parseArgs("device?video=0&framerate=20#video=h265#audio=pcma")
|
||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -framerate 20 -i video="0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -c:a pcm_alaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
args = parseArgs("device?video=0&framerate=20#video=h265")
|
||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -framerate 20 -i "video=0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
args = parseArgs("device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)")
|
||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -i "video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
}
|
||||
|
||||
func TestParseArgsIpCam(t *testing.T) {
|
||||
@@ -151,6 +154,18 @@ func TestParseArgsHwV4l2m2m(t *testing.T) {
|
||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
}
|
||||
|
||||
func TestParseArgsHwRKMPP(t *testing.T) {
|
||||
// [HTTP-MJPEG] video will be transcoded to H264
|
||||
args := parseArgs("http://example.com#video=h264#hardware=rkmpp")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
args = parseArgs("http://example.com#video=h264#rotate=180#hardware=rkmpp")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf "transpose=1,transpose=1" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
args = parseArgs("http://example.com#video=h264#height=320#hardware=rkmpp")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1 -height 320 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
}
|
||||
|
||||
func TestParseArgsHwCuda(t *testing.T) {
|
||||
// [HTTP-MJPEG] video will be transcoded to H264
|
||||
args := parseArgs("http:///example.com#video=h264#hardware=cuda")
|
||||
@@ -213,3 +228,14 @@ func TestDeckLink(t *testing.T) {
|
||||
args := parseArgs(`DeckLink SDI (2)#video=h264#hardware=vaapi#input=-format_code Hp29 -f decklink -i "{input}"`)
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -format_code Hp29 -f decklink -i "DeckLink SDI (2)" -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
}
|
||||
|
||||
func TestDrawText(t *testing.T) {
|
||||
args := parseArgs("http:///example.com#video=h264#drawtext=fontsize=12")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
args = parseArgs("http:///example.com#video=h264#width=640#drawtext=fontsize=12")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=640:-1,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
args = parseArgs("http:///example.com#video=h264#width=640#drawtext=fontsize=12#hardware=vaapi")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "scale=640:-1:out_color_matrix=bt709:out_range=tv,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}',hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ const (
|
||||
EngineCUDA = "cuda" // NVidia on Windows and Linux
|
||||
EngineDXVA2 = "dxva2" // Intel on Windows
|
||||
EngineVideoToolbox = "videotoolbox" // macOS
|
||||
EngineRKMPP = "rkmpp" // Rockchip
|
||||
)
|
||||
|
||||
func Init(bin string) {
|
||||
@@ -58,11 +59,13 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
|
||||
case EngineVAAPI:
|
||||
args.Codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
fixYCbCrRange(args)
|
||||
|
||||
if !args.HasFilters("drawtext=") {
|
||||
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch " + args.Input
|
||||
|
||||
if name == "h264" {
|
||||
fixPixelFormat(args)
|
||||
}
|
||||
|
||||
for i, filter := range args.Filters {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
args.Filters[i] = "scale_vaapi=" + filter[6:]
|
||||
@@ -123,6 +126,24 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
|
||||
|
||||
case EngineV4L2M2M:
|
||||
args.Codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
case EngineRKMPP:
|
||||
args.Codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
for j, filter := range args.Filters {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
args.Filters = append(args.Filters[:j], args.Filters[j+1:]...)
|
||||
|
||||
width, height, _ := strings.Cut(filter[6:], ":")
|
||||
if width != "-1" {
|
||||
args.Codecs[i] += " -width " + width
|
||||
}
|
||||
if height != "-1" {
|
||||
args.Codecs[i] += " -height " + height
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,20 +178,23 @@ func cut(s string, sep byte, pos int) string {
|
||||
return s
|
||||
}
|
||||
|
||||
// fixYCbCrRange convert jpeg/pc range to mpeg/tv range
|
||||
// vaapi(pc, bt709, progressive) == yuvj420p (jpeg/full/pc)
|
||||
// vaapi(tv, bt709, progressive) == yuv420p (mpeg/limited/tv)
|
||||
// https://ffmpeg.org/ffmpeg-all.html#scale-1
|
||||
func fixYCbCrRange(args *ffmpeg.Args) {
|
||||
// fixPixelFormat:
|
||||
// - good h264 pixel: yuv420p(tv, bt709) == yuv420p (mpeg/limited/tv)
|
||||
// - bad h264 pixel: yuvj420p(pc, bt709) == yuvj420p (jpeg/full/pc)
|
||||
// - bad jpeg pixel: yuvj422p(pc, bt470bg)
|
||||
func fixPixelFormat(args *ffmpeg.Args) {
|
||||
// in my tests this filters has same CPU/GPU load:
|
||||
// - "hwupload"
|
||||
// - "hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv"
|
||||
// - "hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12"
|
||||
const fixPixFmt = "out_color_matrix=bt709:out_range=tv:format=nv12"
|
||||
|
||||
for i, filter := range args.Filters {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
if !strings.Contains(filter, "out_range=") {
|
||||
args.Filters[i] = filter + ":out_range=tv"
|
||||
}
|
||||
args.Filters[i] = filter + ":" + fixPixFmt
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// scale=out_color_matrix=bt709:out_range=tv
|
||||
args.Filters = append(args.Filters, "scale=out_range=tv")
|
||||
args.Filters = append(args.Filters, "scale="+fixPixFmt)
|
||||
}
|
||||
|
||||
@@ -6,13 +6,17 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
)
|
||||
|
||||
const ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
|
||||
const ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
|
||||
const ProbeVAAPIH264 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -f null -"
|
||||
const ProbeVAAPIH265 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c hevc_vaapi -f null -"
|
||||
const ProbeVAAPIJPEG = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c mjpeg_vaapi -f null -"
|
||||
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
|
||||
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
|
||||
const (
|
||||
ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
|
||||
ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
|
||||
ProbeRKMPPH264 = "-f lavfi -i testsrc2 -t 1 -c h264_rkmpp_encoder -f null -"
|
||||
ProbeRKMPPH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_rkmpp_encoder -f null -"
|
||||
ProbeVAAPIH264 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -f null -"
|
||||
ProbeVAAPIH265 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c hevc_vaapi -f null -"
|
||||
ProbeVAAPIJPEG = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c mjpeg_vaapi -f null -"
|
||||
ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
|
||||
ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
|
||||
)
|
||||
|
||||
func ProbeAll(bin string) []*api.Source {
|
||||
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
|
||||
@@ -25,6 +29,14 @@ func ProbeAll(bin string) []*api.Source {
|
||||
Name: runToString(bin, ProbeV4L2M2MH265),
|
||||
URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M,
|
||||
},
|
||||
{
|
||||
Name: runToString(bin, ProbeRKMPPH264),
|
||||
URL: "ffmpeg:...#video=h264#hardware=" + EngineRKMPP,
|
||||
},
|
||||
{
|
||||
Name: runToString(bin, ProbeRKMPPH265),
|
||||
URL: "ffmpeg:...#video=h265#hardware=" + EngineRKMPP,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,10 +71,16 @@ func ProbeHardware(bin, name string) string {
|
||||
if run(bin, ProbeV4L2M2MH264) {
|
||||
return EngineV4L2M2M
|
||||
}
|
||||
if run(bin, ProbeRKMPPH264) {
|
||||
return EngineRKMPP
|
||||
}
|
||||
case "h265":
|
||||
if run(bin, ProbeV4L2M2MH265) {
|
||||
return EngineV4L2M2M
|
||||
}
|
||||
if run(bin, ProbeRKMPPH265) {
|
||||
return EngineRKMPP
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# GoPro
|
||||
|
||||
Supported models: HERO9, HERO10, HERO11, HERO12.
|
||||
Supported OS: Linux, Mac, Windows, [HassOS](https://www.home-assistant.io/installation/)
|
||||
|
||||
The other camera models have different APIs. I will try to add them in the next versions.
|
||||
|
||||
## Config
|
||||
|
||||
- USB-connected cameras create a new network interface in the system
|
||||
- Linux users do not need to install anything
|
||||
- Windows users should install the [network driver](https://community.gopro.com/s/article/GoPro-Webcam)
|
||||
- if the camera is detected but the stream does not start - you need to disable firewall
|
||||
|
||||
1. Discover camera address: WebUI > Add > GoPro
|
||||
2. Add camera to config
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
hero12: gopro://172.20.100.51
|
||||
```
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://gopro.github.io/OpenGoPro/
|
||||
@@ -0,0 +1,30 @@
|
||||
package gopro
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/gopro"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("gopro", handleGoPro)
|
||||
|
||||
api.HandleFunc("api/gopro", apiGoPro)
|
||||
}
|
||||
|
||||
func handleGoPro(rawURL string) (core.Producer, error) {
|
||||
return gopro.Dial(rawURL)
|
||||
}
|
||||
|
||||
func apiGoPro(w http.ResponseWriter, r *http.Request) {
|
||||
var items []*api.Source
|
||||
|
||||
for _, host := range gopro.Discovery() {
|
||||
items = append(items, &api.Source{Name: host, URL: "gopro://" + host})
|
||||
}
|
||||
|
||||
api.ResponseSources(w, items)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package homekit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -97,7 +98,7 @@ func Init() {
|
||||
|
||||
srv.mdns = &mdns.ServiceEntry{
|
||||
Name: name,
|
||||
Port: uint16(api.Port()),
|
||||
Port: uint16(api.Port),
|
||||
Info: map[string]string{
|
||||
hap.TXTConfigNumber: "1",
|
||||
hap.TXTFeatureFlags: "0",
|
||||
@@ -121,7 +122,7 @@ func Init() {
|
||||
api.HandleFunc(hap.PathPairSetup, hapPairSetup)
|
||||
api.HandleFunc(hap.PathPairVerify, hapPairVerify)
|
||||
|
||||
log.Trace().Msgf("[homekit] mnds: %s", entries)
|
||||
log.Trace().Msgf("[homekit] mdns: %s", entries)
|
||||
|
||||
go func() {
|
||||
if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil {
|
||||
@@ -134,6 +135,10 @@ var log zerolog.Logger
|
||||
var servers map[string]*server
|
||||
|
||||
func streamHandler(url string) (core.Producer, error) {
|
||||
if srtp.Server == nil {
|
||||
return nil, errors.New("homekit: can't work without SRTP server")
|
||||
}
|
||||
|
||||
return homekit.Dial(url, srtp.Server)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hls"
|
||||
@@ -22,6 +23,8 @@ func Init() {
|
||||
streams.HandleFunc("httpx", handleHTTP)
|
||||
|
||||
streams.HandleFunc("tcp", handleTCP)
|
||||
|
||||
api.HandleFunc("api/stream", apiStream)
|
||||
}
|
||||
|
||||
func handleHTTP(rawURL string) (core.Producer, error) {
|
||||
@@ -89,3 +92,26 @@ func handleTCP(rawURL string) (core.Producer, error) {
|
||||
|
||||
return magic.Open(conn)
|
||||
}
|
||||
|
||||
func apiStream(w http.ResponseWriter, r *http.Request) {
|
||||
dst := r.URL.Query().Get("dst")
|
||||
stream := streams.Get(dst)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := magic.Open(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
stream.AddProducer(client)
|
||||
defer stream.RemoveProducer(client)
|
||||
|
||||
if err = client.Start(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,19 +56,17 @@ func inputMpegTS(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
res := &http.Response{Body: r.Body, Request: r}
|
||||
client, err := mpegts.Open(res.Body)
|
||||
client, err := mpegts.Open(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
stream.AddProducer(client)
|
||||
defer stream.RemoveProducer(client)
|
||||
|
||||
if err = client.Start(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
stream.RemoveProducer(client)
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ package ngrok
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ngrok"
|
||||
"github.com/rs/zerolog"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -39,7 +40,7 @@ func Init() {
|
||||
}
|
||||
|
||||
// Addr: "//localhost:8555", URL: "tcp://1.tcp.eu.ngrok.io:12345"
|
||||
if msg.Addr == "//localhost:"+webrtc.Port && strings.HasPrefix(msg.URL, "tcp://") {
|
||||
if strings.HasPrefix(msg.Addr, "//localhost:") && strings.HasPrefix(msg.URL, "tcp://") {
|
||||
// don't know if really necessary use IP
|
||||
address, err := ConvertHostToIP(msg.URL[6:])
|
||||
if err != nil {
|
||||
@@ -49,7 +50,7 @@ func Init() {
|
||||
|
||||
log.Info().Str("addr", address).Msg("[ngrok] add external candidate for WebRTC")
|
||||
|
||||
webrtc.AddCandidate(address)
|
||||
webrtc.AddCandidate(address, "tcp")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -26,7 +26,7 @@ func Init() {
|
||||
}
|
||||
|
||||
// default config
|
||||
conf.Mod.Listen = "0.0.0.0:8554"
|
||||
conf.Mod.Listen = ":8554"
|
||||
conf.Mod.DefaultQuery = "video&audio"
|
||||
|
||||
app.LoadConfig(&conf)
|
||||
|
||||
@@ -13,7 +13,7 @@ func Init() {
|
||||
}
|
||||
|
||||
// default config
|
||||
cfg.Mod.Listen = "0.0.0.0:8443"
|
||||
cfg.Mod.Listen = ":8443"
|
||||
|
||||
// load config from YAML
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
## Config
|
||||
|
||||
- supported TCP: fixed port (default), disabled
|
||||
- supported UDP: random port (default), fixed port
|
||||
|
||||
| Config examples | TCP | UDP |
|
||||
|-----------------------|-------|--------|
|
||||
| `listen: ":8555/tcp"` | fixed | random |
|
||||
| `listen: ":8555"` | fixed | fixed |
|
||||
| `listen: ""` | no | random |
|
||||
|
||||
## Userful links
|
||||
|
||||
- https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html
|
||||
|
||||
@@ -1,58 +1,66 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/pion/sdp/v3"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Address struct {
|
||||
Host string
|
||||
Port int
|
||||
Host string
|
||||
Port string
|
||||
Network string
|
||||
Offset int
|
||||
}
|
||||
|
||||
var addresses []Address
|
||||
|
||||
func AddCandidate(address string) {
|
||||
var port int
|
||||
|
||||
// try to get port from address string
|
||||
if i := strings.LastIndexByte(address, ':'); i > 0 {
|
||||
if v, _ := strconv.Atoi(address[i+1:]); v != 0 {
|
||||
address = address[:i]
|
||||
port = v
|
||||
func (a *Address) Marshal() string {
|
||||
host := a.Host
|
||||
if host == "stun" {
|
||||
ip, err := webrtc.GetCachedPublicIP()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
host = ip.String()
|
||||
}
|
||||
|
||||
// use default WebRTC port
|
||||
if port == 0 {
|
||||
port, _ = strconv.Atoi(Port)
|
||||
switch a.Network {
|
||||
case "udp":
|
||||
return webrtc.CandidateManualHostUDP(host, a.Port, a.Offset)
|
||||
case "tcp":
|
||||
return webrtc.CandidateManualHostTCPPassive(host, a.Port, a.Offset)
|
||||
}
|
||||
|
||||
addresses = append(addresses, Address{Host: address, Port: port})
|
||||
return ""
|
||||
}
|
||||
|
||||
var addresses []*Address
|
||||
|
||||
func AddCandidate(address, network string) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
offset := -1 - len(addresses) // every next candidate will have a lower priority
|
||||
|
||||
switch network {
|
||||
case "tcp", "udp":
|
||||
addresses = append(addresses, &Address{host, port, network, offset})
|
||||
default:
|
||||
addresses = append(
|
||||
addresses, &Address{host, port, "udp", offset}, &Address{host, port, "tcp", offset},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func GetCandidates() (candidates []string) {
|
||||
for _, address := range addresses {
|
||||
// using stun server for receive public IP-address
|
||||
if address.Host == "stun" {
|
||||
ip, err := webrtc.GetCachedPublicIP()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// this is a copy, original host unchanged
|
||||
address.Host = ip.String()
|
||||
if candidate := address.Marshal(); candidate != "" {
|
||||
candidates = append(candidates, candidate)
|
||||
}
|
||||
|
||||
candidates = append(
|
||||
candidates,
|
||||
webrtc.CandidateManualHostUDP(address.Host, address.Port),
|
||||
webrtc.CandidateManualHostTCPPassive(address.Host, address.Port),
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ func go2rtcClient(url string) (core.Producer, error) {
|
||||
switch msg := msg.(type) {
|
||||
case *pion.ICECandidate:
|
||||
s := msg.ToJSON().Candidate
|
||||
log.Trace().Str("candidate", s).Msg("[webrtc] local")
|
||||
log.Trace().Str("candidate", s).Msg("[webrtc] local ")
|
||||
_ = conn.WriteJSON(&ws.Message{Type: "webrtc/candidate", Value: s})
|
||||
|
||||
case pion.PeerConnectionState:
|
||||
|
||||
@@ -55,7 +55,7 @@ func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer,
|
||||
defer conn.Close()
|
||||
|
||||
// 3. Create Peer Connection
|
||||
api, err := webrtc.NewAPI("")
|
||||
api, err := webrtc.NewAPI()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func openIPCClient(rawURL string, query url.Values) (core.Producer, error) {
|
||||
defer conn.Close()
|
||||
|
||||
// 3. Create Peer Connection
|
||||
api, err := webrtc.NewAPI("")
|
||||
api, err := webrtc.NewAPI()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -49,6 +49,9 @@ func syncHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
case "OPTIONS":
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
|
||||
default:
|
||||
http.Error(w, "", http.StatusMethodNotAllowed)
|
||||
}
|
||||
@@ -195,9 +198,7 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) {
|
||||
case pion.PeerConnectionState:
|
||||
if msg == pion.PeerConnectionStateClosed {
|
||||
stream.RemoveProducer(prod)
|
||||
if _, ok := sessions[id]; ok {
|
||||
delete(sessions, id)
|
||||
}
|
||||
delete(sessions, id)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
+16
-12
@@ -2,7 +2,7 @@ package webrtc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
@@ -23,7 +23,7 @@ func Init() {
|
||||
} `yaml:"webrtc"`
|
||||
}
|
||||
|
||||
cfg.Mod.Listen = "0.0.0.0:8555/tcp"
|
||||
cfg.Mod.Listen = ":8555/tcp"
|
||||
cfg.Mod.IceServers = []pion.ICEServer{
|
||||
{URLs: []string{"stun:stun.l.google.com:19302"}},
|
||||
}
|
||||
@@ -32,10 +32,20 @@ func Init() {
|
||||
|
||||
log = app.GetLogger("webrtc")
|
||||
|
||||
address := cfg.Mod.Listen
|
||||
address, network, _ := strings.Cut(cfg.Mod.Listen, "/")
|
||||
|
||||
var candidateHost []string
|
||||
for _, candidate := range cfg.Mod.Candidates {
|
||||
if strings.HasPrefix(candidate, "host:") {
|
||||
candidateHost = append(candidateHost, candidate[5:])
|
||||
continue
|
||||
}
|
||||
|
||||
AddCandidate(candidate, network)
|
||||
}
|
||||
|
||||
// create pionAPI with custom codecs list and custom network settings
|
||||
serverAPI, err := webrtc.NewAPI(address)
|
||||
serverAPI, err := webrtc.NewServerAPI(address, network, candidateHost)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
@@ -46,9 +56,8 @@ func Init() {
|
||||
|
||||
if address != "" {
|
||||
log.Info().Str("addr", address).Msg("[webrtc] listen")
|
||||
_, Port, _ = net.SplitHostPort(address)
|
||||
|
||||
clientAPI, _ = webrtc.NewAPI("")
|
||||
clientAPI, _ = webrtc.NewAPI()
|
||||
}
|
||||
|
||||
pionConf := pion.Configuration{
|
||||
@@ -65,10 +74,6 @@ func Init() {
|
||||
}
|
||||
}
|
||||
|
||||
for _, candidate := range cfg.Mod.Candidates {
|
||||
AddCandidate(candidate)
|
||||
}
|
||||
|
||||
// async WebRTC server (two API versions)
|
||||
ws.HandleFunc("webrtc", asyncHandler)
|
||||
ws.HandleFunc("webrtc/offer", asyncHandler)
|
||||
@@ -81,7 +86,6 @@ func Init() {
|
||||
streams.HandleFunc("webrtc", streamsHandler)
|
||||
}
|
||||
|
||||
var Port string
|
||||
var log zerolog.Logger
|
||||
|
||||
var PeerConnection func(active bool) (*pion.PeerConnection, error)
|
||||
@@ -138,7 +142,7 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
|
||||
_ = sendAnswer.Wait()
|
||||
|
||||
s := msg.ToJSON().Candidate
|
||||
log.Trace().Str("candidate", s).Msg("[webrtc] local")
|
||||
log.Trace().Str("candidate", s).Msg("[webrtc] local ")
|
||||
tr.Write(&ws.Message{Type: "webrtc/candidate", Value: s})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/dvrip"
|
||||
"github.com/AlexxIT/go2rtc/internal/echo"
|
||||
"github.com/AlexxIT/go2rtc/internal/exec"
|
||||
"github.com/AlexxIT/go2rtc/internal/expr"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
||||
"github.com/AlexxIT/go2rtc/internal/gopro"
|
||||
"github.com/AlexxIT/go2rtc/internal/hass"
|
||||
"github.com/AlexxIT/go2rtc/internal/hls"
|
||||
"github.com/AlexxIT/go2rtc/internal/homekit"
|
||||
@@ -34,8 +36,6 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Version = "1.8.0"
|
||||
|
||||
// 1. Core modules: app, api/ws, streams
|
||||
|
||||
app.Init() // init config and logs
|
||||
@@ -78,10 +78,12 @@ func main() {
|
||||
homekit.Init() // homekit source
|
||||
nest.Init() // nest source
|
||||
bubble.Init() // bubble source
|
||||
expr.Init() // expr source
|
||||
gopro.Init() // gopro source
|
||||
|
||||
// 6. Helper modules
|
||||
|
||||
ngrok.Init() // Ngrok module
|
||||
ngrok.Init() // ngrok module
|
||||
srtp.Init() // SRTP server
|
||||
debug.Init() // debug API
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
func IsADTS(b []byte) bool {
|
||||
_ = b[1]
|
||||
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF0 == 0xF0
|
||||
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF6 == 0xF0
|
||||
}
|
||||
|
||||
func ADTSToCodec(b []byte) *core.Codec {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package aac
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
core.SuperProducer
|
||||
rd *bufio.Reader
|
||||
cl io.Closer
|
||||
}
|
||||
|
||||
func Open(r io.Reader) (*Producer, error) {
|
||||
rd := bufio.NewReader(r)
|
||||
|
||||
b, err := rd.Peek(8)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
codec := ADTSToCodec(b)
|
||||
|
||||
prod := &Producer{rd: rd, cl: r.(io.Closer)}
|
||||
prod.Type = "ADTS producer"
|
||||
prod.Medias = []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
},
|
||||
}
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
func (c *Producer) Start() error {
|
||||
for {
|
||||
b, err := c.rd.Peek(6)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
auSize := ReadADTSSize(b)
|
||||
payload := make([]byte, 2+2+auSize)
|
||||
if _, err = io.ReadFull(c.rd, payload[4:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Recv += int(auSize)
|
||||
|
||||
if len(c.Receivers) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
payload[1] = 16 // header size in bits
|
||||
binary.BigEndian.PutUint16(payload[2:], auSize<<3)
|
||||
|
||||
pkt := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: core.Now90000()},
|
||||
Payload: payload,
|
||||
}
|
||||
c.Receivers[0].WriteRTP(pkt)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Producer) Stop() error {
|
||||
_ = c.SuperProducer.Close()
|
||||
return c.cl.Close()
|
||||
}
|
||||
+5
-1
@@ -28,9 +28,13 @@ func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
|
||||
headers := packet.Payload[2 : 2+headersSize]
|
||||
units := packet.Payload[2+headersSize:]
|
||||
|
||||
for len(headers) > 0 {
|
||||
for len(headers) >= 2 {
|
||||
unitSize := binary.BigEndian.Uint16(headers) >> 3
|
||||
|
||||
if len(units) < int(unitSize) {
|
||||
return
|
||||
}
|
||||
|
||||
unit := units[:unitSize]
|
||||
|
||||
headers = headers[2:]
|
||||
|
||||
@@ -127,3 +127,7 @@ func (r *Reader) ReadSEGolomb() int32 {
|
||||
return int32(b >> 1)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) Left() []byte {
|
||||
return r.buf[r.pos:]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
const ProbeSize = 1024 * 1024 // 1MB
|
||||
// ProbeSize
|
||||
// in my tests MPEG-TS 40Mbit/s 4K-video require more than 1MB for probe
|
||||
const ProbeSize = 5 * 1024 * 1024 // 5MB
|
||||
|
||||
const (
|
||||
BufferDisable = 0
|
||||
|
||||
+6
-5
@@ -73,6 +73,9 @@ func (t *Receiver) Replace(target *Receiver) {
|
||||
// move this receiver senders to new receiver
|
||||
t.mu.Lock()
|
||||
senders := t.senders
|
||||
// fix https://github.com/AlexxIT/go2rtc/issues/828
|
||||
// TODO: fix the reason, not the consequence
|
||||
t.senders = nil
|
||||
t.mu.Unlock()
|
||||
|
||||
target.mu.Lock()
|
||||
@@ -117,9 +120,9 @@ func (s *Sender) HandleRTP(track *Receiver) {
|
||||
|
||||
if GetKind(track.Codec.Name) == KindVideo {
|
||||
if track.Codec.IsRTP() {
|
||||
// H.264 2560x1440 4096kbs can have 700+ packets between 25 frames
|
||||
// H.265 5120x1440 can have 700+ packets between two keyframes
|
||||
bufferSize = 1000
|
||||
// in my tests 40Mbit/s 4K-video can generate up to 1500 items
|
||||
// for the h264.RTPDepay => RTPPay queue
|
||||
bufferSize = 5000
|
||||
} else {
|
||||
bufferSize = 50
|
||||
}
|
||||
@@ -140,9 +143,7 @@ func (s *Sender) HandleRTP(track *Receiver) {
|
||||
go func() {
|
||||
// read packets from buffer channel until it will be closed
|
||||
for packet := range buffer {
|
||||
s.mu.Lock()
|
||||
s.bytes += len(packet.Payload)
|
||||
s.mu.Unlock()
|
||||
s.Handler(packet)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/expr-lang/expr"
|
||||
)
|
||||
|
||||
func newRequest(method, url string, headers map[string]any) (*http.Request, error) {
|
||||
if method == "" {
|
||||
method = "GET"
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func regExp(params ...any) (*regexp.Regexp, error) {
|
||||
exp := params[0].(string)
|
||||
if len(params) >= 2 {
|
||||
// support:
|
||||
// i case-insensitive (default false)
|
||||
// m multi-line mode: ^ and $ match begin/end line (default false)
|
||||
// s let . match \n (default false)
|
||||
// https://pkg.go.dev/regexp/syntax
|
||||
flags := params[1].(string)
|
||||
exp = "(?" + flags + ")" + exp
|
||||
}
|
||||
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)
|
||||
req, err = newRequest(method, url, headers)
|
||||
} 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 Run(input string) (any, error) {
|
||||
program, err := expr.Compile(input, Options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return expr.Run(program, nil)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMatchHost(t *testing.T) {
|
||||
v, err := Run(`
|
||||
let url = "rtsp://user:pass@192.168.1.123/cam/realmonitor?...";
|
||||
let host = match(url, "//[^/]+")[0][2:];
|
||||
host
|
||||
`)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "user:pass@192.168.1.123", v)
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func (a *Args) String() string {
|
||||
b.WriteString(codec)
|
||||
}
|
||||
|
||||
if a.Filters != nil {
|
||||
if len(a.Filters) > 0 {
|
||||
for i, filter := range a.Filters {
|
||||
if i == 0 {
|
||||
b.WriteString(` -vf "`)
|
||||
|
||||
+66
-13
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
@@ -40,6 +41,21 @@ const (
|
||||
CodecAVC = 7
|
||||
)
|
||||
|
||||
const (
|
||||
PacketTypeAVCHeader = iota
|
||||
PacketTypeAVCNALU
|
||||
PacketTypeAVCEnd
|
||||
)
|
||||
|
||||
const (
|
||||
PacketTypeSequenceStart = iota
|
||||
PacketTypeCodedFrames
|
||||
PacketTypeSequenceEnd
|
||||
PacketTypeCodedFramesX
|
||||
PacketTypeMetadata
|
||||
PacketTypeMPEG2TSSequenceStart
|
||||
)
|
||||
|
||||
func (c *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
receiver, _ := c.SuperProducer.GetTrack(media, codec)
|
||||
if media.Kind == core.KindVideo {
|
||||
@@ -70,13 +86,32 @@ func (c *Producer) Start() error {
|
||||
c.audio.WriteRTP(pkt)
|
||||
|
||||
case TagVideo:
|
||||
// frame type 4b, codecID 4b, avc packet type 8b, composition time 24b
|
||||
if c.video == nil || pkt.Payload[1] == 0 {
|
||||
if c.video == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if isExHeader(pkt.Payload) {
|
||||
switch packetType := pkt.Payload[0] & 0b1111; packetType {
|
||||
case PacketTypeCodedFrames:
|
||||
// frame type 4b, packet type 4b, fourCC 32b, composition time 24b
|
||||
pkt.Payload = pkt.Payload[8:]
|
||||
case PacketTypeCodedFramesX:
|
||||
// frame type 4b, packet type 4b, fourCC 32b
|
||||
pkt.Payload = pkt.Payload[5:]
|
||||
default:
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
switch pkt.Payload[1] {
|
||||
case PacketTypeAVCNALU:
|
||||
// frame type 4b, codecID 4b, avc packet type 8b, composition time 24b
|
||||
pkt.Payload = pkt.Payload[5:]
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
pkt.Timestamp = TimeToRTP(pkt.Timestamp, c.video.Codec.ClockRate)
|
||||
pkt.Payload = pkt.Payload[5:]
|
||||
c.video.WriteRTP(pkt)
|
||||
}
|
||||
}
|
||||
@@ -145,20 +180,32 @@ func (c *Producer) probe() error {
|
||||
c.Medias = append(c.Medias, media)
|
||||
|
||||
case TagVideo:
|
||||
_ = pkt.Payload[1] // bounds
|
||||
var codec *core.Codec
|
||||
|
||||
_ = pkt.Payload[0] >> 4 // FrameType
|
||||
codecID := pkt.Payload[0] & 0b1111 // CodecID
|
||||
if isExHeader(pkt.Payload) {
|
||||
if string(pkt.Payload[1:5]) != "hvc1" {
|
||||
continue
|
||||
}
|
||||
|
||||
if codecID != CodecAVC {
|
||||
continue
|
||||
if packetType := pkt.Payload[0] & 0b1111; packetType != PacketTypeSequenceStart {
|
||||
continue
|
||||
}
|
||||
|
||||
codec = h265.ConfigToCodec(pkt.Payload[5:])
|
||||
} 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:])
|
||||
}
|
||||
|
||||
if pkt.Payload[1] != 0 { // check if header
|
||||
continue
|
||||
}
|
||||
|
||||
codec := h264.ConfigToCodec(pkt.Payload[5:])
|
||||
media := &core.Media{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionRecvonly,
|
||||
@@ -229,9 +276,15 @@ func (c *Producer) readPacket() (*rtp.Packet, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//log.Printf("[FLV] %d %.40x", pkt.PayloadType, pkt.Payload)
|
||||
|
||||
return pkt, nil
|
||||
}
|
||||
|
||||
func TimeToRTP(timeMS uint32, clockRate uint32) uint32 {
|
||||
return timeMS * clockRate / 1000
|
||||
}
|
||||
|
||||
func isExHeader(data []byte) bool {
|
||||
return data[0]&0b1000_0000 != 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package gopro
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func Discovery() (urls []string) {
|
||||
ints, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// The socket address for USB connections is 172.2X.1YZ.51:8080
|
||||
// https://gopro.github.io/OpenGoPro/http_2_0#socket-address
|
||||
re := regexp.MustCompile(`^172\.2\d\.1\d\d\.`)
|
||||
|
||||
for _, itf := range ints {
|
||||
addrs, err := itf.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
host := addr.String()
|
||||
if !re.MatchString(host) {
|
||||
continue
|
||||
}
|
||||
|
||||
host = host[:11] + "51" // 172.2x.1xx.xxx
|
||||
res, err := http.Get("http://" + host + ":8080/gopro/webcam/status")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_ = res.Body.Close()
|
||||
|
||||
urls = append(urls, host)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package gopro
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
)
|
||||
|
||||
func Dial(rawURL string) (core.Producer, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := &listener{host: u.Host}
|
||||
|
||||
if err = r.command("/gopro/webcam/stop"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = r.listen(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = r.command("/gopro/webcam/start"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mpegts.Open(r)
|
||||
}
|
||||
|
||||
type listener struct {
|
||||
conn net.PacketConn
|
||||
host string
|
||||
packet []byte
|
||||
packets chan []byte
|
||||
}
|
||||
|
||||
func (r *listener) Read(p []byte) (n int, err error) {
|
||||
if r.packet == nil {
|
||||
var ok bool
|
||||
if r.packet, ok = <-r.packets; !ok {
|
||||
return 0, io.EOF // channel closed
|
||||
}
|
||||
}
|
||||
|
||||
n = copy(p, r.packet)
|
||||
|
||||
if n < len(r.packet) {
|
||||
r.packet = r.packet[n:]
|
||||
} else {
|
||||
r.packet = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *listener) Close() error {
|
||||
return r.conn.Close()
|
||||
}
|
||||
|
||||
func (r *listener) command(api string) error {
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
|
||||
res, err := client.Get("http://" + r.host + ":8080" + api)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return errors.New("gopro: wrong response: " + res.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *listener) listen() (err error) {
|
||||
if r.conn, err = net.ListenPacket("udp4", ":8554"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
r.packets = make(chan []byte, 1024)
|
||||
go r.worker()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *listener) worker() {
|
||||
b := make([]byte, 1500)
|
||||
for {
|
||||
if err := r.conn.SetReadDeadline(time.Now().Add(3 * time.Second)); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
n, _, err := r.conn.ReadFrom(b)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
packet := make([]byte, n)
|
||||
copy(packet, b)
|
||||
|
||||
r.packets <- packet
|
||||
}
|
||||
|
||||
close(r.packets)
|
||||
|
||||
_ = r.command("/gopro/webcam/stop")
|
||||
}
|
||||
@@ -29,6 +29,12 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Memory overflow protection. Can happen if we miss a lot of packets with the marker.
|
||||
// https://github.com/AlexxIT/go2rtc/issues/675
|
||||
if len(buf) > 5*1024*1024 {
|
||||
buf = buf[: 0 : 512*1024]
|
||||
}
|
||||
|
||||
// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
|
||||
// Reolink Duo 2: sends SPS with Marker and PPS without
|
||||
if packet.Marker && len(payload) < PSMaxSize {
|
||||
|
||||
@@ -7,8 +7,26 @@ import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func RepairAVCC(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
vds, sps, pps := GetParameterSet(codec.FmtpLine)
|
||||
ps := h264.JoinNALU(vds, sps, pps)
|
||||
|
||||
return func(packet *rtp.Packet) {
|
||||
switch NALUType(packet.Payload) {
|
||||
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
|
||||
clone := *packet
|
||||
clone.Payload = h264.Join(ps, packet.Payload)
|
||||
handler(&clone)
|
||||
default:
|
||||
handler(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AVCCToCodec(avcc []byte) *core.Codec {
|
||||
buf := bytes.NewBufferString("profile-id=1")
|
||||
|
||||
|
||||
+59
-1
@@ -1,7 +1,40 @@
|
||||
// Package h265 - MPEG4 format related functions
|
||||
package h265
|
||||
|
||||
import "encoding/binary"
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func DecodeConfig(conf []byte) (profile, vps, sps, pps []byte) {
|
||||
profile = conf[1:4]
|
||||
|
||||
b := conf[23:]
|
||||
if binary.BigEndian.Uint16(b[1:]) != 1 {
|
||||
return
|
||||
}
|
||||
vpsSize := binary.BigEndian.Uint16(b[3:])
|
||||
vps = b[5 : 5+vpsSize]
|
||||
|
||||
b = conf[23+5+vpsSize:]
|
||||
if binary.BigEndian.Uint16(b[1:]) != 1 {
|
||||
return
|
||||
}
|
||||
spsSize := binary.BigEndian.Uint16(b[3:])
|
||||
sps = b[5 : 5+spsSize]
|
||||
|
||||
b = conf[23+5+vpsSize+5+spsSize:]
|
||||
if binary.BigEndian.Uint16(b[1:]) != 1 {
|
||||
return
|
||||
}
|
||||
ppsSize := binary.BigEndian.Uint16(b[3:])
|
||||
pps = b[5 : 5+ppsSize]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func EncodeConfig(vps, sps, pps []byte) []byte {
|
||||
vpsSize := uint16(len(vps))
|
||||
@@ -38,3 +71,28 @@ func EncodeConfig(vps, sps, pps []byte) []byte {
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
func ConfigToCodec(conf []byte) *core.Codec {
|
||||
buf := bytes.NewBufferString("profile-id=1")
|
||||
|
||||
_, vps, sps, pps := DecodeConfig(conf)
|
||||
if vps != nil {
|
||||
buf.WriteString(";sprop-vps=")
|
||||
buf.WriteString(base64.StdEncoding.EncodeToString(vps))
|
||||
}
|
||||
if sps != nil {
|
||||
buf.WriteString(";sprop-sps=")
|
||||
buf.WriteString(base64.StdEncoding.EncodeToString(sps))
|
||||
}
|
||||
if pps != nil {
|
||||
buf.WriteString(";sprop-pps=")
|
||||
buf.WriteString(base64.StdEncoding.EncodeToString(pps))
|
||||
}
|
||||
|
||||
return &core.Codec{
|
||||
Name: core.CodecH265,
|
||||
ClockRate: 90000,
|
||||
FmtpLine: buf.String(),
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,14 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
case 1: // end
|
||||
buf = append(buf, data[3:]...)
|
||||
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4))
|
||||
case 3: // 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.
|
||||
nuType = data[2] & 0x3F
|
||||
buf = binary.BigEndian.AppendUint32(buf, uint32(len(data))-1) // NAL unit size
|
||||
buf = append(buf, (data[0]&0x81)|(nuType<<1), data[1])
|
||||
buf = append(buf, data[3:]...)
|
||||
}
|
||||
} else {
|
||||
nuStart = len(buf)
|
||||
|
||||
+2
-1
@@ -50,4 +50,5 @@ Requires ffmpeg built with `--enable-libfdk-aac`
|
||||
- [Extracting HomeKit Pairing Keys](https://pvieito.com/2019/12/extract-homekit-pairing-keys)
|
||||
- [HAP in AirPlay2 receiver](https://github.com/openairplay/airplay2-receiver/blob/master/ap2/pairing/hap.py)
|
||||
- [HomeKit Secure Video Unofficial Specification](https://github.com/Supereg/secure-video-specification)
|
||||
- [Homebridge Camera FFmpeg](https://sunoo.github.io/homebridge-camera-ffmpeg/configs/)
|
||||
- [Homebridge Camera FFmpeg](https://sunoo.github.io/homebridge-camera-ffmpeg/configs/)
|
||||
- https://github.com/ljezny/Particle-HAP/blob/master/HAP-Specification-Non-Commercial-Version.pdf
|
||||
@@ -83,6 +83,10 @@ func (c *Client) DeviceHost() string {
|
||||
}
|
||||
|
||||
func (c *Client) Dial() (err error) {
|
||||
if len(c.ClientID) == 0 || len(c.ClientPrivate) == 0 {
|
||||
return errors.New("hap: can't dial witout client_id or client_private")
|
||||
}
|
||||
|
||||
// update device address (host and/or port) before dial
|
||||
_ = mdns.QueryOrDiscovery(c.DeviceHost(), mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
|
||||
if entry.Complete() && entry.Info["id"] == c.DeviceID {
|
||||
|
||||
+3
-2
@@ -2,10 +2,11 @@ package hass
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -48,7 +49,7 @@ func NewClient(rawURL string) (*Client, error) {
|
||||
defer hassAPI.Close()
|
||||
|
||||
// 2. Create WebRTC client
|
||||
rtcAPI, err := webrtc.NewAPI("")
|
||||
rtcAPI, err := webrtc.NewAPI()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
|
||||
"github.com/AlexxIT/go2rtc/pkg/opus"
|
||||
"github.com/AlexxIT/go2rtc/pkg/srtp"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
@@ -24,6 +25,7 @@ type Consumer struct {
|
||||
sessionID string
|
||||
videoSession *srtp.Session
|
||||
audioSession *srtp.Session
|
||||
audioRTPTime byte
|
||||
}
|
||||
|
||||
func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer {
|
||||
@@ -113,6 +115,7 @@ func (c *Consumer) SetConfig(conf *camera.SelectedStreamConfig) bool {
|
||||
c.audioSession.Remote.SSRC = conf.AudioCodec.RTPParams[0].SSRC
|
||||
c.audioSession.PayloadType = conf.AudioCodec.RTPParams[0].PayloadType
|
||||
c.audioSession.RTCPInterval = toDuration(conf.AudioCodec.RTPParams[0].RTCPInterval)
|
||||
c.audioRTPTime = conf.AudioCodec.CodecParams[0].RTPTime[0]
|
||||
|
||||
c.srtp.AddSession(c.videoSession)
|
||||
c.srtp.AddSession(c.audioSession)
|
||||
@@ -155,6 +158,8 @@ func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re
|
||||
} else {
|
||||
sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)
|
||||
}
|
||||
case core.CodecOpus:
|
||||
sender.Handler = opus.RepackToHAP(c.audioRTPTime, sender.Handler)
|
||||
}
|
||||
|
||||
sender.HandleRTP(track)
|
||||
|
||||
+19
-14
@@ -2,6 +2,7 @@ package homekit
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"slices"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
@@ -20,17 +21,16 @@ func videoToMedia(codecs []camera.VideoCodec) *core.Media {
|
||||
|
||||
for _, codec := range codecs {
|
||||
for _, param := range codec.CodecParams {
|
||||
for _, profileID := range param.ProfileID {
|
||||
for _, level := range param.Level {
|
||||
profile := videoProfiles[profileID] + videoLevels[level]
|
||||
mediaCodec := &core.Codec{
|
||||
Name: videoCodecs[codec.CodecType],
|
||||
ClockRate: 90000,
|
||||
FmtpLine: "profile-level-id=" + profile,
|
||||
}
|
||||
media.Codecs = append(media.Codecs, mediaCodec)
|
||||
}
|
||||
// get best profile and level
|
||||
profileID := slices.Max(param.ProfileID)
|
||||
level := slices.Max(param.Level)
|
||||
profile := videoProfiles[profileID] + videoLevels[level]
|
||||
mediaCodec := &core.Codec{
|
||||
Name: videoCodecs[codec.CodecType],
|
||||
ClockRate: 90000,
|
||||
FmtpLine: "profile-level-id=" + profile,
|
||||
}
|
||||
media.Codecs = append(media.Codecs, mediaCodec)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func audioToMedia(codecs []camera.AudioCodec) *core.Media {
|
||||
}
|
||||
|
||||
if mediaCodec.Name == core.CodecELD {
|
||||
// onli this version works with FFmpeg
|
||||
// only this version works with FFmpeg
|
||||
conf := aac.EncodeConfig(aac.TypeAACELD, 24000, 1, true)
|
||||
mediaCodec.FmtpLine = aac.FMTP + hex.EncodeToString(conf)
|
||||
}
|
||||
@@ -71,6 +71,7 @@ func audioToMedia(codecs []camera.AudioCodec) *core.Media {
|
||||
func trackToVideo(track *core.Receiver, video0 *camera.VideoCodec) *camera.VideoCodec {
|
||||
profileID := video0.CodecParams[0].ProfileID[0]
|
||||
level := video0.CodecParams[0].Level[0]
|
||||
attrs := video0.VideoAttrs[0]
|
||||
|
||||
if track != nil {
|
||||
profile := h264.GetProfileLevelID(track.Codec.FmtpLine)
|
||||
@@ -88,6 +89,12 @@ func trackToVideo(track *core.Receiver, video0 *camera.VideoCodec) *camera.Video
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range video0.VideoAttrs {
|
||||
if s.Width > attrs.Width || s.Height > attrs.Height {
|
||||
attrs = s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &camera.VideoCodec{
|
||||
@@ -98,9 +105,7 @@ func trackToVideo(track *core.Receiver, video0 *camera.VideoCodec) *camera.Video
|
||||
Level: []byte{level},
|
||||
},
|
||||
},
|
||||
VideoAttrs: []camera.VideoAttrs{
|
||||
{Width: 1920, Height: 1080, Framerate: 30},
|
||||
},
|
||||
VideoAttrs: []camera.VideoAttrs{attrs},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,11 +55,8 @@ func proxy(r, w net.Conn, pair ServerPair) error {
|
||||
continue
|
||||
}
|
||||
|
||||
//if n > 512 {
|
||||
// log.Printf("[hap] %d bytes => %s\n%s...", n, w.RemoteAddr(), b[:512])
|
||||
//} else {
|
||||
// log.Printf("[hap] %d bytes => %s\n%s", n, w.RemoteAddr(), b[:n])
|
||||
//}
|
||||
//log.Printf("[hap] %d bytes => %s\n%.512s", n, w.RemoteAddr(), b[:n])
|
||||
|
||||
if _, err = w.Write(b[:n]); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -70,13 +70,15 @@ func (c *Producer) Start() error {
|
||||
break
|
||||
}
|
||||
|
||||
pkt := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: core.Now90000()},
|
||||
Payload: annexb.EncodeToAVCC(buf[:i], true),
|
||||
}
|
||||
c.Receivers[0].WriteRTP(pkt)
|
||||
if len(c.Receivers) > 0 {
|
||||
pkt := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: core.Now90000()},
|
||||
Payload: annexb.EncodeToAVCC(buf[:i], true),
|
||||
}
|
||||
c.Receivers[0].WriteRTP(pkt)
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d", h264.Types(pkt.Payload), len(pkt.Payload))
|
||||
//log.Printf("[AVC] %v, len: %d", h264.Types(pkt.Payload), len(pkt.Payload))
|
||||
}
|
||||
|
||||
buf = buf[i:]
|
||||
}
|
||||
|
||||
+16
-1
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
|
||||
@@ -33,6 +34,9 @@ func Open(r io.Reader) (core.Producer, error) {
|
||||
case bytes.HasPrefix(b, []byte(flv.Signature)):
|
||||
return flv.Open(rd)
|
||||
|
||||
case bytes.HasPrefix(b, []byte{0xFF, 0xF1}):
|
||||
return aac.Open(rd)
|
||||
|
||||
case bytes.HasPrefix(b, []byte("--")):
|
||||
return multipart.Open(rd)
|
||||
|
||||
@@ -40,5 +44,16 @@ func Open(r io.Reader) (core.Producer, error) {
|
||||
return mpegts.Open(rd)
|
||||
}
|
||||
|
||||
return nil, errors.New("magic: unsupported header: " + hex.EncodeToString(b))
|
||||
// support MJPEG with trash on start
|
||||
// https://github.com/AlexxIT/go2rtc/issues/747
|
||||
if b, err = rd.Peek(4096); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if i := bytes.Index(b, []byte{0xFF, 0xD8, 0xFF, 0xDB}); i > 0 {
|
||||
_, _ = io.ReadFull(rd, make([]byte, i))
|
||||
return mjpeg.Open(rd)
|
||||
}
|
||||
|
||||
return nil, errors.New("magic: unsupported header: " + hex.EncodeToString(b[:4]))
|
||||
}
|
||||
|
||||
+1
-1
@@ -219,7 +219,7 @@ func (b *Browser) ListenMulticastUDP() error {
|
||||
},
|
||||
}
|
||||
|
||||
b.Recv, err = lc2.ListenPacket(ctx, "udp4", "0.0.0.0:5353")
|
||||
b.Recv, err = lc2.ListenPacket(ctx, "udp4", ":5353")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -106,6 +106,8 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
handler.Handler = h265.RTPDepay(track.Codec, handler.Handler)
|
||||
} else {
|
||||
handler.Handler = h265.RepairAVCC(track.Codec, handler.Handler)
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
+36
-8
@@ -1,6 +1,7 @@
|
||||
package mpegts
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
@@ -98,6 +99,11 @@ func (d *Demuxer) skip(i byte) {
|
||||
d.pos += i
|
||||
}
|
||||
|
||||
func (d *Demuxer) readBytes(i byte) []byte {
|
||||
d.pos += i
|
||||
return d.buf[d.pos-i : d.pos]
|
||||
}
|
||||
|
||||
func (d *Demuxer) readPSIHeader() {
|
||||
// https://en.wikipedia.org/wiki/Program-specific_information#Table_Sections
|
||||
pointer := d.readByte() // Pointer field
|
||||
@@ -159,7 +165,11 @@ func (d *Demuxer) readPMT() {
|
||||
_ = d.readBits(4) // Reserved bits
|
||||
_ = d.readBits(2) // ES Info length unused bits
|
||||
size = d.readBits(10) // ES Info length
|
||||
d.skip(byte(size))
|
||||
info := d.readBytes(byte(size))
|
||||
|
||||
if streamType == StreamTypePrivate && bytes.HasPrefix(info, opusInfo) {
|
||||
streamType = StreamTypePrivateOPUS
|
||||
}
|
||||
|
||||
d.pes[pid] = &PES{StreamType: streamType}
|
||||
}
|
||||
@@ -175,7 +185,7 @@ func (d *Demuxer) readPES(pid uint16, start bool) *rtp.Packet {
|
||||
|
||||
// if new payload beging
|
||||
if start {
|
||||
if pes.Payload != nil {
|
||||
if len(pes.Payload) != 0 {
|
||||
d.pos = skipRead
|
||||
return pes.GetPacket() // finish previous packet
|
||||
}
|
||||
@@ -314,12 +324,13 @@ const (
|
||||
|
||||
// https://en.wikipedia.org/wiki/Program-specific_information#Elementary_stream_types
|
||||
const (
|
||||
StreamTypeMetadata = 0 // Reserved
|
||||
StreamTypePrivate = 0x06 // PCMU or PCMA or FLAC from FFmpeg
|
||||
StreamTypeAAC = 0x0F
|
||||
StreamTypeH264 = 0x1B
|
||||
StreamTypeH265 = 0x24
|
||||
StreamTypePCMATapo = 0x90
|
||||
StreamTypeMetadata = 0 // Reserved
|
||||
StreamTypePrivate = 0x06 // PCMU or PCMA or FLAC from FFmpeg
|
||||
StreamTypeAAC = 0x0F
|
||||
StreamTypeH264 = 0x1B
|
||||
StreamTypeH265 = 0x24
|
||||
StreamTypePCMATapo = 0x90
|
||||
StreamTypePrivateOPUS = 0xEB
|
||||
)
|
||||
|
||||
// PES - Packetized Elementary Stream
|
||||
@@ -397,6 +408,23 @@ func (p *PES) GetPacket() (pkt *rtp.Packet) {
|
||||
}
|
||||
|
||||
//p.Timestamp += uint32(len(p.Payload)) // update next timestamp!
|
||||
|
||||
case StreamTypePrivateOPUS:
|
||||
p.Sequence++
|
||||
|
||||
pkt = &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: true,
|
||||
PayloadType: p.StreamType,
|
||||
SequenceNumber: p.Sequence,
|
||||
Timestamp: p.PTS,
|
||||
},
|
||||
}
|
||||
|
||||
pkt.Payload, p.Payload = CutOPUSPacket(p.Payload)
|
||||
p.PTS += opusDT
|
||||
return
|
||||
}
|
||||
|
||||
p.Payload = nil
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package mpegts
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/bits"
|
||||
)
|
||||
|
||||
// opusDT - each AU from FFmpeg has 5 OPUS packets. Each packet len = 960 in the 48000 clock.
|
||||
const opusDT = 960 * ClockRate / 48000
|
||||
|
||||
// https://opus-codec.org/docs/
|
||||
var opusInfo = []byte{ // registration_descriptor
|
||||
0x05, // descriptor_tag
|
||||
0x04, // descriptor_length
|
||||
'O', 'p', 'u', 's', // format_identifier
|
||||
}
|
||||
|
||||
//goland:noinspection GoSnakeCaseUsage
|
||||
func CutOPUSPacket(b []byte) (packet []byte, left []byte) {
|
||||
r := bits.NewReader(b)
|
||||
|
||||
size := opus_control_header(r)
|
||||
if size == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
packet = r.ReadBytes(size)
|
||||
left = r.Left()
|
||||
return
|
||||
}
|
||||
|
||||
//goland:noinspection GoSnakeCaseUsage
|
||||
func opus_control_header(r *bits.Reader) int {
|
||||
control_header_prefix := r.ReadBits(11)
|
||||
if control_header_prefix != 0x3FF {
|
||||
return 0
|
||||
}
|
||||
|
||||
start_trim_flag := r.ReadBit()
|
||||
end_trim_flag := r.ReadBit()
|
||||
control_extension_flag := r.ReadBit()
|
||||
_ = r.ReadBits(2) // reserved
|
||||
|
||||
var payload_size int
|
||||
for {
|
||||
i := r.ReadByte()
|
||||
payload_size += int(i)
|
||||
if i < 255 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if start_trim_flag != 0 {
|
||||
_ = r.ReadBits(3)
|
||||
_ = r.ReadBits(13)
|
||||
}
|
||||
if end_trim_flag != 0 {
|
||||
_ = r.ReadBits(3)
|
||||
_ = r.ReadBits(13)
|
||||
}
|
||||
if control_extension_flag != 0 {
|
||||
control_extension_length := r.ReadByte()
|
||||
_ = r.ReadBytes(int(control_extension_length)) // reserved
|
||||
}
|
||||
|
||||
return payload_size
|
||||
}
|
||||
+16
-1
@@ -87,7 +87,7 @@ func (c *Producer) probe() error {
|
||||
case StreamTypeMetadata:
|
||||
for _, streamType := range pkt.Payload {
|
||||
switch streamType {
|
||||
case StreamTypeH264, StreamTypeH265, StreamTypeAAC:
|
||||
case StreamTypeH264, StreamTypeH265, StreamTypeAAC, StreamTypePrivateOPUS:
|
||||
waitType = append(waitType, streamType)
|
||||
}
|
||||
}
|
||||
@@ -118,6 +118,19 @@ func (c *Producer) probe() error {
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.Medias = append(c.Medias, media)
|
||||
|
||||
case StreamTypePrivateOPUS:
|
||||
codec := &core.Codec{
|
||||
Name: core.CodecOpus,
|
||||
ClockRate: 48000,
|
||||
Channels: 2,
|
||||
}
|
||||
media := &core.Media{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.Medias = append(c.Medias, media)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +147,8 @@ func StreamType(codec *core.Codec) uint8 {
|
||||
return StreamTypeAAC
|
||||
case core.CodecPCMA:
|
||||
return StreamTypePCMATapo
|
||||
case core.CodecOpus:
|
||||
return StreamTypePrivateOPUS
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
+3
-2
@@ -2,10 +2,11 @@ package nest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -34,7 +35,7 @@ func NewClient(rawURL string) (*Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtcAPI, err := webrtc.NewAPI("")
|
||||
rtcAPI, err := webrtc.NewAPI()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
## Useful links
|
||||
|
||||
- [RFC 3550: RTP: A Transport Protocol for Real-Time Applications](https://datatracker.ietf.org/doc/html/rfc3550)
|
||||
- [RFC 6716: Definition of the Opus Audio Codec](https://datatracker.ietf.org/doc/html/rfc6716)
|
||||
- [RFC 7587: RTP Payload Format for the Opus Speech and Audio Codec](https://datatracker.ietf.org/doc/html/rfc7587)
|
||||
@@ -0,0 +1,96 @@
|
||||
package opus
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
// Some info about this magic:
|
||||
// - Apple has no respect for RFC 7587 standard and using RFC 3550 for RTP timestamps
|
||||
// - Apple can request packets with 20ms duration over LAN connection and 60ms over LTE
|
||||
// - FFmpeg produce packets with 20ms duration by default and only one frame per packet
|
||||
// - FFmpeg should use "-min_comp 0" option, so every packet will be same duration
|
||||
// - Apple doesn't care about real sample rate of track
|
||||
// - Apple only cares about proper timestamp based on REQUESTED sample rate
|
||||
|
||||
// RepackToHAP - convert standart RTP packet with OPUS to HAP packet
|
||||
// We expect that:
|
||||
// - incoming packet will be 20ms duration and only one frame per packet
|
||||
// - outgouing packet will be 20ms or 60ms duration
|
||||
// - incoming sample rate will be any (but not very big if we needs 60ms packets for output)
|
||||
// - outgouing sample rate will be 16000
|
||||
// https://github.com/AlexxIT/go2rtc/issues/667
|
||||
func RepackToHAP(rtpTime byte, handler core.HandlerFunc) core.HandlerFunc {
|
||||
switch rtpTime {
|
||||
case 20:
|
||||
return repackToHAP20(handler)
|
||||
case 60:
|
||||
return repackToHAP60(handler)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
// we using only one sample rate in the pkg/hap/camera/accessory.go
|
||||
const (
|
||||
timestamp20 = 16000 * 0.020
|
||||
timestamp60 = 16000 * 0.060
|
||||
)
|
||||
|
||||
// repackToHAP20 - just fix RTP timestamp from RFC 7587 to RFC 3550
|
||||
func repackToHAP20(handler core.HandlerFunc) core.HandlerFunc {
|
||||
var timestamp uint32
|
||||
|
||||
return func(pkt *rtp.Packet) {
|
||||
timestamp += timestamp20
|
||||
|
||||
clone := *pkt
|
||||
clone.Timestamp = timestamp
|
||||
handler(&clone)
|
||||
}
|
||||
}
|
||||
|
||||
// repackToHAP60 - collect 20ms frames to single 60ms packet
|
||||
// thanks to @civita idea https://github.com/AlexxIT/go2rtc/pull/843
|
||||
func repackToHAP60(handler core.HandlerFunc) core.HandlerFunc {
|
||||
var sequence uint16
|
||||
var timestamp uint32
|
||||
|
||||
var framesCount byte
|
||||
var framesSize []byte
|
||||
var framesData []byte
|
||||
|
||||
return func(pkt *rtp.Packet) {
|
||||
framesData = append(framesData, pkt.Payload[1:]...)
|
||||
|
||||
if framesCount++; framesCount < 3 {
|
||||
if frameSize := len(pkt.Payload) - 1; frameSize >= 252 {
|
||||
b0 := 252 + byte(frameSize)&0b11
|
||||
framesSize = append(framesSize, b0, byte(frameSize/4)-b0)
|
||||
} else {
|
||||
framesSize = append(framesSize, byte(frameSize))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
toc := pkt.Payload[0]
|
||||
|
||||
payload := make([]byte, 2, 2+len(framesSize)+len(framesData))
|
||||
payload[0] = toc | 0b11 // code 3 (multiple frames per packet)
|
||||
payload[1] = 0b1000_0011 // VBR, no padding, 3 frames
|
||||
payload = append(payload, framesSize...)
|
||||
payload = append(payload, framesData...)
|
||||
|
||||
sequence++
|
||||
timestamp += timestamp60
|
||||
|
||||
clone := *pkt
|
||||
clone.Payload = payload
|
||||
clone.SequenceNumber = sequence
|
||||
clone.Timestamp = timestamp
|
||||
handler(&clone)
|
||||
|
||||
framesCount = 0
|
||||
framesSize = framesSize[:0]
|
||||
framesData = framesData[:0]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package opus
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func Log(handler core.HandlerFunc) core.HandlerFunc {
|
||||
var ts uint32
|
||||
|
||||
return func(pkt *rtp.Packet) {
|
||||
if ts == 0 {
|
||||
ts = pkt.Timestamp
|
||||
}
|
||||
|
||||
toc := pkt.Payload[0]
|
||||
//config := toc >> 3
|
||||
code := toc & 0b11
|
||||
|
||||
frame := parseFrameSize(toc)
|
||||
rate := parseSampleRate(toc)
|
||||
|
||||
log.Printf(
|
||||
"[RTP/OPUS] frame=%s rate=%5d code=%d size=%6d ts=%10d dt=%5d pt=%2d ssrc=%d seq=%d mark=%t",
|
||||
frame, rate, code, len(pkt.Payload), pkt.Timestamp, pkt.Timestamp-ts, pkt.PayloadType, pkt.SSRC, pkt.SequenceNumber, pkt.Marker,
|
||||
)
|
||||
|
||||
ts = pkt.Timestamp
|
||||
|
||||
handler(pkt)
|
||||
}
|
||||
}
|
||||
|
||||
func parseFrameSize(toc byte) time.Duration {
|
||||
switch toc >> 3 {
|
||||
case 0, 4, 8, 12, 14, 18, 22, 26, 30:
|
||||
return 10_000_000
|
||||
case 1, 5, 9, 13, 15, 19, 23, 27, 31:
|
||||
return 20_000_000
|
||||
case 2, 6, 10:
|
||||
return 40_000_000
|
||||
case 3, 7, 11:
|
||||
return 60_000_000
|
||||
case 16, 20, 24, 28:
|
||||
return 2_500_000
|
||||
case 17, 21, 25, 29:
|
||||
return 5_000_000
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func parseSampleRate(toc byte) uint16 {
|
||||
switch toc >> 3 {
|
||||
case 0, 1, 2, 3, 16, 17, 18, 19:
|
||||
return 8000
|
||||
case 4, 5, 6, 7:
|
||||
return 12000
|
||||
case 8, 9, 10, 11, 20, 21, 22, 23:
|
||||
return 16000
|
||||
case 12, 13, 24, 25, 26, 27:
|
||||
return 24000
|
||||
case 14, 15, 28, 29, 30, 31:
|
||||
return 48000
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -90,7 +90,7 @@ func (c *Client) Connect() error {
|
||||
}
|
||||
|
||||
// 4. Create Peer Connection
|
||||
api, err := webrtc.NewAPI("")
|
||||
api, err := webrtc.NewAPI()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+6
-4
@@ -38,9 +38,8 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) {
|
||||
|
||||
// Fix invalid media type (errSDPInvalidValue) caused by
|
||||
// some TP-LINK IP camera, e.g. TL-IPC44GW
|
||||
rawSDP = bytes.ReplaceAll(rawSDP, []byte("m=application/TP-LINK "), []byte("m=application "))
|
||||
// more tplink ipcams
|
||||
rawSDP = bytes.ReplaceAll(rawSDP, []byte("m=application/tp-link "), []byte("m=application "))
|
||||
m := regexp.MustCompile("m=application/[^ ]+")
|
||||
rawSDP = m.ReplaceAll(rawSDP, []byte("m=application"))
|
||||
|
||||
if err == io.EOF {
|
||||
rawSDP = append(rawSDP, '\n')
|
||||
@@ -53,6 +52,9 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// fix buggy camera https://github.com/AlexxIT/go2rtc/issues/771
|
||||
forceDirection := sd.Origin.Username == "CV-RTSPHandler"
|
||||
|
||||
var medias []*core.Media
|
||||
|
||||
for _, md := range sd.MediaDescriptions {
|
||||
@@ -66,7 +68,7 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if media.Direction == "" {
|
||||
if media.Direction == "" || forceDirection {
|
||||
media.Direction = core.DirectionRecvonly
|
||||
}
|
||||
|
||||
|
||||
+53
-1
@@ -1,8 +1,9 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestURLParse(t *testing.T) {
|
||||
@@ -107,3 +108,54 @@ a=sendonly`
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, medias, 3)
|
||||
}
|
||||
|
||||
func TestBugSDP4(t *testing.T) {
|
||||
s := `v=0
|
||||
o=- 14665860 31787219 1 IN IP4 10.0.0.94
|
||||
s=Session streamed by "MERCURY RTSP Server"
|
||||
t=0 0
|
||||
m=video 0 RTP/AVP 96
|
||||
c=IN IP4 0.0.0.0
|
||||
b=AS:4096
|
||||
a=range:npt=0-
|
||||
a=control:track1
|
||||
a=rtpmap:96 H264/90000
|
||||
a=fmtp:96 packetization-mode=1; profile-level-id=640016; sprop-parameter-sets=Z2QAFqzGoCgPaEAAAAMAQAAAB6E=,aOqPLA==
|
||||
m=audio 0 RTP/AVP 8
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=control:track2
|
||||
m=application/MERCURY 0 RTP/AVP smart/1/90000
|
||||
a=rtpmap:95 MERCURY/90000
|
||||
a=control:track3
|
||||
`
|
||||
medias, err := UnmarshalSDP([]byte(s))
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, medias, 3)
|
||||
}
|
||||
|
||||
func TestBugSDP5(t *testing.T) {
|
||||
s := `v=0
|
||||
o=CV-RTSPHandler 1123412 0 IN IP4 192.168.1.22
|
||||
s=Camera
|
||||
c=IN IP4 192.168.1.22
|
||||
t=0 0
|
||||
a=charset:Shift_JIS
|
||||
a=range:npt=0-
|
||||
a=control:*
|
||||
a=etag:1234567890
|
||||
m=video 0 RTP/AVP 99
|
||||
a=rtpmap:99 H264/90000
|
||||
a=fmtp:99 profile-level-id=42A01E;packetization-mode=1;sprop-parameter-sets=Z0KgKedAPAET8uAIEAABd2AAK/IGAAADAC+vCAAAHc1lP//jAAADABfXhAAADuayn//wIA==,aN48gA==
|
||||
a=control:trackID=1
|
||||
a=sendonly
|
||||
m=audio 0 RTP/AVP 127
|
||||
a=rtpmap:127 mpeg4-generic/8000/1
|
||||
a=fmtp:127 streamtype=5; profile-level-id=15; mode=AAC-hbr; sizeLength=13; indexLength=3; indexDeltalength=3; config=1588; CTSDeltaLength=0; DTSDeltaLength=0;
|
||||
a=control:trackID=2
|
||||
`
|
||||
medias, err := UnmarshalSDP([]byte(s))
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, medias, 2)
|
||||
assert.Equal(t, "recvonly", medias[0].Direction)
|
||||
assert.Equal(t, "recvonly", medias[1].Direction)
|
||||
}
|
||||
|
||||
+18
-25
@@ -14,35 +14,28 @@ func QuoteSplit(s string) []string {
|
||||
var a []string
|
||||
|
||||
for len(s) > 0 {
|
||||
is := strings.IndexByte(s, ' ')
|
||||
if is >= 0 {
|
||||
// skip prefix and double spaces
|
||||
if is == 0 {
|
||||
// goto next symbol
|
||||
s = s[1:]
|
||||
continue
|
||||
switch c := s[0]; c {
|
||||
case '\t', '\n', '\r', ' ': // unicode.IsSpace
|
||||
s = s[1:]
|
||||
case '"', '\'': // quote chars
|
||||
if i := strings.IndexByte(s[1:], c); i > 0 {
|
||||
a = append(a, s[1:i+1])
|
||||
s = s[i+2:]
|
||||
} else {
|
||||
return nil // error
|
||||
}
|
||||
|
||||
// check if quote in word
|
||||
if i := strings.IndexByte(s[:is], '"'); i >= 0 {
|
||||
// search quote end
|
||||
if is = strings.Index(s, `" `); is > 0 {
|
||||
is += 1
|
||||
} else {
|
||||
is = -1
|
||||
}
|
||||
default:
|
||||
i := strings.IndexAny(s, "\t\n\r ")
|
||||
if i > 0 {
|
||||
a = append(a, s[:i])
|
||||
s = s[i:]
|
||||
} else {
|
||||
a = append(a, s)
|
||||
s = ""
|
||||
}
|
||||
}
|
||||
|
||||
if is >= 0 {
|
||||
a = append(a, strings.ReplaceAll(s[:is], `"`, ""))
|
||||
s = s[is+1:]
|
||||
} else {
|
||||
//add last word
|
||||
a = append(a, s)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestQuoteSplit(t *testing.T) {
|
||||
s := `
|
||||
python "-c" 'import time
|
||||
print("time", time.time())'
|
||||
`
|
||||
require.Equal(t, []string{"python", "-c", "import time\nprint(\"time\", time.time())"}, QuoteSplit(s))
|
||||
|
||||
s = `ffmpeg -i "video=FaceTime HD Camera" -i "DeckLink SDI (2)"`
|
||||
require.Equal(t, []string{"ffmpeg", "-i", `video=FaceTime HD Camera`, "-i", "DeckLink SDI (2)"}, QuoteSplit(s))
|
||||
}
|
||||
+2
-1
@@ -57,7 +57,8 @@ func (s *Server) DelSession(session *Session) {
|
||||
|
||||
delete(s.sessions, session.Remote.SSRC)
|
||||
|
||||
if len(s.sessions) == 0 {
|
||||
// check s.conn for https://github.com/AlexxIT/go2rtc/issues/734
|
||||
if len(s.sessions) == 0 && s.conn != nil {
|
||||
_ = s.conn.Close()
|
||||
}
|
||||
|
||||
|
||||
+100
-21
@@ -1,10 +1,12 @@
|
||||
package tapo
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -14,6 +16,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
@@ -36,6 +39,7 @@ type Client struct {
|
||||
|
||||
session1 string
|
||||
session2 string
|
||||
request string
|
||||
|
||||
recv int
|
||||
send int
|
||||
@@ -62,33 +66,19 @@ func (c *Client) newConn() (net.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// support raw username/password
|
||||
username := u.User.Username()
|
||||
password, _ := u.User.Password()
|
||||
|
||||
// or cloud password in place of username
|
||||
if password == "" {
|
||||
password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
|
||||
username = "admin"
|
||||
u.User = url.UserPassword(username, password)
|
||||
}
|
||||
|
||||
u.Scheme = "http"
|
||||
u.Path = "/stream"
|
||||
if u.Port() == "" {
|
||||
u.Host += ":8800"
|
||||
}
|
||||
|
||||
// TODO: fix closing connection
|
||||
ctx, pconn := tcp.WithConn()
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", u.String(), nil)
|
||||
req, err := http.NewRequest("POST", "http://"+u.Host+"/stream", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.URL.User = u.User
|
||||
req.Header.Set("Content-Type", "multipart/mixed; boundary=--client-stream-boundary--")
|
||||
|
||||
res, err := tcp.Do(req)
|
||||
conn, res, err := dial(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -98,13 +88,35 @@ func (c *Client) newConn() (net.Conn, error) {
|
||||
}
|
||||
|
||||
if c.decrypt == nil {
|
||||
c.newDectypter(res, username, password)
|
||||
c.newDectypter(res)
|
||||
}
|
||||
|
||||
return *pconn, nil
|
||||
query := u.Query()
|
||||
channel := query.Get("channel")
|
||||
if channel == "" {
|
||||
channel = "0"
|
||||
}
|
||||
|
||||
subtype := query.Get("subtype")
|
||||
switch subtype {
|
||||
case "", "0":
|
||||
subtype = "HD"
|
||||
case "1":
|
||||
subtype = "VGA"
|
||||
}
|
||||
|
||||
c.request = fmt.Sprintf(
|
||||
`{"params":{"preview":{"audio":["default"],"channels":[%s],"resolutions":["%s"]},"method":"get"},"seq":1,"type":"request"}`,
|
||||
channel, subtype,
|
||||
)
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *Client) newDectypter(res *http.Response, username, password string) {
|
||||
func (c *Client) newDectypter(res *http.Response) {
|
||||
username := res.Request.URL.User.Username()
|
||||
password, _ := res.Request.URL.User.Password()
|
||||
|
||||
// extract nonce from response
|
||||
// cipher="AES_128_CBC" username="admin" padding="PKCS7_16" algorithm="MD5" nonce="***"
|
||||
nonce := res.Header.Get("Key-Exchange")
|
||||
@@ -139,7 +151,7 @@ func (c *Client) SetupStream() (err error) {
|
||||
}
|
||||
|
||||
// audio: default, disable, enable
|
||||
c.session1, err = c.Request(c.conn1, []byte(`{"params":{"preview":{"audio":["default"],"channels":[0],"resolutions":["HD"]},"method":"get"},"seq":1,"type":"request"}`))
|
||||
c.session1, err = c.Request(c.conn1, []byte(c.request))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -244,3 +256,70 @@ func (c *Client) Request(conn net.Conn, body []byte) (string, error) {
|
||||
return v.Params.SessionID, nil
|
||||
}
|
||||
}
|
||||
|
||||
func dial(req *http.Request) (net.Conn, *http.Response, error) {
|
||||
conn, err := net.DialTimeout("tcp", req.URL.Host, core.ConnDialTimeout)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
username := req.URL.User.Username()
|
||||
password, _ := req.URL.User.Password()
|
||||
req.URL.User = nil
|
||||
|
||||
if err = req.Write(conn); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
r := bufio.NewReader(conn)
|
||||
|
||||
res, err := http.ReadResponse(r, req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
auth := res.Header.Get("WWW-Authenticate")
|
||||
|
||||
if res.StatusCode != http.StatusUnauthorized || !strings.HasPrefix(auth, "Digest") {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if password == "" {
|
||||
// support cloud password in place of username
|
||||
if strings.Contains(auth, `encrypt_type="3"`) {
|
||||
password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username)))
|
||||
} else {
|
||||
password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
|
||||
}
|
||||
username = "admin"
|
||||
}
|
||||
|
||||
realm := tcp.Between(auth, `realm="`, `"`)
|
||||
nonce := tcp.Between(auth, `nonce="`, `"`)
|
||||
qop := tcp.Between(auth, `qop="`, `"`)
|
||||
uri := req.URL.RequestURI()
|
||||
ha1 := tcp.HexMD5(username, realm, password)
|
||||
ha2 := tcp.HexMD5(req.Method, uri)
|
||||
nc := "00000001"
|
||||
cnonce := "00000001"
|
||||
response := tcp.HexMD5(ha1, nonce, nc, cnonce, qop, ha2)
|
||||
|
||||
header := fmt.Sprintf(
|
||||
`Digest username="%s", realm="%s", nonce="%s", uri="%s", qop=%s, nc=%s, cnonce="%s", response="%s"`,
|
||||
username, realm, nonce, uri, qop, nc, cnonce, response,
|
||||
)
|
||||
|
||||
req.Header.Set("Authorization", header)
|
||||
|
||||
if err = req.Write(conn); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if res, err = http.ReadResponse(r, req); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req.URL.User = url.UserPassword(username, password)
|
||||
|
||||
return conn, res, nil
|
||||
}
|
||||
|
||||
+52
-25
@@ -2,6 +2,7 @@ package tcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -12,7 +13,24 @@ import (
|
||||
|
||||
// Do - http.Client with support Digest Authorization
|
||||
func Do(req *http.Request) (*http.Response, error) {
|
||||
if secureClient == nil {
|
||||
var secure *tls.Config
|
||||
|
||||
switch req.URL.Scheme {
|
||||
case "httpx":
|
||||
secure = &tls.Config{InsecureSkipVerify: true}
|
||||
req.URL.Scheme = "https"
|
||||
case "https":
|
||||
if hostname := req.URL.Hostname(); IsIP(hostname) {
|
||||
secure = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
}
|
||||
|
||||
if secure != nil {
|
||||
ctx := context.WithValue(req.Context(), secureKey, secure)
|
||||
req = req.WithContext(ctx)
|
||||
}
|
||||
|
||||
if client == nil {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
||||
dial := transport.DialContext
|
||||
@@ -23,33 +41,38 @@ func Do(req *http.Request) (*http.Response, error) {
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
conn, err := dial(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secureClient = &http.Client{
|
||||
var conf *tls.Config
|
||||
if v, ok := ctx.Value(secureKey).(*tls.Config); ok {
|
||||
conf = v
|
||||
} else if host, _, err := net.SplitHostPort(addr); err != nil {
|
||||
conf = &tls.Config{ServerName: addr}
|
||||
} else {
|
||||
conf = &tls.Config{ServerName: host}
|
||||
}
|
||||
|
||||
tlsConn := tls.Client(conn, conf)
|
||||
if err = tlsConn.Handshake(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pconn, ok := ctx.Value(connKey).(*net.Conn); ok {
|
||||
*pconn = tlsConn
|
||||
}
|
||||
return tlsConn, err
|
||||
}
|
||||
|
||||
client = &http.Client{
|
||||
Timeout: time.Second * 5000,
|
||||
Transport: transport,
|
||||
}
|
||||
}
|
||||
|
||||
var client *http.Client
|
||||
|
||||
if req.URL.Scheme == "httpx" || (req.URL.Scheme == "https" && IsIP(req.URL.Hostname())) {
|
||||
req.URL.Scheme = "https"
|
||||
|
||||
if insecureClient == nil {
|
||||
transport := secureClient.Transport.(*http.Transport).Clone()
|
||||
transport.TLSClientConfig.InsecureSkipVerify = true
|
||||
|
||||
insecureClient = &http.Client{
|
||||
Timeout: secureClient.Timeout,
|
||||
Transport: transport,
|
||||
}
|
||||
}
|
||||
|
||||
client = insecureClient
|
||||
} else {
|
||||
client = secureClient
|
||||
}
|
||||
|
||||
user := req.URL.User
|
||||
|
||||
// Hikvision won't answer on Basic auth with any headers
|
||||
@@ -88,7 +111,7 @@ func Do(req *http.Request) (*http.Response, error) {
|
||||
response := HexMD5(ha1, nonce, ha2)
|
||||
header = fmt.Sprintf(
|
||||
`Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s"`,
|
||||
user, realm, nonce, uri, response,
|
||||
username, realm, nonce, uri, response,
|
||||
)
|
||||
case "auth":
|
||||
nc := "00000001"
|
||||
@@ -112,8 +135,12 @@ func Do(req *http.Request) (*http.Response, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
var secureClient, insecureClient *http.Client
|
||||
var connKey struct{}
|
||||
var client *http.Client
|
||||
|
||||
type key string
|
||||
|
||||
var connKey = key("conn")
|
||||
var secureKey = key("secure")
|
||||
|
||||
func WithConn() (context.Context, *net.Conn) {
|
||||
pconn := new(net.Conn)
|
||||
|
||||
+23
-20
@@ -1,18 +1,21 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/pion/ice/v2"
|
||||
"net"
|
||||
|
||||
"github.com/pion/interceptor"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ReceiveMTU = Ethernet MTU (1500) - IP Header (20) - UDP Header (8)
|
||||
// https://ffmpeg.org/ffmpeg-all.html#Muxer
|
||||
const ReceiveMTU = 1472
|
||||
|
||||
func NewAPI(address string) (*webrtc.API, error) {
|
||||
func NewAPI() (*webrtc.API, error) {
|
||||
return NewServerAPI("", "", nil)
|
||||
}
|
||||
|
||||
func NewServerAPI(address, network string, candidateHost []string) (*webrtc.API, error) {
|
||||
// for debug logs add to env: `PION_LOG_DEBUG=all`
|
||||
m := &webrtc.MediaEngine{}
|
||||
//if err := m.RegisterDefaultCodecs(); err != nil {
|
||||
@@ -34,33 +37,33 @@ func NewAPI(address string) (*webrtc.API, error) {
|
||||
return name != "hassio" && name != "docker0"
|
||||
})
|
||||
|
||||
// disable mDNS listener
|
||||
s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
|
||||
|
||||
// UDP6 may have problems with DNS resolving for STUN servers
|
||||
s.SetNetworkTypes([]webrtc.NetworkType{
|
||||
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeTCP4,
|
||||
})
|
||||
|
||||
// fix https://github.com/pion/webrtc/pull/2407
|
||||
s.SetDTLSInsecureSkipHelloVerify(true)
|
||||
|
||||
s.SetReceiveMTU(ReceiveMTU)
|
||||
|
||||
s.SetNAT1To1IPs(candidateHost, webrtc.ICECandidateTypeHost)
|
||||
|
||||
// by default enable IPv4 + IPv6 modes
|
||||
s.SetNetworkTypes([]webrtc.NetworkType{
|
||||
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeTCP4,
|
||||
webrtc.NetworkTypeUDP6, webrtc.NetworkTypeTCP6,
|
||||
})
|
||||
|
||||
if address != "" {
|
||||
address, network, _ := strings.Cut(address, "/")
|
||||
if network == "" || network == "udp" {
|
||||
if ln, err := net.ListenPacket("udp4", address); err == nil {
|
||||
udpMux := webrtc.NewICEUDPMux(nil, ln)
|
||||
s.SetICEUDPMux(udpMux)
|
||||
}
|
||||
}
|
||||
if network == "" || network == "tcp" {
|
||||
if ln, err := net.Listen("tcp4", address); err == nil {
|
||||
if ln, err := net.Listen("tcp", address); err == nil {
|
||||
tcpMux := webrtc.NewICETCPMux(nil, ln, 8)
|
||||
s.SetICETCPMux(tcpMux)
|
||||
}
|
||||
}
|
||||
|
||||
if network == "" || network == "udp" {
|
||||
if ln, err := net.ListenPacket("udp", address); err == nil {
|
||||
udpMux := webrtc.NewICEUDPMux(nil, ln)
|
||||
s.SetICEUDPMux(udpMux)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return webrtc.NewAPI(
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestClient(t *testing.T) {
|
||||
api, err := NewAPI("")
|
||||
api, err := NewAPI()
|
||||
require.Nil(t, err)
|
||||
|
||||
pc, err := api.NewPeerConnection(webrtc.Configuration{})
|
||||
|
||||
+12
-18
@@ -260,17 +260,15 @@ func MimeType(codec *core.Codec) string {
|
||||
// for server reflexive candidates, 110 for peer reflexive candidates,
|
||||
// and 0 for relayed candidates.
|
||||
|
||||
// We use new priority 120 for Manual Host. It is lower than real Host,
|
||||
// but more then any other candidates.
|
||||
const PriorityTypeHostUDP = (1 << 24) * int(126)
|
||||
const PriorityTypeHostTCP = (1 << 24) * int(126-27)
|
||||
const PriorityLocalUDP = (1 << 8) * int(65535)
|
||||
const PriorityLocalTCPPassive = (1 << 8) * int((1<<13)*4+8191)
|
||||
const PriorityComponentRTP = 1 * int(256-ice.ComponentRTP)
|
||||
|
||||
const PriorityManualHost = (1 << 24) * uint32(120)
|
||||
const PriorityLocalUDP = (1 << 8) * uint32(65535)
|
||||
const PriorityLocalTCPPassive = (1 << 8) * uint32((1<<13)*4+8191)
|
||||
const PriorityComponentRTP = uint32(256 - ice.ComponentRTP)
|
||||
|
||||
func CandidateManualHostUDP(host string, port int) string {
|
||||
func CandidateManualHostUDP(host, port string, offset int) string {
|
||||
foundation := crc32.ChecksumIEEE([]byte("host" + host + "udp4"))
|
||||
priority := PriorityManualHost + PriorityLocalUDP + PriorityComponentRTP
|
||||
priority := PriorityTypeHostUDP + PriorityLocalUDP + PriorityComponentRTP + offset
|
||||
|
||||
// 1. Foundation
|
||||
// 2. Component, always 1 because RTP
|
||||
@@ -279,19 +277,15 @@ func CandidateManualHostUDP(host string, port int) string {
|
||||
// 5. Host - IP4 or IP6 or domain name
|
||||
// 6. Port
|
||||
// 7. typ host
|
||||
return fmt.Sprintf(
|
||||
"candidate:%d 1 udp %d %s %d typ host",
|
||||
foundation, priority, host, port,
|
||||
)
|
||||
return fmt.Sprintf("candidate:%d 1 udp %d %s %s typ host", foundation, priority, host, port)
|
||||
}
|
||||
|
||||
func CandidateManualHostTCPPassive(address string, port int) string {
|
||||
foundation := crc32.ChecksumIEEE([]byte("host" + address + "tcp4"))
|
||||
priority := PriorityManualHost + PriorityLocalTCPPassive + PriorityComponentRTP
|
||||
func CandidateManualHostTCPPassive(host, port string, offset int) string {
|
||||
foundation := crc32.ChecksumIEEE([]byte("host" + host + "tcp4"))
|
||||
priority := PriorityTypeHostTCP + PriorityLocalTCPPassive + PriorityComponentRTP + offset
|
||||
|
||||
return fmt.Sprintf(
|
||||
"candidate:%d 1 tcp %d %s %d typ host tcptype passive",
|
||||
foundation, priority, address, port,
|
||||
"candidate:%d 1 tcp %d %s %s typ host tcptype passive", foundation, priority, host, port,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAlexa(t *testing.T) {
|
||||
// from https://github.com/AlexxIT/go2rtc/issues/825
|
||||
offer := `v=0
|
||||
o=- 3911343731 3911343731 IN IP4 0.0.0.0
|
||||
s=a 2 z
|
||||
c=IN IP4 0.0.0.0
|
||||
t=0 0
|
||||
a=group:BUNDLE audio0 video0
|
||||
m=audio 1 UDP/TLS/RTP/SAVPF 96 0 8
|
||||
a=candidate:1 1 UDP 2013266431 52.90.193.210 60128 typ host
|
||||
a=candidate:2 1 TCP 1015021823 52.90.193.210 9 typ host tcptype active
|
||||
a=candidate:3 1 TCP 1010827519 52.90.193.210 45962 typ host tcptype passive
|
||||
a=candidate:1 2 UDP 2013266430 52.90.193.210 46109 typ host
|
||||
a=candidate:2 2 TCP 1015021822 52.90.193.210 9 typ host tcptype active
|
||||
a=candidate:3 2 TCP 1010827518 52.90.193.210 53795 typ host tcptype passive
|
||||
a=setup:actpass
|
||||
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||
a=rtpmap:96 opus/48000/2
|
||||
a=rtpmap:0 PCMU/8000
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=rtcp-mux
|
||||
a=sendrecv
|
||||
a=mid:audio0
|
||||
a=ssrc:3573704076 cname:user3856789923@host-9dd1dd33
|
||||
a=ice-ufrag:gxfV
|
||||
a=ice-pwd:KepKrlQ1+LD+RGTAFaqVck
|
||||
a=fingerprint:sha-256 A2:93:53:50:E4:2F:C5:4E:DF:7C:70:99:5A:A7:39:50:1A:63:E5:B2:CA:73:70:7A:C5:F4:01:BF:BD:99:57:FC
|
||||
m=video 1 UDP/TLS/RTP/SAVPF 99
|
||||
a=candidate:1 1 UDP 2013266431 52.90.193.210 60128 typ host
|
||||
a=candidate:1 2 UDP 2013266430 52.90.193.210 46109 typ host
|
||||
a=candidate:2 1 TCP 1015021823 52.90.193.210 9 typ host tcptype active
|
||||
a=candidate:3 1 TCP 1010827519 52.90.193.210 45962 typ host tcptype passive
|
||||
a=candidate:3 2 TCP 1010827518 52.90.193.210 53795 typ host tcptype passive
|
||||
a=candidate:2 2 TCP 1015021822 52.90.193.210 9 typ host tcptype active
|
||||
b=AS:2500
|
||||
a=setup:actpass
|
||||
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||
a=rtpmap:99 H264/90000
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=rtcp-mux
|
||||
a=sendrecv
|
||||
a=mid:video0
|
||||
a=rtcp-fb:99 nack
|
||||
a=rtcp-fb:99 nack pli
|
||||
a=rtcp-fb:99 ccm fir
|
||||
a=ssrc:3778078295 cname:user3856789923@host-9dd1dd33
|
||||
a=ice-ufrag:gxfV
|
||||
a=ice-pwd:KepKrlQ1+LD+RGTAFaqVck
|
||||
a=fingerprint:sha-256 A2:93:53:50:E4:2F:C5:4E:DF:7C:70:99:5A:A7:39:50:1A:63:E5:B2:CA:73:70:7A:C5:F4:01:BF:BD:99:57:FC
|
||||
`
|
||||
|
||||
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
|
||||
require.Nil(t, err)
|
||||
|
||||
conn := NewConn(pc)
|
||||
err = conn.SetOffer(offer)
|
||||
require.Nil(t, err)
|
||||
|
||||
_, err = conn.GetAnswer()
|
||||
require.Nil(t, err)
|
||||
}
|
||||
@@ -246,6 +246,18 @@
|
||||
</script>
|
||||
|
||||
|
||||
<button id="gopro">GoPro</button>
|
||||
<div class="module">
|
||||
<table id="gopro-table"></table>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('gopro').addEventListener('click', async ev => {
|
||||
ev.target.nextElementSibling.style.display = 'block';
|
||||
await getSources('gopro-table', 'api/gopro');
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<button id="hass">Home Assistant</button>
|
||||
<div class="module">
|
||||
<table id="hass-table"></table>
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Logs</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background-color: white;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
background-color: white;
|
||||
text-align: left;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table td, table th {
|
||||
border: 1px solid black;
|
||||
padding: 5px 5px;
|
||||
}
|
||||
|
||||
table tbody td {
|
||||
font-size: 13px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
table thead {
|
||||
background: #CFCFCF;
|
||||
background: linear-gradient(to bottom, #dbdbdb 0%, #d3d3d3 66%, #CFCFCF 100%);
|
||||
border-bottom: 3px solid black;
|
||||
}
|
||||
|
||||
table thead th {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script src="main.js"></script>
|
||||
<div>
|
||||
<button id="clean">Clean</button>
|
||||
<button id="update">Auto Update: ON</button>
|
||||
</div>
|
||||
<br>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 130px">Time</th>
|
||||
<th style="width: 40px">Level</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="log">
|
||||
</tbody>
|
||||
</table>
|
||||
<script>
|
||||
document.getElementById('clean').addEventListener('click', async () => {
|
||||
const r = await fetch('api/log', {method: 'DELETE'});
|
||||
if (r.ok) reload();
|
||||
alert(await r.text());
|
||||
});
|
||||
|
||||
// Sanitizes the input text to prevent XSS when inserting into the DOM
|
||||
function escapeHTML(text) {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
function applyLogStyling(jsonlines) {
|
||||
const KEYS = ['time', 'level', 'message'];
|
||||
const lines = JSON.parse('[' + jsonlines.trimEnd().replaceAll('\n', ',') + ']');
|
||||
return lines.map(line => {
|
||||
const ts = new Date(line['time']);
|
||||
const msg = Object.keys(line).reduce((msg, key) => {
|
||||
return KEYS.indexOf(key) < 0 ? `${msg} ${key}=${line[key]}` : msg;
|
||||
}, line['message']);
|
||||
return `<tr><td>${ts.toLocaleString()}</td><td>${line['level']}</td><td>${escapeHTML(msg)}</td></tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function reload() {
|
||||
const url = new URL('api/log', location.href);
|
||||
fetch(url, {cache: 'no-cache'})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
// Apply styling to the log data
|
||||
document.getElementById('log').innerHTML = applyLogStyling(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('An error occurred:', error);
|
||||
});
|
||||
}
|
||||
|
||||
reload();
|
||||
|
||||
// Handle auto-update switch
|
||||
let autoUpdateEnabled = true;
|
||||
|
||||
const update = document.getElementById('update');
|
||||
update.addEventListener('click', () => {
|
||||
autoUpdateEnabled = !autoUpdateEnabled;
|
||||
update.textContent = `Auto Update: ${autoUpdateEnabled ? 'ON' : 'OFF'}`;
|
||||
});
|
||||
|
||||
// Reload the logs every 5 seconds
|
||||
setInterval(() => {
|
||||
if (autoUpdateEnabled) reload();
|
||||
}, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -47,6 +47,7 @@ nav li {
|
||||
<li><a href="index.html">Streams</a></li>
|
||||
<li><a href="add.html">Add</a></li>
|
||||
<li><a href="editor.html">Config</a></li>
|
||||
<li><a href="log.html">Log</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
` + document.body.innerHTML;
|
||||
|
||||
Reference in New Issue
Block a user