Compare commits

...

89 Commits

Author SHA1 Message Date
Alex X ccec41a10f Update version to 1.8.5 2024-01-01 09:34:44 +03:00
Alex X 9feb98db3f Fix panic on reconnect #828 2024-01-01 09:30:40 +03:00
Alex X a724c5f3ce Fix support Aqara G2H #793 2024-01-01 09:24:43 +03:00
Alex X c60767c8b0 Add support H265 to FLV source #822 2023-12-31 21:06:41 +03:00
Alex X ae13a72fde Fix mdns log message #843 2023-12-30 20:44:50 +03:00
Alex X 458d5e7d0d Add error for wrong homekit source #805 2023-12-30 20:43:40 +03:00
Alex X 89e15d9b57 Add support subtype for Tapo source #792 2023-12-30 13:04:53 +03:00
Alex X 0d2292c311 Add test for issue #825 2023-12-28 16:49:23 +03:00
Alex X 62343af009 Update dependencies 2023-12-28 16:49:09 +03:00
Alex X c8c3b22d19 Fix memory allocation for HomeKit OPUS 2023-12-28 11:54:38 +03:00
Alex X 853e98879b Fix OPUS for HomeKit server #667 #843 2023-12-27 23:05:45 +03:00
Alex X bf5cb33385 Add OpenIPC to readme 2023-12-22 11:33:53 +03:00
Alex X 7ad4d350f8 Fix hardware profiles for H265 templates #809 2023-12-17 18:07:57 +03:00
Alex X c63fc6a2ad Fix exec source leaves zombie processes after fail #814 2023-12-17 17:59:41 +03:00
Alex X 7036d196be Fix H265 support from OpenIPC project 2023-12-15 12:42:07 +03:00
Alex X d3bc18c369 Logs refactoring after #780 2023-12-11 18:07:38 +03:00
Alex X 1f3a32023f Merge pull request #780 from 'skrashevich/log-viewer' 2023-12-11 18:07:25 +03:00
Alex X a46bad0522 Add support hardware resize for Rockchip 2023-12-10 15:58:54 +03:00
Alex X d0dfa1d3dd Add support OPUS inside MPEG-TS 2023-12-10 15:56:53 +03:00
Sergey Krashevich fc5b36acd3 actualise godoc comment for api.logHandler func 2023-12-05 17:54:53 +03:00
Sergey Krashevich 0a8ab9bbd1 Update app.go to remove the unused variable LogFilePath 2023-12-05 17:50:51 +03:00
Sergey Krashevich b60000ac34 Refactor log handling to use in-memory Logger 2023-12-05 17:44:32 +03:00
Alex X 39d87625d7 Merge pull request #798 from MPTres/cors
Fix CORS support in WHEP/WHIP API
2023-12-05 16:16:07 +03:00
MPTres 0da8b46148 remove empty lines. 2023-12-05 13:52:34 +01:00
Alex X 8d9f87061c Add support hardware auto discovery for Rockchip 2023-12-05 15:43:50 +03:00
Alex X 4bdfa62039 Code refactoring for ffmpeg hardware linux 2023-12-05 15:43:14 +03:00
Alex X 67ea2d9d02 Fix support FFmpeg device on Windows 2023-12-05 15:40:16 +03:00
MPTres 39b614fb0f Remove X-PINGOTHER from allowed headers. 2023-12-05 13:37:05 +01:00
MPTres 84469dcd25 CORS. Add support for OPTIONS requests. 2023-12-04 17:14:18 +01:00
Alex X eceb4a476f Add support Rockchip hardware transcoding 2023-12-04 16:54:50 +03:00
Alex X 051a4eabd7 Change example for publish 2023-11-30 15:52:10 +03:00
Alex X e68a304698 Add about new tapo password to readme 2023-11-30 15:51:49 +03:00
Alex X 2e6c6b1d41 Add "new in version" to readme 2023-11-30 13:43:07 +03:00
Alex X 0def6f8de9 Merge pull request #785 from skrashevich/exit-code-check
Ensure exit code is within valid range
2023-11-29 10:26:02 +03:00
Sergey Krashevich 7ac5b4f114 Ensure exit code is within valid range
The exitHandler function now properly validates the exit code provided
in the query string. It checks for conversion errors and ensures the
code is within the valid range of 0 to 125. If the validation fails,
it responds with an HTTP 400 Bad Request error. This prevents potential
misuse of the exit endpoint by restricting the exit codes to expected
values.
2023-11-29 10:03:39 +03:00
Sergey Krashevich ab47d5718f Refactor log handling and add UI auto-update toggle
This commit refactors the log handling in the API to use a switch statement for improved readability and maintainability. It also introduces error messages with more context when reading or truncating the log file fails.

On the frontend, a new auto-update toggle button has been added to the log viewer, allowing users to enable or disable automatic log updates. The button's appearance changes based on its state, providing a clear visual indication of whether auto-update is active. Additionally, the button styling has been updated to ensure consistency across the interface.
2023-11-28 22:55:50 +03:00
Alex X 94aced0fc0 Merge pull request #782 from miguelangel-nubla/patch-1
Typo in codec name
2023-11-28 18:45:55 +03:00
Miguel Angel Nubla 66a4c3d06e Typo in codec name 2023-11-28 12:37:27 +01:00
Sergey Krashevich 8d382afa0f Add log file handling and viewing capabilities
This commit introduces the ability to handle log files through the API and
provides a new log viewing page. The API now supports GET and DELETE methods
for log file operations, allowing retrieval and deletion of log contents.
A new log.html page has been added for viewing logs in the browser, with
automatic refresh every 5 seconds and styling based on log levels.

The app.go file has been updated to include a GetLogFilepath function that
retrieves or generates the log file path. The NewLogger function now accepts
a file parameter to enable file logging. The main.js file has been updated
to include a link to the new log.html page.

