Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 6f2af78392 | |||
| 548d8133eb | |||
| 36ee2b29fb | |||
| 05accb4555 | |||
| f949a278da | |||
| bfae16f3a0 | |||
| d09d21434b | |||
| 2b9926cedb | |||
| af24fd67aa | |||
| e2cd34ffe3 | |||
| ecdf5ba271 | |||
| 995ef5bb36 | |||
| 8165adcab1 | |||
| 91c4a3e7b5 | |||
| cb710ea2be | |||
| 843a3ae9c9 | |||
| de040fb160 | |||
| acec8a76aa | |||
| 6c07c59454 | |||
| 4d708b5385 | |||
| 2e9f3181d4 | |||
| 3ae15d8f80 | |||
| d016529030 | |||
| 09f1553e40 | |||
| 52e4bf1b35 | |||
| bbe6ae0059 | |||
| c02117e626 | |||
| b8fb3acbab | |||
| d4d0064220 | |||
| 855bbdeb60 | |||
| 05893c9203 | |||
| c9c8e73587 | |||
| c7b6eb5d5b | |||
| 96bc88d8ce | |||
| 9a2e9dd6d1 | |||
| b252fcaaa1 | |||
| c582b932c7 | |||
| c3f26c4db8 | |||
| f27f7d28bb | |||
| 0424b1a92a | |||
| 81fb8fc238 | |||
| 037970a4ea | |||
| 3f6e83e87c | |||
| aa5b23fa80 | |||
| 02bde2c8b7 | |||
| cb5e90cc3b | |||
| 209fe09806 | |||
| dca8279e0c | |||
| 8163c7a520 | |||
| 4dffceaf7e | |||
| 9f1e33e0c6 | |||
| 9a7d7e68e2 | |||
| ab18d5d1ca | |||
| 6e53e74742 | |||
| f910bd4fce | |||
| 93e475f3a4 | |||
| e5d8170037 | |||
| 861632f92b | |||
| 9cf75565b5 | |||
| 9368a6b85e |
@@ -14,6 +14,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [DVRIP](#source-dvrip), [HTTP](#source-http) (FLV/MJPEG/JPEG/TS), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
|
||||
- streaming from any sources, supported by [FFmpeg](#source-ffmpeg)
|
||||
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4), [HomeKit](#module-homekit) [HLS](#module-hls) or [MJPEG](#module-mjpeg)
|
||||
- [publish](#publish-stream) any source to popular streaming services (YouTube, Telegram, etc.)
|
||||
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
|
||||
- support H265 for WebRTC in browser (Safari only, [read more](https://github.com/AlexxIT/Blog/issues/5))
|
||||
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
|
||||
@@ -22,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:**
|
||||
@@ -53,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)
|
||||
@@ -67,12 +70,14 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
* [Source: WebTorrent](#source-webtorrent)
|
||||
* [Incoming sources](#incoming-sources)
|
||||
* [Stream to camera](#stream-to-camera)
|
||||
* [Publish stream](#publish-stream)
|
||||
* [Module: API](#module-api)
|
||||
* [Module: RTSP](#module-rtsp)
|
||||
* [Module: RTMP](#module-rtmp)
|
||||
* [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)
|
||||
@@ -122,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
|
||||
|
||||
@@ -165,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
|
||||
|
||||
@@ -183,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
|
||||
@@ -202,6 +209,7 @@ Read more about [incoming sources](#incoming-sources)
|
||||
Supported for sources:
|
||||
|
||||
- [RTSP cameras](#source-rtsp) with [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) (back channel connection)
|
||||
- [DVRIP](#source-dvrip) cameras
|
||||
- [TP-Link Tapo](#source-tapo) cameras
|
||||
- [Hikvision ISAPI](#source-isapi) cameras
|
||||
- [Roborock vacuums](#source-roborock) models with cameras
|
||||
@@ -422,6 +430,10 @@ streams:
|
||||
apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html
|
||||
```
|
||||
|
||||
#### Source: Expr
|
||||
|
||||
Like `echo` source, but uses the built-in [expr](https://github.com/antonmedv/expr) expression language ([read more](https://github.com/AlexxIT/go2rtc/blob/master/internal/expr/README.md)).
|
||||
|
||||
#### Source: HomeKit
|
||||
|
||||
**Important:**
|
||||
@@ -478,7 +490,11 @@ Other names: DVR-IP, NetSurveillance, Sofia protocol (NETsurveillance ActiveX pl
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
camera1: dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
|
||||
only_stream: dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
|
||||
only_tts: dvrip://username:password@192.168.1.123:34567?backchannel=1
|
||||
two_way_audio:
|
||||
- dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
|
||||
- dvrip://username:password@192.168.1.123:34567?backchannel=1
|
||||
```
|
||||
|
||||
#### Source: Tapo
|
||||
@@ -506,6 +522,10 @@ streams:
|
||||
kasa: kasa://user:pass@192.168.1.123:19443/https/stream/mixed
|
||||
```
|
||||
|
||||
#### Source: GoPro
|
||||
|
||||
Support streaming from [GoPro](https://gopro.com/) cameras, connected via USB or Wi-Fi to Linux, Mac, Windows. [Read more](https://github.com/AlexxIT/go2rtc/tree/master/internal/gopro).
|
||||
|
||||
#### Source: Ivideon
|
||||
|
||||
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
|
||||
@@ -590,7 +610,7 @@ This source type support four connection formats.
|
||||
|
||||
**whep**
|
||||
|
||||
[WebRTC/WHEP](https://www.ietf.org/id/draft-murillo-whep-01.html) - is an unapproved standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
|
||||
[WebRTC/WHEP](https://www.ietf.org/id/draft-murillo-whep-02.html) - is an unapproved standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
|
||||
|
||||
**go2rtc**
|
||||
|
||||
@@ -632,7 +652,7 @@ streams:
|
||||
|
||||
By default, go2rtc establishes a connection to the source when any client requests it. Go2rtc drops the connection to the source when it has no clients left.
|
||||
|
||||
- Go2rtc also can accepts incoming sources in [RTSP](#source-rtsp), [HTTP](#source-http) and **WebRTC/WHIP** formats
|
||||
- Go2rtc also can accepts incoming sources in [RTSP](#module-rtsp), [RTMP](#module-rtmp), [HTTP](#source-http) and **WebRTC/WHIP** formats
|
||||
- Go2rtc won't stop such a source if it has no clients
|
||||
- You can push data only to existing stream (create stream with empty source in config)
|
||||
- You can push multiple incoming sources to same stream
|
||||
@@ -693,6 +713,39 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
|
||||
- you can stop active playback by calling the API with the empty `src` parameter
|
||||
- you will see one active producer and one active consumer in go2rtc WebUI info page during streaming
|
||||
|
||||
### Publish stream
|
||||
|
||||
You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:
|
||||
|
||||
- Supported codecs: H264 for video and AAC for audio
|
||||
- Pixel format should be `yuv420p`, for cameras with `yuvj420p` format you SHOULD use [transcoding](#source-ffmpeg)
|
||||
- You don't need to enable [RTMP module](#module-rtmp) listening for this task
|
||||
|
||||
You can use API:
|
||||
|
||||
```
|
||||
POST http://localhost:1984/api/streams?src=camera1&dst=rtmps://...
|
||||
```
|
||||
|
||||
Or config file:
|
||||
|
||||
```yaml
|
||||
publish:
|
||||
# publish stream "tplink_tapo" to Telegram
|
||||
tplink_tapo: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
||||
# publish stream "other_camera" to Telegram and YouTube
|
||||
other_camera:
|
||||
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
||||
- rtmps://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.
|
||||
- **YouTube** > Create > Go live > Stream latency: Ultra low-latency > Copy: Stream URL + Stream key.
|
||||
|
||||
### Module: API
|
||||
|
||||
The HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.
|
||||
@@ -705,6 +758,7 @@ The HTTP API is the main part for interacting with the application. Default addr
|
||||
- you can enable HTTP API only on localhost with `listen: "127.0.0.1:1984"` setting
|
||||
- you can change API `base_path` and host go2rtc on your main app webserver suburl
|
||||
- all files from `static_dir` hosted on root path: `/`
|
||||
- you can use raw TLS cert/key content or path to files
|
||||
|
||||
```yaml
|
||||
api:
|
||||
@@ -753,6 +807,17 @@ By default go2rtc provide RTSP-stream with only one first video and only one fir
|
||||
|
||||
Read more about [codecs filters](#codecs-filters).
|
||||
|
||||
### Module: RTMP
|
||||
|
||||
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.
|
||||
|
||||
```yaml
|
||||
rtmp:
|
||||
listen: ":1935" # by default - disabled!
|
||||
```
|
||||
|
||||
### Module: WebRTC
|
||||
|
||||
In most cases [WebRTC](https://en.wikipedia.org/wiki/WebRTC) uses direct peer-to-peer connection from your browser to go2rtc and sends media data via UDP.
|
||||
@@ -796,7 +861,7 @@ webrtc:
|
||||
|
||||
**Private IP**
|
||||
|
||||
- setup integration with [Ngrok service](#module-ngrok)
|
||||
- setup integration with [ngrok service](#module-ngrok)
|
||||
|
||||
```yaml
|
||||
ngrok:
|
||||
@@ -898,29 +963,29 @@ Link example: https://alexxit.github.io/go2rtc/#share=02SNtgjKXY&pwd=wznEQqznxW&
|
||||
|
||||
TODO: article how it works...
|
||||
|
||||
### Module: Ngrok
|
||||
### Module: ngrok
|
||||
|
||||
With Ngrok integration you can get external access to your streams in situation when you have Internet with private IP-address.
|
||||
With ngrok integration you can get external access to your streams in situations when you have Internet with private IP-address.
|
||||
|
||||
- Ngrok preistalled for **Docker** and **Hass Add-on** users
|
||||
- ngrok is pre-installed for **Docker** and **Hass Add-on** users
|
||||
- you may need external access for two different things:
|
||||
- WebRTC stream, so you need tunnel WebRTC TCP port (ex. 8555)
|
||||
- go2rtc web interface, so you need tunnel API HTTP port (ex. 1984)
|
||||
- Ngrok support authorization for your web interface
|
||||
- Ngrok automatically adds HTTPS to your web interface
|
||||
- ngrok support authorization for your web interface
|
||||
- ngrok automatically adds HTTPS to your web interface
|
||||
|
||||
Ngrok free subscription limitations:
|
||||
The ngrok free subscription has the following limitations:
|
||||
|
||||
- you will always get random external address (not a problem for webrtc stream)
|
||||
- you can forward multiple ports but use only one Ngrok app
|
||||
- You can reserve a free domain for serving the web interface, but the TCP address you get will always be random and change with each restart of the ngrok agent (not a problem for webrtc stream)
|
||||
- You can forward multiple ports from a single agent, but you can only run one ngrok agent on the free plan
|
||||
|
||||
go2rtc will automatically get your external TCP address (if you enable it in ngrok config) and use it with WebRTC connection (if you enable it in webrtc config).
|
||||
|
||||
You need manually download [Ngrok agent app](https://ngrok.com/download) for your OS and register in [Ngrok service](https://ngrok.com/).
|
||||
You need to manually download the [ngrok agent app](https://ngrok.com/download) for your OS and register with the [ngrok service](https://ngrok.com/signup).
|
||||
|
||||
**Tunnel for only WebRTC Stream**
|
||||
|
||||
You need to add your [Ngrok token](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
|
||||
You need to add your [ngrok authtoken](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
|
||||
|
||||
```yaml
|
||||
ngrok:
|
||||
@@ -936,7 +1001,7 @@ ngrok:
|
||||
command: ngrok start --all --config ngrok.yaml
|
||||
```
|
||||
|
||||
Ngrok config example:
|
||||
ngrok config example:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
@@ -952,6 +1017,8 @@ tunnels:
|
||||
proto: tcp
|
||||
```
|
||||
|
||||
See the [ngrok agent documentation](https://ngrok.com/docs/agent/config/) for more details on the ngrok configuration file.
|
||||
|
||||
### Module: Hass
|
||||
|
||||
The best and easiest way to use go2rtc inside the Home Assistant is to install the custom integration [WebRTC Camera](#go2rtc-home-assistant-integration) and custom lovelace card.
|
||||
@@ -1091,7 +1158,7 @@ webrtc:
|
||||
- external access to WebRTC TCP port is not a problem, because it used only for transmit encrypted media data
|
||||
- anyway you need to open this port to your local network and to the Internet in order for WebRTC to work
|
||||
|
||||
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [Ngrok](https://ngrok.com/), etc.
|
||||
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [ngrok](https://ngrok.com/), etc.
|
||||
|
||||
PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted media. It works without problems on the local network. And sometimes also works for external access, even if you haven't opened this port on your router ([read more](https://en.wikipedia.org/wiki/UDP_hole_punching)). But for stable external WebRTC access, you need to open the 8555 port on your router for both TCP and UDP.
|
||||
|
||||
@@ -1122,17 +1189,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
|
||||
|
||||
@@ -1232,9 +1296,15 @@ streams:
|
||||
- [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
|
||||
|
||||
**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
|
||||
|
||||
+75
-24
@@ -1,4 +1,4 @@
|
||||
openapi: 3.0.0
|
||||
openapi: 3.1.0
|
||||
|
||||
info:
|
||||
title: go2rtc
|
||||
@@ -111,9 +111,18 @@ paths:
|
||||
required: false
|
||||
schema: { type: integer }
|
||||
example: 100
|
||||
responses: { }
|
||||
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
/api/restart:
|
||||
post:
|
||||
summary: Restart Daemon
|
||||
description: Restarts the daemon.
|
||||
tags: [ Application ]
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
/api/config:
|
||||
get:
|
||||
@@ -130,14 +139,18 @@ paths:
|
||||
requestBody:
|
||||
content:
|
||||
"*/*": { example: "streams:..." }
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
patch:
|
||||
summary: Merge changes to main config file
|
||||
tags: [ Config ]
|
||||
requestBody:
|
||||
content:
|
||||
"*/*": { example: "streams:..." }
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
@@ -166,7 +179,9 @@ paths:
|
||||
required: false
|
||||
schema: { type: string }
|
||||
example: camera1
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
patch:
|
||||
summary: Update stream source
|
||||
tags: [ Streams list ]
|
||||
@@ -183,7 +198,9 @@ paths:
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: camera1
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
delete:
|
||||
summary: Delete stream
|
||||
tags: [ Streams list ]
|
||||
@@ -194,7 +211,9 @@ paths:
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: camera1
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
post:
|
||||
summary: Send stream from source to destination
|
||||
description: "[Stream to camera](https://github.com/AlexxIT/go2rtc#stream-to-camera)"
|
||||
@@ -212,7 +231,9 @@ paths:
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: camera1
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
@@ -347,7 +368,9 @@ paths:
|
||||
tags: [ Produce stream ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_dst_path"
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/stream.flv?dst={dst}:
|
||||
post:
|
||||
summary: Post stream in FLV format
|
||||
@@ -355,7 +378,9 @@ paths:
|
||||
tags: [ Produce stream ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_dst_path"
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/stream.ts?dst={dst}:
|
||||
post:
|
||||
summary: Post stream in MPEG-TS format
|
||||
@@ -363,7 +388,9 @@ paths:
|
||||
tags: [ Produce stream ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_dst_path"
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/stream.mjpeg?dst={dst}:
|
||||
post:
|
||||
summary: Post stream in MJPEG format
|
||||
@@ -371,7 +398,9 @@ paths:
|
||||
tags: [ Produce stream ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_dst_path"
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
@@ -380,49 +409,65 @@ paths:
|
||||
summary: DVRIP cameras discovery
|
||||
description: "[Source: DVRIP](https://github.com/AlexxIT/go2rtc#source-dvrip)"
|
||||
tags: [ Discovery ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
/api/ffmpeg/devices:
|
||||
get:
|
||||
summary: FFmpeg USB devices discovery
|
||||
description: "[Source: FFmpeg Device](https://github.com/AlexxIT/go2rtc#source-ffmpeg-device)"
|
||||
tags: [ Discovery ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/ffmpeg/hardware:
|
||||
get:
|
||||
summary: FFmpeg hardware transcoding discovery
|
||||
description: "[Hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)"
|
||||
tags: [ Discovery ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/hass:
|
||||
get:
|
||||
summary: Home Assistant cameras discovery
|
||||
description: "[Source: Hass](https://github.com/AlexxIT/go2rtc#source-hass)"
|
||||
tags: [ Discovery ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/homekit:
|
||||
get:
|
||||
summary: HomeKit cameras discovery
|
||||
description: "[Source: HomeKit](https://github.com/AlexxIT/go2rtc#source-homekit)"
|
||||
tags: [ Discovery ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/nest:
|
||||
get:
|
||||
summary: Nest cameras discovery
|
||||
tags: [ Discovery ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/onvif:
|
||||
get:
|
||||
summary: ONVIF cameras discovery
|
||||
description: "[Source: ONVIF](https://github.com/AlexxIT/go2rtc#source-onvif)"
|
||||
tags: [ Discovery ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/roborock:
|
||||
get:
|
||||
summary: Roborock vacuums discovery
|
||||
description: "[Source: Roborock](https://github.com/AlexxIT/go2rtc#source-roborock)"
|
||||
tags: [ Discovery ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
@@ -431,7 +476,9 @@ paths:
|
||||
summary: ONVIF server implementation
|
||||
description: Simple realisation of the ONVIF protocol. Accepts any suburl requests
|
||||
tags: [ ONVIF ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
@@ -440,7 +487,9 @@ paths:
|
||||
summary: RTSPtoWebRTC server implementation
|
||||
description: Simple API for support [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration
|
||||
tags: [ RTSPtoWebRTC ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
@@ -465,7 +514,9 @@ paths:
|
||||
tags: [ WebTorrent ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_src_path"
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
/api/webtorrent:
|
||||
get:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 154 KiB |
@@ -0,0 +1,123 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
)
|
||||
|
||||
var servs = map[string]string{
|
||||
"3E": "Accessory Information",
|
||||
"7E": "Security System",
|
||||
"85": "Motion Sensor",
|
||||
"96": "Battery",
|
||||
"A2": "Protocol Information",
|
||||
"110": "Camera RTP Stream Management",
|
||||
"112": "Microphone",
|
||||
"113": "Speaker",
|
||||
"121": "Doorbell",
|
||||
"129": "Data Stream Transport Management",
|
||||
"204": "Camera Recording Management",
|
||||
"21A": "Camera Operating Mode",
|
||||
"22A": "Wi-Fi Transport",
|
||||
"239": "Accessory Runtime Information",
|
||||
}
|
||||
|
||||
var chars = map[string]string{
|
||||
"14": "Identify",
|
||||
"20": "Manufacturer",
|
||||
"21": "Model",
|
||||
"23": "Name",
|
||||
"30": "Serial Number",
|
||||
"52": "Firmware Revision",
|
||||
"53": "Hardware Revision",
|
||||
"220": "Product Data",
|
||||
"A6": "Accessory Flags",
|
||||
|
||||
"22": "Motion Detected",
|
||||
"75": "Status Active",
|
||||
|
||||
"11A": "Mute",
|
||||
"119": "Volume",
|
||||
|
||||
"B0": "Active",
|
||||
"209": "Selected Camera Recording Configuration",
|
||||
"207": "Supported Audio Recording Configuration",
|
||||
"205": "Supported Camera Recording Configuration",
|
||||
"206": "Supported Video Recording Configuration",
|
||||
"226": "Recording Audio Active",
|
||||
|
||||
"223": "Event Snapshots Active",
|
||||
"225": "Periodic Snapshots Active",
|
||||
"21B": "HomeKit Camera Active",
|
||||
"21C": "Third Party Camera Active",
|
||||
"21D": "Camera Operating Mode Indicator",
|
||||
"11B": "Night Vision",
|
||||
"129": "Supported Data Stream Transport Configuration",
|
||||
"37": "Version",
|
||||
"131": "Setup Data Stream Transport",
|
||||
"130": "Supported Data Stream Transport Configuration",
|
||||
|
||||
"120": "Streaming Status",
|
||||
"115": "Supported Audio Stream Configuration",
|
||||
"116": "Supported RTP Configuration",
|
||||
"114": "Supported Video Stream Configuration",
|
||||
"117": "Selected RTP Stream Configuration",
|
||||
"118": "Setup Endpoints",
|
||||
|
||||
"22B": "Current Transport",
|
||||
"22C": "Wi-Fi Capabilities",
|
||||
"22D": "Wi-Fi Configuration Control",
|
||||
|
||||
"23C": "Ping",
|
||||
|
||||
"68": "Battery Level",
|
||||
"79": "Status Low Battery",
|
||||
"8F": "Charging State",
|
||||
|
||||
"73": "Programmable Switch Event",
|
||||
"232": "Operating State Response",
|
||||
|
||||
"66": "Security System Current State",
|
||||
"67": "Security System Target State",
|
||||
}
|
||||
|
||||
func main() {
|
||||
src := os.Args[1]
|
||||
dst := os.Args[2]
|
||||
|
||||
f, err := os.Open(src)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var v hap.JSONAccessories
|
||||
if err = json.NewDecoder(f).Decode(&v); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, acc := range v.Value {
|
||||
for _, srv := range acc.Services {
|
||||
if srv.Desc == "" {
|
||||
srv.Desc = servs[srv.Type]
|
||||
}
|
||||
for _, chr := range srv.Characters {
|
||||
if chr.Desc == "" {
|
||||
chr.Desc = chars[chr.Type]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f, err = os.Create(dst)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(f)
|
||||
enc.SetIndent("", " ")
|
||||
if err = enc.Encode(v); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -3,42 +3,43 @@ 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/antonmedv/expr v1.15.3
|
||||
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.12
|
||||
github.com/pion/rtp v1.8.3
|
||||
github.com/pion/sdp/v3 v3.0.6
|
||||
github.com/pion/srtp/v2 v2.0.17
|
||||
github.com/pion/srtp/v2 v2.0.18
|
||||
github.com/pion/stun v0.6.1
|
||||
github.com/pion/webrtc/v3 v3.2.19
|
||||
github.com/rs/zerolog v1.30.0
|
||||
github.com/pion/webrtc/v3 v3.2.22
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||
golang.org/x/crypto v0.13.0
|
||||
golang.org/x/crypto v0.15.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/google/uuid v1.4.0 // indirect
|
||||
github.com/kr/pretty v0.2.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pion/datachannel v1.5.5 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.7 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.8 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/mdns v0.0.9 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
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.18.0 // indirect
|
||||
golang.org/x/sys v0.14.0 // indirect
|
||||
golang.org/x/tools v0.15.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
github.com/antonmedv/expr v1.15.3 h1:q3hOJZNvLvhqE8OHBs1cFRdbXFNKuA+bHmRaI+AmRmI=
|
||||
github.com/antonmedv/expr v1.15.3/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE=
|
||||
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=
|
||||
@@ -21,8 +23,12 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
@@ -30,15 +36,17 @@ 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.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE=
|
||||
github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
|
||||
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
|
||||
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -52,11 +60,15 @@ github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew
|
||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.8 h1:BUroldfiIbV9jSnC6cKOMnyiORRWrWWpV11JUyEu5OA=
|
||||
github.com/pion/dtls/v2 v2.2.8/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/ice/v2 v2.3.11 h1:rZjVmUwyT55cmN8ySMpL7rsS8KYsJERsrxJLLxpKhdw=
|
||||
github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
|
||||
github.com/pion/interceptor v0.1.18/go.mod h1:tpvvF4cPM6NGxFA1DUMbhabzQBxdWMATDGEUYOR9x6I=
|
||||
github.com/pion/interceptor v0.1.19 h1:tq0TGBzuZQqipyBhaC1mVUCfCh8XjDKUuibq9rIl5t4=
|
||||
github.com/pion/interceptor v0.1.19/go.mod h1:VANhFxdJezB8mwToMMmrmyHyP9gym6xLqIUch31xryg=
|
||||
github.com/pion/interceptor v0.1.22 h1:khhimAF0/VmGaIfeE+bA3X1jm0lD8C8HOGcU7vpWcPA=
|
||||
github.com/pion/interceptor v0.1.22/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
|
||||
github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc=
|
||||
github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
|
||||
@@ -66,8 +78,13 @@ github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
|
||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||
github.com/pion/rtp v1.8.1 h1:26OxTc6lKg/qLSGir5agLyj0QKaOv8OP5wps2SFnVNQ=
|
||||
github.com/pion/rtcp v1.2.12 h1:bKWiX93XKgDZENEXCijvHRU/wRifm6JV5DGcH6twtSM=
|
||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtp v1.8.1/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.2 h1:oKMM0K1/QYQ5b5qH+ikqDSZRipP5mIxPJcgcvw5sH0w=
|
||||
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
|
||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
|
||||
github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
|
||||
@@ -76,6 +93,8 @@ github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp/v2 v2.0.17 h1:ECuOk+7uIpY6HUlTb0nXhfvu4REG2hjtC4ronYFCZE4=
|
||||
github.com/pion/srtp/v2 v2.0.17/go.mod h1:y5WSHcJY4YfNB/5r7ca5YjHeIr1H3LM1rKArGGs8jMc=
|
||||
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
|
||||
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
||||
@@ -87,16 +106,19 @@ 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.21 h1:c8fy5JcqJkAQBwwy3Sk9huQLTBUSqaggyRlv9Lnh2zY=
|
||||
github.com/pion/webrtc/v3 v3.2.21/go.mod h1:vVURQTBOG5BpWKOJz3nlr23NfTDeyKVmubRNqzQp+Tg=
|
||||
github.com/pion/webrtc/v3 v3.2.22 h1:Hno262T7+V56MgUO30O0ZirZmVSvbXtnau31SB0WSpc=
|
||||
github.com/pion/webrtc/v3 v3.2.22/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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=
|
||||
@@ -124,13 +146,18 @@ 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.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
|
||||
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
|
||||
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -146,15 +173,19 @@ 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.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
|
||||
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
||||
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -167,8 +198,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 +210,11 @@ 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.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -209,8 +241,10 @@ 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.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
|
||||
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
|
||||
golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
|
||||
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
+84
-60
@@ -10,33 +10,36 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -48,16 +51,7 @@ func Init() {
|
||||
HandleFunc("api", apiHandler)
|
||||
HandleFunc("api/config", configHandler)
|
||||
HandleFunc("api/exit", exitHandler)
|
||||
|
||||
// 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/restart", restartHandler)
|
||||
|
||||
Handler = http.DefaultServeMux // 4th
|
||||
|
||||
@@ -73,52 +67,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 != "" {
|
||||
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"
|
||||
@@ -178,7 +194,7 @@ func middlewareLog(next http.Handler) http.Handler {
|
||||
|
||||
func middlewareAuth(username, password string, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") {
|
||||
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") && r.RemoteAddr != "@" {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok || user != username || pass != password {
|
||||
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
|
||||
@@ -200,7 +216,6 @@ func middlewareCORS(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
var ln net.Listener
|
||||
var mu sync.Mutex
|
||||
|
||||
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -222,6 +237,15 @@ func exitHandler(w http.ResponseWriter, r *http.Request) {
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func restartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
go shell.Restart()
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var Version = "1.7.1"
|
||||
var Version = "1.8.4"
|
||||
var UserAgent = "go2rtc/" + Version
|
||||
|
||||
var ConfigPath string
|
||||
|
||||
@@ -23,17 +23,11 @@ func Init() {
|
||||
}
|
||||
|
||||
func handle(url string) (core.Producer, error) {
|
||||
conn := dvrip.NewClient(url)
|
||||
if err := conn.Dial(); err != nil {
|
||||
client, err := dvrip.Dial(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := conn.Play(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := conn.Handle(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
return client, nil
|
||||
}
|
||||
|
||||
const Port = 34569 // UDP port number for dvrip discovery
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -52,7 +52,8 @@ var defaults = map[string]string{
|
||||
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
||||
// `-tune zerolatency` - for minimal latency
|
||||
// `-profile high -level 4.1` - most used streaming profile
|
||||
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p",
|
||||
// `-pix_fmt:v yuv420p` - important for Telegram
|
||||
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
|
||||
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency",
|
||||
"mjpeg": "-c:v mjpeg",
|
||||
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
||||
@@ -63,18 +64,22 @@ var defaults = map[string]string{
|
||||
// `-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",
|
||||
"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
|
||||
|
||||
@@ -13,7 +13,7 @@ func TestParseArgsFile(t *testing.T) {
|
||||
|
||||
// [FILE] video will be transcoded to H264, audio will be skipped
|
||||
args = parseArgs("/media/bbb.mp4#video=h264")
|
||||
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [FILE] video will be copied, audio will be transcoded to pcmu
|
||||
args = parseArgs("/media/bbb.mp4#video=copy#audio=pcmu")
|
||||
@@ -38,8 +38,9 @@ func TestParseArgsDevice(t *testing.T) {
|
||||
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")
|
||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1280x720 -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&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())
|
||||
}
|
||||
|
||||
func TestParseArgsIpCam(t *testing.T) {
|
||||
@@ -49,7 +50,7 @@ func TestParseArgsIpCam(t *testing.T) {
|
||||
|
||||
// [HTTP-MJPEG] video will be transcoded to H264
|
||||
args = parseArgs("http://example.com#video=h264")
|
||||
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 yuvj420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
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 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [HLS] video will be copied, audio will be skipped
|
||||
args = parseArgs("https://example.com#video=copy")
|
||||
@@ -83,7 +84,7 @@ func TestParseArgsAudio(t *testing.T) {
|
||||
|
||||
// [AUDIO] audio will be transcoded to OPUS, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=opus")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -application:a lowdelay -frame_duration 20 -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to PCMU, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=pcmu")
|
||||
@@ -113,23 +114,23 @@ func TestParseArgsAudio(t *testing.T) {
|
||||
func TestParseArgsHwVaapi(t *testing.T) {
|
||||
// [HTTP-MJPEG] video will be transcoded to H264
|
||||
args := parseArgs("http:///example.com#video=h264#hardware=vaapi")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -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 "format=vaapi|nv12,hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -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 "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [RTSP] video with rotation, should be transcoded, so select H264
|
||||
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=vaapi")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,transpose_vaapi=4" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,transpose_vaapi=4,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
|
||||
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=vaapi")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=1280:720:out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
|
||||
args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=vaapi")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -f mjpeg -`, args.String())
|
||||
|
||||
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
|
||||
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=vaapi")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.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 TestParseArgsHwV4l2m2m(t *testing.T) {
|
||||
@@ -207,3 +208,19 @@ func TestParseArgsHwVideotoolbox(t *testing.T) {
|
||||
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=videotoolbox")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
|
||||
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:]
|
||||
@@ -154,3 +158,24 @@ func cut(s string, sep byte, pos int) string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// 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=") {
|
||||
args.Filters[i] = filter + ":" + fixPixFmt
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
args.Filters = append(args.Filters, "scale="+fixPixFmt)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -198,9 +198,11 @@ func (s *server) AddPair(conn net.Conn, id string, public []byte, permissions by
|
||||
"client_public": []string{hex.EncodeToString(public)},
|
||||
"permissions": []string{string('0' + permissions)},
|
||||
}
|
||||
s.pairings = append(s.pairings, query.Encode())
|
||||
s.UpdateStatus()
|
||||
s.PatchConfig()
|
||||
if s.GetPair(conn, id) == nil {
|
||||
s.pairings = append(s.pairings, query.Encode())
|
||||
s.UpdateStatus()
|
||||
s.PatchConfig()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) DelPair(conn net.Conn, id string) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
+152
-3
@@ -1,39 +1,188 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
var conf struct {
|
||||
Mod struct {
|
||||
Listen string `yaml:"listen" json:"listen"`
|
||||
} `yaml:"rtmp"`
|
||||
}
|
||||
|
||||
app.LoadConfig(&conf)
|
||||
|
||||
log = app.GetLogger("rtmp")
|
||||
|
||||
streams.HandleFunc("rtmp", streamsHandle)
|
||||
streams.HandleFunc("rtmps", streamsHandle)
|
||||
streams.HandleFunc("rtmpx", streamsHandle)
|
||||
|
||||
api.HandleFunc("api/stream.flv", apiHandle)
|
||||
|
||||
streams.HandleConsumerFunc("rtmp", streamsConsumerHandle)
|
||||
streams.HandleConsumerFunc("rtmps", streamsConsumerHandle)
|
||||
streams.HandleConsumerFunc("rtmpx", streamsConsumerHandle)
|
||||
|
||||
address := conf.Mod.Listen
|
||||
if address == "" {
|
||||
return
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Str("addr", address).Msg("[rtmp] listen")
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err = tcpHandle(conn); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func tcpHandle(netConn net.Conn) error {
|
||||
rtmpConn, err := rtmp.NewServer(netConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = rtmpConn.ReadCommands(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch rtmpConn.Intent {
|
||||
case rtmp.CommandPlay:
|
||||
stream := streams.Get(rtmpConn.App)
|
||||
if stream == nil {
|
||||
return errors.New("stream not found: " + rtmpConn.App)
|
||||
}
|
||||
|
||||
cons := flv.NewConsumer()
|
||||
if err = stream.AddConsumer(cons); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer stream.RemoveConsumer(cons)
|
||||
|
||||
if err = rtmpConn.WriteStart(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = cons.WriteTo(rtmpConn)
|
||||
|
||||
return nil
|
||||
|
||||
case rtmp.CommandPublish:
|
||||
stream := streams.Get(rtmpConn.App)
|
||||
if stream == nil {
|
||||
return errors.New("stream not found: " + rtmpConn.App)
|
||||
}
|
||||
|
||||
if err = rtmpConn.WriteStart(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prod, err := rtmpConn.Producer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stream.AddProducer(prod)
|
||||
|
||||
defer stream.RemoveProducer(prod)
|
||||
|
||||
_ = prod.Start()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("rtmp: unknown command: " + rtmpConn.Intent)
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
func streamsHandle(url string) (core.Producer, error) {
|
||||
client, err := rtmp.Dial(url)
|
||||
client, err := rtmp.DialPlay(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func streamsConsumerHandle(url string) (core.Consumer, func(), error) {
|
||||
cons := flv.NewConsumer()
|
||||
run := func() {
|
||||
wr, err := rtmp.DialPublish(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = cons.WriteTo(wr)
|
||||
}
|
||||
|
||||
return cons, run, nil
|
||||
}
|
||||
|
||||
func apiHandle(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "", http.StatusMethodNotAllowed)
|
||||
outputFLV(w, r)
|
||||
} else {
|
||||
inputFLV(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func outputFLV(w http.ResponseWriter, r *http.Request) {
|
||||
src := r.URL.Query().Get("src")
|
||||
stream := streams.Get(src)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
cons := flv.NewConsumer()
|
||||
cons.Type = "HTTP-FLV consumer"
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
h := w.Header()
|
||||
h.Set("Content-Type", "video/x-flv")
|
||||
|
||||
_, _ = cons.WriteTo(w)
|
||||
|
||||
stream.RemoveConsumer(cons)
|
||||
}
|
||||
|
||||
func inputFLV(w http.ResponseWriter, r *http.Request) {
|
||||
dst := r.URL.Query().Get("dst")
|
||||
stream := streams.Get(dst)
|
||||
if stream == nil {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -73,3 +73,25 @@ func Location(url string) (string, error) {
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// TODO: rework
|
||||
|
||||
type ConsumerHandler func(url string) (core.Consumer, func(), error)
|
||||
|
||||
var consumerHandlers = map[string]ConsumerHandler{}
|
||||
|
||||
func HandleConsumerFunc(scheme string, handler ConsumerHandler) {
|
||||
consumerHandlers[scheme] = handler
|
||||
}
|
||||
|
||||
func GetConsumer(url string) (core.Consumer, func(), error) {
|
||||
if i := strings.IndexByte(url, ':'); i > 0 {
|
||||
scheme := url[:i]
|
||||
|
||||
if handler, ok := consumerHandlers[scheme]; ok {
|
||||
return handler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, errors.New("streams: unsupported scheme: " + url)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package streams
|
||||
|
||||
import "time"
|
||||
|
||||
func (s *Stream) Publish(url string) error {
|
||||
cons, run, err := GetConsumer(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = s.AddConsumer(cons); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
run()
|
||||
s.RemoveConsumer(cons)
|
||||
|
||||
// TODO: more smart retry
|
||||
time.Sleep(5 * time.Second)
|
||||
_ = s.Publish(url)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Publish(stream *Stream, destination any) {
|
||||
switch v := destination.(type) {
|
||||
case string:
|
||||
if err := stream.Publish(v); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
case []any:
|
||||
for _, v := range v {
|
||||
Publish(stream, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
@@ -13,18 +14,31 @@ import (
|
||||
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Mod map[string]any `yaml:"streams"`
|
||||
Streams map[string]any `yaml:"streams"`
|
||||
Publish map[string]any `yaml:"publish"`
|
||||
}
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
log = app.GetLogger("streams")
|
||||
|
||||
for name, item := range cfg.Mod {
|
||||
for name, item := range cfg.Streams {
|
||||
streams[name] = NewStream(item)
|
||||
}
|
||||
|
||||
api.HandleFunc("api/streams", streamsHandler)
|
||||
|
||||
if cfg.Publish == nil {
|
||||
return
|
||||
}
|
||||
|
||||
time.AfterFunc(time.Second, func() {
|
||||
for name, dst := range cfg.Publish {
|
||||
if stream := Get(name); stream != nil {
|
||||
Publish(stream, dst)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Get(name string) *Stream {
|
||||
@@ -172,6 +186,10 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
api.ResponseJSON(w, stream)
|
||||
}
|
||||
} else if stream = Get(src); stream != nil {
|
||||
if err := stream.Publish(dst); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -195,9 +195,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"
|
||||
@@ -76,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()
|
||||
}
|
||||
+9
-1
@@ -21,12 +21,20 @@ func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
|
||||
|
||||
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
|
||||
|
||||
if len(packet.Payload) < int(2+headersSize) {
|
||||
return
|
||||
}
|
||||
|
||||
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:]
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-5
@@ -117,9 +117,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 +140,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -32,6 +33,8 @@ func (w *WriteBuffer) Write(p []byte) (n int, err error) {
|
||||
} else if n, err = w.Writer.Write(p); err != nil {
|
||||
w.err = err
|
||||
w.done()
|
||||
} else if f, ok := w.Writer.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
w.mu.Unlock()
|
||||
return
|
||||
|
||||
+131
-322
@@ -2,8 +2,8 @@ package dvrip
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -12,49 +12,29 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/pion/rtp"
|
||||
const (
|
||||
Login = 1000
|
||||
OPMonitorClaim = 1413
|
||||
OPMonitorStart = 1410
|
||||
OPTalkClaim = 1434
|
||||
OPTalkStart = 1430
|
||||
OPTalkData = 1432
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
core.Listener
|
||||
|
||||
uri string
|
||||
conn net.Conn
|
||||
reader *bufio.Reader
|
||||
session uint32
|
||||
seq uint32
|
||||
stream string
|
||||
|
||||
medias []*core.Media
|
||||
receivers []*core.Receiver
|
||||
videoTrack *core.Receiver
|
||||
audioTrack *core.Receiver
|
||||
|
||||
videoTS uint32
|
||||
videoDT uint32
|
||||
audioTS uint32
|
||||
audioSeq uint16
|
||||
|
||||
recv uint32
|
||||
rd io.Reader
|
||||
buf []byte
|
||||
}
|
||||
|
||||
type Response map[string]any
|
||||
|
||||
const Login = uint16(1000)
|
||||
const OPMonitorClaim = uint16(1413)
|
||||
const OPMonitorStart = uint16(1410)
|
||||
|
||||
func NewClient(url string) *Client {
|
||||
return &Client{uri: url}
|
||||
}
|
||||
|
||||
func (c *Client) Dial() (err error) {
|
||||
u, err := url.Parse(c.uri)
|
||||
func (c *Client) Dial(rawURL string) (err error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -69,26 +49,27 @@ func (c *Client) Dial() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
c.reader = bufio.NewReader(c.conn)
|
||||
if query := u.Query(); query.Get("backchannel") != "1" {
|
||||
channel := query.Get("channel")
|
||||
if channel == "" {
|
||||
channel = "0"
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
channel := query.Get("channel")
|
||||
if channel == "" {
|
||||
channel = "0"
|
||||
subtype := query.Get("subtype")
|
||||
switch subtype {
|
||||
case "", "0":
|
||||
subtype = "Main"
|
||||
case "1":
|
||||
subtype = "Extra1"
|
||||
}
|
||||
|
||||
c.stream = fmt.Sprintf(
|
||||
`{"Channel":%s,"CombinMode":"NONE","StreamType":"%s","TransMode":"TCP"}`,
|
||||
channel, subtype,
|
||||
)
|
||||
}
|
||||
|
||||
subtype := query.Get("subtype")
|
||||
switch subtype {
|
||||
case "", "0":
|
||||
subtype = "Main"
|
||||
case "1":
|
||||
subtype = "Extra1"
|
||||
}
|
||||
|
||||
c.stream = fmt.Sprintf(
|
||||
`{"Channel":%s,"CombinMode":"NONE","StreamType":"%s","TransMode":"TCP"}`,
|
||||
channel, subtype,
|
||||
)
|
||||
c.rd = bufio.NewReader(c.conn)
|
||||
|
||||
if u.User != nil {
|
||||
pass, _ := u.User.Password()
|
||||
@@ -98,210 +79,84 @@ func (c *Client) Dial() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Login(user, pass string) (err error) {
|
||||
data := fmt.Sprintf(
|
||||
`{"EncryptType":"MD5","LoginType":"DVRIP-Web","PassWord":"%s","UserName":"%s"}`,
|
||||
SofiaHash(pass), user,
|
||||
)
|
||||
|
||||
if err = c.Request(Login, data); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = c.ResponseJSON()
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Play() (err error) {
|
||||
format := `{"Name":"OPMonitor","SessionID":"0x%08X","OPMonitor":{"Action":"%s","Parameter":%s}}`
|
||||
|
||||
data := fmt.Sprintf(format, c.session, "Claim", c.stream)
|
||||
if err = c.Request(OPMonitorClaim, data); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = c.ResponseJSON(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data = fmt.Sprintf(format, c.session, "Start", c.stream)
|
||||
return c.Request(OPMonitorStart, data)
|
||||
}
|
||||
|
||||
func (c *Client) Handle() error {
|
||||
var buf []byte
|
||||
var size int
|
||||
|
||||
var probe byte
|
||||
if c.medias == nil {
|
||||
probe = 1
|
||||
}
|
||||
|
||||
for {
|
||||
b, err := c.Response()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// collect data from multiple packets
|
||||
if size > 0 {
|
||||
buf = append(buf, b...)
|
||||
if len(buf) < size {
|
||||
continue
|
||||
}
|
||||
if len(buf) > size {
|
||||
return errors.New("wrong size")
|
||||
}
|
||||
b = buf
|
||||
}
|
||||
|
||||
dataType := binary.BigEndian.Uint32(b)
|
||||
switch dataType {
|
||||
case 0x1FC, 0x1FE:
|
||||
size = int(binary.LittleEndian.Uint32(b[12:])) + 16
|
||||
case 0x1FD: // PFrame
|
||||
size = int(binary.LittleEndian.Uint32(b[4:])) + 8
|
||||
case 0x1FA, 0x1F9:
|
||||
size = int(binary.LittleEndian.Uint16(b[6:])) + 8
|
||||
default:
|
||||
return fmt.Errorf("unknown type: %X", dataType)
|
||||
}
|
||||
|
||||
if len(b) < size {
|
||||
buf = b
|
||||
continue // need to collect data from next packets
|
||||
}
|
||||
|
||||
//log.Printf("[DVR] type: %d, len: %d", dataType, len(b))
|
||||
|
||||
switch dataType {
|
||||
case 0x1FC, 0x1FE: // video IFrame
|
||||
payload := annexb.EncodeToAVCC(b[16:], false)
|
||||
|
||||
if c.videoTrack == nil {
|
||||
fps := b[5]
|
||||
//width := uint16(b[6]) * 8
|
||||
//height := uint16(b[7]) * 8
|
||||
//println(width, height)
|
||||
ts := b[8:]
|
||||
|
||||
// the exact value of the start TS does not matter
|
||||
c.videoTS = binary.LittleEndian.Uint32(ts)
|
||||
c.videoDT = 90000 / uint32(fps)
|
||||
|
||||
c.AddVideoTrack(b[4], payload)
|
||||
}
|
||||
|
||||
if c.videoTrack != nil {
|
||||
c.videoTS += c.videoDT
|
||||
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: c.videoTS},
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d, ts: %10d", h265.Types(payload), len(payload), packet.Timestamp)
|
||||
|
||||
c.videoTrack.WriteRTP(packet)
|
||||
}
|
||||
|
||||
case 0x1FD: // PFrame
|
||||
if c.videoTrack != nil {
|
||||
c.videoTS += c.videoDT
|
||||
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: c.videoTS},
|
||||
Payload: annexb.EncodeToAVCC(b[8:], false),
|
||||
}
|
||||
|
||||
//log.Printf("[DVR] %v, len: %d, ts: %10d", h265.Types(packet.Payload), len(packet.Payload), packet.Timestamp)
|
||||
|
||||
c.videoTrack.WriteRTP(packet)
|
||||
}
|
||||
|
||||
case 0x1FA, 0x1F9: // audio
|
||||
if c.audioTrack == nil {
|
||||
// the exact value of the start TS does not matter
|
||||
c.audioTS = c.videoTS
|
||||
|
||||
c.AddAudioTrack(b[4], b[5])
|
||||
}
|
||||
|
||||
if c.audioTrack != nil {
|
||||
for b != nil {
|
||||
payload := b[8:size]
|
||||
if len(b) > size {
|
||||
b = b[size:]
|
||||
} else {
|
||||
b = nil
|
||||
}
|
||||
|
||||
c.audioTS += uint32(len(payload))
|
||||
c.audioSeq++
|
||||
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: true,
|
||||
SequenceNumber: c.audioSeq,
|
||||
Timestamp: c.audioTS,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
//log.Printf("[DVR] len: %d, ts: %10d", len(packet.Payload), packet.Timestamp)
|
||||
|
||||
c.audioTrack.WriteRTP(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if probe != 0 {
|
||||
probe++
|
||||
if (c.videoTS > 0 && c.audioTS > 0) || probe == 20 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
size = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Client) Request(cmd uint16, data string) (err error) {
|
||||
func (c *Client) Login(user, pass string) (err error) {
|
||||
data := fmt.Sprintf(
|
||||
`{"EncryptType":"MD5","LoginType":"DVRIP-Web","PassWord":"%s","UserName":"%s"}`+"\x0A\x00",
|
||||
SofiaHash(pass), user,
|
||||
)
|
||||
|
||||
if _, err = c.WriteCmd(Login, []byte(data)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = c.ReadJSON()
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Play() error {
|
||||
format := `{"Name":"OPMonitor","SessionID":"0x%08X","OPMonitor":{"Action":"%s","Parameter":%s}}` + "\x0A\x00"
|
||||
|
||||
data := fmt.Sprintf(format, c.session, "Claim", c.stream)
|
||||
if _, err := c.WriteCmd(OPMonitorClaim, []byte(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.ReadJSON(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data = fmt.Sprintf(format, c.session, "Start", c.stream)
|
||||
_, err := c.WriteCmd(OPMonitorStart, []byte(data))
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) Talk() error {
|
||||
format := `{"Name":"OPTalk","SessionID":"0x%08X","OPTalk":{"Action":"%s"}}` + "\x0A\x00"
|
||||
|
||||
data := fmt.Sprintf(format, c.session, "Claim")
|
||||
if _, err := c.WriteCmd(OPTalkClaim, []byte(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.ReadJSON(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data = fmt.Sprintf(format, c.session, "Start")
|
||||
_, err := c.WriteCmd(OPTalkStart, []byte(data))
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) WriteCmd(cmd uint16, payload []byte) (n int, err error) {
|
||||
b := make([]byte, 20, 128)
|
||||
b[0] = 255
|
||||
binary.LittleEndian.PutUint32(b[4:], c.session)
|
||||
binary.LittleEndian.PutUint32(b[8:], c.seq)
|
||||
binary.LittleEndian.PutUint16(b[14:], cmd)
|
||||
binary.LittleEndian.PutUint32(b[16:], uint32(len(data))+2)
|
||||
b = append(b, data...)
|
||||
b = append(b, 0x0A, 0x00)
|
||||
binary.LittleEndian.PutUint32(b[16:], uint32(len(payload)))
|
||||
b = append(b, payload...)
|
||||
|
||||
c.seq++
|
||||
|
||||
if err = c.conn.SetWriteDeadline(time.Now().Add(time.Second * 5)); err != nil {
|
||||
return
|
||||
return 0, err
|
||||
}
|
||||
|
||||
_, err = c.conn.Write(b)
|
||||
return
|
||||
return c.conn.Write(b)
|
||||
}
|
||||
|
||||
func (c *Client) Response() (b []byte, err error) {
|
||||
func (c *Client) ReadChunk() (b []byte, err error) {
|
||||
if err = c.conn.SetReadDeadline(time.Now().Add(time.Second * 5)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b = make([]byte, 20)
|
||||
if _, err = io.ReadFull(c.reader, b); err != nil {
|
||||
if _, err = io.ReadFull(c.rd, b); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.recv += 20
|
||||
|
||||
if b[0] != 255 {
|
||||
return nil, errors.New("read error")
|
||||
}
|
||||
@@ -310,17 +165,59 @@ func (c *Client) Response() (b []byte, err error) {
|
||||
size := binary.LittleEndian.Uint32(b[16:])
|
||||
|
||||
b = make([]byte, size)
|
||||
if _, err = io.ReadFull(c.reader, b); err != nil {
|
||||
if _, err = io.ReadFull(c.rd, b); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.recv += size
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) ResponseJSON() (res Response, err error) {
|
||||
b, err := c.Response()
|
||||
func (c *Client) ReadPacket() (pType byte, payload []byte, err error) {
|
||||
var b []byte
|
||||
|
||||
// many cameras may split packet to multiple chunks
|
||||
// some rare cameras may put multiple packets to single chunk
|
||||
for len(c.buf) < 16 {
|
||||
if b, err = c.ReadChunk(); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
c.buf = append(c.buf, b...)
|
||||
}
|
||||
|
||||
if !bytes.HasPrefix(c.buf, []byte{0, 0, 1}) {
|
||||
return 0, nil, fmt.Errorf("dvrip: wrong packet: %0.16x", c.buf)
|
||||
}
|
||||
|
||||
var size int
|
||||
|
||||
switch pType = c.buf[3]; pType {
|
||||
case 0xFC, 0xFE:
|
||||
size = int(binary.LittleEndian.Uint32(c.buf[12:])) + 16
|
||||
case 0xFD: // PFrame
|
||||
size = int(binary.LittleEndian.Uint32(c.buf[4:])) + 8
|
||||
case 0xFA, 0xF9:
|
||||
size = int(binary.LittleEndian.Uint16(c.buf[6:])) + 8
|
||||
default:
|
||||
return 0, nil, fmt.Errorf("dvrip: unknown packet type: %X", pType)
|
||||
}
|
||||
|
||||
for len(c.buf) < size {
|
||||
if b, err = c.ReadChunk(); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
c.buf = append(c.buf, b...)
|
||||
}
|
||||
|
||||
payload = c.buf[:size]
|
||||
c.buf = c.buf[size:]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type Response map[string]any
|
||||
|
||||
func (c *Client) ReadJSON() (res Response, err error) {
|
||||
b, err := c.ReadChunk()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -336,94 +233,6 @@ func (c *Client) ResponseJSON() (res Response, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) AddVideoTrack(mediaCode byte, payload []byte) {
|
||||
var codec *core.Codec
|
||||
switch mediaCode {
|
||||
case 0x02, 0x12:
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecH264,
|
||||
ClockRate: 90000,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
FmtpLine: h264.GetFmtpLine(payload),
|
||||
}
|
||||
|
||||
case 0x03, 0x13, 0x43, 0x53:
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecH265,
|
||||
ClockRate: 90000,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
FmtpLine: "profile-id=1",
|
||||
}
|
||||
|
||||
for {
|
||||
size := 4 + int(binary.BigEndian.Uint32(payload))
|
||||
|
||||
switch h265.NALUType(payload) {
|
||||
case h265.NALUTypeVPS:
|
||||
codec.FmtpLine += ";sprop-vps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
||||
case h265.NALUTypeSPS:
|
||||
codec.FmtpLine += ";sprop-sps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
||||
case h265.NALUTypePPS:
|
||||
codec.FmtpLine += ";sprop-pps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
||||
}
|
||||
|
||||
if size < len(payload) {
|
||||
payload = payload[size:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
println("[DVRIP] unsupported video codec:", mediaCode)
|
||||
return
|
||||
}
|
||||
|
||||
media := &core.Media{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.medias = append(c.medias, media)
|
||||
|
||||
c.videoTrack = core.NewReceiver(media, codec)
|
||||
c.receivers = append(c.receivers, c.videoTrack)
|
||||
}
|
||||
|
||||
var sampleRates = []uint32{4000, 8000, 11025, 16000, 20000, 22050, 32000, 44100, 48000}
|
||||
|
||||
func (c *Client) AddAudioTrack(mediaCode byte, sampleRate byte) {
|
||||
// https://github.com/vigoss30611/buildroot-ltc/blob/master/system/qm/ipc/ProtocolService/src/ZhiNuo/inc/zn_dh_base_type.h
|
||||
// PCM8 = 7, G729, IMA_ADPCM, G711U, G721, PCM8_VWIS, MS_ADPCM, G711A, PCM16
|
||||
var codec *core.Codec
|
||||
switch mediaCode {
|
||||
case 10: // G711U
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecPCMU,
|
||||
}
|
||||
case 14: // G711A
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecPCMA,
|
||||
}
|
||||
default:
|
||||
println("[DVRIP] unsupported audio codec:", mediaCode)
|
||||
return
|
||||
}
|
||||
|
||||
if sampleRate <= byte(len(sampleRates)) {
|
||||
codec.ClockRate = sampleRates[sampleRate-1]
|
||||
}
|
||||
|
||||
media := &core.Media{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.medias = append(c.medias, media)
|
||||
|
||||
c.audioTrack = core.NewReceiver(media, codec)
|
||||
c.receivers = append(c.receivers, c.audioTrack)
|
||||
}
|
||||
|
||||
func SofiaHash(password string) string {
|
||||
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package dvrip
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
core.SuperConsumer
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (c *Consumer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Consumer) Start() error {
|
||||
if err := c.client.conn.SetReadDeadline(time.Time{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b := make([]byte, 4096)
|
||||
for {
|
||||
if _, err := c.client.rd.Read(b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) Stop() error {
|
||||
_ = c.SuperConsumer.Close()
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
if err := c.client.Talk(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const PacketSize = 320
|
||||
|
||||
buf := make([]byte, 8+PacketSize)
|
||||
binary.BigEndian.PutUint32(buf, 0x1FA)
|
||||
|
||||
switch track.Codec.Name {
|
||||
case core.CodecPCMU:
|
||||
buf[4] = 10
|
||||
case core.CodecPCMA:
|
||||
buf[4] = 14
|
||||
}
|
||||
|
||||
//for i, rate := range sampleRates {
|
||||
// if rate == track.Codec.ClockRate {
|
||||
// buf[5] = byte(i) + 1
|
||||
// break
|
||||
// }
|
||||
//}
|
||||
buf[5] = 2 // ClockRate=8000
|
||||
|
||||
binary.LittleEndian.PutUint16(buf[6:], PacketSize)
|
||||
|
||||
var payload []byte
|
||||
|
||||
sender := core.NewSender(media, track.Codec)
|
||||
sender.Handler = func(packet *rtp.Packet) {
|
||||
payload = append(payload, packet.Payload...)
|
||||
|
||||
for len(payload) >= PacketSize {
|
||||
buf = append(buf[:8], payload[:PacketSize]...)
|
||||
if n, err := c.client.WriteCmd(OPTalkData, buf); err != nil {
|
||||
c.Send += n
|
||||
}
|
||||
|
||||
payload = payload[PacketSize:]
|
||||
}
|
||||
}
|
||||
|
||||
sender.HandleRTP(track)
|
||||
c.Senders = append(c.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package dvrip
|
||||
|
||||
import "github.com/AlexxIT/go2rtc/pkg/core"
|
||||
|
||||
func Dial(url string) (core.Producer, error) {
|
||||
client := &Client{}
|
||||
if err := client.Dial(url); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if client.stream != "" {
|
||||
prod := &Producer{client: client}
|
||||
prod.Type = "DVRIP active producer"
|
||||
if err := prod.probe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return prod, nil
|
||||
} else {
|
||||
cons := &Consumer{client: client}
|
||||
cons.Type = "DVRIP active consumer"
|
||||
cons.Medias = []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},
|
||||
{Name: core.CodecPCMU, ClockRate: 8000, PayloadType: 0},
|
||||
},
|
||||
},
|
||||
}
|
||||
return cons, nil
|
||||
}
|
||||
}
|
||||
+247
-22
@@ -1,41 +1,266 @@
|
||||
package dvrip
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func (c *Client) GetMedias() []*core.Media {
|
||||
return c.medias
|
||||
type Producer struct {
|
||||
core.SuperProducer
|
||||
|
||||
client *Client
|
||||
|
||||
video, audio *core.Receiver
|
||||
|
||||
videoTS uint32
|
||||
videoDT uint32
|
||||
audioTS uint32
|
||||
audioSeq uint16
|
||||
}
|
||||
|
||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
for _, track := range c.receivers {
|
||||
if track.Codec == codec {
|
||||
return track, nil
|
||||
func (c *Producer) Start() error {
|
||||
for {
|
||||
pType, b, err := c.client.ReadPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//log.Printf("[DVR] type: %d, len: %d", dataType, len(b))
|
||||
|
||||
switch pType {
|
||||
case 0xFC, 0xFE, 0xFD:
|
||||
if c.video == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var payload []byte
|
||||
if pType != 0xFD {
|
||||
payload = b[16:] // iframe
|
||||
} else {
|
||||
payload = b[8:] // pframe
|
||||
}
|
||||
|
||||
c.videoTS += c.videoDT
|
||||
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: c.videoTS},
|
||||
Payload: annexb.EncodeToAVCC(payload, false),
|
||||
}
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d, ts: %10d", h265.Types(payload), len(payload), packet.Timestamp)
|
||||
|
||||
c.video.WriteRTP(packet)
|
||||
|
||||
case 0xFA: // audio
|
||||
if c.audio == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
payload := b[8:]
|
||||
|
||||
c.audioTS += uint32(len(payload))
|
||||
c.audioSeq++
|
||||
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: true,
|
||||
SequenceNumber: c.audioSeq,
|
||||
Timestamp: c.audioTS,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
//log.Printf("[DVR] len: %d, ts: %10d", len(packet.Payload), packet.Timestamp)
|
||||
|
||||
c.audio.WriteRTP(packet)
|
||||
|
||||
case 0xF9: // unknown
|
||||
|
||||
default:
|
||||
println(fmt.Sprintf("dvrip: unknown packet type: %d", pType))
|
||||
}
|
||||
}
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
return c.Handle()
|
||||
func (c *Producer) Stop() error {
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
for _, receiver := range c.receivers {
|
||||
receiver.Close()
|
||||
func (c *Producer) probe() error {
|
||||
if err := c.client.Play(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rd := core.NewReadBuffer(c.client.rd)
|
||||
rd.BufferSize = core.ProbeSize
|
||||
defer func() {
|
||||
c.client.buf = nil
|
||||
rd.Reset()
|
||||
}()
|
||||
|
||||
c.client.rd = rd
|
||||
|
||||
// some awful cameras has VERY rare keyframes
|
||||
// so we wait video+audio for default probe time
|
||||
// and wait anything for 15 seconds
|
||||
timeoutBoth := time.Now().Add(core.ProbeTimeout)
|
||||
timeoutAny := time.Now().Add(time.Second * 15)
|
||||
|
||||
for {
|
||||
if now := time.Now(); now.Before(timeoutBoth) {
|
||||
if c.video != nil && c.audio != nil {
|
||||
return nil
|
||||
}
|
||||
} else if now.Before(timeoutAny) {
|
||||
if c.video != nil || c.audio != nil {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return errors.New("dvrip: can't probe medias")
|
||||
}
|
||||
|
||||
tag, b, err := c.client.ReadPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tag {
|
||||
case 0xFC, 0xFE: // video
|
||||
if c.video != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fps := b[5]
|
||||
//width := uint16(b[6]) * 8
|
||||
//height := uint16(b[7]) * 8
|
||||
//println(width, height)
|
||||
ts := b[8:]
|
||||
|
||||
// the exact value of the start TS does not matter
|
||||
c.videoTS = binary.LittleEndian.Uint32(ts)
|
||||
c.videoDT = 90000 / uint32(fps)
|
||||
|
||||
payload := annexb.EncodeToAVCC(b[16:], false)
|
||||
c.addVideoTrack(b[4], payload)
|
||||
|
||||
case 0xFA: // audio
|
||||
if c.audio != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// the exact value of the start TS does not matter
|
||||
c.audioTS = c.videoTS
|
||||
|
||||
c.addAudioTrack(b[4], b[5])
|
||||
}
|
||||
}
|
||||
return c.Close()
|
||||
}
|
||||
|
||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
info := &core.Info{
|
||||
Type: "DVRIP active producer",
|
||||
RemoteAddr: c.conn.RemoteAddr().String(),
|
||||
Medias: c.medias,
|
||||
Receivers: c.receivers,
|
||||
Recv: int(c.recv),
|
||||
func (c *Producer) addVideoTrack(mediaCode byte, payload []byte) {
|
||||
var codec *core.Codec
|
||||
switch mediaCode {
|
||||
case 0x02, 0x12:
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecH264,
|
||||
ClockRate: 90000,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
FmtpLine: h264.GetFmtpLine(payload),
|
||||
}
|
||||
|
||||
case 0x03, 0x13, 0x43, 0x53:
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecH265,
|
||||
ClockRate: 90000,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
FmtpLine: "profile-id=1",
|
||||
}
|
||||
|
||||
for {
|
||||
size := 4 + int(binary.BigEndian.Uint32(payload))
|
||||
|
||||
switch h265.NALUType(payload) {
|
||||
case h265.NALUTypeVPS:
|
||||
codec.FmtpLine += ";sprop-vps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
||||
case h265.NALUTypeSPS:
|
||||
codec.FmtpLine += ";sprop-sps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
||||
case h265.NALUTypePPS:
|
||||
codec.FmtpLine += ";sprop-pps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
||||
}
|
||||
|
||||
if size < len(payload) {
|
||||
payload = payload[size:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
println("[DVRIP] unsupported video codec:", mediaCode)
|
||||
return
|
||||
}
|
||||
return json.Marshal(info)
|
||||
|
||||
media := &core.Media{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.Medias = append(c.Medias, media)
|
||||
|
||||
c.video = core.NewReceiver(media, codec)
|
||||
c.Receivers = append(c.Receivers, c.video)
|
||||
}
|
||||
|
||||
var sampleRates = []uint32{4000, 8000, 11025, 16000, 20000, 22050, 32000, 44100, 48000}
|
||||
|
||||
func (c *Producer) addAudioTrack(mediaCode byte, sampleRate byte) {
|
||||
// https://github.com/vigoss30611/buildroot-ltc/blob/master/system/qm/ipc/ProtocolService/src/ZhiNuo/inc/zn_dh_base_type.h
|
||||
// PCM8 = 7, G729, IMA_ADPCM, G711U, G721, PCM8_VWIS, MS_ADPCM, G711A, PCM16
|
||||
var codec *core.Codec
|
||||
switch mediaCode {
|
||||
case 10: // G711U
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecPCMU,
|
||||
}
|
||||
case 14: // G711A
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecPCMA,
|
||||
}
|
||||
default:
|
||||
println("[DVRIP] unsupported audio codec:", mediaCode)
|
||||
return
|
||||
}
|
||||
|
||||
if sampleRate <= byte(len(sampleRates)) {
|
||||
codec.ClockRate = sampleRates[sampleRate-1]
|
||||
}
|
||||
|
||||
media := &core.Media{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.Medias = append(c.Medias, media)
|
||||
|
||||
c.audio = core.NewReceiver(media, codec)
|
||||
c.Receivers = append(c.Receivers, c.audio)
|
||||
}
|
||||
|
||||
//func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
// info := &core.Info{
|
||||
// Type: "DVRIP active producer",
|
||||
// RemoteAddr: c.conn.RemoteAddr().String(),
|
||||
// Medias: c.Medias,
|
||||
// Receivers: c.Receivers,
|
||||
// Recv: c.Recv,
|
||||
// }
|
||||
// return json.Marshal(info)
|
||||
//}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/antonmedv/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)
|
||||
}
|
||||
+41
-2
@@ -60,6 +60,9 @@ func (a *AMF) ReadItem() (any, error) {
|
||||
case TypeObject:
|
||||
return a.ReadObject()
|
||||
|
||||
case TypeEcmaArray:
|
||||
return a.ReadEcmaArray()
|
||||
|
||||
case TypeNull:
|
||||
return nil, nil
|
||||
|
||||
@@ -174,7 +177,18 @@ func (a *AMF) WriteString(s string) {
|
||||
|
||||
func (a *AMF) WriteObject(obj map[string]any) {
|
||||
a.buf = append(a.buf, TypeObject)
|
||||
a.writeKV(obj)
|
||||
a.buf = append(a.buf, 0, 0, TypeObjectEnd)
|
||||
}
|
||||
|
||||
func (a *AMF) WriteEcmaArray(obj map[string]any) {
|
||||
n := len(obj)
|
||||
a.buf = append(a.buf, TypeEcmaArray, byte(n>>24), byte(n>>16), byte(n>>8), byte(n))
|
||||
a.writeKV(obj)
|
||||
a.buf = append(a.buf, 0, 0, TypeObjectEnd)
|
||||
}
|
||||
|
||||
func (a *AMF) writeKV(obj map[string]any) {
|
||||
for k, v := range obj {
|
||||
n := len(k)
|
||||
a.buf = append(a.buf, byte(n>>8), byte(n))
|
||||
@@ -185,16 +199,41 @@ func (a *AMF) WriteObject(obj map[string]any) {
|
||||
a.WriteString(v)
|
||||
case int:
|
||||
a.WriteNumber(float64(v))
|
||||
case uint16:
|
||||
a.WriteNumber(float64(v))
|
||||
case uint32:
|
||||
a.WriteNumber(float64(v))
|
||||
case float64:
|
||||
a.WriteNumber(v)
|
||||
case bool:
|
||||
a.WriteBool(v)
|
||||
default:
|
||||
panic(v)
|
||||
}
|
||||
}
|
||||
|
||||
a.buf = append(a.buf, 0, 0, TypeObjectEnd)
|
||||
}
|
||||
|
||||
func (a *AMF) WriteNull() {
|
||||
a.buf = append(a.buf, TypeNull)
|
||||
}
|
||||
|
||||
func EncodeItems(items ...any) []byte {
|
||||
a := &AMF{}
|
||||
for _, item := range items {
|
||||
switch v := item.(type) {
|
||||
case float64:
|
||||
a.WriteNumber(v)
|
||||
case int:
|
||||
a.WriteNumber(float64(v))
|
||||
case string:
|
||||
a.WriteString(v)
|
||||
case map[string]any:
|
||||
a.WriteObject(v)
|
||||
case nil:
|
||||
a.WriteNull()
|
||||
default:
|
||||
panic(v)
|
||||
}
|
||||
}
|
||||
return a.Bytes()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
package amf
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewReader(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
actual string
|
||||
expect []any
|
||||
}{
|
||||
{
|
||||
name: "ffmpeg-http",
|
||||
actual: "02000a6f6e4d65746144617461080000001000086475726174696f6e000000000000000000000577696474680040940000000000000006686569676874004086800000000000000d766964656f646174617261746500409e62770000000000096672616d6572617465004038000000000000000c766964656f636f646563696400401c000000000000000d617564696f646174617261746500405ea93000000000000f617564696f73616d706c65726174650040e5888000000000000f617564696f73616d706c6573697a65004030000000000000000673746572656f0101000c617564696f636f6465636964004024000000000000000b6d616a6f725f6272616e640200046d703432000d6d696e6f725f76657273696f6e020001300011636f6d70617469626c655f6272616e647302000c69736f6d617663316d7034320007656e636f64657202000c4c61766636302e352e313030000866696c6573697a65000000000000000000000009",
|
||||
expect: []any{
|
||||
"onMetaData",
|
||||
map[string]any{
|
||||
"compatible_brands": "isomavc1mp42",
|
||||
"major_brand": "mp42",
|
||||
"minor_version": "0",
|
||||
"encoder": "Lavf60.5.100",
|
||||
|
||||
"filesize": float64(0),
|
||||
"duration": float64(0),
|
||||
|
||||
"videocodecid": float64(7),
|
||||
"width": float64(1280),
|
||||
"height": float64(720),
|
||||
"framerate": float64(24),
|
||||
"videodatarate": 1944.6162109375,
|
||||
|
||||
"audiocodecid": float64(10),
|
||||
"audiosamplerate": float64(44100),
|
||||
"stereo": true,
|
||||
"audiosamplesize": float64(16),
|
||||
"audiodatarate": 122.6435546875,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ffmpeg-file",
|
||||
actual: "02000a6f6e4d65746144617461080000000800086475726174696f6e004000000000000000000577696474680040940000000000000006686569676874004086800000000000000d766964656f646174617261746500000000000000000000096672616d6572617465004039000000000000000c766964656f636f646563696400401c0000000000000007656e636f64657202000c4c61766636302e352e313030000866696c6573697a6500411f541400000000000009",
|
||||
expect: []any{
|
||||
"onMetaData",
|
||||
map[string]any{
|
||||
"encoder": "Lavf60.5.100",
|
||||
|
||||
"filesize": float64(513285),
|
||||
"duration": float64(2),
|
||||
|
||||
"videocodecid": float64(7),
|
||||
"width": float64(1280),
|
||||
"height": float64(720),
|
||||
"framerate": float64(25),
|
||||
"videodatarate": float64(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reolink-1",
|
||||
actual: "0200075f726573756c74003ff0000000000000030006666d7356657202000d464d532f332c302c312c313233000c6361706162696c697469657300403f0000000000000000090300056c6576656c0200067374617475730004636f646502001d4e6574436f6e6e656374696f6e2e436f6e6e6563742e53756363657373000b6465736372697074696f6e020015436f6e6e656374696f6e207375636365656465642e000e6f626a656374456e636f64696e67000000000000000000000009",
|
||||
expect: []any{
|
||||
"_result", float64(1),
|
||||
map[string]any{
|
||||
"capabilities": float64(31),
|
||||
"fmsVer": "FMS/3,0,1,123",
|
||||
},
|
||||
map[string]any{
|
||||
"code": "NetConnection.Connect.Success",
|
||||
"description": "Connection succeeded.",
|
||||
"level": "status",
|
||||
"objectEncoding": float64(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reolink-2",
|
||||
actual: "0200075f726573756c7400400000000000000005003ff0000000000000",
|
||||
expect: []any{
|
||||
"_result", float64(2), nil, float64(1),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reolink-3",
|
||||
actual: "0200086f6e537461747573000000000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e506c61792e5374617274000b6465736372697074696f6e020015537461727420766964656f206f6e2064656d616e64000009",
|
||||
expect: []any{
|
||||
"onStatus", float64(0), nil,
|
||||
map[string]any{
|
||||
"code": "NetStream.Play.Start",
|
||||
"description": "Start video on demand",
|
||||
"level": "status",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reolink-4",
|
||||
actual: "0200117c52746d7053616d706c6541636365737301010101",
|
||||
expect: []any{
|
||||
"|RtmpSampleAccess", true, true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reolink-5",
|
||||
actual: "02000a6f6e4d6574614461746103000577696474680040a4000000000000000668656967687400409e000000000000000c646973706c617957696474680040a4000000000000000d646973706c617948656967687400409e00000000000000086475726174696f6e000000000000000000000c766964656f636f646563696400401c000000000000000c617564696f636f6465636964004024000000000000000f617564696f73616d706c65726174650040cf40000000000000096672616d657261746500403e000000000000000009",
|
||||
expect: []any{
|
||||
"onMetaData",
|
||||
map[string]any{
|
||||
"duration": float64(0),
|
||||
|
||||
"videocodecid": float64(7),
|
||||
"width": float64(2560),
|
||||
"height": float64(1920),
|
||||
"displayWidth": float64(2560),
|
||||
"displayHeight": float64(1920),
|
||||
"framerate": float64(30),
|
||||
|
||||
"audiocodecid": float64(10),
|
||||
"audiosamplerate": float64(16000),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mediamtx",
|
||||
actual: "02000d40736574446174614672616d6502000a6f6e4d6574614461746103000d766964656f6461746172617465000000000000000000000c766964656f636f646563696400401c000000000000000d617564696f6461746172617465000000000000000000000c617564696f636f6465636964004024000000000000000009",
|
||||
expect: []any{
|
||||
"@setDataFrame",
|
||||
"onMetaData",
|
||||
map[string]any{
|
||||
"videocodecid": float64(7),
|
||||
"videodatarate": float64(0),
|
||||
"audiocodecid": float64(10),
|
||||
"audiodatarate": float64(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "obs-connect",
|
||||
actual: "020007636f6e6e656374003ff000000000000003000361707002000c617070312f73747265616d3100047479706502000a6e6f6e70726976617465000e737570706f727473476f4177617901010008666c61736856657202001f464d4c452f332e302028636f6d70617469626c653b20464d53632f312e3029000673776655726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d310005746355726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d31000009",
|
||||
expect: []any{
|
||||
"connect", 1,
|
||||
map[string]any{
|
||||
"app": "app1/stream1",
|
||||
"flashVer": "FMLE/3.0 (compatible; FMSc/1.0)",
|
||||
"supportsGoAway": true,
|
||||
"swfUrl": "rtmp://192.168.10.101/app1/stream1",
|
||||
"tcUrl": "rtmp://192.168.10.101/app1/stream1",
|
||||
"type": "nonprivate",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "obs-key",
|
||||
actual: "02000d72656c6561736553747265616d004000000000000000050200046b657931",
|
||||
expect: []any{
|
||||
"releaseStream", float64(2), nil, "key1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "obs",
|
||||
actual: "02000d40736574446174614672616d6502000a6f6e4d65746144617461080000001400086475726174696f6e000000000000000000000866696c6553697a65000000000000000000000577696474680040840000000000000006686569676874004076800000000000000c766964656f636f646563696400401c000000000000000d766964656f64617461726174650040a388000000000000096672616d6572617465004039000000000000000c617564696f636f6465636964004024000000000000000d617564696f6461746172617465004064000000000000000f617564696f73616d706c65726174650040e5888000000000000f617564696f73616d706c6573697a65004030000000000000000d617564696f6368616e6e656c73004000000000000000000673746572656f01010003322e3101000003332e3101000003342e3001000003342e3101000003352e3101000003372e3101000007656e636f6465720200376f62732d6f7574707574206d6f64756c6520286c69626f62732076657273696f6e2032392e302e302d36322d6739303031323131663829000009",
|
||||
expect: []any{
|
||||
"@setDataFrame", "onMetaData", map[string]any{
|
||||
"2.1": false,
|
||||
"3.1": false,
|
||||
"4.0": false,
|
||||
"4.1": false,
|
||||
"5.1": false,
|
||||
"7.1": false,
|
||||
"audiochannels": float64(2),
|
||||
"audiocodecid": float64(10),
|
||||
"audiodatarate": float64(160),
|
||||
"audiosamplerate": float64(44100),
|
||||
"audiosamplesize": float64(16),
|
||||
"duration": float64(0),
|
||||
"encoder": "obs-output module (libobs version 29.0.0-62-g9001211f8)",
|
||||
"fileSize": float64(0),
|
||||
"framerate": float64(25),
|
||||
"height": float64(360),
|
||||
"stereo": true,
|
||||
"videocodecid": float64(7),
|
||||
"videodatarate": float64(2500),
|
||||
"width": float64(640),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "telegram-2",
|
||||
actual: "0200075f726573756c7400400000000000000005",
|
||||
expect: []any{
|
||||
"_result", float64(2), nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "telegram-4",
|
||||
actual: "0200075f726573756c7400401000000000000005003ff0000000000000",
|
||||
expect: []any{
|
||||
"_result", float64(4), nil, float64(1),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
b, err := hex.DecodeString(test.actual)
|
||||
require.Nil(t, err)
|
||||
|
||||
rd := NewReader(b)
|
||||
v, err := rd.ReadItems()
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, test.expect, v)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package flv
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
core.SuperConsumer
|
||||
wr *core.WriteBuffer
|
||||
muxer *Muxer
|
||||
}
|
||||
|
||||
func NewConsumer() *Consumer {
|
||||
c := &Consumer{
|
||||
wr: core.NewWriteBuffer(nil),
|
||||
muxer: &Muxer{},
|
||||
}
|
||||
c.Medias = []*core.Media{
|
||||
{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecH264},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecAAC},
|
||||
},
|
||||
},
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
sender := core.NewSender(media, track.Codec)
|
||||
|
||||
switch track.Codec.Name {
|
||||
case core.CodecH264:
|
||||
payload := c.muxer.GetPayloader(track.Codec)
|
||||
|
||||
sender.Handler = func(pkt *rtp.Packet) {
|
||||
b := payload(pkt)
|
||||
if n, err := c.wr.Write(b); err == nil {
|
||||
c.Send += n
|
||||
}
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
sender.Handler = h264.RTPDepay(track.Codec, sender.Handler)
|
||||
} else {
|
||||
sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)
|
||||
}
|
||||
|
||||
case core.CodecAAC:
|
||||
payload := c.muxer.GetPayloader(track.Codec)
|
||||
|
||||
sender.Handler = func(pkt *rtp.Packet) {
|
||||
b := payload(pkt)
|
||||
if n, err := c.wr.Write(b); err == nil {
|
||||
c.Send += n
|
||||
}
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
sender.Handler = aac.RTPDepay(sender.Handler)
|
||||
}
|
||||
}
|
||||
|
||||
sender.HandleRTP(track)
|
||||
c.Senders = append(c.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
|
||||
b := c.muxer.GetInit()
|
||||
if _, err := wr.Write(b); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return c.wr.WriteTo(wr)
|
||||
}
|
||||
|
||||
func (c *Consumer) Stop() error {
|
||||
_ = c.SuperConsumer.Close()
|
||||
return c.wr.Close()
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package flv
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Muxer struct {
|
||||
codecs []*core.Codec
|
||||
}
|
||||
|
||||
const (
|
||||
FlagsVideo = 0b001
|
||||
FlagsAudio = 0b100
|
||||
)
|
||||
|
||||
func (m *Muxer) GetInit() []byte {
|
||||
b := []byte{
|
||||
'F', 'L', 'V', // signature
|
||||
1, // version
|
||||
0, // flags (has video/audio)
|
||||
0, 0, 0, 9, // header size
|
||||
0, 0, 0, 0, // tag 0 size
|
||||
}
|
||||
|
||||
obj := map[string]any{}
|
||||
|
||||
for _, codec := range m.codecs {
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
b[4] |= FlagsVideo
|
||||
obj["videocodecid"] = CodecAVC
|
||||
|
||||
case core.CodecAAC:
|
||||
b[4] |= FlagsAudio
|
||||
obj["audiocodecid"] = CodecAAC
|
||||
obj["audiosamplerate"] = codec.ClockRate
|
||||
obj["audiosamplesize"] = 16
|
||||
obj["stereo"] = codec.Channels == 2
|
||||
}
|
||||
}
|
||||
|
||||
data := amf.EncodeItems("@setDataFrame", "onMetaData", obj)
|
||||
b = append(b, EncodeTag(TagData, 0, data)...)
|
||||
|
||||
for _, codec := range m.codecs {
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||
if len(sps) == 0 {
|
||||
sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
|
||||
}
|
||||
if len(pps) == 0 {
|
||||
pps = []byte{0x68, 0xce, 0x38, 0x80}
|
||||
}
|
||||
|
||||
config := h264.EncodeConfig(sps, pps)
|
||||
video := append(encodeAVData(codec, 0), config...)
|
||||
b = append(b, EncodeTag(TagVideo, 0, video)...)
|
||||
|
||||
case core.CodecAAC:
|
||||
s := core.Between(codec.FmtpLine, "config=", ";")
|
||||
config, _ := hex.DecodeString(s)
|
||||
audio := append(encodeAVData(codec, 0), config...)
|
||||
b = append(b, EncodeTag(TagAudio, 0, audio)...)
|
||||
}
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (m *Muxer) GetPayloader(codec *core.Codec) func(packet *rtp.Packet) []byte {
|
||||
m.codecs = append(m.codecs, codec)
|
||||
|
||||
var ts0 uint32
|
||||
var k = codec.ClockRate / 1000
|
||||
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
buf := encodeAVData(codec, 1)
|
||||
|
||||
return func(packet *rtp.Packet) []byte {
|
||||
if h264.IsKeyframe(packet.Payload) {
|
||||
buf[0] = 1<<4 | 7
|
||||
} else {
|
||||
buf[0] = 2<<4 | 7
|
||||
}
|
||||
|
||||
buf = append(buf[:5], packet.Payload...) // reset buffer to previous place
|
||||
|
||||
if ts0 == 0 {
|
||||
ts0 = packet.Timestamp
|
||||
}
|
||||
|
||||
timeMS := (packet.Timestamp - ts0) / k
|
||||
return EncodeTag(TagVideo, timeMS, buf)
|
||||
}
|
||||
|
||||
case core.CodecAAC:
|
||||
buf := encodeAVData(codec, 1)
|
||||
|
||||
return func(packet *rtp.Packet) []byte {
|
||||
buf = append(buf[:2], packet.Payload...)
|
||||
|
||||
if ts0 == 0 {
|
||||
ts0 = packet.Timestamp
|
||||
}
|
||||
|
||||
timeMS := (packet.Timestamp - ts0) / k
|
||||
return EncodeTag(TagAudio, timeMS, buf)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func EncodeTag(tagType byte, timeMS uint32, payload []byte) []byte {
|
||||
payloadSize := uint32(len(payload))
|
||||
tagSize := payloadSize + 11
|
||||
|
||||
b := make([]byte, tagSize+4)
|
||||
b[0] = tagType
|
||||
b[1] = byte(payloadSize >> 16)
|
||||
b[2] = byte(payloadSize >> 8)
|
||||
b[3] = byte(payloadSize)
|
||||
b[4] = byte(timeMS >> 16)
|
||||
b[5] = byte(timeMS >> 8)
|
||||
b[6] = byte(timeMS)
|
||||
b[7] = byte(timeMS >> 24)
|
||||
copy(b[11:], payload)
|
||||
|
||||
binary.BigEndian.PutUint32(b[tagSize:], tagSize)
|
||||
return b
|
||||
}
|
||||
|
||||
func encodeAVData(codec *core.Codec, isFrame byte) []byte {
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
return []byte{
|
||||
1<<4 | 7, // keyframe + AVC
|
||||
isFrame, // 0 - config, 1 - frame
|
||||
0, 0, 0, // composition time = 0
|
||||
}
|
||||
|
||||
case core.CodecAAC:
|
||||
var b0 byte = 10 << 4 // AAC
|
||||
|
||||
switch codec.ClockRate {
|
||||
case 11025:
|
||||
b0 |= 1 << 2
|
||||
case 22050:
|
||||
b0 |= 2 << 2
|
||||
case 44100:
|
||||
b0 |= 3 << 2
|
||||
}
|
||||
|
||||
b0 |= 1 << 1 // 16 bits
|
||||
|
||||
if codec.Channels == 2 {
|
||||
b0 |= 1
|
||||
}
|
||||
|
||||
return []byte{b0, isFrame} // 0 - config, 1 - frame
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+4
-1
@@ -170,7 +170,10 @@ func (c *Producer) probe() error {
|
||||
if !bytes.Contains(pkt.Payload, []byte("onMetaData")) {
|
||||
waitType = append(waitType, TagData)
|
||||
}
|
||||
if bytes.Contains(pkt.Payload, []byte("videocodecid")) {
|
||||
// Dahua cameras doesn't send videocodecid
|
||||
if bytes.Contains(pkt.Payload, []byte("videocodecid")) ||
|
||||
bytes.Contains(pkt.Payload, []byte("width")) ||
|
||||
bytes.Contains(pkt.Payload, []byte("framerate")) {
|
||||
waitType = append(waitType, TagVideo)
|
||||
}
|
||||
if bytes.Contains(pkt.Payload, []byte("audiocodecid")) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -83,3 +84,12 @@ func TestGetProfileLevelID(t *testing.T) {
|
||||
profile = GetProfileLevelID(s)
|
||||
require.Equal(t, "640029", profile)
|
||||
}
|
||||
|
||||
func TestDecodeSPS2(t *testing.T) {
|
||||
s := "6764001fad84010c20086100430802184010c200843b50740932"
|
||||
b, err := hex.DecodeString(s)
|
||||
require.Nil(t, err)
|
||||
|
||||
sps := DecodeSPS(b)
|
||||
assert.Nil(t, sps) // broken SPS?
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+23
-3
@@ -115,9 +115,14 @@ func DecodeSPS(sps []byte) *SPS {
|
||||
s.seq_scaling_matrix_present_flag = r.ReadBit()
|
||||
if s.seq_scaling_matrix_present_flag != 0 {
|
||||
for i := byte(0); i < n; i++ {
|
||||
ssl := r.ReadBit() // seq_scaling_list_present_flag[i]
|
||||
if ssl != 0 {
|
||||
return nil // not implemented
|
||||
//goland:noinspection GoSnakeCaseUsage
|
||||
seq_scaling_list_present_flag := r.ReadBit()
|
||||
if seq_scaling_list_present_flag != 0 {
|
||||
if i < 6 {
|
||||
s.scaling_list(r, 16)
|
||||
} else {
|
||||
s.scaling_list(r, 64)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,3 +214,18 @@ func DecodeSPS(sps []byte) *SPS {
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
//goland:noinspection GoSnakeCaseUsage
|
||||
func (s *SPS) scaling_list(r *bits.Reader, sizeOfScalingList int) {
|
||||
lastScale := int32(8)
|
||||
nextScale := int32(8)
|
||||
for j := 0; j < sizeOfScalingList; j++ {
|
||||
if nextScale != 0 {
|
||||
delta_scale := r.ReadSEGolomb()
|
||||
nextScale = (lastScale + delta_scale + 256) % 256
|
||||
}
|
||||
if nextScale != 0 {
|
||||
lastScale = nextScale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,8 @@ func (a *Accessory) GetCharacterByID(iid uint64) *Character {
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
Desc string `json:"description,omitempty"`
|
||||
|
||||
Type string `json:"type"`
|
||||
IID uint64 `json:"iid"`
|
||||
Primary bool `json:"primary,omitempty"`
|
||||
|
||||
@@ -13,13 +13,14 @@ import (
|
||||
// Value should be omit for PW
|
||||
// Value may be empty for PR
|
||||
type Character struct {
|
||||
Desc string `json:"description,omitempty"`
|
||||
|
||||
IID uint64 `json:"iid"`
|
||||
Type string `json:"type"`
|
||||
Format string `json:"format"`
|
||||
Value any `json:"value,omitempty"`
|
||||
Perms []string `json:"perms"`
|
||||
|
||||
//Descr string `json:"description,omitempty"`
|
||||
//MaxLen int `json:"maxLen,omitempty"`
|
||||
//Unit string `json:"unit,omitempty"`
|
||||
//MinValue any `json:"minValue,omitempty"`
|
||||
|
||||
+31
-4
@@ -41,6 +41,9 @@ type Client struct {
|
||||
|
||||
Conn net.Conn
|
||||
reader *bufio.Reader
|
||||
|
||||
res chan *http.Response
|
||||
err error
|
||||
}
|
||||
|
||||
func NewClient(rawURL string) (*Client, error) {
|
||||
@@ -214,7 +217,7 @@ func (c *Client) Dial() (err error) {
|
||||
return
|
||||
}
|
||||
// new reader for new conn
|
||||
c.reader = bufio.NewReaderSize(c.Conn, 32*1024) // 32K like default request body
|
||||
c.reader = bufio.NewReader(c.Conn)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -223,9 +226,33 @@ func (c *Client) Close() error {
|
||||
if c.Conn == nil {
|
||||
return nil
|
||||
}
|
||||
conn := c.Conn
|
||||
c.Conn = nil
|
||||
return conn.Close()
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func (c *Client) eventsReader() {
|
||||
c.res = make(chan *http.Response)
|
||||
|
||||
for {
|
||||
var res *http.Response
|
||||
if res, c.err = ReadResponse(c.reader, nil); c.err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var body []byte
|
||||
if body, c.err = io.ReadAll(res.Body); c.err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
res.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
if res.Proto != ProtoEvent {
|
||||
c.res <- res
|
||||
} else if c.OnEvent != nil {
|
||||
c.OnEvent(res)
|
||||
}
|
||||
}
|
||||
|
||||
close(c.res)
|
||||
}
|
||||
|
||||
func (c *Client) GetAccessories() ([]*Accessory, error) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package hap
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -22,6 +23,9 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
||||
if err := req.Write(c.Conn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.res != nil {
|
||||
return <-c.res, c.err
|
||||
}
|
||||
return http.ReadResponse(c.reader, req)
|
||||
}
|
||||
|
||||
@@ -54,3 +58,27 @@ func (c *Client) Post(path, contentType string, body io.Reader) (*http.Response,
|
||||
func (c *Client) Put(path, contentType string, body io.Reader) (*http.Response, error) {
|
||||
return c.Request("PUT", path, contentType, body)
|
||||
}
|
||||
|
||||
const ProtoEvent = "EVENT/1.0"
|
||||
|
||||
func ReadResponse(r *bufio.Reader, req *http.Request) (*http.Response, error) {
|
||||
b, err := r.Peek(9)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if string(b) != ProtoEvent {
|
||||
return http.ReadResponse(r, req)
|
||||
}
|
||||
|
||||
copy(b, "HTTP/1.1 ")
|
||||
|
||||
res, err := http.ReadResponse(r, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.Proto = ProtoEvent
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package hap
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type EventReader struct {
|
||||
r io.Reader
|
||||
ch chan []byte
|
||||
err error
|
||||
left []byte
|
||||
}
|
||||
|
||||
func NewEventReader(r io.Reader) *EventReader {
|
||||
e := &EventReader{r: r, ch: make(chan []byte, 1)}
|
||||
go e.background()
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *EventReader) background() {
|
||||
b := make([]byte, 32*1024)
|
||||
for {
|
||||
n, err := e.r.Read(b)
|
||||
if err != nil {
|
||||
e.err = err
|
||||
return
|
||||
}
|
||||
|
||||
if n >= 6 && string(b[:6]) == "EVENT " {
|
||||
panic("TODO")
|
||||
}
|
||||
|
||||
// copy because will be overwriten
|
||||
buf := make([]byte, n)
|
||||
copy(buf, b)
|
||||
e.ch <- buf
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EventReader) Read(p []byte) (n int, err error) {
|
||||
if e.err != nil {
|
||||
return 0, e.err
|
||||
}
|
||||
|
||||
// if something left after previous reading
|
||||
if e.left != nil {
|
||||
// if still something left
|
||||
if n = copy(p, e.left); n < len(e.left) {
|
||||
e.left = e.left[n:]
|
||||
} else {
|
||||
e.left = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second * 5):
|
||||
return 0, os.ErrDeadlineExceeded
|
||||
case b := <-e.ch:
|
||||
if n = copy(p, b); n < len(b) {
|
||||
e.left = b[n:]
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+2
-1
@@ -66,7 +66,8 @@ type JSONCharacters struct {
|
||||
type JSONCharacter struct {
|
||||
AID uint8 `json:"aid"`
|
||||
IID uint64 `json:"iid"`
|
||||
Value any `json:"value"`
|
||||
Value any `json:"value,omitempty"`
|
||||
Event any `json:"ev,omitempty"`
|
||||
}
|
||||
|
||||
func SanitizePin(pin string) (string, error) {
|
||||
|
||||
+49
-57
@@ -1,7 +1,9 @@
|
||||
package secure
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
@@ -14,6 +16,9 @@ import (
|
||||
type Conn struct {
|
||||
conn net.Conn
|
||||
|
||||
rd *bufio.Reader
|
||||
wr *bufio.Writer
|
||||
|
||||
encryptKey []byte
|
||||
decryptKey []byte
|
||||
encryptCnt uint64
|
||||
@@ -33,11 +38,19 @@ func Client(conn net.Conn, sharedKey []byte, isClient bool) (net.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isClient {
|
||||
return &Conn{conn: conn, encryptKey: key2, decryptKey: key1}, nil
|
||||
} else {
|
||||
return &Conn{conn: conn, encryptKey: key1, decryptKey: key2}, nil
|
||||
c := &Conn{
|
||||
conn: conn,
|
||||
rd: bufio.NewReaderSize(conn, 32*1024),
|
||||
wr: bufio.NewWriterSize(conn, 32*1024),
|
||||
}
|
||||
|
||||
if isClient {
|
||||
c.encryptKey, c.decryptKey = key2, key1
|
||||
} else {
|
||||
c.encryptKey, c.decryptKey = key1, key2
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -50,84 +63,63 @@ const (
|
||||
)
|
||||
|
||||
func (c *Conn) Read(b []byte) (n int, err error) {
|
||||
verify := make([]byte, VerifySize) // = packet length
|
||||
buf := make([]byte, PacketSizeMax+Overhead)
|
||||
nonce := make([]byte, NonceSize)
|
||||
|
||||
for {
|
||||
if len(b) < PacketSizeMax {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = io.ReadFull(c.conn, verify); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
size := binary.LittleEndian.Uint16(verify)
|
||||
ciphertext := buf[:size+Overhead]
|
||||
|
||||
if _, err = io.ReadFull(c.conn, ciphertext); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
binary.LittleEndian.PutUint64(nonce, c.decryptCnt)
|
||||
c.decryptCnt++
|
||||
|
||||
// put decrypted text to b's end
|
||||
_, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, b[:0], nonce, ciphertext, verify)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
n += int(size) // plaintext size
|
||||
|
||||
// Finish when all bytes fit in b
|
||||
if size < PacketSizeMax {
|
||||
return
|
||||
}
|
||||
|
||||
b = b[size:]
|
||||
if cap(b) < PacketSizeMax {
|
||||
return 0, errors.New("hap: read buffer is too small")
|
||||
}
|
||||
|
||||
verify := make([]byte, 2) // verify = plain message size
|
||||
if _, err = io.ReadFull(c.rd, verify); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
n = int(binary.LittleEndian.Uint16(verify))
|
||||
ciphertext := make([]byte, n+Overhead)
|
||||
|
||||
if _, err = io.ReadFull(c.rd, ciphertext); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
nonce := make([]byte, NonceSize)
|
||||
binary.LittleEndian.PutUint64(nonce, c.decryptCnt)
|
||||
c.decryptCnt++
|
||||
|
||||
_, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, b[:0], nonce, ciphertext, verify)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) Write(b []byte) (n int, err error) {
|
||||
c.mx.Lock()
|
||||
defer c.mx.Unlock()
|
||||
|
||||
buf := make([]byte, 0, PacketSizeMax+Overhead)
|
||||
nonce := make([]byte, NonceSize)
|
||||
buf := make([]byte, NonceSize+PacketSizeMax+Overhead)
|
||||
verify := buf[:VerifySize] // part of write buffer
|
||||
verify := make([]byte, VerifySize)
|
||||
|
||||
for {
|
||||
for len(b) > 0 {
|
||||
size := len(b)
|
||||
if size > PacketSizeMax {
|
||||
size = PacketSizeMax
|
||||
}
|
||||
|
||||
binary.LittleEndian.PutUint16(verify, uint16(size))
|
||||
if _, err = c.wr.Write(verify); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
binary.LittleEndian.PutUint64(nonce, c.encryptCnt)
|
||||
c.encryptCnt++
|
||||
|
||||
// put encrypted text to writing buffer just after size (2 bytes)
|
||||
_, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf[2:2], nonce, b[:size], verify)
|
||||
_, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf, nonce, b[:size], verify)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = c.conn.Write(buf[:VerifySize+size+Overhead]); err != nil {
|
||||
if _, err = c.wr.Write(buf[:size+Overhead]); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
n += size // plaintext size
|
||||
|
||||
if size < PacketSizeMax {
|
||||
break
|
||||
}
|
||||
|
||||
b = b[PacketSizeMax:]
|
||||
b = b[size:]
|
||||
n += size
|
||||
}
|
||||
|
||||
err = c.wr.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -146,7 +146,7 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co
|
||||
|
||||
clientPublic := s.GetPair(conn, plainM3.Identifier)
|
||||
if clientPublic == nil {
|
||||
return fmt.Errorf("hap: PairVerify from: %s, with unknown client_id: %s", plainM3.Identifier)
|
||||
return fmt.Errorf("hap: PairVerify from: %s, with unknown client_id: %s", conn.RemoteAddr(), plainM3.Identifier)
|
||||
}
|
||||
|
||||
b = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic)
|
||||
|
||||
+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
|
||||
}
|
||||
|
||||
@@ -64,20 +64,24 @@ func (c *Producer) Start() error {
|
||||
|
||||
buf = append(buf, b[:n]...)
|
||||
|
||||
i := annexb.IndexFrame(buf)
|
||||
if i < 0 {
|
||||
continue
|
||||
for {
|
||||
i := annexb.IndexFrame(buf)
|
||||
if i < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
buf = buf[i:]
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
buf = buf[i:]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ func (k *Keyframe) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
sender.Handler = h264.RTPDepay(track.Codec, sender.Handler)
|
||||
} else {
|
||||
sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)
|
||||
}
|
||||
|
||||
case core.CodecH265:
|
||||
@@ -66,9 +68,7 @@ func (k *Keyframe) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
sender.Handler = h264.RTPDepay(track.Codec, sender.Handler)
|
||||
} else {
|
||||
sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)
|
||||
sender.Handler = h265.RTPDepay(track.Codec, sender.Handler)
|
||||
}
|
||||
|
||||
case core.CodecJPEG:
|
||||
|
||||
+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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package mdns
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func SetsockoptInt(fd uintptr, level, opt int, value int) (err error) {
|
||||
// change SO_REUSEADDR and REUSEPORT flags simultaneously for BSD-like OS
|
||||
// https://github.com/AlexxIT/go2rtc/issues/626
|
||||
// https://stackoverflow.com/questions/14388706/how-do-so-reuseaddr-and-so-reuseport-differ/14388707
|
||||
if opt == syscall.SO_REUSEADDR {
|
||||
if err = syscall.SetsockoptInt(int(fd), level, opt, value); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
opt = syscall.SO_REUSEPORT
|
||||
}
|
||||
|
||||
return syscall.SetsockoptInt(int(fd), level, opt, value)
|
||||
}
|
||||
|
||||
func SetsockoptIPMreq(fd uintptr, level, opt int, mreq *syscall.IPMreq) (err error) {
|
||||
return syscall.SetsockoptIPMreq(int(fd), level, opt, mreq)
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
//go:build darwin || linux
|
||||
|
||||
package mdns
|
||||
|
||||
import "syscall"
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func SetsockoptInt(fd uintptr, level, opt int, value int) (err error) {
|
||||
return syscall.SetsockoptInt(int(fd), level, opt, value)
|
||||
+16
-9
@@ -2,7 +2,6 @@ package mp4
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
@@ -43,13 +42,17 @@ func (m *Muxer) GetInit() ([]byte, error) {
|
||||
pps = []byte{0x68, 0xce, 0x38, 0x80}
|
||||
}
|
||||
|
||||
s := h264.DecodeSPS(sps)
|
||||
if s == nil {
|
||||
return nil, errors.New("mp4: can't parse SPS")
|
||||
var width, height uint16
|
||||
if s := h264.DecodeSPS(sps); s != nil {
|
||||
width = s.Width()
|
||||
height = s.Height()
|
||||
} else {
|
||||
width = 1920
|
||||
height = 1080
|
||||
}
|
||||
|
||||
mv.WriteVideoTrack(
|
||||
uint32(i+1), codec.Name, codec.ClockRate, s.Width(), s.Height(), h264.EncodeConfig(sps, pps),
|
||||
uint32(i+1), codec.Name, codec.ClockRate, width, height, h264.EncodeConfig(sps, pps),
|
||||
)
|
||||
|
||||
case core.CodecH265:
|
||||
@@ -65,13 +68,17 @@ func (m *Muxer) GetInit() ([]byte, error) {
|
||||
pps = []byte{0x44, 0x01, 0xc0, 0x73, 0xc0, 0x4c, 0x90}
|
||||
}
|
||||
|
||||
s := h265.DecodeSPS(sps)
|
||||
if s == nil {
|
||||
return nil, errors.New("mp4: can't parse SPS")
|
||||
var width, height uint16
|
||||
if s := h265.DecodeSPS(sps); s != nil {
|
||||
width = s.Width()
|
||||
height = s.Height()
|
||||
} else {
|
||||
width = 1920
|
||||
height = 1080
|
||||
}
|
||||
|
||||
mv.WriteVideoTrack(
|
||||
uint32(i+1), codec.Name, codec.ClockRate, s.Width(), s.Height(), h265.EncodeConfig(vps, sps, pps),
|
||||
uint32(i+1), codec.Name, codec.ClockRate, width, height, h265.EncodeConfig(vps, sps, pps),
|
||||
)
|
||||
|
||||
case core.CodecAAC:
|
||||
|
||||
+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
|
||||
}
|
||||
|
||||
+5
-4
@@ -5,7 +5,6 @@ import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -13,6 +12,8 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
const PathDevice = "/onvif/device_service"
|
||||
@@ -78,10 +79,10 @@ func (c *Client) GetURI() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
uri := FindTagValue(b, "Uri")
|
||||
uri = html.UnescapeString(uri)
|
||||
rawURL := FindTagValue(b, "Uri")
|
||||
rawURL = strings.TrimSpace(html.UnescapeString(rawURL))
|
||||
|
||||
u, err := url.Parse(uri)
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func FindTagValue(b []byte, tag string) string {
|
||||
re := regexp.MustCompile(`<[^/>]*` + tag + `[^>]*>([^<]+)`)
|
||||
re := regexp.MustCompile(`(?s)[:<]` + tag + `>([^<]+)`)
|
||||
m := re.FindSubmatch(b)
|
||||
if len(m) != 2 {
|
||||
return ""
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"html"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetStreamUri(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
xml string
|
||||
url string
|
||||
}{
|
||||
{
|
||||
name: "Dahua stream default",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes" ?><s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:trt="http://www.onvif.org/ver10/media/wsdl"><s:Header/><s:Body><trt:GetStreamUriResponse><trt:MediaUri><tt:Uri>rtsp://192.168.1.123:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif</tt:Uri><tt:InvalidAfterConnect>true</tt:InvalidAfterConnect><tt:InvalidAfterReboot>true</tt:InvalidAfterReboot><tt:Timeout>PT0S</tt:Timeout></trt:MediaUri></trt:GetStreamUriResponse></s:Body></s:Envelope>`,
|
||||
url: "rtsp://192.168.1.123:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif",
|
||||
},
|
||||
{
|
||||
name: "Dahua snapshot default",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes" ?><s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:trt="http://www.onvif.org/ver10/media/wsdl"><s:Header/><s:Body><trt:GetSnapshotUriResponse><trt:MediaUri><tt:Uri>http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&subtype=1</tt:Uri><tt:InvalidAfterConnect>false</tt:InvalidAfterConnect><tt:InvalidAfterReboot>false</tt:InvalidAfterReboot><tt:Timeout>PT0S</tt:Timeout></trt:MediaUri></trt:GetSnapshotUriResponse></s:Body></s:Envelope>`,
|
||||
url: "http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&subtype=1",
|
||||
},
|
||||
{
|
||||
name: "Dahua stream formatted",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding"
|
||||
xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema"
|
||||
xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
|
||||
<s:Header />
|
||||
<s:Body>
|
||||
<trt:GetStreamUriResponse>
|
||||
<trt:MediaUri>
|
||||
<tt:Uri>
|
||||
rtsp://192.168.1.123:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif</tt:Uri>
|
||||
<tt:InvalidAfterConnect>true</tt:InvalidAfterConnect>
|
||||
<tt:InvalidAfterReboot>true</tt:InvalidAfterReboot>
|
||||
<tt:Timeout>PT0S</tt:Timeout>
|
||||
</trt:MediaUri>
|
||||
</trt:GetStreamUriResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>`,
|
||||
url: "rtsp://192.168.1.123:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif",
|
||||
},
|
||||
{
|
||||
name: "Dahua snapshot formatted",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding"
|
||||
xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema"
|
||||
xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
|
||||
<s:Header />
|
||||
<s:Body>
|
||||
<trt:GetSnapshotUriResponse>
|
||||
<trt:MediaUri>
|
||||
<tt:Uri>
|
||||
http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&subtype=1</tt:Uri>
|
||||
<tt:InvalidAfterConnect>false</tt:InvalidAfterConnect>
|
||||
<tt:InvalidAfterReboot>false</tt:InvalidAfterReboot>
|
||||
<tt:Timeout>PT0S</tt:Timeout>
|
||||
</trt:MediaUri>
|
||||
</trt:GetSnapshotUriResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>`,
|
||||
url: "http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&subtype=1",
|
||||
},
|
||||
{
|
||||
name: "Unknown",
|
||||
xml: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<SOAP-ENV:Envelope ...>
|
||||
<SOAP-ENV:Header></SOAP-ENV:Header>
|
||||
<SOAP-ENV:Body>
|
||||
<MC1:GetStreamUriResponse>
|
||||
<MC1:MediaUri>
|
||||
<MC2:Uri>
|
||||
rtsp://192.168.5.53:8090/profile1=r
|
||||
</MC2:Uri>
|
||||
</MC1:MediaUri>
|
||||
</MC1:GetStreamUriResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`,
|
||||
url: "rtsp://192.168.5.53:8090/profile1=r",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
uri := FindTagValue([]byte(test.xml), "Uri")
|
||||
uri = strings.TrimSpace(html.UnescapeString(uri))
|
||||
u, err := url.Parse(uri)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, test.url, u.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCapabilities(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
xml string
|
||||
}{
|
||||
{
|
||||
name: "Dahua default",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes" ?><s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:tds="http://www.onvif.org/ver10/device/wsdl"><s:Header/><s:Body><tds:GetCapabilitiesResponse><tds:Capabilities><tt:Analytics><tt:XAddr>http://192.168.1.123/onvif/analytics_service</tt:XAddr><tt:RuleSupport>true</tt:RuleSupport><tt:AnalyticsModuleSupport>true</tt:AnalyticsModuleSupport></tt:Analytics><tt:Device><tt:XAddr>http://192.168.1.123/onvif/device_service</tt:XAddr><tt:Network><tt:IPFilter>false</tt:IPFilter><tt:ZeroConfiguration>false</tt:ZeroConfiguration><tt:IPVersion6>false</tt:IPVersion6><tt:DynDNS>false</tt:DynDNS><tt:Extension><tt:Dot11Configuration>false</tt:Dot11Configuration></tt:Extension></tt:Network><tt:System><tt:DiscoveryResolve>false</tt:DiscoveryResolve><tt:DiscoveryBye>true</tt:DiscoveryBye><tt:RemoteDiscovery>false</tt:RemoteDiscovery><tt:SystemBackup>false</tt:SystemBackup><tt:SystemLogging>true</tt:SystemLogging><tt:FirmwareUpgrade>true</tt:FirmwareUpgrade><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>00</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>10</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>20</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>30</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>40</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>42</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>16</tt:Major><tt:Minor>12</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>18</tt:Major><tt:Minor>06</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>18</tt:Major><tt:Minor>12</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>19</tt:Major><tt:Minor>06</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>19</tt:Major><tt:Minor>12</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>20</tt:Major><tt:Minor>06</tt:Minor></tt:SupportedVersions><tt:Extension><tt:HttpFirmwareUpgrade>true</tt:HttpFirmwareUpgrade><tt:HttpSystemBackup>false</tt:HttpSystemBackup><tt:HttpSystemLogging>false</tt:HttpSystemLogging><tt:HttpSupportInformation>false</tt:HttpSupportInformation></tt:Extension></tt:System><tt:IO><tt:InputConnectors>2</tt:InputConnectors><tt:RelayOutputs>1</tt:RelayOutputs><tt:Extension><tt:Auxiliary>false</tt:Auxiliary><tt:AuxiliaryCommands></tt:AuxiliaryCommands><tt:Extension></tt:Extension></tt:Extension></tt:IO><tt:Security><tt:TLS1.1>false</tt:TLS1.1><tt:TLS1.2>false</tt:TLS1.2><tt:OnboardKeyGeneration>false</tt:OnboardKeyGeneration><tt:AccessPolicyConfig>false</tt:AccessPolicyConfig><tt:X.509Token>false</tt:X.509Token><tt:SAMLToken>false</tt:SAMLToken><tt:KerberosToken>false</tt:KerberosToken><tt:RELToken>false</tt:RELToken><tt:Extension><tt:TLS1.0>false</tt:TLS1.0><tt:Extension><tt:Dot1X>false</tt:Dot1X><tt:SupportedEAPMethod>0</tt:SupportedEAPMethod><tt:RemoteUserHandling>false</tt:RemoteUserHandling></tt:Extension></tt:Extension></tt:Security></tt:Device><tt:Events><tt:XAddr>http://192.168.1.123/onvif/event_service</tt:XAddr><tt:WSSubscriptionPolicySupport>true</tt:WSSubscriptionPolicySupport><tt:WSPullPointSupport>true</tt:WSPullPointSupport><tt:WSPausableSubscriptionManagerInterfaceSupport>false</tt:WSPausableSubscriptionManagerInterfaceSupport></tt:Events><tt:Imaging><tt:XAddr>http://192.168.1.123/onvif/imaging_service</tt:XAddr></tt:Imaging><tt:Media><tt:XAddr>http://192.168.1.123/onvif/media_service</tt:XAddr><tt:StreamingCapabilities><tt:RTPMulticast>true</tt:RTPMulticast><tt:RTP_TCP>true</tt:RTP_TCP><tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP></tt:StreamingCapabilities><tt:Extension><tt:ProfileCapabilities><tt:MaximumNumberOfProfiles>6</tt:MaximumNumberOfProfiles></tt:ProfileCapabilities></tt:Extension></tt:Media><tt:Extension><tt:DeviceIO><tt:XAddr>http://192.168.1.123/onvif/deviceIO_service</tt:XAddr><tt:VideoSources>1</tt:VideoSources><tt:VideoOutputs>0</tt:VideoOutputs><tt:AudioSources>1</tt:AudioSources><tt:AudioOutputs>1</tt:AudioOutputs><tt:RelayOutputs>1</tt:RelayOutputs></tt:DeviceIO></tt:Extension></tds:Capabilities></tds:GetCapabilitiesResponse></s:Body></s:Envelope>`,
|
||||
},
|
||||
{
|
||||
name: "Dahua formatted",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding"
|
||||
xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema"
|
||||
xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
|
||||
<s:Header />
|
||||
<s:Body>
|
||||
<tds:GetCapabilitiesResponse>
|
||||
<tds:Capabilities>
|
||||
<tt:Analytics>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/analytics_service</tt:XAddr>
|
||||
<tt:RuleSupport>true</tt:RuleSupport>
|
||||
<tt:AnalyticsModuleSupport>true</tt:AnalyticsModuleSupport>
|
||||
</tt:Analytics>
|
||||
<tt:Device>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/device_service</tt:XAddr>
|
||||
<tt:Network>
|
||||
<tt:IPFilter>false</tt:IPFilter>
|
||||
<tt:ZeroConfiguration>false</tt:ZeroConfiguration>
|
||||
<tt:IPVersion6>false</tt:IPVersion6>
|
||||
<tt:DynDNS>false</tt:DynDNS>
|
||||
<tt:Extension>
|
||||
<tt:Dot11Configuration>false</tt:Dot11Configuration>
|
||||
</tt:Extension>
|
||||
</tt:Network>
|
||||
<tt:System>
|
||||
...
|
||||
</tt:System>
|
||||
<tt:IO>
|
||||
<tt:InputConnectors>2</tt:InputConnectors>
|
||||
<tt:RelayOutputs>1</tt:RelayOutputs>
|
||||
<tt:Extension>
|
||||
<tt:Auxiliary>false</tt:Auxiliary>
|
||||
<tt:AuxiliaryCommands></tt:AuxiliaryCommands>
|
||||
<tt:Extension></tt:Extension>
|
||||
</tt:Extension>
|
||||
</tt:IO>
|
||||
<tt:Security>
|
||||
...
|
||||
</tt:Security>
|
||||
</tt:Device>
|
||||
<tt:Events>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/event_service</tt:XAddr>
|
||||
<tt:WSSubscriptionPolicySupport>true</tt:WSSubscriptionPolicySupport>
|
||||
<tt:WSPullPointSupport>true</tt:WSPullPointSupport>
|
||||
<tt:WSPausableSubscriptionManagerInterfaceSupport>false</tt:WSPausableSubscriptionManagerInterfaceSupport>
|
||||
</tt:Events>
|
||||
<tt:Imaging>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/imaging_service</tt:XAddr>
|
||||
</tt:Imaging>
|
||||
<tt:Media>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/media_service</tt:XAddr>
|
||||
<tt:StreamingCapabilities>
|
||||
<tt:RTPMulticast>true</tt:RTPMulticast>
|
||||
<tt:RTP_TCP>true</tt:RTP_TCP>
|
||||
<tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP>
|
||||
</tt:StreamingCapabilities>
|
||||
<tt:Extension>
|
||||
<tt:ProfileCapabilities>
|
||||
<tt:MaximumNumberOfProfiles>6</tt:MaximumNumberOfProfiles>
|
||||
</tt:ProfileCapabilities>
|
||||
</tt:Extension>
|
||||
</tt:Media>
|
||||
<tt:Extension>
|
||||
<tt:DeviceIO>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/deviceIO_service</tt:XAddr>
|
||||
<tt:VideoSources>1</tt:VideoSources>
|
||||
<tt:VideoOutputs>0</tt:VideoOutputs>
|
||||
<tt:AudioSources>1</tt:AudioSources>
|
||||
<tt:AudioOutputs>1</tt:AudioOutputs>
|
||||
<tt:RelayOutputs>1</tt:RelayOutputs>
|
||||
</tt:DeviceIO>
|
||||
</tt:Extension>
|
||||
</tds:Capabilities>
|
||||
</tds:GetCapabilitiesResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
rawURL := FindTagValue([]byte(test.xml), "Media.+?XAddr")
|
||||
require.Equal(t, "http://192.168.1.123/onvif/media_service", rawURL)
|
||||
|
||||
rawURL = FindTagValue([]byte(test.xml), "Imaging.+?XAddr")
|
||||
require.Equal(t, "http://192.168.1.123/onvif/imaging_service", rawURL)
|
||||
})
|
||||
}
|
||||
}
|
||||
+11
-8
@@ -6,16 +6,17 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/roborock/iot"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
"log"
|
||||
"net/rpc"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/roborock/iot"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -38,13 +39,15 @@ func NewClient(url string) *Client {
|
||||
return &Client{url: url}
|
||||
}
|
||||
|
||||
func (c *Client) Dial() (err error) {
|
||||
func (c *Client) Dial() error {
|
||||
u, err := url.Parse(c.url)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.iot, err = iot.Dial(c.url)
|
||||
if c.iot, err = iot.Dial(c.url); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.pin = u.Query().Get("pin")
|
||||
if c.pin != "" {
|
||||
@@ -87,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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
## Logs
|
||||
|
||||
```
|
||||
request []interface {}{"connect", 1, map[string]interface {}{"app":"s", "flashVer":"FMLE/3.0 (compatible; FMSc/1.0)", "tcUrl":"rtmps://xxx.rtmp.t.me/s/xxxxx"}}
|
||||
response []interface {}{"_result", 1, map[string]interface {}{"capabilities":31, "fmsVer":"FMS/3,0,1,123"}, map[string]interface {}{"code":"NetConnection.Connect.Success", "description":"Connection succeeded.", "level":"status", "objectEncoding":0}}
|
||||
request []interface {}{"releaseStream", 2, interface {}(nil), "xxxxx"}
|
||||
request []interface {}{"FCPublish", 3, interface {}(nil), "xxxxx"}
|
||||
request []interface {}{"createStream", 4, interface {}(nil)}
|
||||
response []interface {}{"_result", 2, interface {}(nil)}
|
||||
response []interface {}{"_result", 4, interface {}(nil), 1}
|
||||
request []interface {}{"publish", 5, interface {}(nil), "xxxxx", "live"}
|
||||
response []interface {}{"onStatus", 0, interface {}(nil), map[string]interface {}{"code":"NetStream.Publish.Start", "description":"xxxxx is now published", "detail":"xxxxx", "level":"status"}}
|
||||
```
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://en.wikipedia.org/wiki/Flash_Video
|
||||
- https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol
|
||||
- https://rtmp.veriskope.com/pdf/rtmp_specification_1.0.pdf
|
||||
@@ -0,0 +1,155 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
func DialPlay(rawURL string) (core.Producer, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := tcp.Dial(u, core.ConnDialTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtmpConn, err := NewClient(conn, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = rtmpConn.play(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rtmpConn.Producer()
|
||||
}
|
||||
|
||||
func DialPublish(rawURL string) (io.Writer, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := tcp.Dial(u, core.ConnDialTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := NewClient(conn, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = client.publish(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func NewClient(conn net.Conn, u *url.URL) (*Conn, error) {
|
||||
c := &Conn{
|
||||
url: u.String(),
|
||||
|
||||
conn: conn,
|
||||
rd: bufio.NewReaderSize(conn, core.BufferSize),
|
||||
wr: conn,
|
||||
|
||||
chunks: map[uint8]*header{},
|
||||
|
||||
rdPacketSize: 128,
|
||||
wrPacketSize: 4096, // OBS - 4096, Reolink - 4096
|
||||
}
|
||||
|
||||
if args := strings.Split(u.Path, "/"); len(args) >= 2 {
|
||||
c.App = args[1]
|
||||
if len(args) >= 3 {
|
||||
c.Stream = args[2]
|
||||
if u.RawQuery != "" {
|
||||
c.Stream += "?" + u.RawQuery
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.clienHandshake(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.writePacketSize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Conn) clienHandshake() error {
|
||||
// simple handshake without real random and check response
|
||||
b := make([]byte, 1+1536)
|
||||
b[0] = 0x03
|
||||
// write C0+C1
|
||||
if _, err := c.conn.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
// read S0+S1
|
||||
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||
return err
|
||||
}
|
||||
// write S1
|
||||
if _, err := c.conn.Write(b[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
// read C1, skip check
|
||||
if _, err := io.ReadFull(c.rd, b[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) play() error {
|
||||
if err := c.writeConnect(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writeCreateStream(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writePlay(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) publish() error {
|
||||
if err := c.writeConnect(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writeReleaseStream(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writeCreateStream(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writePublish(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
_, _, _, err := c.readMessage()
|
||||
//log.Printf("!!! %d %d %.30x", msgType, timeMS, b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
|
||||
)
|
||||
|
||||
const (
|
||||
TypeSetPacketSize = 1
|
||||
TypeServerBandwidth = 5
|
||||
TypeClientBandwidth = 6
|
||||
TypeAudio = 8
|
||||
TypeVideo = 9
|
||||
TypeData = 18
|
||||
TypeCommand = 20
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
App string
|
||||
Stream string
|
||||
Intent string
|
||||
|
||||
rdPacketSize uint32
|
||||
wrPacketSize uint32
|
||||
|
||||
chunks map[byte]*header
|
||||
streamID byte
|
||||
url string
|
||||
|
||||
conn net.Conn
|
||||
rd io.Reader
|
||||
wr io.Writer
|
||||
|
||||
rdBuf []byte
|
||||
wrBuf []byte
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Conn) readResponse(transID float64) ([]any, error) {
|
||||
for {
|
||||
msgType, _, b, err := c.readMessage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch msgType {
|
||||
case TypeSetPacketSize:
|
||||
c.rdPacketSize = binary.BigEndian.Uint32(b)
|
||||
case TypeCommand:
|
||||
items, _ := amf.NewReader(b).ReadItems()
|
||||
if len(items) >= 3 && items[1] == transID {
|
||||
return items, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type header struct {
|
||||
timeMS uint32
|
||||
dataSize uint32
|
||||
tagType byte
|
||||
streamID uint32
|
||||
}
|
||||
|
||||
//var ErrNotImplemented = errors.New("rtmp: not implemented")
|
||||
|
||||
func (c *Conn) readMessage() (byte, uint32, []byte, error) {
|
||||
b, err := c.readSize(1) // doesn't support big chunkID!!!
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
hdrType := b[0] >> 6
|
||||
chunkID := b[0] & 0b111111
|
||||
|
||||
// storing header information for support header type 3
|
||||
hdr, ok := c.chunks[chunkID]
|
||||
if !ok {
|
||||
hdr = &header{}
|
||||
c.chunks[chunkID] = hdr
|
||||
}
|
||||
|
||||
switch hdrType {
|
||||
case 0: // 12 byte header (full header)
|
||||
if b, err = c.readSize(11); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
_ = b[7]
|
||||
hdr.timeMS = Uint24(b)
|
||||
hdr.dataSize = Uint24(b[3:])
|
||||
hdr.tagType = b[6]
|
||||
hdr.streamID = binary.LittleEndian.Uint32(b[7:])
|
||||
|
||||
case 1: // 8 bytes - like type b00, not including message ID (4 last bytes)
|
||||
if b, err = c.readSize(7); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
_ = b[6]
|
||||
hdr.timeMS = Uint24(b) // timestamp
|
||||
hdr.dataSize = Uint24(b[3:]) // msgdatalen
|
||||
hdr.tagType = b[6] // msgtypeid
|
||||
|
||||
case 2: // 4 bytes - Basic Header and timestamp (3 bytes) are included
|
||||
if b, err = c.readSize(3); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
hdr.timeMS = Uint24(b) // timestamp
|
||||
|
||||
case 3: // 1 byte - only the Basic Header is included
|
||||
// use here hdr from previous msg with same session ID (sid)
|
||||
}
|
||||
|
||||
timeMS := hdr.timeMS
|
||||
if timeMS == 0xFFFFFF {
|
||||
if b, err = c.readSize(4); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
timeMS = binary.BigEndian.Uint32(b)
|
||||
}
|
||||
|
||||
//log.Printf("[rtmp] hdr=%d chunkID=%d timeMS=%d size=%d tagType=%d streamID=%d", hdrType, chunkID, hdr.timeMS, hdr.dataSize, hdr.tagType, hdr.streamID)
|
||||
|
||||
// 1. Response zero size
|
||||
if hdr.dataSize == 0 {
|
||||
return hdr.tagType, timeMS, nil, nil
|
||||
}
|
||||
|
||||
b = make([]byte, hdr.dataSize)
|
||||
|
||||
// 2. Response small packet
|
||||
if hdr.dataSize <= c.rdPacketSize {
|
||||
if _, err = io.ReadFull(c.rd, b); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
return hdr.tagType, timeMS, b, nil
|
||||
}
|
||||
|
||||
// 3. Response big packet
|
||||
var i0 uint32
|
||||
for i1 := c.rdPacketSize; i1 < hdr.dataSize; i1 += c.rdPacketSize {
|
||||
if _, err = io.ReadFull(c.rd, b[i0:i1]); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
if _, err = c.readSize(1); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
if hdr.timeMS == 0xFFFFFF {
|
||||
if _, err = c.readSize(4); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
i0 = i1
|
||||
}
|
||||
|
||||
if _, err = io.ReadFull(c.rd, b[i0:]); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
return hdr.tagType, timeMS, b, nil
|
||||
}
|
||||
func (c *Conn) writeMessage(chunkID, tagType byte, timeMS uint32, payload []byte) error {
|
||||
c.mu.Lock()
|
||||
c.resetBuffer()
|
||||
|
||||
b := payload
|
||||
size := uint32(len(b))
|
||||
|
||||
if size > c.wrPacketSize {
|
||||
c.appendType0(chunkID, tagType, timeMS, size, b[:c.wrPacketSize])
|
||||
|
||||
for {
|
||||
b = b[c.wrPacketSize:]
|
||||
if uint32(len(b)) > c.wrPacketSize {
|
||||
c.appendType3(chunkID, b[:c.wrPacketSize])
|
||||
} else {
|
||||
c.appendType3(chunkID, b)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.appendType0(chunkID, tagType, timeMS, size, b)
|
||||
}
|
||||
|
||||
//log.Printf("%d %2d %5d %6d %.32x", chunkID, tagType, timeMS, size, payload)
|
||||
|
||||
_, err := c.wr.Write(c.wrBuf)
|
||||
c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Conn) resetBuffer() {
|
||||
c.wrBuf = c.wrBuf[:0]
|
||||
}
|
||||
|
||||
func (c *Conn) appendType0(chunkID, tagType byte, timeMS, size uint32, payload []byte) {
|
||||
// TODO: timeMS more than 24 bit
|
||||
c.wrBuf = append(c.wrBuf,
|
||||
chunkID,
|
||||
byte(timeMS>>16), byte(timeMS>>8), byte(timeMS),
|
||||
byte(size>>16), byte(size>>8), byte(size),
|
||||
tagType,
|
||||
c.streamID, 0, 0, 0, // little endian streamID
|
||||
)
|
||||
c.wrBuf = append(c.wrBuf, payload...)
|
||||
}
|
||||
|
||||
func (c *Conn) appendType3(chunkID byte, payload []byte) {
|
||||
c.wrBuf = append(c.wrBuf, 3<<6|chunkID)
|
||||
c.wrBuf = append(c.wrBuf, payload...)
|
||||
}
|
||||
|
||||
func (c *Conn) writePacketSize() error {
|
||||
b := binary.BigEndian.AppendUint32(nil, c.wrPacketSize)
|
||||
return c.writeMessage(2, TypeSetPacketSize, 0, b)
|
||||
}
|
||||
|
||||
func (c *Conn) writeConnect() error {
|
||||
b := amf.EncodeItems("connect", 1, map[string]any{
|
||||
"app": c.App,
|
||||
"flashVer": "FMLE/3.0 (compatible; FMSc/1.0)",
|
||||
"tcUrl": c.url,
|
||||
})
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := c.readResponse(1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
code := getString(v, 3, "code")
|
||||
if code != "NetConnection.Connect.Success" {
|
||||
return fmt.Errorf("rtmp: wrong response %#v", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) writeReleaseStream() error {
|
||||
b := amf.EncodeItems("releaseStream", 2, nil, c.Stream)
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
b = amf.EncodeItems("FCPublish", 3, nil, c.Stream)
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (c *Conn) writeCreateStream() error {
|
||||
b := amf.EncodeItems("createStream", 4, nil)
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := c.readResponse(4)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(v) == 4 {
|
||||
if f, ok := v[3].(float64); ok {
|
||||
c.streamID = byte(f)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("rtmp: wrong response %#v", v)
|
||||
}
|
||||
|
||||
func (c *Conn) writePublish() error {
|
||||
b := amf.EncodeItems("publish", 5, nil, c.Stream, "live")
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := c.readResponse(0)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
code := getString(v, 3, "code")
|
||||
if code != "NetStream.Publish.Start" {
|
||||
return fmt.Errorf("rtmp: wrong response %#v", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) writePlay() error {
|
||||
b := amf.EncodeItems("play", 5, nil, c.Stream)
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := c.readResponse(0)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
code := getString(v, 3, "code")
|
||||
if !strings.HasPrefix(code, "NetStream.Play.") {
|
||||
return fmt.Errorf("rtmp: wrong response %#v", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) readSize(n uint32) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
if _, err := io.ReadAtLeast(c.rd, b, int(n)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func PutUint24(b []byte, v uint32) {
|
||||
_ = b[2]
|
||||
b[0] = byte(v >> 16)
|
||||
b[1] = byte(v >> 8)
|
||||
b[2] = byte(v)
|
||||
}
|
||||
|
||||
func Uint24(b []byte) uint32 {
|
||||
_ = b[2]
|
||||
return uint32(b[0])<<16 | uint32(b[1])<<8 | uint32(b[2])
|
||||
}
|
||||
|
||||
func getString(v []any, i int, key string) string {
|
||||
if len(v) <= i {
|
||||
return ""
|
||||
}
|
||||
if v, ok := v[i].(map[string]any); ok {
|
||||
if s, ok := v[key].(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv"
|
||||
)
|
||||
|
||||
func (c *Conn) Producer() (core.Producer, error) {
|
||||
c.rdBuf = []byte{
|
||||
'F', 'L', 'V', // signature
|
||||
1, // version
|
||||
0, // flags (has video/audio)
|
||||
0, 0, 0, 9, // header size
|
||||
}
|
||||
|
||||
return flv.Open(c)
|
||||
}
|
||||
|
||||
// Read - convert RTMP to FLV format
|
||||
func (c *Conn) Read(p []byte) (n int, err error) {
|
||||
// 1. Check temporary tempbuffer
|
||||
if len(c.rdBuf) == 0 {
|
||||
msgType, timeMS, payload, err2 := c.readMessage()
|
||||
if err2 != nil {
|
||||
return 0, err2
|
||||
}
|
||||
|
||||
// previous tag size (4 byte) + header (11 byte) + payload
|
||||
n = 4 + 11 + len(payload)
|
||||
|
||||
// 2. Check if the message fits in the buffer
|
||||
if n <= len(p) {
|
||||
encodeFLV(p, msgType, timeMS, payload)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Put the message into a temporary buffer
|
||||
c.rdBuf = make([]byte, n)
|
||||
encodeFLV(c.rdBuf, msgType, timeMS, payload)
|
||||
}
|
||||
|
||||
// 4. Send temporary buffer
|
||||
n = copy(p, c.rdBuf)
|
||||
c.rdBuf = c.rdBuf[n:]
|
||||
return
|
||||
}
|
||||
|
||||
func encodeFLV(b []byte, msgType byte, time uint32, payload []byte) {
|
||||
_ = b[4+11]
|
||||
|
||||
b[0] = 0
|
||||
b[1] = 0
|
||||
b[2] = 0
|
||||
b[3] = 0
|
||||
b[4+0] = msgType
|
||||
PutUint24(b[4+1:], uint32(len(payload)))
|
||||
PutUint24(b[4+4:], time)
|
||||
b[4+7] = byte(time >> 24)
|
||||
|
||||
copy(b[4+11:], payload)
|
||||
}
|
||||
|
||||
// Write - convert FLV format to RTMP format
|
||||
func (c *Conn) Write(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
|
||||
if p[0] == 'F' {
|
||||
p = p[9+4:] // skip first msg with FLV header
|
||||
|
||||
for len(p) > 0 {
|
||||
size := 11 + uint16(p[2])<<8 + uint16(p[3]) + 4
|
||||
if _, err = c.Write(p[:size]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
p = p[size:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// decode FLV: 11 bytes header + payload + 4 byte size
|
||||
tagType := p[0]
|
||||
timeMS := uint32(p[4])<<16 | uint32(p[5])<<8 | uint32(p[6]) | uint32(p[7])<<24
|
||||
payload := p[11 : len(p)-4]
|
||||
|
||||
err = c.writeMessage(4, tagType, timeMS, payload)
|
||||
return
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
func Dial(rawURL string) (core.Producer, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := tcp.Dial(u, "1935", core.ConnDialTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rd, err := NewReader(u, conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return flv.Open(rd)
|
||||
}
|
||||
@@ -1,488 +0,0 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
|
||||
)
|
||||
|
||||
const (
|
||||
MsgSetPacketSize = 1
|
||||
MsgServerBandwidth = 5
|
||||
MsgClientBandwidth = 6
|
||||
MsgCommand = 20
|
||||
|
||||
//MsgAck = 3
|
||||
//MsgControl = 4
|
||||
//MsgAudioPacket = 8
|
||||
//MsgVideoPacket = 9
|
||||
//MsgDataExt = 15
|
||||
//MsgCommandExt = 17
|
||||
//MsgData = 18
|
||||
)
|
||||
|
||||
var ErrResponse = errors.New("rtmp: wrong response")
|
||||
|
||||
type Reader struct {
|
||||
url string
|
||||
app string
|
||||
stream string
|
||||
pktSize uint32
|
||||
|
||||
headers map[uint32]*header
|
||||
|
||||
conn net.Conn
|
||||
rd io.Reader
|
||||
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func NewReader(u *url.URL, conn net.Conn) (*Reader, error) {
|
||||
rd := &Reader{
|
||||
url: u.String(),
|
||||
headers: map[uint32]*header{},
|
||||
conn: conn,
|
||||
rd: bufio.NewReaderSize(conn, core.BufferSize),
|
||||
}
|
||||
|
||||
if args := strings.Split(u.Path, "/"); len(args) >= 2 {
|
||||
rd.app = args[1]
|
||||
if len(args) >= 3 {
|
||||
rd.stream = args[2]
|
||||
if u.RawQuery != "" {
|
||||
rd.stream += "?" + u.RawQuery
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := rd.handshake(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rd.sendConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rd.sendConnect(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rd.sendPlay(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rd.buf = []byte{
|
||||
'F', 'L', 'V', // signature
|
||||
1, // version
|
||||
0, // flags (has video/audio)
|
||||
0, 0, 0, 9, // header size
|
||||
}
|
||||
|
||||
return rd, nil
|
||||
}
|
||||
|
||||
func (c *Reader) Read(p []byte) (n int, err error) {
|
||||
// 1. Check temporary tempbuffer
|
||||
if len(c.buf) == 0 {
|
||||
msgType, timeMS, payload, err2 := c.readMessage()
|
||||
if err2 != nil {
|
||||
return 0, err2
|
||||
}
|
||||
|
||||
payloadSize := len(payload)
|
||||
|
||||
// previous tag size (4 byte) + header (11 byte) + payload
|
||||
n = 4 + 11 + payloadSize
|
||||
|
||||
// 2. Check if the message fits in the buffer
|
||||
if n <= len(p) {
|
||||
encodeFLV(p, msgType, timeMS, uint32(payloadSize), payload)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Put the message into a temporary buffer
|
||||
c.buf = make([]byte, n)
|
||||
encodeFLV(c.buf, msgType, timeMS, uint32(payloadSize), payload)
|
||||
}
|
||||
|
||||
// 4. Send temporary buffer
|
||||
n = copy(p, c.buf)
|
||||
c.buf = c.buf[n:]
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Reader) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func encodeFLV(b []byte, msgType byte, time, size uint32, payload []byte) {
|
||||
b[0] = 0
|
||||
b[1] = 0
|
||||
b[2] = 0
|
||||
b[3] = 0
|
||||
b[4+0] = msgType
|
||||
PutUint24(b[4+1:], size)
|
||||
PutUint24(b[4+4:], time)
|
||||
b[4+7] = byte(time >> 24)
|
||||
|
||||
copy(b[4+11:], payload)
|
||||
}
|
||||
|
||||
type header struct {
|
||||
msgTime uint32
|
||||
msgSize uint32
|
||||
msgType byte
|
||||
}
|
||||
|
||||
func (c *Reader) readMessage() (byte, uint32, []byte, error) {
|
||||
hdrType, sid, err := c.readHeader()
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
// storing header information for support header type 3
|
||||
hdr, ok := c.headers[sid]
|
||||
if !ok {
|
||||
hdr = &header{}
|
||||
c.headers[sid] = hdr
|
||||
}
|
||||
|
||||
var b []byte
|
||||
|
||||
// https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol#Packet_structure
|
||||
switch hdrType {
|
||||
case 0: // 12 byte header (full header)
|
||||
if b, err = c.readSize(11); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
_ = b[7]
|
||||
hdr.msgTime = Uint24(b) // timestamp
|
||||
hdr.msgSize = Uint24(b[3:]) // msgdatalen
|
||||
hdr.msgType = b[6] // msgtypeid
|
||||
_ = binary.BigEndian.Uint32(b[7:]) // msgsid
|
||||
|
||||
case 1: // 8 bytes - like type b00, not including message ID (4 last bytes)
|
||||
if b, err = c.readSize(7); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
_ = b[6]
|
||||
hdr.msgTime = Uint24(b) // timestamp
|
||||
hdr.msgSize = Uint24(b[3:]) // msgdatalen
|
||||
hdr.msgType = b[6] // msgtypeid
|
||||
|
||||
case 2: // 4 bytes - Basic Header and timestamp (3 bytes) are included
|
||||
if b, err = c.readSize(3); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
hdr.msgTime = Uint24(b) // timestamp
|
||||
|
||||
case 3: // 1 byte - only the Basic Header is included
|
||||
// use here hdr from previous msg with same session ID (sid)
|
||||
}
|
||||
|
||||
timeMS := hdr.msgTime
|
||||
if timeMS == 0xFFFFFF {
|
||||
if b, err = c.readSize(4); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
timeMS = binary.BigEndian.Uint32(b)
|
||||
}
|
||||
|
||||
//log.Printf("[Reader] hdrType=%d sid=%d msdTime=%d msgSize=%d msgType=%d", hdrType, sid, hdr.msgTime, hdr.msgSize, hdr.msgType)
|
||||
|
||||
// 1. Response zero size
|
||||
if hdr.msgSize == 0 {
|
||||
return hdr.msgType, timeMS, nil, nil
|
||||
}
|
||||
|
||||
b = make([]byte, hdr.msgSize)
|
||||
|
||||
// 2. Response small packet
|
||||
if c.pktSize == 0 || hdr.msgSize < c.pktSize {
|
||||
if _, err = io.ReadFull(c.rd, b); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
return hdr.msgType, timeMS, b, nil
|
||||
}
|
||||
|
||||
// 3. Response big packet
|
||||
var i0 uint32
|
||||
for i1 := c.pktSize; i1 < hdr.msgSize; i1 += c.pktSize {
|
||||
if _, err = io.ReadFull(c.rd, b[i0:i1]); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
if _, _, err = c.readHeader(); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
if hdr.msgTime == 0xFFFFFF {
|
||||
if _, err = c.readSize(4); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
i0 = i1
|
||||
}
|
||||
|
||||
if _, err = io.ReadFull(c.rd, b[i0:]); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
return hdr.msgType, timeMS, b, nil
|
||||
}
|
||||
|
||||
func (c *Reader) handshake() error {
|
||||
// simple handshake without real random and check response
|
||||
const randomSize = 4 + 4 + 1528
|
||||
|
||||
b := make([]byte, 1+randomSize)
|
||||
b[0] = 0x03
|
||||
if _, err := c.conn.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b[0] != 3 {
|
||||
return errors.New("Reader: wrong handshake")
|
||||
}
|
||||
|
||||
if _, err := c.conn.Write(b[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(c.rd, b[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Reader) sendConfig() error {
|
||||
b := make([]byte, 5)
|
||||
binary.BigEndian.PutUint32(b, 65536)
|
||||
if err := c.sendRequest(MsgSetPacketSize, 0, b[:4]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint32(b, 2500000)
|
||||
if err := c.sendRequest(MsgServerBandwidth, 0, b[:4]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint32(b, 10000000) // ack size
|
||||
b[4] = 2 // limit type
|
||||
if err := c.sendRequest(MsgClientBandwidth, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Reader) sendConnect() error {
|
||||
msg := amf.AMF{}
|
||||
msg.WriteString("connect")
|
||||
msg.WriteNumber(1)
|
||||
msg.WriteObject(map[string]any{
|
||||
"app": c.app,
|
||||
"flashVer": "MAC 32,0,0,0",
|
||||
"tcUrl": c.url,
|
||||
"fpad": false, // ?
|
||||
"capabilities": 15, // ?
|
||||
"audioCodecs": 4071, // ?
|
||||
"videoCodecs": 252, // ?
|
||||
"videoFunction": 1, // ?
|
||||
})
|
||||
|
||||
if err := c.sendRequest(MsgCommand, 0, msg.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := c.waitCode("_result", float64(1)) // result with same ID
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s != "NetConnection.Connect.Success" {
|
||||
return errors.New("Reader: wrong code: " + s)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Reader) sendPlay() error {
|
||||
msg := amf.NewWriter()
|
||||
msg.WriteString("createStream")
|
||||
msg.WriteNumber(2)
|
||||
msg.WriteNull()
|
||||
|
||||
if err := c.sendRequest(MsgCommand, 0, msg.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args, err := c.waitResponse("_result", float64(2)) // result with same ID
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) < 4 {
|
||||
return ErrResponse
|
||||
}
|
||||
|
||||
sid, _ := args[3].(float64)
|
||||
|
||||
msg = amf.NewWriter()
|
||||
msg.WriteString("play")
|
||||
msg.WriteNumber(0)
|
||||
msg.WriteNull()
|
||||
msg.WriteString(c.stream)
|
||||
|
||||
if err = c.sendRequest(MsgCommand, uint32(sid), msg.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := c.waitCode("onStatus", float64(0)) // events has zero transaction ID
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch s {
|
||||
case "NetStream.Play.Start", "NetStream.Play.Reset":
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("Reader: wrong code: " + s)
|
||||
}
|
||||
|
||||
func (c *Reader) sendRequest(msgType byte, streamID uint32, payload []byte) error {
|
||||
n := len(payload)
|
||||
b := make([]byte, 12+n)
|
||||
_ = b[12]
|
||||
|
||||
switch msgType {
|
||||
case MsgSetPacketSize, MsgServerBandwidth, MsgClientBandwidth:
|
||||
b[0] = 0x02 // chunk ID
|
||||
case MsgCommand:
|
||||
if streamID == 0 {
|
||||
b[0] = 0x03 // chunk ID
|
||||
} else {
|
||||
b[0] = 0x08 // chunk ID
|
||||
}
|
||||
}
|
||||
|
||||
//PutUint24(b[1:], 0) // timestamp
|
||||
PutUint24(b[4:], uint32(n)) // payload size
|
||||
b[7] = msgType // message type
|
||||
binary.BigEndian.PutUint32(b[8:], streamID) // message stream ID
|
||||
copy(b[12:], payload)
|
||||
|
||||
if _, err := c.conn.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Reader) readHeader() (byte, uint32, error) {
|
||||
b, err := c.readSize(1)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
hdrType := b[0] >> 6
|
||||
sid := uint32(b[0] & 0b111111)
|
||||
|
||||
switch sid {
|
||||
case 0:
|
||||
if b, err = c.readSize(1); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
sid = 64 + uint32(b[0])
|
||||
case 1:
|
||||
if b, err = c.readSize(2); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
sid = 64 + uint32(binary.BigEndian.Uint16(b))
|
||||
}
|
||||
|
||||
return hdrType, sid, nil
|
||||
}
|
||||
|
||||
func (c *Reader) readSize(n uint32) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
if _, err := io.ReadAtLeast(c.rd, b, int(n)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (c *Reader) waitResponse(cmd any, tid any) ([]any, error) {
|
||||
for {
|
||||
msgType, _, b, err := c.readMessage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch msgType {
|
||||
case MsgSetPacketSize:
|
||||
c.pktSize = binary.BigEndian.Uint32(b)
|
||||
|
||||
case MsgCommand:
|
||||
var v []any
|
||||
if v, err = amf.NewReader(b).ReadItems(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(v) < 4 {
|
||||
return nil, ErrResponse
|
||||
}
|
||||
|
||||
if v[0] == cmd && v[1] == tid {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Reader) waitCode(cmd any, tid any) (string, error) {
|
||||
args, err := c.waitResponse(cmd, tid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(args) < 4 {
|
||||
return "", ErrResponse
|
||||
}
|
||||
|
||||
m, _ := args[3].(map[string]any)
|
||||
if m == nil {
|
||||
return "", ErrResponse
|
||||
}
|
||||
|
||||
v, _ := m["code"]
|
||||
if v == nil {
|
||||
return "", ErrResponse
|
||||
}
|
||||
|
||||
s, _ := v.(string)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func PutUint24(b []byte, v uint32) {
|
||||
_ = b[2]
|
||||
b[0] = byte(v >> 16)
|
||||
b[1] = byte(v >> 8)
|
||||
b[2] = byte(v)
|
||||
}
|
||||
|
||||
func Uint24(b []byte) uint32 {
|
||||
_ = b[2]
|
||||
return uint32(b[0])<<16 | uint32(b[1])<<8 | uint32(b[2])
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
|
||||
)
|
||||
|
||||
func NewServer(conn net.Conn) (*Conn, error) {
|
||||
c := &Conn{
|
||||
conn: conn,
|
||||
rd: bufio.NewReaderSize(conn, core.BufferSize),
|
||||
wr: conn,
|
||||
|
||||
chunks: map[uint8]*header{},
|
||||
|
||||
rdPacketSize: 128,
|
||||
wrPacketSize: 4096,
|
||||
}
|
||||
|
||||
if err := c.serverHandshake(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.writePacketSize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Conn) serverHandshake() error {
|
||||
b := make([]byte, 1+1536)
|
||||
// read C0+C1
|
||||
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||
return err
|
||||
}
|
||||
// write S0+S1, skip random
|
||||
if _, err := c.conn.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
// read S1, skip check
|
||||
if _, err := io.ReadFull(c.rd, make([]byte, 1536)); err != nil {
|
||||
return err
|
||||
}
|
||||
// write C1
|
||||
if _, err := c.conn.Write(b[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) ReadCommands() error {
|
||||
for {
|
||||
msgType, _, b, err := c.readMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//log.Printf("%d %.256x", msgType, b)
|
||||
|
||||
switch msgType {
|
||||
case TypeSetPacketSize:
|
||||
c.rdPacketSize = binary.BigEndian.Uint32(b)
|
||||
case TypeCommand:
|
||||
if err = c.acceptCommand(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Intent != "" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
CommandConnect = "connect"
|
||||
CommandReleaseStream = "releaseStream"
|
||||
CommandFCPublish = "FCPublish"
|
||||
CommandCreateStream = "createStream"
|
||||
CommandPublish = "publish"
|
||||
CommandPlay = "play"
|
||||
)
|
||||
|
||||
func (c *Conn) acceptCommand(b []byte) error {
|
||||
items, err := amf.NewReader(b).ReadItems()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
//log.Printf("%#v", items)
|
||||
|
||||
if len(items) < 2 {
|
||||
return fmt.Errorf("rtmp: read command %x", b)
|
||||
}
|
||||
|
||||
cmd, ok := items[0].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("rtmp: read command %x", b)
|
||||
}
|
||||
|
||||
tID, ok := items[1].(float64) // transaction ID
|
||||
if !ok {
|
||||
return fmt.Errorf("rtmp: read command %x", b)
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case CommandConnect:
|
||||
if len(items) == 3 {
|
||||
if v, ok := items[2].(map[string]any); ok {
|
||||
c.App, _ = v["app"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if c.App == "" {
|
||||
return fmt.Errorf("rtmp: read command %x", b)
|
||||
}
|
||||
|
||||
payload := amf.EncodeItems(
|
||||
"_result", tID,
|
||||
map[string]any{"fmsVer": "FMS/3,0,1,123"},
|
||||
map[string]any{"code": "NetConnection.Connect.Success"},
|
||||
)
|
||||
return c.writeMessage(3, TypeCommand, 0, payload)
|
||||
|
||||
case CommandReleaseStream:
|
||||
payload := amf.EncodeItems("_result", tID, nil)
|
||||
return c.writeMessage(3, TypeCommand, 0, payload)
|
||||
|
||||
case CommandCreateStream:
|
||||
payload := amf.EncodeItems("_result", tID, nil, 1)
|
||||
return c.writeMessage(3, TypeCommand, 0, payload)
|
||||
|
||||
case CommandPublish, CommandPlay: // response later
|
||||
c.Intent = cmd
|
||||
c.streamID = 1
|
||||
|
||||
case CommandFCPublish: // no response
|
||||
|
||||
default:
|
||||
println("rtmp: unknown command: " + cmd)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) WriteStart() error {
|
||||
var code string
|
||||
if c.Intent == CommandPublish {
|
||||
code = "NetStream.Publish.Start"
|
||||
} else {
|
||||
code = "NetStream.Play.Start"
|
||||
}
|
||||
|
||||
payload := amf.EncodeItems("onStatus", 0, nil, map[string]any{"code": code})
|
||||
return c.writeMessage(3, TypeCommand, 0, payload)
|
||||
}
|
||||
+1
-1
@@ -35,7 +35,7 @@ func (c *Conn) Dial() (err error) {
|
||||
if c.Timeout != 0 {
|
||||
timeout = time.Second * time.Duration(c.Timeout)
|
||||
}
|
||||
conn, err = tcp.Dial(c.URL, "554", timeout)
|
||||
conn, err = tcp.Dial(c.URL, timeout)
|
||||
} else {
|
||||
conn, err = websocket.Dial(c.Transport)
|
||||
}
|
||||
|
||||
+5
-5
@@ -111,12 +111,12 @@ func (c *Conn) Handle() (err error) {
|
||||
|
||||
if c.Timeout == 0 {
|
||||
// polling frames from remote RTSP Server (ex Camera)
|
||||
if len(c.receivers) > 0 {
|
||||
// if we receiving video/audio from camera
|
||||
timeout = time.Second * 5
|
||||
} else {
|
||||
timeout = time.Second * 5
|
||||
|
||||
if len(c.receivers) == 0 {
|
||||
// if we only send audio to camera
|
||||
timeout = time.Second * 30
|
||||
// https://github.com/AlexxIT/go2rtc/issues/659
|
||||
timeout += keepaliveDT
|
||||
}
|
||||
} else {
|
||||
timeout = time.Second * time.Duration(c.Timeout)
|
||||
|
||||
+2
-3
@@ -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')
|
||||
|
||||
+26
-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,27 @@ 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)
|
||||
}
|
||||
|
||||
+37
-25
@@ -2,7 +2,9 @@ package shell
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -12,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
|
||||
}
|
||||
|
||||
@@ -75,3 +70,20 @@ func RunUntilSignal() {
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
println("exit with signal:", (<-sigs).String())
|
||||
}
|
||||
|
||||
// Restart idea taken from https://github.com/tillberg/autorestart
|
||||
// Copyright (c) 2015, Dan Tillberg
|
||||
func Restart() {
|
||||
path, err := exec.LookPath(os.Args[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
path, err = filepath.Abs(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
path = filepath.Clean(path)
|
||||
if err = syscall.Exec(path, os.Args, os.Environ()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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="0" -i "DeckLink SDI (2)"`
|
||||
require.Equal(t, []string{"ffmpeg", "-i", "video=\"0\"", "-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()
|
||||
}
|
||||
|
||||
|
||||
+79
-20
@@ -1,10 +1,12 @@
|
||||
package tapo
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -14,6 +16,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
@@ -62,33 +65,19 @@ func (c *Client) newConn() (net.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// support raw username/password
|
||||
username := u.User.Username()
|
||||
password, _ := u.User.Password()
|
||||
|
||||
// or cloud password in place of username
|
||||
if password == "" {
|
||||
password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
|
||||
username = "admin"
|
||||
u.User = url.UserPassword(username, password)
|
||||
}
|
||||
|
||||
u.Scheme = "http"
|
||||
u.Path = "/stream"
|
||||
if u.Port() == "" {
|
||||
u.Host += ":8800"
|
||||
}
|
||||
|
||||
// TODO: fix closing connection
|
||||
ctx, pconn := tcp.WithConn()
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", u.String(), nil)
|
||||
req, err := http.NewRequest("POST", "http://"+u.Host+"/stream", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.URL.User = u.User
|
||||
req.Header.Set("Content-Type", "multipart/mixed; boundary=--client-stream-boundary--")
|
||||
|
||||
res, err := tcp.Do(req)
|
||||
conn, res, err := dial(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -98,13 +87,16 @@ func (c *Client) newConn() (net.Conn, error) {
|
||||
}
|
||||
|
||||
if c.decrypt == nil {
|
||||
c.newDectypter(res, username, password)
|
||||
c.newDectypter(res)
|
||||
}
|
||||
|
||||
return *pconn, nil
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *Client) newDectypter(res *http.Response, username, password string) {
|
||||
func (c *Client) newDectypter(res *http.Response) {
|
||||
username := res.Request.URL.User.Username()
|
||||
password, _ := res.Request.URL.User.Password()
|
||||
|
||||
// extract nonce from response
|
||||
// cipher="AES_128_CBC" username="admin" padding="PKCS7_16" algorithm="MD5" nonce="***"
|
||||
nonce := res.Header.Get("Key-Exchange")
|
||||
@@ -244,3 +236,70 @@ func (c *Client) Request(conn net.Conn, body []byte) (string, error) {
|
||||
return v.Params.SessionID, nil
|
||||
}
|
||||
}
|
||||
|
||||
func dial(req *http.Request) (net.Conn, *http.Response, error) {
|
||||
conn, err := net.DialTimeout("tcp", req.URL.Host, core.ConnDialTimeout)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
username := req.URL.User.Username()
|
||||
password, _ := req.URL.User.Password()
|
||||
req.URL.User = nil
|
||||
|
||||
if err = req.Write(conn); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
r := bufio.NewReader(conn)
|
||||
|
||||
res, err := http.ReadResponse(r, req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
auth := res.Header.Get("WWW-Authenticate")
|
||||
|
||||
if res.StatusCode != http.StatusUnauthorized || !strings.HasPrefix(auth, "Digest") {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if password == "" {
|
||||
// support cloud password in place of username
|
||||
if strings.Contains(auth, `encrypt_type="3"`) {
|
||||
password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username)))
|
||||
} else {
|
||||
password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
|
||||
}
|
||||
username = "admin"
|
||||
}
|
||||
|
||||
realm := tcp.Between(auth, `realm="`, `"`)
|
||||
nonce := tcp.Between(auth, `nonce="`, `"`)
|
||||
qop := tcp.Between(auth, `qop="`, `"`)
|
||||
uri := req.URL.RequestURI()
|
||||
ha1 := tcp.HexMD5(username, realm, password)
|
||||
ha2 := tcp.HexMD5(req.Method, uri)
|
||||
nc := "00000001"
|
||||
cnonce := "00000001"
|
||||
response := tcp.HexMD5(ha1, nonce, nc, cnonce, qop, ha2)
|
||||
|
||||
header := fmt.Sprintf(
|
||||
`Digest username="%s", realm="%s", nonce="%s", uri="%s", qop=%s, nc=%s, cnonce="%s", response="%s"`,
|
||||
username, realm, nonce, uri, qop, nc, cnonce, response,
|
||||
)
|
||||
|
||||
req.Header.Set("Authorization", header)
|
||||
|
||||
if err = req.Write(conn); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if res, err = http.ReadResponse(r, req); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req.URL.User = url.UserPassword(username, password)
|
||||
|
||||
return conn, res, nil
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
if c.sender == nil {
|
||||
if err := c.SetupBackchannel(); err != nil {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
muxer := mpegts.NewMuxer()
|
||||
|
||||
+12
-3
@@ -10,13 +10,22 @@ import (
|
||||
)
|
||||
|
||||
// Dial - for RTSP(S|X) and RTMP(S|X)
|
||||
func Dial(u *url.URL, port string, timeout time.Duration) (net.Conn, error) {
|
||||
func Dial(u *url.URL, timeout time.Duration) (net.Conn, error) {
|
||||
var address string
|
||||
var hostname string // without port
|
||||
if i := strings.IndexByte(u.Host, ':'); i > 0 {
|
||||
address = u.Host
|
||||
hostname = u.Host[:i]
|
||||
} else {
|
||||
switch u.Scheme {
|
||||
case "rtsp", "rtsps", "rtspx":
|
||||
address = u.Host + ":554"
|
||||
case "rtmp":
|
||||
address = u.Host + ":1935"
|
||||
case "rtmps", "rtmpx":
|
||||
address = u.Host + ":443"
|
||||
}
|
||||
hostname = u.Host
|
||||
u.Host += ":" + port
|
||||
}
|
||||
|
||||
var secure *tls.Config
|
||||
@@ -33,7 +42,7 @@ func Dial(u *url.URL, port string, timeout time.Duration) (net.Conn, error) {
|
||||
return nil, errors.New("unsupported scheme: " + u.Scheme)
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", u.Host, timeout)
|
||||
conn, err := net.DialTimeout("tcp", address, timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user