This enhancement improves the observability and management of the application
by providing real-time access to logs and the ability to clear them directly
from the web interface.
2023-11-26 23:21:57 +03:00
Alex X 051c5ff913 Fix buggy SDP from D-Link cameras #771 2023-11-22 17:42:41 +03:00
Alex X a87dafbbec Update version to 1.8.4 2023-11-19 18:38:26 +03:00
Alex X 742cb7699b Improve magic producer about support mjpeg with trash on start 2023-11-18 17:08:57 +03:00
Alex X 43449e7b08 Fix api port for homekit module 2023-11-18 11:48:19 +03:00
Alex X 33512e73bd Add support ADTS to magic producer 2023-11-17 22:28:28 +03:00
Alex X b367ffee6d Merge pull request #759 from russorat/ror/ngrok
fix: updating ngrok readme
2023-11-17 13:59:12 +03:00
Russ Savage 69447df6b3 fix: updating ngrok readme
Signed-off-by: Russ Savage <russorat@users.noreply.github.com>
2023-11-16 21:16:53 -08:00
Alex X a6eac4ff02 Merge pull request #754 from inode64/master
Include support for Gentoo distribution
2023-11-15 21:20:46 +03:00
INODE64 1eaf879a76 Include support for Gentoo distribution 2023-11-15 17:45:36 +01:00
Alex X c9ae6dcc03 Fix https source, again 2023-11-15 17:31:59 +03:00
Alex X befa6bd356 Update version to 1.8.3 2023-11-15 12:20:47 +03:00
Alex X 100ab62ab4 Update dependencies 2023-11-15 12:16:34 +03:00
Alex X a0f999d9c9 Add readme about gopro source 2023-11-15 12:10:16 +03:00
Alex X 9bda2f7e60 Add support gopro source 2023-11-15 11:41:42 +03:00
Alex X 54b19999c6 Fix support raw username/password for tapo source #748 2023-11-14 14:22:17 +03:00
Alex X aa3c081352 Add support incoming H264 bitstream #745 2023-11-13 22:56:07 +03:00
Alex X 2d16ee8884 Code refactoring for mpegts input 2023-11-13 22:55:12 +03:00
Alex X ec96a14807 Fix digest auth in some cases 2023-11-13 22:44:28 +03:00
Alex X af72548a43 Fix panic for broken RTP with AAC #697 2023-11-13 21:56:35 +03:00
Alex X 6d85b36f47 Fix homekit source panic on stop producer #734 2023-11-13 21:51:52 +03:00
Alex X 28830a697d Add support unix socket for api module 2023-11-12 21:50:29 +03:00
Alex X 5d3953a948 Fix support Tapo C210 firmware v1.3.9 #733 2023-11-12 16:41:43 +03:00
Alex X 4d6432d38d Add about expr source to readme 2023-11-11 15:21:24 +03:00
Alex X bcbebd5a36 Fix custom https client 2023-11-05 08:39:07 +03:00
Alex X 50e2a626a6 Update version to 1.8.2 2023-11-04 18:49:56 +03:00
Alex X f4fe8c3769 Increase ProbeSize up to 5MB 2023-11-04 15:14:23 +03:00
Alex X e42085a237 Remove lock on sender buffer processing 2023-11-04 15:14:04 +03:00
Alex X a060b3447c Increase buffer for RTSP input 2023-11-04 15:13:39 +03:00
Alex X d7784b24c6 Fix memory overflow on bad RTSP sources #675 2023-11-04 09:42:50 +03:00
Alex X 39645cb3d8 Remove unnecessary 0.0.0.0 from listeners 2023-11-03 12:45:40 +03:00
Alex X 36166caccc Fix raw conn for https client 2023-11-03 12:40:42 +03:00
Alex X 0f1dc73d55 Update WebRTC candidates logic 2023-11-03 11:13:54 +03:00
Alex X 6b29c37433 Update webrtc trace logs for local candidates 2023-11-02 14:58:30 +03:00
Alex X 535bacf9d6 Fix ngrok 2023-11-02 14:57:52 +03:00
Alex X e6fb4081f7 Add drawtext tests for ffmpeg 2023-11-02 14:57:22 +03:00
Alex X eb04fafaa4 Add more ffmpeg transcoding presets 2023-11-02 14:56:58 +03:00
Alex X b4ed738d17 Add IPv6 support to WebRTC #721 2023-10-30 21:18:09 +03:00
Alex X 6a9ae93fa1 Update pixel format for h264 vaapi hardware 2023-10-30 19:06:56 +03:00
Alex X 2dd47654e6 Fix panic for HomeKit source without SRTP module #712 2023-10-27 17:08:07 +03:00
Alex X c27e735c17 Fix wrong SDP for MERCURY camera #708 2023-10-27 14:37:12 +03:00
Alex X 8bc65e4c91 Update codecs table in readme 2023-10-27 07:41:00 +03:00
Alex X 0a476a74b3 Add QNAP to readme 2023-10-27 07:19:48 +03:00
Alex X b5be4ce03b Add expr source 2023-10-26 21:07:48 +03:00
Alex X f291f1d827 Rewrite shell cmd parser 2023-10-25 16:49:01 +03:00
Alex X 041ce885c7 Merge pull request #704 from testwill/map
chore: unnecessary guard around call to delete
2023-10-24 12:09:15 +04:00
Alex X df16f28825 Update poster 2023-10-24 10:52:47 +03:00
Alex X a8867bc3cb Add Synology NAS to readme 2023-10-24 10:34:55 +03:00
Alex X b2b115ec9c Merge pull request #705 from skrashevich/openapi-add-restart-handler
add restart handler to openapi spec
2023-10-19 16:51:43 +03:00
Sergey Krashevich 95de3a1f3e Update openapi.yaml 2023-10-19 16:40:14 +03:00
guoguangwu dd4376cd37 chore: unnecessary guard around call to delete 2023-10-19 21:21:09 +08:00
80 changed files with 2197 additions and 504 deletions
+105 -39
View File
@@ -23,7 +23,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- mixing tracks from different sources to single stream
- auto match client supported codecs
- [2-way audio](#two-way-audio) for some cameras
- streaming from private networks via [Ngrok](#module-ngrok)
- streaming from private networks via [ngrok](#module-ngrok)
- can be [integrated to](#module-api) any smart home platform or be used as [standalone app](#go2rtc-binary)
**Inspired by:**
@@ -54,11 +54,13 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
* [Source: FFmpeg Device](#source-ffmpeg-device)
* [Source: Exec](#source-exec)
* [Source: Echo](#source-echo)
* [Source: Expr](#source-expr)
* [Source: HomeKit](#source-homekit)
* [Source: Bubble](#source-bubble)
* [Source: DVRIP](#source-dvrip)
* [Source: Tapo](#source-tapo)
* [Source: Kasa](#source-kasa)
* [Source: GoPro](#source-gopro)
* [Source: Ivideon](#source-ivideon)
* [Source: Hass](#source-hass)
* [Source: ISAPI](#source-isapi)
@@ -75,7 +77,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
* [Module: WebRTC](#module-webrtc)
* [Module: HomeKit](#module-homekit)
* [Module: WebTorrent](#module-webtorrent)
* [Module: Ngrok](#module-ngrok)
* [Module: ngrok](#module-ngrok)
* [Module: Hass](#module-hass)
* [Module: MP4](#module-mp4)
* [Module: HLS](#module-hls)
@@ -125,7 +127,7 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
### go2rtc: Docker
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [Ngrok](#module-ngrok) and [Python](#source-echo).
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [ngrok](#module-ngrok) and [Python](#source-echo).
### go2rtc: Home Assistant Add-on
@@ -168,7 +170,7 @@ Available modules:
- [hls](#module-hls) - HLS TS or fMP4 stream Server
- [mjpeg](#module-mjpeg) - MJPEG Server
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
- [ngrok](#module-ngrok) - ngrok integration (external access for private network)
- [hass](#module-hass) - Home Assistant integration
- [log](#module-log) - logs config
@@ -186,11 +188,13 @@ Available source types:
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
- [exec](#source-exec) - get media from external app output
- [echo](#source-echo) - get stream link from bash or python
- [expr](#source-expr) - get stream link via built-in expression language
- [homekit](#source-homekit) - streaming from HomeKit Camera
- [bubble](#source-bubble) - streaming from ESeeCloud/dvr163 NVR
- [dvrip](#source-dvrip) - streaming from DVR-IP NVR
- [tapo](#source-tapo) - TP-Link Tapo cameras with [two way audio](#two-way-audio) support
- [kasa](#source-tapo) - TP-Link Kasa cameras
- [gopro](#source-gopro) - GoPro cameras
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
- [hass](#source-hass) - Home Assistant integration
- [isapi](#source-isapi) - two way audio for Hikvision (ISAPI) cameras
@@ -301,6 +305,8 @@ streams:
#### Source: ONVIF
*[New in v1.5.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.5.0)*
The source is not very useful if you already know RTSP and snapshot links for your camera. But it can be useful if you don't.
**WebUI > Add** webpage support ONVIF autodiscovery. Your server must be on the same subnet as the camera. If you use docker, you must use "network host".
@@ -393,7 +399,7 @@ streams:
#### Source: Exec
Exec source can run any external application and expect data from it. Two transports are supported - **pipe** and **RTSP**.
Exec source can run any external application and expect data from it. Two transports are supported - **pipe** (*from [v1.5.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.5.0)*) and **RTSP**.
If you want to use **RTSP** transport - the command must contain the `{output}` argument in any place. On launch, it will be replaced by the local address of the RTSP server.
@@ -426,6 +432,12 @@ streams:
apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html
```
#### Source: Expr
*[New in v1.8.2](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.2)*
Like `echo` source, but uses the built-in [expr](https://github.com/antonmedv/expr) expression language ([read more](https://github.com/AlexxIT/go2rtc/blob/master/internal/expr/README.md)).
#### Source: HomeKit
**Important:**
@@ -461,6 +473,8 @@ RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g
#### Source: Bubble
*[New in v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*
Other names: [ESeeCloud](http://www.eseecloud.com/), [dvr163](http://help.dvr163.com/).
- you can skip `username`, `password`, `port`, `ch` and `stream` if they are default
@@ -473,6 +487,8 @@ streams:
#### Source: DVRIP
*[New in v1.2.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.2.0)*
Other names: DVR-IP, NetSurveillance, Sofia protocol (NETsurveillance ActiveX plugin XMeye SDK).
- you can skip `username`, `password`, `port`, `channel` and `subtype` if they are default
@@ -491,22 +507,34 @@ streams:
#### Source: Tapo
*[New in v1.2.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.2.0)*
[TP-Link Tapo](https://www.tapo.com/) proprietary camera protocol with **two way audio** support.
- stream quality is the same as [RTSP protocol](https://www.tapo.com/en/faq/34/)
- use the **cloud password**, this is not the RTSP password! you do not need to add a login!
- you can also use UPPERCASE MD5 hash from your cloud password with `admin` username
- some new camera firmwares requires SHA256 instead of MD5
```yaml
streams:
# cloud password without username
camera1: tapo://cloud-password@192.168.1.123
# admin username and UPPERCASE MD5 cloud-password hash
camera2: tapo://admin:MD5-PASSWORD-HASH@192.168.1.123
camera2: tapo://admin:UPPERCASE-MD5@192.168.1.123
# admin username and UPPERCASE SHA256 cloud-password hash
camera3: tapo://admin:UPPERCASE-SHA256@192.168.1.123
```
```bash
echo -n "cloud password" | md5 | awk '{print toupper($0)}'
echo -n "cloud password" | shasum -a 256 | awk '{print toupper($0)}'
```
#### Source: Kasa
*[New in v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*
[TP-Link Kasa](https://www.kasasmart.com/) non-standard protocol [more info](https://medium.com/@hu3vjeen/reverse-engineering-tp-link-kc100-bac4641bf1cd).
```yaml
@@ -514,6 +542,12 @@ streams:
kasa: kasa://user:pass@192.168.1.123:19443/https/stream/mixed
```
#### Source: GoPro
*[New in v1.8.3](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.3)*
Support streaming from [GoPro](https://gopro.com/) cameras, connected via USB or Wi-Fi to Linux, Mac, Windows. [Read more](https://github.com/AlexxIT/go2rtc/tree/master/internal/gopro).
#### Source: Ivideon
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
@@ -541,7 +575,7 @@ streams:
aqara_g3: hass:Camera-Hub-G3-AB12
```
**WebRTC Cameras**
**WebRTC Cameras** (*from [v1.6.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)*)
Any cameras in WebRTC format are supported. But at the moment Home Assistant only supports some [Nest](https://www.home-assistant.io/integrations/nest/) cameras in this fomat.
@@ -561,6 +595,8 @@ By default, the Home Assistant API does not allow you to get dynamic RTSP link t
#### Source: ISAPI
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
This source type support only backchannel audio for Hikvision ISAPI protocol. So it should be used as second source in addition to the RTSP protocol.
```yaml
@@ -572,6 +608,8 @@ streams:
#### Source: Nest
*[New in v1.6.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)*
Currently only WebRTC cameras are supported. Stream reconnects every 5 minutes.
For simplicity, it is recommended to connect the Nest/WebRTC camera to the [Home Assistant](#source-hass). But if you can somehow get the below parameters - Nest/WebRTC source will work without Hass.
@@ -583,6 +621,8 @@ streams:
#### Source: Roborock
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
This source type support Roborock vacuums with cameras. Known working models:
- Roborock S6 MaxV - only video (the vacuum has no microphone)
@@ -594,6 +634,8 @@ If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456
#### Source: WebRTC
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
This source type support four connection formats.
**whep**
@@ -604,15 +646,15 @@ This source type support four connection formats.
This format is only supported in go2rtc. Unlike WHEP it supports asynchronous WebRTC connection and two way audio.
**openipc**
**openipc** (*from [v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*)
Support connection to [OpenIPC](https://openipc.org/) cameras.
**wyze**
**wyze** (*from [v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*)
Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC protocol. You can use [docker-wyze-bridge](https://github.com/mrlt8/docker-wyze-bridge) project to get connection credentials.
**kinesis**
**kinesis** (*from [v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*)
Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-streams/), using WebRTC protocol. You need to specify signalling WebSocket URL with all credentials in query params, `client_id` and `ice_servers` list in [JSON format](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer).
@@ -629,6 +671,8 @@ streams:
#### Source: WebTorrent
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
This source can get a stream from another go2rtc via [WebTorrent](#module-webtorrent) protocol.
```yaml
@@ -667,6 +711,8 @@ By default, go2rtc establishes a connection to the source when any client reques
#### Incoming: Browser
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
You can turn the browser of any PC or mobile into an IP-camera with support video and two way audio. Or even broadcast your PC screen:
1. Create empty stream in the `go2rtc.yaml`
@@ -677,12 +723,16 @@ You can turn the browser of any PC or mobile into an IP-camera with support vide
#### Incoming: WebRTC/WHIP
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
You can use **OBS Studio** or any other broadcast software with [WHIP](https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html) protocol support. This standard has not yet been approved. But you can download OBS Studio [dev version](https://github.com/obsproject/obs-studio/actions/runs/3969201209):
- Settings > Stream > Service: WHIP > http://192.168.1.123:1984/api/webrtc?dst=camera1
#### Stream to camera
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
go2rtc support play audio files (ex. music or [TTS](https://www.home-assistant.io/integrations/#text-to-speech)) and live streams (ex. radio) on cameras with [two way audio](#two-way-audio) support (RTSP/ONVIF cameras, TP-Link Tapo, Hikvision ISAPI, Roborock vacuums, any Browser).
API example:
@@ -703,6 +753,8 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
### Publish stream
*[New in v1.8.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.0)*
You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:
- Supported codecs: H264 for video and AAC for audio
@@ -728,7 +780,7 @@ publish:
streams:
# for TP-Link cameras it's important to use transcoding because of wrong pixel format
tplink_tapo: ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware=vaapi#audio=aac
tplink_tapo: ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware#audio=aac
```
- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.
@@ -797,6 +849,8 @@ Read more about [codecs filters](#codecs-filters).
### Module: RTMP
*[New in v1.8.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.0)*
You can get any stream as RTMP-stream: `rtmp://192.168.1.123/{stream_name}`. Only H264/AAC codecs supported right now.
[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has differnt problems with this format.
@@ -849,7 +903,7 @@ webrtc:
**Private IP**
- setup integration with [Ngrok service](#module-ngrok)
- setup integration with [ngrok service](#module-ngrok)
```yaml
ngrok:
@@ -875,6 +929,8 @@ webrtc:
### Module: HomeKit
*[New in v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*
HomeKit module can work in two modes:
- export any H264 camera to Apple HomeKit
@@ -927,6 +983,8 @@ homekit:
### Module: WebTorrent
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
This module support:
- Share any local stream via [WebTorrent](https://webtorrent.io/) technology
@@ -951,29 +1009,29 @@ Link example: https://alexxit.github.io/go2rtc/#share=02SNtgjKXY&pwd=wznEQqznxW&
TODO: article how it works...
### Module: Ngrok
### Module: ngrok
With Ngrok integration you can get external access to your streams in situation when you have Internet with private IP-address.
With ngrok integration you can get external access to your streams in situations when you have Internet with private IP-address.
- Ngrok preistalled for **Docker** and **Hass Add-on** users
- ngrok is pre-installed for **Docker** and **Hass Add-on** users
- you may need external access for two different things:
- WebRTC stream, so you need tunnel WebRTC TCP port (ex. 8555)
- go2rtc web interface, so you need tunnel API HTTP port (ex. 1984)
- Ngrok support authorization for your web interface
- Ngrok automatically adds HTTPS to your web interface
- ngrok support authorization for your web interface
- ngrok automatically adds HTTPS to your web interface
Ngrok free subscription limitations:
The ngrok free subscription has the following limitations:
- you will always get random external address (not a problem for webrtc stream)
- you can forward multiple ports but use only one Ngrok app
- You can reserve a free domain for serving the web interface, but the TCP address you get will always be random and change with each restart of the ngrok agent (not a problem for webrtc stream)
- You can forward multiple ports from a single agent, but you can only run one ngrok agent on the free plan
go2rtc will automatically get your external TCP address (if you enable it in ngrok config) and use it with WebRTC connection (if you enable it in webrtc config).
You need manually download [Ngrok agent app](https://ngrok.com/download) for your OS and register in [Ngrok service](https://ngrok.com/).
You need to manually download the [ngrok agent app](https://ngrok.com/download) for your OS and register with the [ngrok service](https://ngrok.com/signup).
**Tunnel for only WebRTC Stream**
You need to add your [Ngrok token](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
You need to add your [ngrok authtoken](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
```yaml
ngrok:
@@ -989,7 +1047,7 @@ ngrok:
command: ngrok start --all --config ngrok.yaml
```
Ngrok config example:
ngrok config example:
```yaml
version: "2"
@@ -1005,6 +1063,8 @@ tunnels:
proto: tcp
```
See the [ngrok agent documentation](https://ngrok.com/docs/agent/config/) for more details on the ngrok configuration file.
### Module: Hass
The best and easiest way to use go2rtc inside the Home Assistant is to install the custom integration [WebRTC Camera](#go2rtc-home-assistant-integration) and custom lovelace card.
@@ -1068,6 +1128,8 @@ Read more about [codecs filters](#codecs-filters).
### Module: HLS
*[New in v1.1.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.1.0)*
[HLS](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) is the worst technology for real-time streaming. It can only be useful on devices that do not support more modern technology, like [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4).
The go2rtc implementation differs from the standards and may not work with all players.
@@ -1144,7 +1206,7 @@ webrtc:
- external access to WebRTC TCP port is not a problem, because it used only for transmit encrypted media data
- anyway you need to open this port to your local network and to the Internet in order for WebRTC to work
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [Ngrok](https://ngrok.com/), etc.
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [ngrok](https://ngrok.com/), etc.
PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted media. It works without problems on the local network. And sometimes also works for external access, even if you haven't opened this port on your router ([read more](https://en.wikipedia.org/wiki/UDP_hole_punching)). But for stable external WebRTC access, you need to open the 8555 port on your router for both TCP and UDP.
@@ -1175,17 +1237,14 @@ Some examples:
`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
| Device | WebRTC | MSE | HTTP | HLS |
|---------------------|-------------------------------|-------------------------------|------------------------------------|------------------------|
| *latency* | best | medium | bad | bad |
| Desktop Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
| Desktop Edge | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
| Android Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
| Desktop Firefox | H264, OPUS, PCMU, PCMA | H264, AAC, FLAC*, OPUS | H264, AAC, FLAC*, OPUS | no |
| Desktop Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** | H264, H265, AAC, FLAC* |
| iPad Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** | H264, H265, AAC, FLAC* |
| iPhone Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | **no!** | **no!** | H264, H265, AAC, FLAC* |
| macOS [Hass App][1] | no | no | no | H264, H265, AAC, FLAC* |
| Device | WebRTC | MSE | HTTP* | HLS |
|--------------------------------------------------------------------------|-----------------------------------------|-----------------------------------------|----------------------------------------------|-----------------------------|
| *latency* | best | medium | bad | bad |
| - Desktop Chrome 107+ <br/> - Desktop Edge <br/> - Android Chrome 107+ | H264 <br/> PCMU, PCMA <br/> OPUS | H264, H265* <br/> AAC, FLAC* <br/> OPUS | H264, H265* <br/> AAC, FLAC* <br/> OPUS, MP3 | no |
| Desktop Firefox | H264 <br/> PCMU, PCMA <br/> OPUS | H264 <br/> AAC, FLAC* <br/> OPUS | H264 <br/> AAC, FLAC* <br/> OPUS | no |
| - Desktop Safari 14+ <br/> - iPad Safari 14+ <br/> - iPhone Safari 17.1+ | H264, H265* <br/> PCMU, PCMA <br/> OPUS | H264, H265 <br/> AAC, FLAC* | **no!** | H264, H265 <br/> AAC, FLAC* |
| iPhone Safari 14+ | H264, H265* <br/> PCMU, PCMA <br/> OPUS | **no!** | **no!** | H264, H265 <br/> AAC, FLAC* |
| macOS [Hass App][1] | no | no | no | H264, H265 <br/> AAC, FLAC* |
[1]: https://apps.apple.com/app/home-assistant/id1099568401
@@ -1212,8 +1271,8 @@ Some examples:
- H264 = H.264 = AVC (Advanced Video Coding)
- H265 = H.265 = HEVC (High Efficiency Video Coding)
- PCMU = G.711 PCM (A-law) = PCM A-law (`alaw`)
- PCMA = G.711 PCM (µ-law) = PCM mu-law (`mulaw`)
- PCMA = G.711 PCM (A-law) = PCM A-law (`alaw`)
- PCMU = G.711 PCM (µ-law) = PCM mu-law (`mulaw`)
- PCM = L16 = PCM signed 16-bit big-endian (`s16be`)
- AAC = MPEG4-GENERIC
- MP3 = MPEG-1 Audio Layer III or MPEG-2 Audio Layer III
@@ -1280,14 +1339,21 @@ streams:
- [Frigate 12+](https://frigate.video/) - open source NVR built around real-time AI object detection
- [Frigate Lovelace Card](https://github.com/dermotduffy/frigate-hass-card) - custom card for Home Assistant
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge
- [OpenIPC](https://github.com/OpenIPC/firmware/tree/master/general/package/go2rtc) - Alternative IP Camera firmware from an open community
- [wz_mini_hacks](https://github.com/gtxaspec/wz_mini_hacks) - Custom firmware for Wyze cameras
- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - A small project that provides a Video/Audio Stream from Eufy cameras that don't directly support RTSP
- [ioBroker.euSec](https://github.com/bropat/ioBroker.eusec) - [ioBroker](https://www.iobroker.net/) adapter for control Eufy security devices
- [wz_mini_hacks](https://github.com/gtxaspec/wz_mini_hacks) - Custom firmware for Wyze cameras
- [MMM-go2rtc](https://github.com/Anonym-tsk/MMM-go2rtc) - MagicMirror² Module
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge
**Distributions**
- [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=go2rtc)
- [Gentoo](https://github.com/inode64/inode64-overlay/tree/main/media-video/go2rtc)
- [NixOS](https://search.nixos.org/packages?query=go2rtc)
- [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/)
- [QNAP](https://www.myqnap.org/product/go2rtc/)
- [Synology NAS](https://synocommunity.com/package/go2rtc)
- [Unraid](https://unraid.net/community/apps?q=go2rtc)
## Cameras experience
+8 -1
View File
@@ -115,7 +115,14 @@ paths:
default:
description: Default response
/api/restart:
post:
summary: Restart Daemon
description: Restarts the daemon.
tags: [ Application ]
responses:
default:
description: Default response
/api/config:
get:
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 154 KiB

+18 -15
View File
@@ -3,33 +3,36 @@ module github.com/AlexxIT/go2rtc
go 1.21
require (
github.com/gorilla/websocket v1.5.0
github.com/miekg/dns v1.1.56
github.com/asticode/go-astits v1.13.0
github.com/expr-lang/expr v1.15.7
github.com/gorilla/websocket v1.5.1
github.com/miekg/dns v1.1.57
github.com/pion/ice/v2 v2.3.11
github.com/pion/interceptor v0.1.22
github.com/pion/rtcp v1.2.10
github.com/pion/rtp v1.8.2
github.com/pion/interceptor v0.1.25
github.com/pion/rtcp v1.2.13
github.com/pion/rtp v1.8.3
github.com/pion/sdp/v3 v3.0.6
github.com/pion/srtp/v2 v2.0.17
github.com/pion/srtp/v2 v2.0.18
github.com/pion/stun v0.6.1
github.com/pion/webrtc/v3 v3.2.21
github.com/pion/webrtc/v3 v3.2.24
github.com/rs/zerolog v1.31.0
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
github.com/stretchr/testify v1.8.4
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
golang.org/x/crypto v0.14.0
golang.org/x/crypto v0.17.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/asticode/go-astikit v0.30.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pion/datachannel v1.5.5 // indirect
github.com/pion/dtls/v2 v2.2.7 // indirect
github.com/pion/dtls/v2 v2.2.8 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.9 // indirect
github.com/pion/randutil v0.1.0 // indirect
@@ -37,8 +40,8 @@ require (
github.com/pion/transport/v2 v2.2.4 // indirect
github.com/pion/turn/v2 v2.1.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/mod v0.13.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/tools v0.14.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/tools v0.16.1 // indirect
)
+41 -51
View File
@@ -1,7 +1,13 @@
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/expr-lang/expr v1.15.7 h1:BK0JcWUkoW6nrbLBo6xCKhz4BvH5DSOOu1Gx5lucyZo=
github.com/expr-lang/expr v1.15.7/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
@@ -19,10 +25,11 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
@@ -30,17 +37,14 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE=
github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -52,15 +56,13 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/dtls/v2 v2.2.8 h1:BUroldfiIbV9jSnC6cKOMnyiORRWrWWpV11JUyEu5OA=
github.com/pion/dtls/v2 v2.2.8/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/ice/v2 v2.3.11 h1:rZjVmUwyT55cmN8ySMpL7rsS8KYsJERsrxJLLxpKhdw=
github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
github.com/pion/interceptor v0.1.18/go.mod h1:tpvvF4cPM6NGxFA1DUMbhabzQBxdWMATDGEUYOR9x6I=
github.com/pion/interceptor v0.1.19 h1:tq0TGBzuZQqipyBhaC1mVUCfCh8XjDKUuibq9rIl5t4=
github.com/pion/interceptor v0.1.19/go.mod h1:VANhFxdJezB8mwToMMmrmyHyP9gym6xLqIUch31xryg=
github.com/pion/interceptor v0.1.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=
@@ -68,20 +70,21 @@ github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
github.com/pion/rtp v1.8.1 h1:26OxTc6lKg/qLSGir5agLyj0QKaOv8OP5wps2SFnVNQ=
github.com/pion/rtp v1.8.1/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/rtp v1.8.2 h1:oKMM0K1/QYQ5b5qH+ikqDSZRipP5mIxPJcgcvw5sH0w=
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtcp v1.2.13 h1:+EQijuisKwm/8VBs8nWllr0bIndR7Lf7cZG200mpbNo=
github.com/pion/rtcp v1.2.13/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/srtp/v2 v2.0.17 h1:ECuOk+7uIpY6HUlTb0nXhfvu4REG2hjtC4ronYFCZE4=
github.com/pion/srtp/v2 v2.0.17/go.mod h1:y5WSHcJY4YfNB/5r7ca5YjHeIr1H3LM1rKArGGs8jMc=
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
@@ -93,20 +96,16 @@ 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/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.19 h1:XNu5e62mkzafw1qYuKtQ+Dviw4JpbzC/SLx3zZt49JY=
github.com/pion/webrtc/v3 v3.2.19/go.mod h1:vVURQTBOG5BpWKOJz3nlr23NfTDeyKVmubRNqzQp+Tg=
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.24 h1:MiFL5DMo2bDaaIFWr0DDpwiV/L4EGbLZb+xoRvfEo1Y=
github.com/pion/webrtc/v3 v3.2.24/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
@@ -117,6 +116,7 @@ github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDf
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -136,17 +136,14 @@ golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.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=
@@ -162,18 +159,16 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -186,8 +181,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -200,10 +193,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -230,10 +222,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+97 -69
View File
@@ -10,6 +10,7 @@ import (
"strconv"
"strings"
"sync"
"syscall"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/pkg/shell"
@@ -19,25 +20,26 @@ import (
func Init() {
var cfg struct {
Mod struct {
Listen string `yaml:"listen"`
Username string `yaml:"username"`
Password string `yaml:"password"`
BasePath string `yaml:"base_path"`
StaticDir string `yaml:"static_dir"`
Origin string `yaml:"origin"`
TLSListen string `yaml:"tls_listen"`
TLSCert string `yaml:"tls_cert"`
TLSKey string `yaml:"tls_key"`
Listen string `yaml:"listen"`
Username string `yaml:"username"`
Password string `yaml:"password"`
BasePath string `yaml:"base_path"`
StaticDir string `yaml:"static_dir"`
Origin string `yaml:"origin"`
TLSListen string `yaml:"tls_listen"`
TLSCert string `yaml:"tls_cert"`
TLSKey string `yaml:"tls_key"`
UnixListen string `yaml:"unix_listen"`
} `yaml:"api"`
}
// default config
cfg.Mod.Listen = "0.0.0.0:1984"
cfg.Mod.Listen = ":1984"
// load config from YAML
app.LoadConfig(&cfg)
if cfg.Mod.Listen == "" {
if cfg.Mod.Listen == "" && cfg.Mod.UnixListen == "" && cfg.Mod.TLSListen == "" {
return
}
@@ -50,16 +52,7 @@ func Init() {
HandleFunc("api/config", configHandler)
HandleFunc("api/exit", exitHandler)
HandleFunc("api/restart", restartHandler)
// ensure we can listen without errors
var err error
ln, err = net.Listen("tcp", cfg.Mod.Listen)
if err != nil {
log.Fatal().Err(err).Msg("[api] listen")
return
}
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
HandleFunc("api/log", logHandler)
Handler = http.DefaultServeMux // 4th
@@ -75,59 +68,74 @@ func Init() {
Handler = middlewareLog(Handler) // 1st
}
go func() {
s := http.Server{}
s.Handler = Handler
if err = s.Serve(ln); err != nil {
log.Fatal().Err(err).Msg("[api] serve")
}
}()
if cfg.Mod.Listen != "" {
go listen("tcp", cfg.Mod.Listen)
}
if cfg.Mod.UnixListen != "" {
_ = syscall.Unlink(cfg.Mod.UnixListen)
go listen("unix", cfg.Mod.UnixListen)
}
// Initialize the HTTPS server
if cfg.Mod.TLSListen != "" && cfg.Mod.TLSCert != "" && cfg.Mod.TLSKey != "" {
var cert tls.Certificate
if strings.IndexByte(cfg.Mod.TLSCert, '\n') < 0 && strings.IndexByte(cfg.Mod.TLSKey, '\n') < 0 {
// check if file path
cert, err = tls.LoadX509KeyPair(cfg.Mod.TLSCert, cfg.Mod.TLSKey)
} else {
// if text file content
cert, err = tls.X509KeyPair([]byte(cfg.Mod.TLSCert), []byte(cfg.Mod.TLSKey))
}
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
tlsListener, err := net.Listen("tcp", cfg.Mod.TLSListen)
if err != nil {
log.Fatal().Err(err).Caller().Send()
return
}
log.Info().Str("addr", cfg.Mod.TLSListen).Msg("[api] tls listen")
tlsServer := &http.Server{
Handler: Handler,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
},
}
go func() {
if err := tlsServer.ServeTLS(tlsListener, "", ""); err != nil {
log.Fatal().Err(err).Msg("[api] tls serve")
}
}()
go tlsListen("tcp", cfg.Mod.TLSListen, cfg.Mod.TLSCert, cfg.Mod.TLSKey)
}
}
func Port() int {
if ln == nil {
return 0
func listen(network, address string) {
ln, err := net.Listen(network, address)
if err != nil {
log.Error().Err(err).Msg("[api] listen")
return
}
log.Info().Str("addr", address).Msg("[api] listen")
if network == "tcp" {
Port = ln.Addr().(*net.TCPAddr).Port
}
server := http.Server{Handler: Handler}
if err = server.Serve(ln); err != nil {
log.Fatal().Err(err).Msg("[api] serve")
}
return ln.Addr().(*net.TCPAddr).Port
}
func tlsListen(network, address, certFile, keyFile string) {
var cert tls.Certificate
var err error
if strings.IndexByte(certFile, '\n') < 0 && strings.IndexByte(keyFile, '\n') < 0 {
// check if file path
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
} else {
// if text file content
cert, err = tls.X509KeyPair([]byte(certFile), []byte(keyFile))
}
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
ln, err := net.Listen(network, address)
if err != nil {
log.Error().Err(err).Msg("[api] tls listen")
return
}
log.Info().Str("addr", address).Msg("[api] tls listen")
server := &http.Server{
Handler: Handler,
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
}
if err = server.ServeTLS(ln, "", ""); err != nil {
log.Fatal().Err(err).Msg("[api] tls serve")
}
}
var Port int
const (
MimeJSON = "application/json"
MimeText = "text/plain"
@@ -187,7 +195,7 @@ func middlewareLog(next http.Handler) http.Handler {
func middlewareAuth(username, password string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") {
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") && r.RemoteAddr != "@" {
user, pass, ok := r.BasicAuth()
if !ok || user != username || pass != password {
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
@@ -204,12 +212,11 @@ func middlewareCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
next.ServeHTTP(w, r)
})
}
var ln net.Listener
var mu sync.Mutex
func apiHandler(w http.ResponseWriter, r *http.Request) {
@@ -227,7 +234,14 @@ func exitHandler(w http.ResponseWriter, r *http.Request) {
}
s := r.URL.Query().Get("code")
code, _ := strconv.Atoi(s)
code, err := strconv.Atoi(s)
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02
if err != nil || code < 0 || code > 125 {
http.Error(w, "Code must be in the range [0, 125]", http.StatusBadRequest)
return
}
os.Exit(code)
}
@@ -240,6 +254,20 @@ func restartHandler(w http.ResponseWriter, r *http.Request) {
go shell.Restart()
}
func logHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
// Send current state of the log file immediately
w.Header().Set("Content-Type", "application/jsonlines")
_, _ = app.MemoryLog.WriteTo(w)
case "DELETE":
app.MemoryLog.Reset()
Response(w, "OK", "text/plain")
default:
http.Error(w, "Method not allowed", http.StatusBadRequest)
}
}
type Source struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
+1 -39
View File
@@ -4,20 +4,17 @@ import (
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/yaml"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
var Version = "1.8.1"
var Version = "1.8.5"
var UserAgent = "go2rtc/" + Version
var ConfigPath string
@@ -86,26 +83,6 @@ func Init() {
migrateStore()
}
func NewLogger(format string, level string) zerolog.Logger {
var writer io.Writer = os.Stdout
if format != "json" {
writer = zerolog.ConsoleWriter{
Out: writer, TimeFormat: "15:04:05.000",
NoColor: writer != os.Stdout || format == "text",
}
}
zerolog.TimeFieldFormat = time.RFC3339Nano
lvl, err := zerolog.ParseLevel(level)
if err != nil || lvl == zerolog.NoLevel {
lvl = zerolog.InfoLevel
}
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
}
func LoadConfig(v any) {
for _, data := range configs {
if err := yaml.Unmarshal(data, v); err != nil {
@@ -114,18 +91,6 @@ func LoadConfig(v any) {
}
}
func GetLogger(module string) zerolog.Logger {
if s, ok := modules[module]; ok {
lvl, err := zerolog.ParseLevel(s)
if err == nil {
return log.Level(lvl)
}
log.Warn().Err(err).Caller().Send()
}
return log.Logger
}
func PatchConfig(key string, value any, path ...string) error {
if ConfigPath == "" {
return errors.New("config file disabled")
@@ -156,6 +121,3 @@ func (c *Config) Set(value string) error {
}
var configs [][]byte
// modules log levels
var modules map[string]string
+117
View File
@@ -0,0 +1,117 @@
package app
import (
"io"
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
var MemoryLog *circularBuffer
func NewLogger(format string, level string) zerolog.Logger {
var writer io.Writer = os.Stdout
if format != "json" {
writer = zerolog.ConsoleWriter{
Out: writer, TimeFormat: "15:04:05.000", NoColor: format == "text",
}
}
MemoryLog = newBuffer(16)
writer = zerolog.MultiLevelWriter(writer, MemoryLog)
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
lvl, err := zerolog.ParseLevel(level)
if err != nil || lvl == zerolog.NoLevel {
lvl = zerolog.InfoLevel
}
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
}
func GetLogger(module string) zerolog.Logger {
if s, ok := modules[module]; ok {
lvl, err := zerolog.ParseLevel(s)
if err == nil {
return log.Level(lvl)
}
log.Warn().Err(err).Caller().Send()
}
return log.Logger
}
// modules log levels
var modules map[string]string
const chunkSize = 1 << 16
type circularBuffer struct {
chunks [][]byte
r, w int
}
func newBuffer(chunks int) *circularBuffer {
b := &circularBuffer{chunks: make([][]byte, 0, chunks)}
// create first chunk
b.chunks = append(b.chunks, make([]byte, 0, chunkSize))
return b
}
func (b *circularBuffer) Write(p []byte) (n int, err error) {
n = len(p)
// check if chunk has size
if len(b.chunks[b.w])+n > chunkSize {
// increase write chunk index
if b.w++; b.w == cap(b.chunks) {
b.w = 0
}
// check overflow
if b.r == b.w {
// increase read chunk index
if b.r++; b.r == cap(b.chunks) {
b.r = 0
}
}
// check if current chunk exists
if b.w == len(b.chunks) {
// allocate new chunk
b.chunks = append(b.chunks, make([]byte, 0, chunkSize))
} else {
// reset len of current chunk
b.chunks[b.w] = b.chunks[b.w][:0]
}
}
b.chunks[b.w] = append(b.chunks[b.w], p...)
return
}
func (b *circularBuffer) WriteTo(w io.Writer) (n int64, err error) {
for i := b.r; ; {
var nn int
if nn, err = w.Write(b.chunks[i]); err != nil {
return
}
n += int64(nn)
if i == b.w {
break
}
if i++; i == cap(b.chunks) {
i = 0
}
}
return
}
func (b *circularBuffer) Reset() {
b.chunks[0] = b.chunks[0][:0]
b.r = 0
b.w = 0
}
+6 -1
View File
@@ -83,7 +83,12 @@ func handlePipe(url string, cmd *exec.Cmd) (core.Producer, error) {
return nil, err
}
return magic.Open(r)
prod, err := magic.Open(r)
if err != nil {
_ = r.Close()
}
return prod, err
}
func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
+91
View File
@@ -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/) |
+28
View File
@@ -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
})
}
+5 -15
View File
@@ -45,30 +45,20 @@ func queryToInput(query url.Values) string {
}
if video != "" {
input += ` -i video="` + video + `"`
input += ` -i "video=` + video
if audio != "" {
input += `:audio="` + audio + `"`
input += `:audio=` + audio
}
input += `"`
} else {
input += ` -i audio="` + audio + `"`
input += ` -i "audio=` + audio + `"`
}
return input
}
func deviceInputSuffix(video, audio string) string {
switch {
case video != "" && audio != "":
return `video="` + video + `":audio=` + audio + `"`
case video != "":
return `video="` + video + `"`
case audio != "":
return `audio="` + audio + `"`
}
return ""
}
func initDevices() {
cmd := exec.Command(
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
+18 -7
View File
@@ -61,47 +61,58 @@ var defaults = map[string]string{
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
// https://github.com/pion/webrtc/issues/1514
// https://ffmpeg.org/ffmpeg-resampler.html
// `-async 1` or `-min_comp 0` - force frame_size=960, important for WebRTC audio quality
"opus": "-c:a libopus -application:a lowdelay -frame_duration 20 -min_comp 0",
// `-async 1` or `-min_comp 0` - force resampling for static timestamp inc, important for WebRTC audio quality
"opus": "-c:a libopus -application:a lowdelay -min_comp 0",
"opus/16000": "-c:a libopus -application:a lowdelay -min_comp 0 -ar:a 16000 -ac:a 1",
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
"pcmu/8000": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
"pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
"pcma/8000": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
"pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
"aac": "-c:a aac", // keep sample rate and channels
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
"mp3": "-c:a libmp3lame -q:a 8",
"pcm": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
"pcm/8000": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
"pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
"pcml": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
"pcml/8000": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
"pcml/44100": "-c:a pcm_s16le -ar:a 44100 -ac:a 1",
// hardware Intel and AMD on Linux
// better not to set `-async_depth:v 1` like for QSV, because framedrops
// `-bf 0` - disable B-frames is very important
"h264/vaapi": "-c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0",
"h265/vaapi": "-c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0",
"h265/vaapi": "-c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0",
"mjpeg/vaapi": "-c:v mjpeg_vaapi",
// hardware Raspberry
"h264/v4l2m2m": "-c:v h264_v4l2m2m -g 50 -bf 0",
"h265/v4l2m2m": "-c:v hevc_v4l2m2m -g 50 -bf 0",
// hardware Rockchip
// important to use custom ffmpeg https://github.com/AlexxIT/go2rtc/issues/768
// hevc - doesn't have a profile setting
"h264/rkmpp": "-c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1",
"h265/rkmpp": "-c:v hevc_rkmpp_encoder -g 50 -bf 0 -level:v 5.1",
// hardware NVidia on Linux and Windows
// preset=p2 - faster, tune=ll - low latency
"h264/cuda": "-c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
"h265/cuda": "-c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto",
"h265/cuda": "-c:v hevc_nvenc -g 50 -bf 0 -profile:v main -level:v auto",
// hardware Intel on Windows
"h264/dxva2": "-c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1",
"h265/dxva2": "-c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1",
"mjpeg/dxva2": "-c:v mjpeg_qsv -profile:v high -level:v 5.1",
"h265/dxva2": "-c:v hevc_qsv -g 50 -bf 0 -profile:v main -level:v 5.1 -async_depth:v 1",
"mjpeg/dxva2": "-c:v mjpeg_qsv",
// hardware macOS
"h264/videotoolbox": "-c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1",
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1",
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v main -level:v 5.1",
}
// configTemplate - return template from config (defaults) if exist or return raw template
+29 -3
View File
@@ -35,12 +35,15 @@ func TestParseArgsFile(t *testing.T) {
func TestParseArgsDevice(t *testing.T) {
// [DEVICE] video will be output for MJPEG to pipe, with size 1920x1080
args := parseArgs("device?video=0&video_size=1920x1080")
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i "video=0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped
//args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma")
args = parseArgs("device?video=0&framerate=20#video=h265#audio=pcma")
require.Equal(t, `ffmpeg -hide_banner -f dshow -framerate 20 -i video="0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -c:a pcm_alaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
args = parseArgs("device?video=0&framerate=20#video=h265")
require.Equal(t, `ffmpeg -hide_banner -f dshow -framerate 20 -i "video=0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
args = parseArgs("device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)")
require.Equal(t, `ffmpeg -hide_banner -f dshow -i "video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsIpCam(t *testing.T) {
@@ -151,6 +154,18 @@ func TestParseArgsHwV4l2m2m(t *testing.T) {
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsHwRKMPP(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http://example.com#video=h264#hardware=rkmpp")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
args = parseArgs("http://example.com#video=h264#rotate=180#hardware=rkmpp")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf "transpose=1,transpose=1" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
args = parseArgs("http://example.com#video=h264#height=320#hardware=rkmpp")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1 -height 320 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsHwCuda(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=cuda")
@@ -213,3 +228,14 @@ func TestDeckLink(t *testing.T) {
args := parseArgs(`DeckLink SDI (2)#video=h264#hardware=vaapi#input=-format_code Hp29 -f decklink -i "{input}"`)
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -format_code Hp29 -f decklink -i "DeckLink SDI (2)" -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestDrawText(t *testing.T) {
args := parseArgs("http:///example.com#video=h264#drawtext=fontsize=12")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
args = parseArgs("http:///example.com#video=h264#width=640#drawtext=fontsize=12")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=640:-1,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
args = parseArgs("http:///example.com#video=h264#width=640#drawtext=fontsize=12#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "scale=640:-1:out_color_matrix=bt709:out_range=tv,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}',hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
+36 -12
View File
@@ -18,6 +18,7 @@ const (
EngineCUDA = "cuda" // NVidia on Windows and Linux
EngineDXVA2 = "dxva2" // Intel on Windows
EngineVideoToolbox = "videotoolbox" // macOS
EngineRKMPP = "rkmpp" // Rockchip
)
func Init(bin string) {
@@ -58,11 +59,13 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
case EngineVAAPI:
args.Codecs[i] = defaults[name+"/"+engine]
fixYCbCrRange(args)
if !args.HasFilters("drawtext=") {
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch " + args.Input
if name == "h264" {
fixPixelFormat(args)
}
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_vaapi=" + filter[6:]
@@ -123,6 +126,24 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
case EngineV4L2M2M:
args.Codecs[i] = defaults[name+"/"+engine]
case EngineRKMPP:
args.Codecs[i] = defaults[name+"/"+engine]
for j, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters = append(args.Filters[:j], args.Filters[j+1:]...)
width, height, _ := strings.Cut(filter[6:], ":")
if width != "-1" {
args.Codecs[i] += " -width " + width
}
if height != "-1" {
args.Codecs[i] += " -height " + height
}
break
}
}
}
}
}
@@ -157,20 +178,23 @@ func cut(s string, sep byte, pos int) string {
return s
}
// fixYCbCrRange convert jpeg/pc range to mpeg/tv range
// vaapi(pc, bt709, progressive) == yuvj420p (jpeg/full/pc)
// vaapi(tv, bt709, progressive) == yuv420p (mpeg/limited/tv)
// https://ffmpeg.org/ffmpeg-all.html#scale-1
func fixYCbCrRange(args *ffmpeg.Args) {
// fixPixelFormat:
// - good h264 pixel: yuv420p(tv, bt709) == yuv420p (mpeg/limited/tv)
// - bad h264 pixel: yuvj420p(pc, bt709) == yuvj420p (jpeg/full/pc)
// - bad jpeg pixel: yuvj422p(pc, bt470bg)
func fixPixelFormat(args *ffmpeg.Args) {
// in my tests this filters has same CPU/GPU load:
// - "hwupload"
// - "hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv"
// - "hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12"
const fixPixFmt = "out_color_matrix=bt709:out_range=tv:format=nv12"
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
if !strings.Contains(filter, "out_range=") {
args.Filters[i] = filter + ":out_range=tv"
}
args.Filters[i] = filter + ":" + fixPixFmt
return
}
}
// scale=out_color_matrix=bt709:out_range=tv
args.Filters = append(args.Filters, "scale=out_range=tv")
args.Filters = append(args.Filters, "scale="+fixPixFmt)
}
+25 -7
View File
@@ -6,13 +6,17 @@ import (
"github.com/AlexxIT/go2rtc/internal/api"
)
const ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
const ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
const ProbeVAAPIH264 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -f null -"
const ProbeVAAPIH265 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c hevc_vaapi -f null -"
const ProbeVAAPIJPEG = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c mjpeg_vaapi -f null -"
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
const (
ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
ProbeRKMPPH264 = "-f lavfi -i testsrc2 -t 1 -c h264_rkmpp_encoder -f null -"
ProbeRKMPPH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_rkmpp_encoder -f null -"
ProbeVAAPIH264 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -f null -"
ProbeVAAPIH265 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c hevc_vaapi -f null -"
ProbeVAAPIJPEG = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c mjpeg_vaapi -f null -"
ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
)
func ProbeAll(bin string) []*api.Source {
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
@@ -25,6 +29,14 @@ func ProbeAll(bin string) []*api.Source {
Name: runToString(bin, ProbeV4L2M2MH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M,
},
{
Name: runToString(bin, ProbeRKMPPH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineRKMPP,
},
{
Name: runToString(bin, ProbeRKMPPH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineRKMPP,
},
}
}
@@ -59,10 +71,16 @@ func ProbeHardware(bin, name string) string {
if run(bin, ProbeV4L2M2MH264) {
return EngineV4L2M2M
}
if run(bin, ProbeRKMPPH264) {
return EngineRKMPP
}
case "h265":
if run(bin, ProbeV4L2M2MH265) {
return EngineV4L2M2M
}
if run(bin, ProbeRKMPPH265) {
return EngineRKMPP
}
}
return EngineSoftware
+25
View File
@@ -0,0 +1,25 @@
# GoPro
Supported models: HERO9, HERO10, HERO11, HERO12.
Supported OS: Linux, Mac, Windows, [HassOS](https://www.home-assistant.io/installation/)
The other camera models have different APIs. I will try to add them in the next versions.
## Config
- USB-connected cameras create a new network interface in the system
- Linux users do not need to install anything
- Windows users should install the [network driver](https://community.gopro.com/s/article/GoPro-Webcam)
- if the camera is detected but the stream does not start - you need to disable firewall
1. Discover camera address: WebUI > Add > GoPro
2. Add camera to config
```yaml
streams:
hero12: gopro://172.20.100.51
```
## Useful links
- https://gopro.github.io/OpenGoPro/
+30
View File
@@ -0,0 +1,30 @@
package gopro
import (
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/gopro"
)
func Init() {
streams.HandleFunc("gopro", handleGoPro)
api.HandleFunc("api/gopro", apiGoPro)
}
func handleGoPro(rawURL string) (core.Producer, error) {
return gopro.Dial(rawURL)
}
func apiGoPro(w http.ResponseWriter, r *http.Request) {
var items []*api.Source
for _, host := range gopro.Discovery() {
items = append(items, &api.Source{Name: host, URL: "gopro://" + host})
}
api.ResponseSources(w, items)
}
+7 -2
View File
@@ -1,6 +1,7 @@
package homekit
import (
"errors"
"io"
"net"
"net/http"
@@ -97,7 +98,7 @@ func Init() {
srv.mdns = &mdns.ServiceEntry{
Name: name,
Port: uint16(api.Port()),
Port: uint16(api.Port),
Info: map[string]string{
hap.TXTConfigNumber: "1",
hap.TXTFeatureFlags: "0",
@@ -121,7 +122,7 @@ func Init() {
api.HandleFunc(hap.PathPairSetup, hapPairSetup)
api.HandleFunc(hap.PathPairVerify, hapPairVerify)
log.Trace().Msgf("[homekit] mnds: %s", entries)
log.Trace().Msgf("[homekit] mdns: %s", entries)
go func() {
if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil {
@@ -134,6 +135,10 @@ var log zerolog.Logger
var servers map[string]*server
func streamHandler(url string) (core.Producer, error) {
if srtp.Server == nil {
return nil, errors.New("homekit: can't work without SRTP server")
}
return homekit.Dial(url, srtp.Server)
}
+26
View File
@@ -7,6 +7,7 @@ import (
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hls"
@@ -22,6 +23,8 @@ func Init() {
streams.HandleFunc("httpx", handleHTTP)
streams.HandleFunc("tcp", handleTCP)
api.HandleFunc("api/stream", apiStream)
}
func handleHTTP(rawURL string) (core.Producer, error) {
@@ -89,3 +92,26 @@ func handleTCP(rawURL string) (core.Producer, error) {
return magic.Open(conn)
}
func apiStream(w http.ResponseWriter, r *http.Request) {
dst := r.URL.Query().Get("dst")
stream := streams.Get(dst)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
client, err := magic.Open(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stream.AddProducer(client)
defer stream.RemoveProducer(client)
if err = client.Start(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
+2 -4
View File
@@ -56,19 +56,17 @@ func inputMpegTS(w http.ResponseWriter, r *http.Request) {
return
}
res := &http.Response{Body: r.Body, Request: r}
client, err := mpegts.Open(res.Body)
client, err := mpegts.Open(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stream.AddProducer(client)
defer stream.RemoveProducer(client)
if err = client.Start(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stream.RemoveProducer(client)
}
+5 -4
View File
@@ -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")
}
}
})
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+11
View File
@@ -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
+41 -33
View File
@@ -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
}
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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
}
+4 -3
View File
@@ -49,6 +49,9 @@ func syncHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "", http.StatusBadRequest)
}
case "OPTIONS":
w.WriteHeader(http.StatusNoContent)
default:
http.Error(w, "", http.StatusMethodNotAllowed)
}
@@ -195,9 +198,7 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) {
case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateClosed {
stream.RemoveProducer(prod)
if _, ok := sessions[id]; ok {
delete(sessions, id)
}
delete(sessions, id)
}
}
})
+16 -12
View File
@@ -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})
}
})
+5 -1
View File
@@ -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
View File
@@ -10,7 +10,7 @@ import (
func IsADTS(b []byte) bool {
_ = b[1]
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF0 == 0xF0
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF6 == 0xF0
}
func ADTSToCodec(b []byte) *core.Codec {
+73
View File
@@ -0,0 +1,73 @@
package aac
import (
"bufio"
"encoding/binary"
"io"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
)
type Producer struct {
core.SuperProducer
rd *bufio.Reader
cl io.Closer
}
func Open(r io.Reader) (*Producer, error) {
rd := bufio.NewReader(r)
b, err := rd.Peek(8)
if err != nil {
return nil, err
}
codec := ADTSToCodec(b)
prod := &Producer{rd: rd, cl: r.(io.Closer)}
prod.Type = "ADTS producer"
prod.Medias = []*core.Media{
{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
},
}
return prod, nil
}
func (c *Producer) Start() error {
for {
b, err := c.rd.Peek(6)
if err != nil {
return err
}
auSize := ReadADTSSize(b)
payload := make([]byte, 2+2+auSize)
if _, err = io.ReadFull(c.rd, payload[4:]); err != nil {
return err
}
c.Recv += int(auSize)
if len(c.Receivers) == 0 {
continue
}
payload[1] = 16 // header size in bits
binary.BigEndian.PutUint16(payload[2:], auSize<<3)
pkt := &rtp.Packet{
Header: rtp.Header{Timestamp: core.Now90000()},
Payload: payload,
}
c.Receivers[0].WriteRTP(pkt)
}
}
func (c *Producer) Stop() error {
_ = c.SuperProducer.Close()
return c.cl.Close()
}
+5 -1
View File
@@ -28,9 +28,13 @@ func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
headers := packet.Payload[2 : 2+headersSize]
units := packet.Payload[2+headersSize:]
for len(headers) > 0 {
for len(headers) >= 2 {
unitSize := binary.BigEndian.Uint16(headers) >> 3
if len(units) < int(unitSize) {
return
}
unit := units[:unitSize]
headers = headers[2:]
+4
View File
@@ -127,3 +127,7 @@ func (r *Reader) ReadSEGolomb() int32 {
return int32(b >> 1)
}
}
func (r *Reader) Left() []byte {
return r.buf[r.pos:]
}
+3 -1
View File
@@ -5,7 +5,9 @@ import (
"io"
)
const ProbeSize = 1024 * 1024 // 1MB
// ProbeSize
// in my tests MPEG-TS 40Mbit/s 4K-video require more than 1MB for probe
const ProbeSize = 5 * 1024 * 1024 // 5MB
const (
BufferDisable = 0
+6 -5
View File
@@ -73,6 +73,9 @@ func (t *Receiver) Replace(target *Receiver) {
// move this receiver senders to new receiver
t.mu.Lock()
senders := t.senders
// fix https://github.com/AlexxIT/go2rtc/issues/828
// TODO: fix the reason, not the consequence
t.senders = nil
t.mu.Unlock()
target.mu.Lock()
@@ -117,9 +120,9 @@ func (s *Sender) HandleRTP(track *Receiver) {
if GetKind(track.Codec.Name) == KindVideo {
if track.Codec.IsRTP() {
// H.264 2560x1440 4096kbs can have 700+ packets between 25 frames
// H.265 5120x1440 can have 700+ packets between two keyframes
bufferSize = 1000
// in my tests 40Mbit/s 4K-video can generate up to 1500 items
// for the h264.RTPDepay => RTPPay queue
bufferSize = 5000
} else {
bufferSize = 50
}
@@ -140,9 +143,7 @@ func (s *Sender) HandleRTP(track *Receiver) {
go func() {
// read packets from buffer channel until it will be closed
for packet := range buffer {
s.mu.Lock()
s.bytes += len(packet.Payload)
s.mu.Unlock()
s.Handler(packet)
}
+115
View File
@@ -0,0 +1,115 @@
package expr
import (
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/expr-lang/expr"
)
func newRequest(method, url string, headers map[string]any) (*http.Request, error) {
if method == "" {
method = "GET"
}
req, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, err
}
for k, v := range headers {
req.Header.Set(k, fmt.Sprintf("%v", v))
}
return req, nil
}
func regExp(params ...any) (*regexp.Regexp, error) {
exp := params[0].(string)
if len(params) >= 2 {
// support:
// i case-insensitive (default false)
// m multi-line mode: ^ and $ match begin/end line (default false)
// s let . match \n (default false)
// https://pkg.go.dev/regexp/syntax
flags := params[1].(string)
exp = "(?" + flags + ")" + exp
}
return regexp.Compile(exp)
}
var Options = []expr.Option{
expr.Function(
"fetch",
func(params ...any) (any, error) {
var req *http.Request
var err error
url := params[0].(string)
if len(params) == 2 {
options := params[1].(map[string]any)
method, _ := options["method"].(string)
headers, _ := options["headers"].(map[string]any)
req, err = newRequest(method, url, headers)
} else {
req, err = http.NewRequest("GET", url, nil)
}
if err != nil {
return nil, err
}
res, err := tcp.Do(req)
if err != nil {
return nil, err
}
b, _ := io.ReadAll(res.Body)
return map[string]any{
"ok": res.StatusCode < 400,
"status": res.Status,
"text": string(b),
"json": func() (v any) {
_ = json.Unmarshal(b, &v)
return
},
}, nil
},
//new(func(url string) map[string]any),
//new(func(url string, options map[string]any) map[string]any),
),
expr.Function(
"match",
func(params ...any) (any, error) {
re, err := regExp(params[1:]...)
if err != nil {
return nil, err
}
str := params[0].(string)
return re.FindStringSubmatch(str), nil
},
//new(func(str, expr string) []string),
//new(func(str, expr, flags string) []string),
),
expr.Function(
"RegExp",
func(params ...any) (any, error) {
return regExp(params)
},
),
}
func Run(input string) (any, error) {
program, err := expr.Compile(input, Options...)
if err != nil {
return nil, err
}
return expr.Run(program, nil)
}
+17
View File
@@ -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)
}
+1 -1
View File
@@ -74,7 +74,7 @@ func (a *Args) String() string {
b.WriteString(codec)
}
if a.Filters != nil {
if len(a.Filters) > 0 {
for i, filter := range a.Filters {
if i == 0 {
b.WriteString(` -vf "`)
+66 -13
View File
@@ -10,6 +10,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/pion/rtp"
)
@@ -40,6 +41,21 @@ const (
CodecAVC = 7
)
const (
PacketTypeAVCHeader = iota
PacketTypeAVCNALU
PacketTypeAVCEnd
)
const (
PacketTypeSequenceStart = iota
PacketTypeCodedFrames
PacketTypeSequenceEnd
PacketTypeCodedFramesX
PacketTypeMetadata
PacketTypeMPEG2TSSequenceStart
)
func (c *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
receiver, _ := c.SuperProducer.GetTrack(media, codec)
if media.Kind == core.KindVideo {
@@ -70,13 +86,32 @@ func (c *Producer) Start() error {
c.audio.WriteRTP(pkt)
case TagVideo:
// frame type 4b, codecID 4b, avc packet type 8b, composition time 24b
if c.video == nil || pkt.Payload[1] == 0 {
if c.video == nil {
continue
}
if isExHeader(pkt.Payload) {
switch packetType := pkt.Payload[0] & 0b1111; packetType {
case PacketTypeCodedFrames:
// frame type 4b, packet type 4b, fourCC 32b, composition time 24b
pkt.Payload = pkt.Payload[8:]
case PacketTypeCodedFramesX:
// frame type 4b, packet type 4b, fourCC 32b
pkt.Payload = pkt.Payload[5:]
default:
continue
}
} else {
switch pkt.Payload[1] {
case PacketTypeAVCNALU:
// frame type 4b, codecID 4b, avc packet type 8b, composition time 24b
pkt.Payload = pkt.Payload[5:]
default:
continue
}
}
pkt.Timestamp = TimeToRTP(pkt.Timestamp, c.video.Codec.ClockRate)
pkt.Payload = pkt.Payload[5:]
c.video.WriteRTP(pkt)
}
}
@@ -145,20 +180,32 @@ func (c *Producer) probe() error {
c.Medias = append(c.Medias, media)
case TagVideo:
_ = pkt.Payload[1] // bounds
var codec *core.Codec
_ = pkt.Payload[0] >> 4 // FrameType
codecID := pkt.Payload[0] & 0b1111 // CodecID
if isExHeader(pkt.Payload) {
if string(pkt.Payload[1:5]) != "hvc1" {
continue
}
if codecID != CodecAVC {
continue
if packetType := pkt.Payload[0] & 0b1111; packetType != PacketTypeSequenceStart {
continue
}
codec = h265.ConfigToCodec(pkt.Payload[5:])
} else {
_ = pkt.Payload[0] >> 4 // FrameType
if codecID := pkt.Payload[0] & 0b1111; codecID != CodecAVC {
continue
}
if packetType := pkt.Payload[1]; packetType != PacketTypeAVCHeader { // check if header
continue
}
codec = h264.ConfigToCodec(pkt.Payload[5:])
}
if pkt.Payload[1] != 0 { // check if header
continue
}
codec := h264.ConfigToCodec(pkt.Payload[5:])
media := &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
@@ -229,9 +276,15 @@ func (c *Producer) readPacket() (*rtp.Packet, error) {
return nil, err
}
//log.Printf("[FLV] %d %.40x", pkt.PayloadType, pkt.Payload)
return pkt, nil
}
func TimeToRTP(timeMS uint32, clockRate uint32) uint32 {
return timeMS * clockRate / 1000
}
func isExHeader(data []byte) bool {
return data[0]&0b1000_0000 != 0
}
+43
View File
@@ -0,0 +1,43 @@
package gopro
import (
"net"
"net/http"
"regexp"
)
func Discovery() (urls []string) {
ints, err := net.Interfaces()
if err != nil {
return nil
}
// The socket address for USB connections is 172.2X.1YZ.51:8080
// https://gopro.github.io/OpenGoPro/http_2_0#socket-address
re := regexp.MustCompile(`^172\.2\d\.1\d\d\.`)
for _, itf := range ints {
addrs, err := itf.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
host := addr.String()
if !re.MatchString(host) {
continue
}
host = host[:11] + "51" // 172.2x.1xx.xxx
res, err := http.Get("http://" + host + ":8080/gopro/webcam/status")
if err != nil {
continue
}
_ = res.Body.Close()
urls = append(urls, host)
}
}
return
}
+117
View File
@@ -0,0 +1,117 @@
package gopro
import (
"errors"
"io"
"net"
"net/http"
"net/url"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
)
func Dial(rawURL string) (core.Producer, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
r := &listener{host: u.Host}
if err = r.command("/gopro/webcam/stop"); err != nil {
return nil, err
}
if err = r.listen(); err != nil {
return nil, err
}
if err = r.command("/gopro/webcam/start"); err != nil {
return nil, err
}
return mpegts.Open(r)
}
type listener struct {
conn net.PacketConn
host string
packet []byte
packets chan []byte
}
func (r *listener) Read(p []byte) (n int, err error) {
if r.packet == nil {
var ok bool
if r.packet, ok = <-r.packets; !ok {
return 0, io.EOF // channel closed
}
}
n = copy(p, r.packet)
if n < len(r.packet) {
r.packet = r.packet[n:]
} else {
r.packet = nil
}
return
}
func (r *listener) Close() error {
return r.conn.Close()
}
func (r *listener) command(api string) error {
client := &http.Client{Timeout: 5 * time.Second}
res, err := client.Get("http://" + r.host + ":8080" + api)
if err != nil {
return err
}
_ = res.Body.Close()
if res.StatusCode != http.StatusOK {
return errors.New("gopro: wrong response: " + res.Status)
}
return nil
}
func (r *listener) listen() (err error) {
if r.conn, err = net.ListenPacket("udp4", ":8554"); err != nil {
return
}
r.packets = make(chan []byte, 1024)
go r.worker()
return
}
func (r *listener) worker() {
b := make([]byte, 1500)
for {
if err := r.conn.SetReadDeadline(time.Now().Add(3 * time.Second)); err != nil {
break
}
n, _, err := r.conn.ReadFrom(b)
if err != nil {
break
}
packet := make([]byte, n)
copy(packet, b)
r.packets <- packet
}
close(r.packets)
_ = r.command("/gopro/webcam/stop")
}
+6
View File
@@ -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 {
+18
View File
@@ -7,8 +7,26 @@ import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/pion/rtp"
)
func RepairAVCC(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
vds, sps, pps := GetParameterSet(codec.FmtpLine)
ps := h264.JoinNALU(vds, sps, pps)
return func(packet *rtp.Packet) {
switch NALUType(packet.Payload) {
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
clone := *packet
clone.Payload = h264.Join(ps, packet.Payload)
handler(&clone)
default:
handler(packet)
}
}
}
func AVCCToCodec(avcc []byte) *core.Codec {
buf := bytes.NewBufferString("profile-id=1")
+59 -1
View File
@@ -1,7 +1,40 @@
// Package h265 - MPEG4 format related functions
package h265
import "encoding/binary"
import (
"bytes"
"encoding/base64"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func DecodeConfig(conf []byte) (profile, vps, sps, pps []byte) {
profile = conf[1:4]
b := conf[23:]
if binary.BigEndian.Uint16(b[1:]) != 1 {
return
}
vpsSize := binary.BigEndian.Uint16(b[3:])
vps = b[5 : 5+vpsSize]
b = conf[23+5+vpsSize:]
if binary.BigEndian.Uint16(b[1:]) != 1 {
return
}
spsSize := binary.BigEndian.Uint16(b[3:])
sps = b[5 : 5+spsSize]
b = conf[23+5+vpsSize+5+spsSize:]
if binary.BigEndian.Uint16(b[1:]) != 1 {
return
}
ppsSize := binary.BigEndian.Uint16(b[3:])
pps = b[5 : 5+ppsSize]
return
}
func EncodeConfig(vps, sps, pps []byte) []byte {
vpsSize := uint16(len(vps))
@@ -38,3 +71,28 @@ func EncodeConfig(vps, sps, pps []byte) []byte {
return buf
}
func ConfigToCodec(conf []byte) *core.Codec {
buf := bytes.NewBufferString("profile-id=1")
_, vps, sps, pps := DecodeConfig(conf)
if vps != nil {
buf.WriteString(";sprop-vps=")
buf.WriteString(base64.StdEncoding.EncodeToString(vps))
}
if sps != nil {
buf.WriteString(";sprop-sps=")
buf.WriteString(base64.StdEncoding.EncodeToString(sps))
}
if pps != nil {
buf.WriteString(";sprop-pps=")
buf.WriteString(base64.StdEncoding.EncodeToString(pps))
}
return &core.Codec{
Name: core.CodecH265,
ClockRate: 90000,
FmtpLine: buf.String(),
PayloadType: core.PayloadTypeRAW,
}
}
+8
View File
@@ -55,6 +55,14 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
case 1: // end
buf = append(buf, data[3:]...)
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4))
case 3: // wrong RFC 7798 realisation from OpenIPC project
// A non-fragmented NAL unit MUST NOT be transmitted in one FU; i.e.,
// the Start bit and End bit must not both be set to 1 in the same FU
// header.
nuType = data[2] & 0x3F
buf = binary.BigEndian.AppendUint32(buf, uint32(len(data))-1) // NAL unit size
buf = append(buf, (data[0]&0x81)|(nuType<<1), data[1])
buf = append(buf, data[3:]...)
}
} else {
nuStart = len(buf)
+2 -1
View File
@@ -50,4 +50,5 @@ Requires ffmpeg built with `--enable-libfdk-aac`
- [Extracting HomeKit Pairing Keys](https://pvieito.com/2019/12/extract-homekit-pairing-keys)
- [HAP in AirPlay2 receiver](https://github.com/openairplay/airplay2-receiver/blob/master/ap2/pairing/hap.py)
- [HomeKit Secure Video Unofficial Specification](https://github.com/Supereg/secure-video-specification)
- [Homebridge Camera FFmpeg](https://sunoo.github.io/homebridge-camera-ffmpeg/configs/)
- [Homebridge Camera FFmpeg](https://sunoo.github.io/homebridge-camera-ffmpeg/configs/)
- https://github.com/ljezny/Particle-HAP/blob/master/HAP-Specification-Non-Commercial-Version.pdf
+4
View File
@@ -83,6 +83,10 @@ func (c *Client) DeviceHost() string {
}
func (c *Client) Dial() (err error) {
if len(c.ClientID) == 0 || len(c.ClientPrivate) == 0 {
return errors.New("hap: can't dial witout client_id or client_private")
}
// update device address (host and/or port) before dial
_ = mdns.QueryOrDiscovery(c.DeviceHost(), mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
if entry.Complete() && entry.Info["id"] == c.DeviceID {
+3 -2
View File
@@ -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
}
+5
View File
@@ -10,6 +10,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
"github.com/AlexxIT/go2rtc/pkg/opus"
"github.com/AlexxIT/go2rtc/pkg/srtp"
"github.com/pion/rtp"
)
@@ -24,6 +25,7 @@ type Consumer struct {
sessionID string
videoSession *srtp.Session
audioSession *srtp.Session
audioRTPTime byte
}
func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer {
@@ -113,6 +115,7 @@ func (c *Consumer) SetConfig(conf *camera.SelectedStreamConfig) bool {
c.audioSession.Remote.SSRC = conf.AudioCodec.RTPParams[0].SSRC
c.audioSession.PayloadType = conf.AudioCodec.RTPParams[0].PayloadType
c.audioSession.RTCPInterval = toDuration(conf.AudioCodec.RTPParams[0].RTCPInterval)
c.audioRTPTime = conf.AudioCodec.CodecParams[0].RTPTime[0]
c.srtp.AddSession(c.videoSession)
c.srtp.AddSession(c.audioSession)
@@ -155,6 +158,8 @@ func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re
} else {
sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)
}
case core.CodecOpus:
sender.Handler = opus.RepackToHAP(c.audioRTPTime, sender.Handler)
}
sender.HandleRTP(track)
+19 -14
View File
@@ -2,6 +2,7 @@ package homekit
import (
"encoding/hex"
"slices"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
@@ -20,17 +21,16 @@ func videoToMedia(codecs []camera.VideoCodec) *core.Media {
for _, codec := range codecs {
for _, param := range codec.CodecParams {
for _, profileID := range param.ProfileID {
for _, level := range param.Level {
profile := videoProfiles[profileID] + videoLevels[level]
mediaCodec := &core.Codec{
Name: videoCodecs[codec.CodecType],
ClockRate: 90000,
FmtpLine: "profile-level-id=" + profile,
}
media.Codecs = append(media.Codecs, mediaCodec)
}
// get best profile and level
profileID := slices.Max(param.ProfileID)
level := slices.Max(param.Level)
profile := videoProfiles[profileID] + videoLevels[level]
mediaCodec := &core.Codec{
Name: videoCodecs[codec.CodecType],
ClockRate: 90000,
FmtpLine: "profile-level-id=" + profile,
}
media.Codecs = append(media.Codecs, mediaCodec)
}
}
@@ -55,7 +55,7 @@ func audioToMedia(codecs []camera.AudioCodec) *core.Media {
}
if mediaCodec.Name == core.CodecELD {
// onli this version works with FFmpeg
// only this version works with FFmpeg
conf := aac.EncodeConfig(aac.TypeAACELD, 24000, 1, true)
mediaCodec.FmtpLine = aac.FMTP + hex.EncodeToString(conf)
}
@@ -71,6 +71,7 @@ func audioToMedia(codecs []camera.AudioCodec) *core.Media {
func trackToVideo(track *core.Receiver, video0 *camera.VideoCodec) *camera.VideoCodec {
profileID := video0.CodecParams[0].ProfileID[0]
level := video0.CodecParams[0].Level[0]
attrs := video0.VideoAttrs[0]
if track != nil {
profile := h264.GetProfileLevelID(track.Codec.FmtpLine)
@@ -88,6 +89,12 @@ func trackToVideo(track *core.Receiver, video0 *camera.VideoCodec) *camera.Video
break
}
}
for _, s := range video0.VideoAttrs {
if s.Width > attrs.Width || s.Height > attrs.Height {
attrs = s
}
}
}
return &camera.VideoCodec{
@@ -98,9 +105,7 @@ func trackToVideo(track *core.Receiver, video0 *camera.VideoCodec) *camera.Video
Level: []byte{level},
},
},
VideoAttrs: []camera.VideoAttrs{
{Width: 1920, Height: 1080, Framerate: 30},
},
VideoAttrs: []camera.VideoAttrs{attrs},
}
}
+2 -5
View File
@@ -55,11 +55,8 @@ func proxy(r, w net.Conn, pair ServerPair) error {
continue
}
//if n > 512 {
// log.Printf("[hap] %d bytes => %s\n%s...", n, w.RemoteAddr(), b[:512])
//} else {
// log.Printf("[hap] %d bytes => %s\n%s", n, w.RemoteAddr(), b[:n])
//}
//log.Printf("[hap] %d bytes => %s\n%.512s", n, w.RemoteAddr(), b[:n])
if _, err = w.Write(b[:n]); err != nil {
break
}
+8 -6
View File
@@ -70,13 +70,15 @@ func (c *Producer) Start() error {
break
}
pkt := &rtp.Packet{
Header: rtp.Header{Timestamp: core.Now90000()},
Payload: annexb.EncodeToAVCC(buf[:i], true),
}
c.Receivers[0].WriteRTP(pkt)
if len(c.Receivers) > 0 {
pkt := &rtp.Packet{
Header: rtp.Header{Timestamp: core.Now90000()},
Payload: annexb.EncodeToAVCC(buf[:i], true),
}
c.Receivers[0].WriteRTP(pkt)
//log.Printf("[AVC] %v, len: %d", h264.Types(pkt.Payload), len(pkt.Payload))
//log.Printf("[AVC] %v, len: %d", h264.Types(pkt.Payload), len(pkt.Payload))
}
buf = buf[i:]
}
+16 -1
View File
@@ -6,6 +6,7 @@ import (
"errors"
"io"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/flv"
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
@@ -33,6 +34,9 @@ func Open(r io.Reader) (core.Producer, error) {
case bytes.HasPrefix(b, []byte(flv.Signature)):
return flv.Open(rd)
case bytes.HasPrefix(b, []byte{0xFF, 0xF1}):
return aac.Open(rd)
case bytes.HasPrefix(b, []byte("--")):
return multipart.Open(rd)
@@ -40,5 +44,16 @@ func Open(r io.Reader) (core.Producer, error) {
return mpegts.Open(rd)
}
return nil, errors.New("magic: unsupported header: " + hex.EncodeToString(b))
// support MJPEG with trash on start
// https://github.com/AlexxIT/go2rtc/issues/747
if b, err = rd.Peek(4096); err != nil {
return nil, err
}
if i := bytes.Index(b, []byte{0xFF, 0xD8, 0xFF, 0xDB}); i > 0 {
_, _ = io.ReadFull(rd, make([]byte, i))
return mjpeg.Open(rd)
}
return nil, errors.New("magic: unsupported header: " + hex.EncodeToString(b[:4]))
}
+1 -1
View File
@@ -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
}
+2
View File
@@ -106,6 +106,8 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
if track.Codec.IsRTP() {
handler.Handler = h265.RTPDepay(track.Codec, handler.Handler)
} else {
handler.Handler = h265.RepairAVCC(track.Codec, handler.Handler)
}
default:
+36 -8
View File
@@ -1,6 +1,7 @@
package mpegts
import (
"bytes"
"errors"
"io"
@@ -98,6 +99,11 @@ func (d *Demuxer) skip(i byte) {
d.pos += i
}
func (d *Demuxer) readBytes(i byte) []byte {
d.pos += i
return d.buf[d.pos-i : d.pos]
}
func (d *Demuxer) readPSIHeader() {
// https://en.wikipedia.org/wiki/Program-specific_information#Table_Sections
pointer := d.readByte() // Pointer field
@@ -159,7 +165,11 @@ func (d *Demuxer) readPMT() {
_ = d.readBits(4) // Reserved bits
_ = d.readBits(2) // ES Info length unused bits
size = d.readBits(10) // ES Info length
d.skip(byte(size))
info := d.readBytes(byte(size))
if streamType == StreamTypePrivate && bytes.HasPrefix(info, opusInfo) {
streamType = StreamTypePrivateOPUS
}
d.pes[pid] = &PES{StreamType: streamType}
}
@@ -175,7 +185,7 @@ func (d *Demuxer) readPES(pid uint16, start bool) *rtp.Packet {
// if new payload beging
if start {
if pes.Payload != nil {
if len(pes.Payload) != 0 {
d.pos = skipRead
return pes.GetPacket() // finish previous packet
}
@@ -314,12 +324,13 @@ const (
// https://en.wikipedia.org/wiki/Program-specific_information#Elementary_stream_types
const (
StreamTypeMetadata = 0 // Reserved
StreamTypePrivate = 0x06 // PCMU or PCMA or FLAC from FFmpeg
StreamTypeAAC = 0x0F
StreamTypeH264 = 0x1B
StreamTypeH265 = 0x24
StreamTypePCMATapo = 0x90
StreamTypeMetadata = 0 // Reserved
StreamTypePrivate = 0x06 // PCMU or PCMA or FLAC from FFmpeg
StreamTypeAAC = 0x0F
StreamTypeH264 = 0x1B
StreamTypeH265 = 0x24
StreamTypePCMATapo = 0x90
StreamTypePrivateOPUS = 0xEB
)
// PES - Packetized Elementary Stream
@@ -397,6 +408,23 @@ func (p *PES) GetPacket() (pkt *rtp.Packet) {
}
//p.Timestamp += uint32(len(p.Payload)) // update next timestamp!
case StreamTypePrivateOPUS:
p.Sequence++
pkt = &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: p.StreamType,
SequenceNumber: p.Sequence,
Timestamp: p.PTS,
},
}
pkt.Payload, p.Payload = CutOPUSPacket(p.Payload)
p.PTS += opusDT
return
}
p.Payload = nil
+66
View File
@@ -0,0 +1,66 @@
package mpegts
import (
"github.com/AlexxIT/go2rtc/pkg/bits"
)
// opusDT - each AU from FFmpeg has 5 OPUS packets. Each packet len = 960 in the 48000 clock.
const opusDT = 960 * ClockRate / 48000
// https://opus-codec.org/docs/
var opusInfo = []byte{ // registration_descriptor
0x05, // descriptor_tag
0x04, // descriptor_length
'O', 'p', 'u', 's', // format_identifier
}
//goland:noinspection GoSnakeCaseUsage
func CutOPUSPacket(b []byte) (packet []byte, left []byte) {
r := bits.NewReader(b)
size := opus_control_header(r)
if size == 0 {
return nil, nil
}
packet = r.ReadBytes(size)
left = r.Left()
return
}
//goland:noinspection GoSnakeCaseUsage
func opus_control_header(r *bits.Reader) int {
control_header_prefix := r.ReadBits(11)
if control_header_prefix != 0x3FF {
return 0
}
start_trim_flag := r.ReadBit()
end_trim_flag := r.ReadBit()
control_extension_flag := r.ReadBit()
_ = r.ReadBits(2) // reserved
var payload_size int
for {
i := r.ReadByte()
payload_size += int(i)
if i < 255 {
break
}
}
if start_trim_flag != 0 {
_ = r.ReadBits(3)
_ = r.ReadBits(13)
}
if end_trim_flag != 0 {
_ = r.ReadBits(3)
_ = r.ReadBits(13)
}
if control_extension_flag != 0 {
control_extension_length := r.ReadByte()
_ = r.ReadBytes(int(control_extension_length)) // reserved
}
return payload_size
}
+16 -1
View File
@@ -87,7 +87,7 @@ func (c *Producer) probe() error {
case StreamTypeMetadata:
for _, streamType := range pkt.Payload {
switch streamType {
case StreamTypeH264, StreamTypeH265, StreamTypeAAC:
case StreamTypeH264, StreamTypeH265, StreamTypeAAC, StreamTypePrivateOPUS:
waitType = append(waitType, streamType)
}
}
@@ -118,6 +118,19 @@ func (c *Producer) probe() error {
Codecs: []*core.Codec{codec},
}
c.Medias = append(c.Medias, media)
case StreamTypePrivateOPUS:
codec := &core.Codec{
Name: core.CodecOpus,
ClockRate: 48000,
Channels: 2,
}
media := &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
}
c.Medias = append(c.Medias, media)
}
}
@@ -134,6 +147,8 @@ func StreamType(codec *core.Codec) uint8 {
return StreamTypeAAC
case core.CodecPCMA:
return StreamTypePCMATapo
case core.CodecOpus:
return StreamTypePrivateOPUS
}
return 0
}
+3 -2
View File
@@ -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
View File
@@ -0,0 +1,5 @@
## Useful links
- [RFC 3550: RTP: A Transport Protocol for Real-Time Applications](https://datatracker.ietf.org/doc/html/rfc3550)
- [RFC 6716: Definition of the Opus Audio Codec](https://datatracker.ietf.org/doc/html/rfc6716)
- [RFC 7587: RTP Payload Format for the Opus Speech and Audio Codec](https://datatracker.ietf.org/doc/html/rfc7587)
+96
View File
@@ -0,0 +1,96 @@
package opus
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
)
// Some info about this magic:
// - Apple has no respect for RFC 7587 standard and using RFC 3550 for RTP timestamps
// - Apple can request packets with 20ms duration over LAN connection and 60ms over LTE
// - FFmpeg produce packets with 20ms duration by default and only one frame per packet
// - FFmpeg should use "-min_comp 0" option, so every packet will be same duration
// - Apple doesn't care about real sample rate of track
// - Apple only cares about proper timestamp based on REQUESTED sample rate
// RepackToHAP - convert standart RTP packet with OPUS to HAP packet
// We expect that:
// - incoming packet will be 20ms duration and only one frame per packet
// - outgouing packet will be 20ms or 60ms duration
// - incoming sample rate will be any (but not very big if we needs 60ms packets for output)
// - outgouing sample rate will be 16000
// https://github.com/AlexxIT/go2rtc/issues/667
func RepackToHAP(rtpTime byte, handler core.HandlerFunc) core.HandlerFunc {
switch rtpTime {
case 20:
return repackToHAP20(handler)
case 60:
return repackToHAP60(handler)
}
return handler
}
// we using only one sample rate in the pkg/hap/camera/accessory.go
const (
timestamp20 = 16000 * 0.020
timestamp60 = 16000 * 0.060
)
// repackToHAP20 - just fix RTP timestamp from RFC 7587 to RFC 3550
func repackToHAP20(handler core.HandlerFunc) core.HandlerFunc {
var timestamp uint32
return func(pkt *rtp.Packet) {
timestamp += timestamp20
clone := *pkt
clone.Timestamp = timestamp
handler(&clone)
}
}
// repackToHAP60 - collect 20ms frames to single 60ms packet
// thanks to @civita idea https://github.com/AlexxIT/go2rtc/pull/843
func repackToHAP60(handler core.HandlerFunc) core.HandlerFunc {
var sequence uint16
var timestamp uint32
var framesCount byte
var framesSize []byte
var framesData []byte
return func(pkt *rtp.Packet) {
framesData = append(framesData, pkt.Payload[1:]...)
if framesCount++; framesCount < 3 {
if frameSize := len(pkt.Payload) - 1; frameSize >= 252 {
b0 := 252 + byte(frameSize)&0b11
framesSize = append(framesSize, b0, byte(frameSize/4)-b0)
} else {
framesSize = append(framesSize, byte(frameSize))
}
return
}
toc := pkt.Payload[0]
payload := make([]byte, 2, 2+len(framesSize)+len(framesData))
payload[0] = toc | 0b11 // code 3 (multiple frames per packet)
payload[1] = 0b1000_0011 // VBR, no padding, 3 frames
payload = append(payload, framesSize...)
payload = append(payload, framesData...)
sequence++
timestamp += timestamp60
clone := *pkt
clone.Payload = payload
clone.SequenceNumber = sequence
clone.Timestamp = timestamp
handler(&clone)
framesCount = 0
framesSize = framesSize[:0]
framesData = framesData[:0]
}
}
+69
View File
@@ -0,0 +1,69 @@
package opus
import (
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
"github.com/rs/zerolog/log"
)
func Log(handler core.HandlerFunc) core.HandlerFunc {
var ts uint32
return func(pkt *rtp.Packet) {
if ts == 0 {
ts = pkt.Timestamp
}
toc := pkt.Payload[0]
//config := toc >> 3
code := toc & 0b11
frame := parseFrameSize(toc)
rate := parseSampleRate(toc)
log.Printf(
"[RTP/OPUS] frame=%s rate=%5d code=%d size=%6d ts=%10d dt=%5d pt=%2d ssrc=%d seq=%d mark=%t",
frame, rate, code, len(pkt.Payload), pkt.Timestamp, pkt.Timestamp-ts, pkt.PayloadType, pkt.SSRC, pkt.SequenceNumber, pkt.Marker,
)
ts = pkt.Timestamp
handler(pkt)
}
}
func parseFrameSize(toc byte) time.Duration {
switch toc >> 3 {
case 0, 4, 8, 12, 14, 18, 22, 26, 30:
return 10_000_000
case 1, 5, 9, 13, 15, 19, 23, 27, 31:
return 20_000_000
case 2, 6, 10:
return 40_000_000
case 3, 7, 11:
return 60_000_000
case 16, 20, 24, 28:
return 2_500_000
case 17, 21, 25, 29:
return 5_000_000
}
return 0
}
func parseSampleRate(toc byte) uint16 {
switch toc >> 3 {
case 0, 1, 2, 3, 16, 17, 18, 19:
return 8000
case 4, 5, 6, 7:
return 12000
case 8, 9, 10, 11, 20, 21, 22, 23:
return 16000
case 12, 13, 24, 25, 26, 27:
return 24000
case 14, 15, 28, 29, 30, 31:
return 48000
}
return 0
}
+1 -1
View File
@@ -90,7 +90,7 @@ func (c *Client) Connect() error {
}
// 4. Create Peer Connection
api, err := webrtc.NewAPI("")
api, err := webrtc.NewAPI()
if err != nil {
return err
}
+6 -4
View File
@@ -38,9 +38,8 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) {
// Fix invalid media type (errSDPInvalidValue) caused by
// some TP-LINK IP camera, e.g. TL-IPC44GW
rawSDP = bytes.ReplaceAll(rawSDP, []byte("m=application/TP-LINK "), []byte("m=application "))
// more tplink ipcams
rawSDP = bytes.ReplaceAll(rawSDP, []byte("m=application/tp-link "), []byte("m=application "))
m := regexp.MustCompile("m=application/[^ ]+")
rawSDP = m.ReplaceAll(rawSDP, []byte("m=application"))
if err == io.EOF {
rawSDP = append(rawSDP, '\n')
@@ -53,6 +52,9 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) {
}
}
// fix buggy camera https://github.com/AlexxIT/go2rtc/issues/771
forceDirection := sd.Origin.Username == "CV-RTSPHandler"
var medias []*core.Media
for _, md := range sd.MediaDescriptions {
@@ -66,7 +68,7 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) {
}
}
if media.Direction == "" {
if media.Direction == "" || forceDirection {
media.Direction = core.DirectionRecvonly
}
+53 -1
View File
@@ -1,8 +1,9 @@
package rtsp
import (
"github.com/stretchr/testify/assert"
"testing"
"github.com/stretchr/testify/assert"
)
func TestURLParse(t *testing.T) {
@@ -107,3 +108,54 @@ a=sendonly`
assert.Nil(t, err)
assert.Len(t, medias, 3)
}
func TestBugSDP4(t *testing.T) {
s := `v=0
o=- 14665860 31787219 1 IN IP4 10.0.0.94
s=Session streamed by "MERCURY RTSP Server"
t=0 0
m=video 0 RTP/AVP 96
c=IN IP4 0.0.0.0
b=AS:4096
a=range:npt=0-
a=control:track1
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1; profile-level-id=640016; sprop-parameter-sets=Z2QAFqzGoCgPaEAAAAMAQAAAB6E=,aOqPLA==
m=audio 0 RTP/AVP 8
a=rtpmap:8 PCMA/8000
a=control:track2
m=application/MERCURY 0 RTP/AVP smart/1/90000
a=rtpmap:95 MERCURY/90000
a=control:track3
`
medias, err := UnmarshalSDP([]byte(s))
assert.Nil(t, err)
assert.Len(t, medias, 3)
}
func TestBugSDP5(t *testing.T) {
s := `v=0
o=CV-RTSPHandler 1123412 0 IN IP4 192.168.1.22
s=Camera
c=IN IP4 192.168.1.22
t=0 0
a=charset:Shift_JIS
a=range:npt=0-
a=control:*
a=etag:1234567890
m=video 0 RTP/AVP 99
a=rtpmap:99 H264/90000
a=fmtp:99 profile-level-id=42A01E;packetization-mode=1;sprop-parameter-sets=Z0KgKedAPAET8uAIEAABd2AAK/IGAAADAC+vCAAAHc1lP//jAAADABfXhAAADuayn//wIA==,aN48gA==
a=control:trackID=1
a=sendonly
m=audio 0 RTP/AVP 127
a=rtpmap:127 mpeg4-generic/8000/1
a=fmtp:127 streamtype=5; profile-level-id=15; mode=AAC-hbr; sizeLength=13; indexLength=3; indexDeltalength=3; config=1588; CTSDeltaLength=0; DTSDeltaLength=0;
a=control:trackID=2
`
medias, err := UnmarshalSDP([]byte(s))
assert.Nil(t, err)
assert.Len(t, medias, 2)
assert.Equal(t, "recvonly", medias[0].Direction)
assert.Equal(t, "recvonly", medias[1].Direction)
}
+18 -25
View File
@@ -14,35 +14,28 @@ func QuoteSplit(s string) []string {
var a []string
for len(s) > 0 {
is := strings.IndexByte(s, ' ')
if is >= 0 {
// skip prefix and double spaces
if is == 0 {
// goto next symbol
s = s[1:]
continue
switch c := s[0]; c {
case '\t', '\n', '\r', ' ': // unicode.IsSpace
s = s[1:]
case '"', '\'': // quote chars
if i := strings.IndexByte(s[1:], c); i > 0 {
a = append(a, s[1:i+1])
s = s[i+2:]
} else {
return nil // error
}
// check if quote in word
if i := strings.IndexByte(s[:is], '"'); i >= 0 {
// search quote end
if is = strings.Index(s, `" `); is > 0 {
is += 1
} else {
is = -1
}
default:
i := strings.IndexAny(s, "\t\n\r ")
if i > 0 {
a = append(a, s[:i])
s = s[i:]
} else {
a = append(a, s)
s = ""
}
}
if is >= 0 {
a = append(a, strings.ReplaceAll(s[:is], `"`, ""))
s = s[is+1:]
} else {
//add last word
a = append(a, s)
break
}
}
return a
}
+18
View File
@@ -0,0 +1,18 @@
package shell
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestQuoteSplit(t *testing.T) {
s := `
python "-c" 'import time
print("time", time.time())'
`
require.Equal(t, []string{"python", "-c", "import time\nprint(\"time\", time.time())"}, QuoteSplit(s))
s = `ffmpeg -i "video=FaceTime HD Camera" -i "DeckLink SDI (2)"`
require.Equal(t, []string{"ffmpeg", "-i", `video=FaceTime HD Camera`, "-i", "DeckLink SDI (2)"}, QuoteSplit(s))
}
+2 -1
View File
@@ -57,7 +57,8 @@ func (s *Server) DelSession(session *Session) {
delete(s.sessions, session.Remote.SSRC)
if len(s.sessions) == 0 {
// check s.conn for https://github.com/AlexxIT/go2rtc/issues/734
if len(s.sessions) == 0 && s.conn != nil {
_ = s.conn.Close()
}
+100 -21
View File
@@ -1,10 +1,12 @@
package tapo
import (
"bufio"
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
@@ -14,6 +16,7 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
@@ -36,6 +39,7 @@ type Client struct {
session1 string
session2 string
request string
recv int
send int
@@ -62,33 +66,19 @@ func (c *Client) newConn() (net.Conn, error) {
return nil, err
}
// support raw username/password
username := u.User.Username()
password, _ := u.User.Password()
// or cloud password in place of username
if password == "" {
password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
username = "admin"
u.User = url.UserPassword(username, password)
}
u.Scheme = "http"
u.Path = "/stream"
if u.Port() == "" {
u.Host += ":8800"
}
// TODO: fix closing connection
ctx, pconn := tcp.WithConn()
req, err := http.NewRequestWithContext(ctx, "POST", u.String(), nil)
req, err := http.NewRequest("POST", "http://"+u.Host+"/stream", nil)
if err != nil {
return nil, err
}
req.URL.User = u.User
req.Header.Set("Content-Type", "multipart/mixed; boundary=--client-stream-boundary--")
res, err := tcp.Do(req)
conn, res, err := dial(req)
if err != nil {
return nil, err
}
@@ -98,13 +88,35 @@ func (c *Client) newConn() (net.Conn, error) {
}
if c.decrypt == nil {
c.newDectypter(res, username, password)
c.newDectypter(res)
}
return *pconn, nil
query := u.Query()
channel := query.Get("channel")
if channel == "" {
channel = "0"
}
subtype := query.Get("subtype")
switch subtype {
case "", "0":
subtype = "HD"
case "1":
subtype = "VGA"
}
c.request = fmt.Sprintf(
`{"params":{"preview":{"audio":["default"],"channels":[%s],"resolutions":["%s"]},"method":"get"},"seq":1,"type":"request"}`,
channel, subtype,
)
return conn, nil
}
func (c *Client) newDectypter(res *http.Response, username, password string) {
func (c *Client) newDectypter(res *http.Response) {
username := res.Request.URL.User.Username()
password, _ := res.Request.URL.User.Password()
// extract nonce from response
// cipher="AES_128_CBC" username="admin" padding="PKCS7_16" algorithm="MD5" nonce="***"
nonce := res.Header.Get("Key-Exchange")
@@ -139,7 +151,7 @@ func (c *Client) SetupStream() (err error) {
}
// audio: default, disable, enable
c.session1, err = c.Request(c.conn1, []byte(`{"params":{"preview":{"audio":["default"],"channels":[0],"resolutions":["HD"]},"method":"get"},"seq":1,"type":"request"}`))
c.session1, err = c.Request(c.conn1, []byte(c.request))
return
}
@@ -244,3 +256,70 @@ func (c *Client) Request(conn net.Conn, body []byte) (string, error) {
return v.Params.SessionID, nil
}
}
func dial(req *http.Request) (net.Conn, *http.Response, error) {
conn, err := net.DialTimeout("tcp", req.URL.Host, core.ConnDialTimeout)
if err != nil {
return nil, nil, err
}
username := req.URL.User.Username()
password, _ := req.URL.User.Password()
req.URL.User = nil
if err = req.Write(conn); err != nil {
return nil, nil, err
}
r := bufio.NewReader(conn)
res, err := http.ReadResponse(r, req)
if err != nil {
return nil, nil, err
}
auth := res.Header.Get("WWW-Authenticate")
if res.StatusCode != http.StatusUnauthorized || !strings.HasPrefix(auth, "Digest") {
return nil, nil, err
}
if password == "" {
// support cloud password in place of username
if strings.Contains(auth, `encrypt_type="3"`) {
password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username)))
} else {
password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
}
username = "admin"
}
realm := tcp.Between(auth, `realm="`, `"`)
nonce := tcp.Between(auth, `nonce="`, `"`)
qop := tcp.Between(auth, `qop="`, `"`)
uri := req.URL.RequestURI()
ha1 := tcp.HexMD5(username, realm, password)
ha2 := tcp.HexMD5(req.Method, uri)
nc := "00000001"
cnonce := "00000001"
response := tcp.HexMD5(ha1, nonce, nc, cnonce, qop, ha2)
header := fmt.Sprintf(
`Digest username="%s", realm="%s", nonce="%s", uri="%s", qop=%s, nc=%s, cnonce="%s", response="%s"`,
username, realm, nonce, uri, qop, nc, cnonce, response,
)
req.Header.Set("Authorization", header)
if err = req.Write(conn); err != nil {
return nil, nil, err
}
if res, err = http.ReadResponse(r, req); err != nil {
return nil, nil, err
}
req.URL.User = url.UserPassword(username, password)
return conn, res, nil
}
+52 -25
View File
@@ -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
View File
@@ -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(
+1 -1
View File
@@ -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
View File
@@ -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,
)
}
+71
View File
@@ -0,0 +1,71 @@
package webrtc
import (
"testing"
"github.com/pion/webrtc/v3"
"github.com/stretchr/testify/require"
)
func TestAlexa(t *testing.T) {
// from https://github.com/AlexxIT/go2rtc/issues/825
offer := `v=0
o=- 3911343731 3911343731 IN IP4 0.0.0.0
s=a 2 z
c=IN IP4 0.0.0.0
t=0 0
a=group:BUNDLE audio0 video0
m=audio 1 UDP/TLS/RTP/SAVPF 96 0 8
a=candidate:1 1 UDP 2013266431 52.90.193.210 60128 typ host
a=candidate:2 1 TCP 1015021823 52.90.193.210 9 typ host tcptype active
a=candidate:3 1 TCP 1010827519 52.90.193.210 45962 typ host tcptype passive
a=candidate:1 2 UDP 2013266430 52.90.193.210 46109 typ host
a=candidate:2 2 TCP 1015021822 52.90.193.210 9 typ host tcptype active
a=candidate:3 2 TCP 1010827518 52.90.193.210 53795 typ host tcptype passive
a=setup:actpass
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=rtpmap:96 opus/48000/2
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtcp:9 IN IP4 0.0.0.0
a=rtcp-mux
a=sendrecv
a=mid:audio0
a=ssrc:3573704076 cname:user3856789923@host-9dd1dd33
a=ice-ufrag:gxfV
a=ice-pwd:KepKrlQ1+LD+RGTAFaqVck
a=fingerprint:sha-256 A2:93:53:50:E4:2F:C5:4E:DF:7C:70:99:5A:A7:39:50:1A:63:E5:B2:CA:73:70:7A:C5:F4:01:BF:BD:99:57:FC
m=video 1 UDP/TLS/RTP/SAVPF 99
a=candidate:1 1 UDP 2013266431 52.90.193.210 60128 typ host
a=candidate:1 2 UDP 2013266430 52.90.193.210 46109 typ host
a=candidate:2 1 TCP 1015021823 52.90.193.210 9 typ host tcptype active
a=candidate:3 1 TCP 1010827519 52.90.193.210 45962 typ host tcptype passive
a=candidate:3 2 TCP 1010827518 52.90.193.210 53795 typ host tcptype passive
a=candidate:2 2 TCP 1015021822 52.90.193.210 9 typ host tcptype active
b=AS:2500
a=setup:actpass
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=rtpmap:99 H264/90000
a=rtcp:9 IN IP4 0.0.0.0
a=rtcp-mux
a=sendrecv
a=mid:video0
a=rtcp-fb:99 nack
a=rtcp-fb:99 nack pli
a=rtcp-fb:99 ccm fir
a=ssrc:3778078295 cname:user3856789923@host-9dd1dd33
a=ice-ufrag:gxfV
a=ice-pwd:KepKrlQ1+LD+RGTAFaqVck
a=fingerprint:sha-256 A2:93:53:50:E4:2F:C5:4E:DF:7C:70:99:5A:A7:39:50:1A:63:E5:B2:CA:73:70:7A:C5:F4:01:BF:BD:99:57:FC
`
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
require.Nil(t, err)
conn := NewConn(pc)
err = conn.SetOffer(offer)
require.Nil(t, err)
_, err = conn.GetAnswer()
require.Nil(t, err)
}
+12
View File
@@ -246,6 +246,18 @@
</script>
<button id="gopro">GoPro</button>
<div class="module">
<table id="gopro-table"></table>
</div>
<script>
document.getElementById('gopro').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
await getSources('gopro-table', 'api/gopro');
});
</script>
<button id="hass">Home Assistant</button>
<div class="module">
<table id="hass-table"></table>
+130
View File
@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Logs</title>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
body {
font-family: Arial, Helvetica, sans-serif;
background-color: white;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
html, body {
width: 100%;
height: 100%;
}
table {
background-color: white;
text-align: left;
border-collapse: collapse;
}
table td, table th {
border: 1px solid black;
padding: 5px 5px;
}
table tbody td {
font-size: 13px;
vertical-align: top;
}
table thead {
background: #CFCFCF;
background: linear-gradient(to bottom, #dbdbdb 0%, #d3d3d3 66%, #CFCFCF 100%);
border-bottom: 3px solid black;
}
table thead th {
font-size: 15px;
font-weight: bold;
color: black;
text-align: center;
}
</style>
</head>
<body>
<script src="main.js"></script>
<div>
<button id="clean">Clean</button>
<button id="update">Auto Update: ON</button>
</div>
<br>
<table>
<thead>
<tr>
<th style="width: 130px">Time</th>
<th style="width: 40px">Level</th>
<th>Message</th>
</tr>
</thead>
<tbody id="log">
</tbody>
</table>
<script>
document.getElementById('clean').addEventListener('click', async () => {
const r = await fetch('api/log', {method: 'DELETE'});
if (r.ok) reload();
alert(await r.text());
});
// Sanitizes the input text to prevent XSS when inserting into the DOM
function escapeHTML(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
.replace(/\n/g, '<br>');
}
function applyLogStyling(jsonlines) {
const KEYS = ['time', 'level', 'message'];
const lines = JSON.parse('[' + jsonlines.trimEnd().replaceAll('\n', ',') + ']');
return lines.map(line => {
const ts = new Date(line['time']);
const msg = Object.keys(line).reduce((msg, key) => {
return KEYS.indexOf(key) < 0 ? `${msg} ${key}=${line[key]}` : msg;
}, line['message']);
return `<tr><td>${ts.toLocaleString()}</td><td>${line['level']}</td><td>${escapeHTML(msg)}</td></tr>`;
}).join('');
}
function reload() {
const url = new URL('api/log', location.href);
fetch(url, {cache: 'no-cache'})
.then(response => response.text())
.then(data => {
// Apply styling to the log data
document.getElementById('log').innerHTML = applyLogStyling(data);
})
.catch(error => {
console.error('An error occurred:', error);
});
}
reload();
// Handle auto-update switch
let autoUpdateEnabled = true;
const update = document.getElementById('update');
update.addEventListener('click', () => {
autoUpdateEnabled = !autoUpdateEnabled;
update.textContent = `Auto Update: ${autoUpdateEnabled ? 'ON' : 'OFF'}`;
});
// Reload the logs every 5 seconds
setInterval(() => {
if (autoUpdateEnabled) reload();
}, 5000);
</script>
</body>
</html>
+1
View File
@@ -47,6 +47,7 @@ nav li {
<li><a href="index.html">Streams</a></li>
<li><a href="add.html">Add</a></li>
<li><a href="editor.html">Config</a></li>
<li><a href="log.html">Log</a></li>
</ul>
</nav>
` + document.body.innerHTML;