Compare commits

...

131 Commits

Author SHA1 Message Date
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
Alex X 20d45bff92 Update to version 1.8.1 2023-10-15 20:15:35 +03:00
Alex X 4ad67e9f6f Update external dependencies 2023-10-15 20:15:26 +03:00
Alex X e367940bd9 Fix version in API 2023-10-15 09:37:24 +03:00
Alex X 6f2af78392 Update version to 1.8.0 2023-10-14 17:19:19 +03:00
Alex X 548d8133eb Update readme with new features 2023-10-14 17:17:13 +03:00
Alex X 36ee2b29fb Update TLS files handling 2023-10-14 15:51:43 +03:00
Alex X 05accb4555 Add support media config param for JS player 2023-10-14 11:41:45 +03:00
Alex X f949a278da Fix HLS JS error on latest iOS 2023-10-14 11:40:49 +03:00
Alex X bfae16f3a0 Improve SPS parser 2023-10-14 08:11:03 +03:00
Alex X d09d21434b Panic if shell can't restart process 2023-10-14 08:06:09 +03:00
Alex X 2b9926cedb Support broken SPS for MP4 2023-10-14 08:04:20 +03:00
Alex X af24fd67aa Fix snapshots for some streams 2023-10-13 14:46:24 +03:00
Alex X e2cd34ffe3 Fix hap secure connection 2023-10-13 11:25:17 +03:00
Alex X ecdf5ba271 Fix homekit proxy after events handler 2023-10-13 11:23:00 +03:00
Alex X 995ef5bb36 Add support RTMP from Dahua cameras 2023-10-12 17:55:03 +03:00
Alex X 8165adcab1 Rewrite hap secure connection 2023-10-12 17:03:58 +03:00
Alex X 91c4a3e7b5 Add ffmpeg test for DeckLink 2023-10-11 22:35:53 +03:00
Alex X cb710ea2be Update dvrip source processing 2023-10-11 22:23:22 +03:00
Alex X 843a3ae9c9 Total rework DVRIP source + add two way audio #633 2023-10-11 19:47:26 +03:00
Alex X de040fb160 Fix panic for homekit source (nil conn) #628 2023-10-11 14:34:01 +03:00
Alex X acec8a76aa Fix panic from roborock source (iot.Dial error) #601 2023-10-11 14:26:02 +03:00
Alex X 6c07c59454 Fix panic on aac.RTPDepay #635 2023-10-11 14:21:56 +03:00
Alex X 4d708b5385 Fix send audio to RTSP (cuts out after 30 seconds) #659 2023-10-11 13:56:40 +03:00
Alex X 2e9f3181d4 Fix onvif source: invalid control character in URL #662 2023-10-11 13:43:45 +03:00
Alex X 3ae15d8f80 Add support TLS cert/key as file path #680 2023-10-11 11:50:30 +03:00
Alex X d016529030 Merge pull request #632 from skrashevich/230911-fix-hap-pairing-dups
Fix: duplicate pairing strings in config
2023-10-11 11:33:05 +03:00
Alex X 09f1553e40 Fix SO_REUSEPORT for macOS #626 2023-10-11 11:31:37 +03:00
Alex X 52e4bf1b35 Add retry for publish 2023-10-11 07:14:43 +03:00
Alex X bbe6ae0059 Update publish RTMP examples 2023-10-11 06:58:17 +03:00
Alex X c02117e626 Add support incoming RTMP 2023-10-11 06:54:50 +03:00
Alex X b8fb3acbab Fix Tapo error on setup 2023-10-11 06:52:39 +03:00
Alex X d4d0064220 Change publish start time from 5 to 1 second 2023-10-11 06:52:23 +03:00
Alex X 855bbdeb60 Update ffmpeg tests 2023-10-10 12:25:07 +03:00
Alex X 05893c9203 Add fix for YCbCr range on hardware transcoding 2023-10-10 11:29:22 +03:00
Alex X c9c8e73587 Add feature auto publish on app start 2023-10-09 23:09:28 +03:00
Alex X c7b6eb5d5b Fix ffmpeg pix_fmt for H264 transcoding 2023-10-09 23:08:18 +03:00
Alex X 96bc88d8ce Add copyright for restart func 2023-10-09 17:37:53 +03:00
Alex X 9a2e9dd6d1 Add support /api/restart #652 2023-10-09 17:12:25 +03:00
Alex X b252fcaaa1 Code refactoring after #661 2023-10-05 17:31:59 +03:00
Alex X c582b932c7 Merge pull request #661 from skrashevich/230930-unpkg 2023-10-05 17:31:52 +03:00
Alex X c3f26c4db8 Fix logger for rtmp 2023-10-05 16:37:58 +03:00
Sergey Krashevich f27f7d28bb Update editor.html 2023-09-30 12:38:31 +03:00
Alex X 0424b1a92a Merge pull request #656 from skrashevich/230927-fix-whep-link
fix broken link in README
2023-09-27 14:35:09 +03:00
Sergey Krashevich 81fb8fc238 Update README.md 2023-09-27 14:11:03 +03:00
Alex X 037970a4ea Add support ManagedMediaSource for Safari 17 2023-09-26 13:25:50 +03:00
Alex X 3f6e83e87c Merge pull request #653 from skrashevich/230925-fix-openapi-spec
fix openapi specs
2023-09-25 10:33:05 +03:00
Sergey Krashevich aa5b23fa80 fix openapi specs 2023-09-25 07:41:55 +03:00
Alex X 02bde2c8b7 Add RTMP publish to WebUI 2023-09-17 20:39:31 +03:00
Alex X cb5e90cc3b Add RTMP server and publish to RMTP logic 2023-09-17 20:31:36 +03:00
Alex X 209fe09806 Add active publish logic to streams 2023-09-17 20:29:28 +03:00
Alex X dca8279e0c Update AMF tests 2023-09-17 20:28:09 +03:00
Alex X 8163c7a520 Update tcp.Dial func 2023-09-17 20:28:09 +03:00
Alex X 4dffceaf7e Update FLV muxer 2023-09-17 14:07:55 +03:00
Alex X 9f1e33e0c6 Add output to HTTP-FLV 2023-09-16 11:14:56 +03:00
Alex X 9a7d7e68e2 Add tests to AMF reader 2023-09-16 11:12:51 +03:00
Alex X ab18d5d1ca Improve magic bitstream producer 2023-09-16 11:11:23 +03:00
Alex X 6e53e74742 Add WriteEcmaArray func for AMF proto 2023-09-16 11:10:32 +03:00
Alex X f910bd4fce Add auto Flush to core WriteBuffer 2023-09-16 11:09:46 +03:00
Alexey Khit 93e475f3a4 Fix support ONVIF client with line breaks #638 2023-09-13 18:08:57 +03:00
Alexey Khit e5d8170037 Add HomeKit accessories parser 2023-09-12 21:04:55 +03:00
Alexey Khit 861632f92b Add support events for HomeKit client 2023-09-12 21:04:19 +03:00
Alexey Khit 9cf75565b5 Fix error for HAP server 2023-09-12 21:02:40 +03:00
Sergey Krashevich 9368a6b85e Add conditional check before adding a new pair in the server's AddPair function 2023-09-11 10:34:31 +03:00
Alexey Khit c8ac6b2271 Update version to 1.7.1 2023-09-10 20:19:20 +03:00
Alexey Khit 28f5c2b974 Update dependencies 2023-09-10 20:01:39 +03:00
Alexey Khit daa2522a52 Fix panic for HomeKit source 2023-09-10 16:10:00 +03:00
Alexey Khit 863f8ec19b Fix malformed HTTP version for HomeKit source #620 2023-09-10 16:08:06 +03:00
Alexey Khit 8f98fc4547 Fix after #614 fix 2023-09-10 16:03:49 +03:00
Alexey Khit 398afbe49f Update default deadline from 3 to 5 seconds 2023-09-10 16:03:08 +03:00
Alexey Khit ad8c0ab2fb Fix HomeKit pairing for some cameras 2023-09-10 14:56:00 +03:00
Alexey Khit 37130576e9 Add support webrtc go2rtc source with auth #539 2023-09-10 07:58:20 +03:00
Alex X 486fea2227 Merge pull request #611 from felipecrs/patch-1
Clarify import from go2rtc to hass generic camera
2023-09-10 07:18:17 +03:00
Felipe Santos 6d7357b151 Update README.md 2023-09-09 15:16:58 -03:00
Alex X 452d7577f8 Merge pull request #614 from skrashevich/230905-fix-runtime-crash
Refactor LocalIP method to correctly handle non-TCP connections
2023-09-09 17:37:52 +03:00
Alexey Khit 124398115e Restore fix for Chinese buggy cameras 2023-09-06 13:15:04 +03:00
Sergey Krashevich 541a7b28a7 Refactor LocalIP method to correctly handle non-TCP connections 2023-09-05 10:27:12 +03:00
Felipe Santos 947b0970ad Update README.md 2023-09-04 13:23:16 -03:00
Felipe Santos 447fd5b3eb Clarify import from go2rtc to hass generic camera
The protocol is not set by default. According to my tests, only TCP works.
2023-09-04 13:22:11 -03:00
Alexey Khit 064ffef462 Add check config changes during WebUI 2023-09-04 12:05:17 +03:00
Alexey Khit 05360ac284 Fix patch YAML without new line on end of file 2023-09-04 11:52:13 +03:00
Alexey Khit 08dabc7331 Add support HomeKit doorbells 2023-09-02 20:34:39 +03:00
Alexey Khit d724df7db2 Fix HomeKit PIN in docs 2023-09-02 19:25:48 +03:00
110 changed files with 4261 additions and 1551 deletions
+104 -34
View File
@@ -14,6 +14,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [DVRIP](#source-dvrip), [HTTP](#source-http) (FLV/MJPEG/JPEG/TS), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
- streaming from any sources, supported by [FFmpeg](#source-ffmpeg)
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4), [HomeKit](#module-homekit) [HLS](#module-hls) or [MJPEG](#module-mjpeg)
- [publish](#publish-stream) any source to popular streaming services (YouTube, Telegram, etc.)
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
- support H265 for WebRTC in browser (Safari only, [read more](https://github.com/AlexxIT/Blog/issues/5))
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
@@ -22,7 +23,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- mixing tracks from different sources to single stream
- auto match client supported codecs
- [2-way audio](#two-way-audio) for some cameras
- streaming from private networks via [Ngrok](#module-ngrok)
- streaming from private networks via [ngrok](#module-ngrok)
- can be [integrated to](#module-api) any smart home platform or be used as [standalone app](#go2rtc-binary)
**Inspired by:**
@@ -53,11 +54,13 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
* [Source: FFmpeg Device](#source-ffmpeg-device)
* [Source: Exec](#source-exec)
* [Source: Echo](#source-echo)
* [Source: Expr](#source-expr)
* [Source: HomeKit](#source-homekit)
* [Source: Bubble](#source-bubble)
* [Source: DVRIP](#source-dvrip)
* [Source: Tapo](#source-tapo)
* [Source: Kasa](#source-kasa)
* [Source: GoPro](#source-gopro)
* [Source: Ivideon](#source-ivideon)
* [Source: Hass](#source-hass)
* [Source: ISAPI](#source-isapi)
@@ -67,12 +70,14 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
* [Source: WebTorrent](#source-webtorrent)
* [Incoming sources](#incoming-sources)
* [Stream to camera](#stream-to-camera)
* [Publish stream](#publish-stream)
* [Module: API](#module-api)
* [Module: RTSP](#module-rtsp)
* [Module: RTMP](#module-rtmp)
* [Module: WebRTC](#module-webrtc)
* [Module: HomeKit](#module-homekit)
* [Module: WebTorrent](#module-webtorrent)
* [Module: Ngrok](#module-ngrok)
* [Module: ngrok](#module-ngrok)
* [Module: Hass](#module-hass)
* [Module: MP4](#module-mp4)
* [Module: HLS](#module-hls)
@@ -122,7 +127,7 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
### go2rtc: Docker
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [Ngrok](#module-ngrok) and [Python](#source-echo).
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [ngrok](#module-ngrok) and [Python](#source-echo).
### go2rtc: Home Assistant Add-on
@@ -165,7 +170,7 @@ Available modules:
- [hls](#module-hls) - HLS TS or fMP4 stream Server
- [mjpeg](#module-mjpeg) - MJPEG Server
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
- [ngrok](#module-ngrok) - ngrok integration (external access for private network)
- [hass](#module-hass) - Home Assistant integration
- [log](#module-log) - logs config
@@ -183,11 +188,13 @@ Available source types:
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
- [exec](#source-exec) - get media from external app output
- [echo](#source-echo) - get stream link from bash or python
- [expr](#source-expr) - get stream link via built-in expression language
- [homekit](#source-homekit) - streaming from HomeKit Camera
- [bubble](#source-bubble) - streaming from ESeeCloud/dvr163 NVR
- [dvrip](#source-dvrip) - streaming from DVR-IP NVR
- [tapo](#source-tapo) - TP-Link Tapo cameras with [two way audio](#two-way-audio) support
- [kasa](#source-tapo) - TP-Link Kasa cameras
- [gopro](#source-gopro) - GoPro cameras
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
- [hass](#source-hass) - Home Assistant integration
- [isapi](#source-isapi) - two way audio for Hikvision (ISAPI) cameras
@@ -202,6 +209,7 @@ Read more about [incoming sources](#incoming-sources)
Supported for sources:
- [RTSP cameras](#source-rtsp) with [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) (back channel connection)
- [DVRIP](#source-dvrip) cameras
- [TP-Link Tapo](#source-tapo) cameras
- [Hikvision ISAPI](#source-isapi) cameras
- [Roborock vacuums](#source-roborock) models with cameras
@@ -422,6 +430,10 @@ streams:
apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html
```
#### Source: Expr
Like `echo` source, but uses the built-in [expr](https://github.com/antonmedv/expr) expression language ([read more](https://github.com/AlexxIT/go2rtc/blob/master/internal/expr/README.md)).
#### Source: HomeKit
**Important:**
@@ -478,7 +490,11 @@ Other names: DVR-IP, NetSurveillance, Sofia protocol (NETsurveillance ActiveX pl
```yaml
streams:
camera1: dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
only_stream: dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
only_tts: dvrip://username:password@192.168.1.123:34567?backchannel=1
two_way_audio:
- dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
- dvrip://username:password@192.168.1.123:34567?backchannel=1
```
#### Source: Tapo
@@ -506,6 +522,10 @@ streams:
kasa: kasa://user:pass@192.168.1.123:19443/https/stream/mixed
```
#### Source: GoPro
Support streaming from [GoPro](https://gopro.com/) cameras, connected via USB or Wi-Fi to Linux, Mac, Windows. [Read more](https://github.com/AlexxIT/go2rtc/tree/master/internal/gopro).
#### Source: Ivideon
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
@@ -590,7 +610,7 @@ This source type support four connection formats.
**whep**
[WebRTC/WHEP](https://www.ietf.org/id/draft-murillo-whep-01.html) - is an unapproved standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
[WebRTC/WHEP](https://www.ietf.org/id/draft-murillo-whep-02.html) - is an unapproved standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
**go2rtc**
@@ -632,7 +652,7 @@ streams:
By default, go2rtc establishes a connection to the source when any client requests it. Go2rtc drops the connection to the source when it has no clients left.
- Go2rtc also can accepts incoming sources in [RTSP](#source-rtsp), [HTTP](#source-http) and **WebRTC/WHIP** formats
- Go2rtc also can accepts incoming sources in [RTSP](#module-rtsp), [RTMP](#module-rtmp), [HTTP](#source-http) and **WebRTC/WHIP** formats
- Go2rtc won't stop such a source if it has no clients
- You can push data only to existing stream (create stream with empty source in config)
- You can push multiple incoming sources to same stream
@@ -693,6 +713,39 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
- you can stop active playback by calling the API with the empty `src` parameter
- you will see one active producer and one active consumer in go2rtc WebUI info page during streaming
### Publish stream
You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:
- Supported codecs: H264 for video and AAC for audio
- Pixel format should be `yuv420p`, for cameras with `yuvj420p` format you SHOULD use [transcoding](#source-ffmpeg)
- You don't need to enable [RTMP module](#module-rtmp) listening for this task
You can use API:
```
POST http://localhost:1984/api/streams?src=camera1&dst=rtmps://...
```
Or config file:
```yaml
publish:
# publish stream "tplink_tapo" to Telegram
tplink_tapo: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
# publish stream "other_camera" to Telegram and YouTube
other_camera:
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
- rtmps://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
streams:
# for TP-Link cameras it's important to use transcoding because of wrong pixel format
tplink_tapo: ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware=vaapi#audio=aac
```
- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.
- **YouTube** > Create > Go live > Stream latency: Ultra low-latency > Copy: Stream URL + Stream key.
### Module: API
The HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.
@@ -705,6 +758,7 @@ The HTTP API is the main part for interacting with the application. Default addr
- you can enable HTTP API only on localhost with `listen: "127.0.0.1:1984"` setting
- you can change API `base_path` and host go2rtc on your main app webserver suburl
- all files from `static_dir` hosted on root path: `/`
- you can use raw TLS cert/key content or path to files
```yaml
api:
@@ -753,6 +807,17 @@ By default go2rtc provide RTSP-stream with only one first video and only one fir
Read more about [codecs filters](#codecs-filters).
### Module: RTMP
You can get any stream as RTMP-stream: `rtmp://192.168.1.123/{stream_name}`. Only H264/AAC codecs supported right now.
[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has differnt problems with this format.
```yaml
rtmp:
listen: ":1935" # by default - disabled!
```
### Module: WebRTC
In most cases [WebRTC](https://en.wikipedia.org/wiki/WebRTC) uses direct peer-to-peer connection from your browser to go2rtc and sends media data via UDP.
@@ -796,7 +861,7 @@ webrtc:
**Private IP**
- setup integration with [Ngrok service](#module-ngrok)
- setup integration with [ngrok service](#module-ngrok)
```yaml
ngrok:
@@ -837,7 +902,7 @@ HomeKit module can work in two modes:
streams:
dahua1: rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0
homekit:
dahua1: # same stream ID from streams list, default PIN - 195502224
dahua1: # same stream ID from streams list, default PIN - 19550224
```
**Full config**
@@ -851,7 +916,7 @@ streams:
homekit:
dahua1: # same stream ID from streams list
pin: 12345678 # custom PIN, default: 195502224
pin: 12345678 # custom PIN, default: 19550224
name: Dahua camera # custom camera name, default: generated from stream ID
device_id: dahua1 # custom ID, default: generated from stream ID
device_private: dahua1 # custom key, default: generated from stream ID
@@ -898,29 +963,29 @@ Link example: https://alexxit.github.io/go2rtc/#share=02SNtgjKXY&pwd=wznEQqznxW&
TODO: article how it works...
### Module: Ngrok
### Module: ngrok
With Ngrok integration you can get external access to your streams in situation when you have Internet with private IP-address.
With ngrok integration you can get external access to your streams in situations when you have Internet with private IP-address.
- Ngrok preistalled for **Docker** and **Hass Add-on** users
- ngrok is pre-installed for **Docker** and **Hass Add-on** users
- you may need external access for two different things:
- WebRTC stream, so you need tunnel WebRTC TCP port (ex. 8555)
- go2rtc web interface, so you need tunnel API HTTP port (ex. 1984)
- Ngrok support authorization for your web interface
- Ngrok automatically adds HTTPS to your web interface
- ngrok support authorization for your web interface
- ngrok automatically adds HTTPS to your web interface
Ngrok free subscription limitations:
The ngrok free subscription has the following limitations:
- you will always get random external address (not a problem for webrtc stream)
- you can forward multiple ports but use only one Ngrok app
- You can reserve a free domain for serving the web interface, but the TCP address you get will always be random and change with each restart of the ngrok agent (not a problem for webrtc stream)
- You can forward multiple ports from a single agent, but you can only run one ngrok agent on the free plan
go2rtc will automatically get your external TCP address (if you enable it in ngrok config) and use it with WebRTC connection (if you enable it in webrtc config).
You need manually download [Ngrok agent app](https://ngrok.com/download) for your OS and register in [Ngrok service](https://ngrok.com/).
You need to manually download the [ngrok agent app](https://ngrok.com/download) for your OS and register with the [ngrok service](https://ngrok.com/signup).
**Tunnel for only WebRTC Stream**
You need to add your [Ngrok token](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
You need to add your [ngrok authtoken](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
```yaml
ngrok:
@@ -936,7 +1001,7 @@ ngrok:
command: ngrok start --all --config ngrok.yaml
```
Ngrok config example:
ngrok config example:
```yaml
version: "2"
@@ -952,6 +1017,8 @@ tunnels:
proto: tcp
```
See the [ngrok agent documentation](https://ngrok.com/docs/agent/config/) for more details on the ngrok configuration file.
### Module: Hass
The best and easiest way to use go2rtc inside the Home Assistant is to install the custom integration [WebRTC Camera](#go2rtc-home-assistant-integration) and custom lovelace card.
@@ -965,7 +1032,7 @@ You have several options on how to add a camera to Home Assistant:
- Install any [go2rtc](#fast-start)
- Add your stream to [go2rtc config](#configuration)
- Hass > Settings > Integrations > Add Integration > [ONVIF](https://my.home-assistant.io/redirect/config_flow_start/?domain=onvif) > Host: `127.0.0.1`, Port: `1984`
- Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://127.0.0.1:8554/camera1` (change to your stream name)
- Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > Stream Source URL: `rtsp://127.0.0.1:8554/camera1` (change to your stream name, leave everything else as is)
You have several options on how to watch the stream from the cameras in Home Assistant:
@@ -1091,7 +1158,7 @@ webrtc:
- external access to WebRTC TCP port is not a problem, because it used only for transmit encrypted media data
- anyway you need to open this port to your local network and to the Internet in order for WebRTC to work
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [Ngrok](https://ngrok.com/), etc.
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [ngrok](https://ngrok.com/), etc.
PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted media. It works without problems on the local network. And sometimes also works for external access, even if you haven't opened this port on your router ([read more](https://en.wikipedia.org/wiki/UDP_hole_punching)). But for stable external WebRTC access, you need to open the 8555 port on your router for both TCP and UDP.
@@ -1122,17 +1189,14 @@ Some examples:
`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
| Device | WebRTC | MSE | HTTP | HLS |
|---------------------|-------------------------------|-------------------------------|------------------------------------|------------------------|
| *latency* | best | medium | bad | bad |
| Desktop Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
| Desktop Edge | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
| Android Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
| Desktop Firefox | H264, OPUS, PCMU, PCMA | H264, AAC, FLAC*, OPUS | H264, AAC, FLAC*, OPUS | no |
| Desktop Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** | H264, H265, AAC, FLAC* |
| iPad Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** | H264, H265, AAC, FLAC* |
| iPhone Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | **no!** | **no!** | H264, H265, AAC, FLAC* |
| macOS [Hass App][1] | no | no | no | H264, H265, AAC, FLAC* |
| Device | WebRTC | MSE | HTTP* | HLS |
|--------------------------------------------------------------------------|-----------------------------------------|-----------------------------------------|----------------------------------------------|-----------------------------|
| *latency* | best | medium | bad | bad |
| - Desktop Chrome 107+ <br/> - Desktop Edge <br/> - Android Chrome 107+ | H264 <br/> PCMU, PCMA <br/> OPUS | H264, H265* <br/> AAC, FLAC* <br/> OPUS | H264, H265* <br/> AAC, FLAC* <br/> OPUS, MP3 | no |
| Desktop Firefox | H264 <br/> PCMU, PCMA <br/> OPUS | H264 <br/> AAC, FLAC* <br/> OPUS | H264 <br/> AAC, FLAC* <br/> OPUS | no |
| - Desktop Safari 14+ <br/> - iPad Safari 14+ <br/> - iPhone Safari 17.1+ | H264, H265* <br/> PCMU, PCMA <br/> OPUS | H264, H265 <br/> AAC, FLAC* | **no!** | H264, H265 <br/> AAC, FLAC* |
| iPhone Safari 14+ | H264, H265* <br/> PCMU, PCMA <br/> OPUS | **no!** | **no!** | H264, H265 <br/> AAC, FLAC* |
| macOS [Hass App][1] | no | no | no | H264, H265 <br/> AAC, FLAC* |
[1]: https://apps.apple.com/app/home-assistant/id1099568401
@@ -1232,9 +1296,15 @@ streams:
- [ioBroker.euSec](https://github.com/bropat/ioBroker.eusec) - [ioBroker](https://www.iobroker.net/) adapter for control Eufy security devices
- [wz_mini_hacks](https://github.com/gtxaspec/wz_mini_hacks) - Custom firmware for Wyze cameras
- [MMM-go2rtc](https://github.com/Anonym-tsk/MMM-go2rtc) - MagicMirror² Module
**Distributions**
- [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=go2rtc)
- [Gentoo](https://github.com/inode64/inode64-overlay/tree/main/media-video/go2rtc)
- [NixOS](https://search.nixos.org/packages?query=go2rtc)
- [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/)
- [QNAP](https://www.myqnap.org/product/go2rtc/)
- [Synology NAS](https://synocommunity.com/package/go2rtc)
- [Unraid](https://unraid.net/community/apps?q=go2rtc)
## Cameras experience
+75 -24
View File
@@ -1,4 +1,4 @@
openapi: 3.0.0
openapi: 3.1.0
info:
title: go2rtc
@@ -111,9 +111,18 @@ paths:
required: false
schema: { type: integer }
example: 100
responses: { }
responses:
default:
description: Default response
/api/restart:
post:
summary: Restart Daemon
description: Restarts the daemon.
tags: [ Application ]
responses:
default:
description: Default response
/api/config:
get:
@@ -130,14 +139,18 @@ paths:
requestBody:
content:
"*/*": { example: "streams:..." }
responses: { }
responses:
default:
description: Default response
patch:
summary: Merge changes to main config file
tags: [ Config ]
requestBody:
content:
"*/*": { example: "streams:..." }
responses: { }
responses:
default:
description: Default response
@@ -166,7 +179,9 @@ paths:
required: false
schema: { type: string }
example: camera1
responses: { }
responses:
default:
description: Default response
patch:
summary: Update stream source
tags: [ Streams list ]
@@ -183,7 +198,9 @@ paths:
required: true
schema: { type: string }
example: camera1
responses: { }
responses:
default:
description: Default response
delete:
summary: Delete stream
tags: [ Streams list ]
@@ -194,7 +211,9 @@ paths:
required: true
schema: { type: string }
example: camera1
responses: { }
responses:
default:
description: Default response
post:
summary: Send stream from source to destination
description: "[Stream to camera](https://github.com/AlexxIT/go2rtc#stream-to-camera)"
@@ -212,7 +231,9 @@ paths:
required: true
schema: { type: string }
example: camera1
responses: { }
responses:
default:
description: Default response
@@ -347,7 +368,9 @@ paths:
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses: { }
responses:
default:
description: Default response
/api/stream.flv?dst={dst}:
post:
summary: Post stream in FLV format
@@ -355,7 +378,9 @@ paths:
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses: { }
responses:
default:
description: Default response
/api/stream.ts?dst={dst}:
post:
summary: Post stream in MPEG-TS format
@@ -363,7 +388,9 @@ paths:
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses: { }
responses:
default:
description: Default response
/api/stream.mjpeg?dst={dst}:
post:
summary: Post stream in MJPEG format
@@ -371,7 +398,9 @@ paths:
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses: { }
responses:
default:
description: Default response
@@ -380,49 +409,65 @@ paths:
summary: DVRIP cameras discovery
description: "[Source: DVRIP](https://github.com/AlexxIT/go2rtc#source-dvrip)"
tags: [ Discovery ]
responses: { }
responses:
default:
description: Default response
/api/ffmpeg/devices:
get:
summary: FFmpeg USB devices discovery
description: "[Source: FFmpeg Device](https://github.com/AlexxIT/go2rtc#source-ffmpeg-device)"
tags: [ Discovery ]
responses: { }
responses:
default:
description: Default response
/api/ffmpeg/hardware:
get:
summary: FFmpeg hardware transcoding discovery
description: "[Hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)"
tags: [ Discovery ]
responses: { }
responses:
default:
description: Default response
/api/hass:
get:
summary: Home Assistant cameras discovery
description: "[Source: Hass](https://github.com/AlexxIT/go2rtc#source-hass)"
tags: [ Discovery ]
responses: { }
responses:
default:
description: Default response
/api/homekit:
get:
summary: HomeKit cameras discovery
description: "[Source: HomeKit](https://github.com/AlexxIT/go2rtc#source-homekit)"
tags: [ Discovery ]
responses: { }
responses:
default:
description: Default response
/api/nest:
get:
summary: Nest cameras discovery
tags: [ Discovery ]
responses: { }
responses:
default:
description: Default response
/api/onvif:
get:
summary: ONVIF cameras discovery
description: "[Source: ONVIF](https://github.com/AlexxIT/go2rtc#source-onvif)"
tags: [ Discovery ]
responses: { }
responses:
default:
description: Default response
/api/roborock:
get:
summary: Roborock vacuums discovery
description: "[Source: Roborock](https://github.com/AlexxIT/go2rtc#source-roborock)"
tags: [ Discovery ]
responses: { }
responses:
default:
description: Default response
@@ -431,7 +476,9 @@ paths:
summary: ONVIF server implementation
description: Simple realisation of the ONVIF protocol. Accepts any suburl requests
tags: [ ONVIF ]
responses: { }
responses:
default:
description: Default response
@@ -440,7 +487,9 @@ paths:
summary: RTSPtoWebRTC server implementation
description: Simple API for support [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration
tags: [ RTSPtoWebRTC ]
responses: { }
responses:
default:
description: Default response
@@ -465,7 +514,9 @@ paths:
tags: [ WebTorrent ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses: { }
responses:
default:
description: Default response
/api/webtorrent:
get:
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 154 KiB

+123
View File
@@ -0,0 +1,123 @@
package main
import (
"encoding/json"
"os"
"github.com/AlexxIT/go2rtc/pkg/hap"
)
var servs = map[string]string{
"3E": "Accessory Information",
"7E": "Security System",
"85": "Motion Sensor",
"96": "Battery",
"A2": "Protocol Information",
"110": "Camera RTP Stream Management",
"112": "Microphone",
"113": "Speaker",
"121": "Doorbell",
"129": "Data Stream Transport Management",
"204": "Camera Recording Management",
"21A": "Camera Operating Mode",
"22A": "Wi-Fi Transport",
"239": "Accessory Runtime Information",
}
var chars = map[string]string{
"14": "Identify",
"20": "Manufacturer",
"21": "Model",
"23": "Name",
"30": "Serial Number",
"52": "Firmware Revision",
"53": "Hardware Revision",
"220": "Product Data",
"A6": "Accessory Flags",
"22": "Motion Detected",
"75": "Status Active",
"11A": "Mute",
"119": "Volume",
"B0": "Active",
"209": "Selected Camera Recording Configuration",
"207": "Supported Audio Recording Configuration",
"205": "Supported Camera Recording Configuration",
"206": "Supported Video Recording Configuration",
"226": "Recording Audio Active",
"223": "Event Snapshots Active",
"225": "Periodic Snapshots Active",
"21B": "HomeKit Camera Active",
"21C": "Third Party Camera Active",
"21D": "Camera Operating Mode Indicator",
"11B": "Night Vision",
"129": "Supported Data Stream Transport Configuration",
"37": "Version",
"131": "Setup Data Stream Transport",
"130": "Supported Data Stream Transport Configuration",
"120": "Streaming Status",
"115": "Supported Audio Stream Configuration",
"116": "Supported RTP Configuration",
"114": "Supported Video Stream Configuration",
"117": "Selected RTP Stream Configuration",
"118": "Setup Endpoints",
"22B": "Current Transport",
"22C": "Wi-Fi Capabilities",
"22D": "Wi-Fi Configuration Control",
"23C": "Ping",
"68": "Battery Level",
"79": "Status Low Battery",
"8F": "Charging State",
"73": "Programmable Switch Event",
"232": "Operating State Response",
"66": "Security System Current State",
"67": "Security System Target State",
}
func main() {
src := os.Args[1]
dst := os.Args[2]
f, err := os.Open(src)
if err != nil {
panic(err)
}
var v hap.JSONAccessories
if err = json.NewDecoder(f).Decode(&v); err != nil {
panic(err)
}
for _, acc := range v.Value {
for _, srv := range acc.Services {
if srv.Desc == "" {
srv.Desc = servs[srv.Type]
}
for _, chr := range srv.Characters {
if chr.Desc == "" {
chr.Desc = chars[chr.Type]
}
}
}
}
f, err = os.Create(dst)
if err != nil {
panic(err)
}
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
if err = enc.Encode(v); err != nil {
panic(err)
}
}
+22 -21
View File
@@ -3,42 +3,43 @@ module github.com/AlexxIT/go2rtc
go 1.21
require (
github.com/gorilla/websocket v1.5.0
github.com/miekg/dns v1.1.55
github.com/pion/ice/v2 v2.3.10
github.com/pion/interceptor v0.1.17
github.com/pion/rtcp v1.2.10
github.com/pion/rtp v1.8.1
github.com/antonmedv/expr v1.15.3
github.com/gorilla/websocket v1.5.1
github.com/miekg/dns v1.1.57
github.com/pion/ice/v2 v2.3.11
github.com/pion/interceptor v0.1.25
github.com/pion/rtcp v1.2.12
github.com/pion/rtp v1.8.3
github.com/pion/sdp/v3 v3.0.6
github.com/pion/srtp/v2 v2.0.16
github.com/pion/srtp/v2 v2.0.18
github.com/pion/stun v0.6.1
github.com/pion/webrtc/v3 v3.2.17
github.com/rs/zerolog v1.30.0
github.com/pion/webrtc/v3 v3.2.22
github.com/rs/zerolog v1.31.0
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
github.com/stretchr/testify v1.8.4
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
golang.org/x/crypto v0.12.0
golang.org/x/crypto v0.15.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pion/datachannel v1.5.5 // indirect
github.com/pion/dtls/v2 v2.2.7 // indirect
github.com/pion/dtls/v2 v2.2.8 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.7 // indirect
github.com/pion/mdns v0.0.9 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.8 // indirect
github.com/pion/transport/v2 v2.2.1 // indirect
github.com/pion/turn/v2 v2.1.3 // indirect
github.com/pion/sctp v1.8.9 // indirect
github.com/pion/transport/v2 v2.2.4 // indirect
github.com/pion/turn/v2 v2.1.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/tools v0.12.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/tools v0.15.0 // indirect
)
+84 -44
View File
@@ -1,3 +1,5 @@
github.com/antonmedv/expr v1.15.3 h1:q3hOJZNvLvhqE8OHBs1cFRdbXFNKuA+bHmRaI+AmRmI=
github.com/antonmedv/expr v1.15.3/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -19,11 +21,14 @@ 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.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
@@ -31,15 +36,17 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE=
github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -53,47 +60,65 @@ github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/ice/v2 v2.3.10 h1:T3bUJKqh7pGEdMyTngUcTeQd6io9X8JjgsVWZDannnY=
github.com/pion/ice/v2 v2.3.10/go.mod h1:hHGCibDfmXGqukayQw979xEctASp2Pe5Oe0iDU8pRus=
github.com/pion/interceptor v0.1.17 h1:prJtgwFh/gB8zMqGZoOgJPHivOwVAp61i2aG61Du/1w=
github.com/pion/interceptor v0.1.17/go.mod h1:SY8kpmfVBvrbUzvj2bsXz7OJt5JvmVNZ+4Kjq7FcwrI=
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.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.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U=
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
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.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/rtp v1.8.0/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/rtp v1.8.1 h1:26OxTc6lKg/qLSGir5agLyj0QKaOv8OP5wps2SFnVNQ=
github.com/pion/rtcp v1.2.12 h1:bKWiX93XKgDZENEXCijvHRU/wRifm6JV5DGcH6twtSM=
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtp v1.8.1/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/rtp v1.8.2 h1:oKMM0K1/QYQ5b5qH+ikqDSZRipP5mIxPJcgcvw5sH0w=
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sctp v1.8.8 h1:5EdnnKI4gpyR1a1TwbiS/wxEgcUWBHsc7ILAjARJB+U=
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.16 h1:impT2XBrHKsDpXr1x5hHIRydwssrSWKpmw3KvSfXbso=
github.com/pion/srtp/v2 v2.0.16/go.mod h1:NCLCV+U+NpxQ+vXhfOETet4OgKioIgrFjZmIM3ldJYE=
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=
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
github.com/pion/transport/v2 v2.2.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ=
github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA=
github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
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/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
github.com/pion/webrtc/v3 v3.2.17 h1:4ra4H3atxp02e891dz8ZOye2Rgfsv8E2VUksyS1EW28=
github.com/pion/webrtc/v3 v3.2.17/go.mod h1:stMj0DIIhmUF0yOSR02uPAoKapzYbDIthSwW/Uk+AGs=
github.com/pion/turn/v2 v2.1.4 h1:2xn8rduI5W6sCZQkEnIUDAkrBQNl2eYIBCHMZ3QMmP8=
github.com/pion/turn/v2 v2.1.4/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
github.com/pion/webrtc/v3 v3.2.21 h1:c8fy5JcqJkAQBwwy3Sk9huQLTBUSqaggyRlv9Lnh2zY=
github.com/pion/webrtc/v3 v3.2.21/go.mod h1:vVURQTBOG5BpWKOJz3nlr23NfTDeyKVmubRNqzQp+Tg=
github.com/pion/webrtc/v3 v3.2.22 h1:Hno262T7+V56MgUO30O0ZirZmVSvbXtnau31SB0WSpc=
github.com/pion/webrtc/v3 v3.2.22/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
@@ -106,7 +131,6 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
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=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
@@ -119,14 +143,21 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
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 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -136,21 +167,25 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
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 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -163,48 +198,53 @@ 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=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.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.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+84 -60
View File
@@ -10,33 +10,36 @@ import (
"strconv"
"strings"
"sync"
"syscall"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/rs/zerolog"
)
func Init() {
var cfg struct {
Mod struct {
Listen string `yaml:"listen"`
Username string `yaml:"username"`
Password string `yaml:"password"`
BasePath string `yaml:"base_path"`
StaticDir string `yaml:"static_dir"`
Origin string `yaml:"origin"`
TLSListen string `yaml:"tls_listen"`
TLSCert string `yaml:"tls_cert"`
TLSKey string `yaml:"tls_key"`
Listen string `yaml:"listen"`
Username string `yaml:"username"`
Password string `yaml:"password"`
BasePath string `yaml:"base_path"`
StaticDir string `yaml:"static_dir"`
Origin string `yaml:"origin"`
TLSListen string `yaml:"tls_listen"`
TLSCert string `yaml:"tls_cert"`
TLSKey string `yaml:"tls_key"`
UnixListen string `yaml:"unix_listen"`
} `yaml:"api"`
}
// default config
cfg.Mod.Listen = "0.0.0.0:1984"
cfg.Mod.Listen = ":1984"
// load config from YAML
app.LoadConfig(&cfg)
if cfg.Mod.Listen == "" {
if cfg.Mod.Listen == "" && cfg.Mod.UnixListen == "" && cfg.Mod.TLSListen == "" {
return
}
@@ -48,16 +51,7 @@ func Init() {
HandleFunc("api", apiHandler)
HandleFunc("api/config", configHandler)
HandleFunc("api/exit", exitHandler)
// ensure we can listen without errors
var err error
ln, err = net.Listen("tcp", cfg.Mod.Listen)
if err != nil {
log.Fatal().Err(err).Msg("[api] listen")
return
}
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
HandleFunc("api/restart", restartHandler)
Handler = http.DefaultServeMux // 4th
@@ -73,52 +67,74 @@ func Init() {
Handler = middlewareLog(Handler) // 1st
}
go func() {
s := http.Server{}
s.Handler = Handler
if err = s.Serve(ln); err != nil {
log.Fatal().Err(err).Msg("[api] serve")
}
}()
if cfg.Mod.Listen != "" {
go listen("tcp", cfg.Mod.Listen)
}
if cfg.Mod.UnixListen != "" {
_ = syscall.Unlink(cfg.Mod.UnixListen)
go listen("unix", cfg.Mod.UnixListen)
}
// Initialize the HTTPS server
if cfg.Mod.TLSListen != "" && cfg.Mod.TLSCert != "" && cfg.Mod.TLSKey != "" {
cert, err := tls.X509KeyPair([]byte(cfg.Mod.TLSCert), []byte(cfg.Mod.TLSKey))
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
tlsListener, err := net.Listen("tcp", cfg.Mod.TLSListen)
if err != nil {
log.Fatal().Err(err).Caller().Send()
return
}
log.Info().Str("addr", cfg.Mod.TLSListen).Msg("[api] tls listen")
tlsServer := &http.Server{
Handler: Handler,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
},
}
go func() {
if err := tlsServer.ServeTLS(tlsListener, "", ""); err != nil {
log.Fatal().Err(err).Msg("[api] tls serve")
}
}()
go tlsListen("tcp", cfg.Mod.TLSListen, cfg.Mod.TLSCert, cfg.Mod.TLSKey)
}
}
func Port() int {
if ln == nil {
return 0
func listen(network, address string) {
ln, err := net.Listen(network, address)
if err != nil {
log.Error().Err(err).Msg("[api] listen")
return
}
log.Info().Str("addr", address).Msg("[api] listen")
if network == "tcp" {
Port = ln.Addr().(*net.TCPAddr).Port
}
server := http.Server{Handler: Handler}
if err = server.Serve(ln); err != nil {
log.Fatal().Err(err).Msg("[api] serve")
}
return ln.Addr().(*net.TCPAddr).Port
}
func tlsListen(network, address, certFile, keyFile string) {
var cert tls.Certificate
var err error
if strings.IndexByte(certFile, '\n') < 0 && strings.IndexByte(keyFile, '\n') < 0 {
// check if file path
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
} else {
// if text file content
cert, err = tls.X509KeyPair([]byte(certFile), []byte(keyFile))
}
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
ln, err := net.Listen(network, address)
if err != nil {
log.Error().Err(err).Msg("[api] tls listen")
return
}
log.Info().Str("addr", address).Msg("[api] tls listen")
server := &http.Server{
Handler: Handler,
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
}
if err = server.ServeTLS(ln, "", ""); err != nil {
log.Fatal().Err(err).Msg("[api] tls serve")
}
}
var Port int
const (
MimeJSON = "application/json"
MimeText = "text/plain"
@@ -178,7 +194,7 @@ func middlewareLog(next http.Handler) http.Handler {
func middlewareAuth(username, password string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") {
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") && r.RemoteAddr != "@" {
user, pass, ok := r.BasicAuth()
if !ok || user != username || pass != password {
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
@@ -200,7 +216,6 @@ func middlewareCORS(next http.Handler) http.Handler {
})
}
var ln net.Listener
var mu sync.Mutex
func apiHandler(w http.ResponseWriter, r *http.Request) {
@@ -222,6 +237,15 @@ func exitHandler(w http.ResponseWriter, r *http.Request) {
os.Exit(code)
}
func restartHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "", http.StatusBadRequest)
return
}
go shell.Restart()
}
type Source struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"github.com/rs/zerolog/log"
)
var Version = "1.7.0"
var Version = "1.8.4"
var UserAgent = "go2rtc/" + Version
var ConfigPath string
+3 -9
View File
@@ -23,17 +23,11 @@ func Init() {
}
func handle(url string) (core.Producer, error) {
conn := dvrip.NewClient(url)
if err := conn.Dial(); err != nil {
client, err := dvrip.Dial(url)
if err != nil {
return nil, err
}
if err := conn.Play(); err != nil {
return nil, err
}
if err := conn.Handle(); err != nil {
return nil, err
}
return conn, nil
return client, nil
}
const Port = 34569 // UDP port number for dvrip discovery
+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
})
}
+6 -1
View File
@@ -52,7 +52,8 @@ var defaults = map[string]string{
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
// `-tune zerolatency` - for minimal latency
// `-profile high -level 4.1` - most used streaming profile
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p",
// `-pix_fmt:v yuv420p` - important for Telegram
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency",
"mjpeg": "-c:v mjpeg",
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
@@ -63,18 +64,22 @@ var defaults = map[string]string{
// `-async 1` or `-min_comp 0` - force frame_size=960, important for WebRTC audio quality
"opus": "-c:a libopus -application:a lowdelay -frame_duration 20 -min_comp 0",
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
"pcmu/8000": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
"pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
"pcma/8000": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
"pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
"aac": "-c:a aac", // keep sample rate and channels
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
"mp3": "-c:a libmp3lame -q:a 8",
"pcm": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
"pcm/8000": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
"pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
"pcml": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
"pcml/8000": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
"pcml/44100": "-c:a pcm_s16le -ar:a 44100 -ac:a 1",
// hardware Intel and AMD on Linux
+27 -10
View File
@@ -13,7 +13,7 @@ func TestParseArgsFile(t *testing.T) {
// [FILE] video will be transcoded to H264, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=h264")
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be copied, audio will be transcoded to pcmu
args = parseArgs("/media/bbb.mp4#video=copy#audio=pcmu")
@@ -38,8 +38,9 @@ func TestParseArgsDevice(t *testing.T) {
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped
args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma")
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1280x720 -framerate 20 -i video="0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -c:a pcm_alaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
//args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma")
args = parseArgs("device?video=0&framerate=20#video=h265#audio=pcma")
require.Equal(t, `ffmpeg -hide_banner -f dshow -framerate 20 -i video="0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -c:a pcm_alaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsIpCam(t *testing.T) {
@@ -49,7 +50,7 @@ func TestParseArgsIpCam(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args = parseArgs("http://example.com#video=h264")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [HLS] video will be copied, audio will be skipped
args = parseArgs("https://example.com#video=copy")
@@ -83,7 +84,7 @@ func TestParseArgsAudio(t *testing.T) {
// [AUDIO] audio will be transcoded to OPUS, video will be skipped
args = parseArgs("rtsp:///example.com#audio=opus")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -application:a lowdelay -frame_duration 20 -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMU, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcmu")
@@ -113,23 +114,23 @@ func TestParseArgsAudio(t *testing.T) {
func TestParseArgsHwVaapi(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,transpose_vaapi=4" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,transpose_vaapi=4,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=1280:720:out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, args.String())
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -f mjpeg -`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsHwV4l2m2m(t *testing.T) {
@@ -207,3 +208,19 @@ func TestParseArgsHwVideotoolbox(t *testing.T) {
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=videotoolbox")
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestDeckLink(t *testing.T) {
args := parseArgs(`DeckLink SDI (2)#video=h264#hardware=vaapi#input=-format_code Hp29 -f decklink -i "{input}"`)
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -format_code Hp29 -f decklink -i "DeckLink SDI (2)" -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestDrawText(t *testing.T) {
args := parseArgs("http:///example.com#video=h264#drawtext=fontsize=12")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
args = parseArgs("http:///example.com#video=h264#width=640#drawtext=fontsize=12")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=640:-1,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
args = parseArgs("http:///example.com#video=h264#width=640#drawtext=fontsize=12#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "scale=640:-1:out_color_matrix=bt709:out_range=tv,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}',hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
+25
View File
@@ -61,6 +61,10 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
if !args.HasFilters("drawtext=") {
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch " + args.Input
if name == "h264" {
fixPixelFormat(args)
}
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_vaapi=" + filter[6:]
@@ -154,3 +158,24 @@ func cut(s string, sep byte, pos int) string {
}
return s
}
// fixPixelFormat:
// - good h264 pixel: yuv420p(tv, bt709) == yuv420p (mpeg/limited/tv)
// - bad h264 pixel: yuvj420p(pc, bt709) == yuvj420p (jpeg/full/pc)
// - bad jpeg pixel: yuvj422p(pc, bt470bg)
func fixPixelFormat(args *ffmpeg.Args) {
// in my tests this filters has same CPU/GPU load:
// - "hwupload"
// - "hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv"
// - "hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12"
const fixPixFmt = "out_color_matrix=bt709:out_range=tv:format=nv12"
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = filter + ":" + fixPixFmt
return
}
}
args.Filters = append(args.Filters, "scale="+fixPixFmt)
}
+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)
}
+2 -1
View File
@@ -71,7 +71,8 @@ func discovery() ([]*api.Source, error) {
err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
log.Trace().Msgf("[homekit] mdns=%s", entry)
if entry.Complete() && entry.Info[hap.TXTCategory] == hap.CategoryCamera {
category := entry.Info[hap.TXTCategory]
if entry.Complete() && (category == hap.CategoryCamera || category == hap.CategoryDoorbell) {
source := &api.Source{
Name: entry.Name,
Info: entry.Info[hap.TXTModel],
+6 -1
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",
@@ -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)
}
+5 -3
View File
@@ -198,9 +198,11 @@ func (s *server) AddPair(conn net.Conn, id string, public []byte, permissions by
"client_public": []string{hex.EncodeToString(public)},
"permissions": []string{string('0' + permissions)},
}
s.pairings = append(s.pairings, query.Encode())
s.UpdateStatus()
s.PatchConfig()
if s.GetPair(conn, id) == nil {
s.pairings = append(s.pairings, query.Encode())
s.UpdateStatus()
s.PatchConfig()
}
}
func (s *server) DelPair(conn net.Conn, id string) {
+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")
}
}
})
+152 -3
View File
@@ -1,39 +1,188 @@
package rtmp
import (
"errors"
"io"
"net"
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/flv"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/rs/zerolog/log"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
)
func Init() {
var conf struct {
Mod struct {
Listen string `yaml:"listen" json:"listen"`
} `yaml:"rtmp"`
}
app.LoadConfig(&conf)
log = app.GetLogger("rtmp")
streams.HandleFunc("rtmp", streamsHandle)
streams.HandleFunc("rtmps", streamsHandle)
streams.HandleFunc("rtmpx", streamsHandle)
api.HandleFunc("api/stream.flv", apiHandle)
streams.HandleConsumerFunc("rtmp", streamsConsumerHandle)
streams.HandleConsumerFunc("rtmps", streamsConsumerHandle)
streams.HandleConsumerFunc("rtmpx", streamsConsumerHandle)
address := conf.Mod.Listen
if address == "" {
return
}
ln, err := net.Listen("tcp", address)
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
log.Info().Str("addr", address).Msg("[rtmp] listen")
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go func() {
if err = tcpHandle(conn); err != nil {
log.Error().Err(err).Caller().Send()
}
}()
}
}()
}
func tcpHandle(netConn net.Conn) error {
rtmpConn, err := rtmp.NewServer(netConn)
if err != nil {
return err
}
if err = rtmpConn.ReadCommands(); err != nil {
return err
}
switch rtmpConn.Intent {
case rtmp.CommandPlay:
stream := streams.Get(rtmpConn.App)
if stream == nil {
return errors.New("stream not found: " + rtmpConn.App)
}
cons := flv.NewConsumer()
if err = stream.AddConsumer(cons); err != nil {
return err
}
defer stream.RemoveConsumer(cons)
if err = rtmpConn.WriteStart(); err != nil {
return err
}
_, _ = cons.WriteTo(rtmpConn)
return nil
case rtmp.CommandPublish:
stream := streams.Get(rtmpConn.App)
if stream == nil {
return errors.New("stream not found: " + rtmpConn.App)
}
if err = rtmpConn.WriteStart(); err != nil {
return err
}
prod, err := rtmpConn.Producer()
if err != nil {
return err
}
stream.AddProducer(prod)
defer stream.RemoveProducer(prod)
_ = prod.Start()
return nil
}
return errors.New("rtmp: unknown command: " + rtmpConn.Intent)
}
var log zerolog.Logger
func streamsHandle(url string) (core.Producer, error) {
client, err := rtmp.Dial(url)
client, err := rtmp.DialPlay(url)
if err != nil {
return nil, err
}
return client, nil
}
func streamsConsumerHandle(url string) (core.Consumer, func(), error) {
cons := flv.NewConsumer()
run := func() {
wr, err := rtmp.DialPublish(url)
if err != nil {
return
}
_, err = cons.WriteTo(wr)
}
return cons, run, nil
}
func apiHandle(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "", http.StatusMethodNotAllowed)
outputFLV(w, r)
} else {
inputFLV(w, r)
}
}
func outputFLV(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
cons := flv.NewConsumer()
cons.Type = "HTTP-FLV consumer"
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
h := w.Header()
h.Set("Content-Type", "video/x-flv")
_, _ = cons.WriteTo(w)
stream.RemoveConsumer(cons)
}
func inputFLV(w http.ResponseWriter, r *http.Request) {
dst := r.URL.Query().Get("dst")
stream := streams.Get(dst)
if stream == nil {
+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)
+22
View File
@@ -73,3 +73,25 @@ func Location(url string) (string, error) {
return "", nil
}
// TODO: rework
type ConsumerHandler func(url string) (core.Consumer, func(), error)
var consumerHandlers = map[string]ConsumerHandler{}
func HandleConsumerFunc(scheme string, handler ConsumerHandler) {
consumerHandlers[scheme] = handler
}
func GetConsumer(url string) (core.Consumer, func(), error) {
if i := strings.IndexByte(url, ':'); i > 0 {
scheme := url[:i]
if handler, ok := consumerHandlers[scheme]; ok {
return handler(url)
}
}
return nil, nil, errors.New("streams: unsupported scheme: " + url)
}
+38
View File
@@ -0,0 +1,38 @@
package streams
import "time"
func (s *Stream) Publish(url string) error {
cons, run, err := GetConsumer(url)
if err != nil {
return err
}
if err = s.AddConsumer(cons); err != nil {
return err
}
go func() {
run()
s.RemoveConsumer(cons)
// TODO: more smart retry
time.Sleep(5 * time.Second)
_ = s.Publish(url)
}()
return nil
}
func Publish(stream *Stream, destination any) {
switch v := destination.(type) {
case string:
if err := stream.Publish(v); err != nil {
log.Error().Err(err).Caller().Send()
}
case []any:
for _, v := range v {
Publish(stream, v)
}
}
}
+20 -2
View File
@@ -5,6 +5,7 @@ import (
"net/url"
"regexp"
"sync"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
@@ -13,18 +14,31 @@ import (
func Init() {
var cfg struct {
Mod map[string]any `yaml:"streams"`
Streams map[string]any `yaml:"streams"`
Publish map[string]any `yaml:"publish"`
}
app.LoadConfig(&cfg)
log = app.GetLogger("streams")
for name, item := range cfg.Mod {
for name, item := range cfg.Streams {
streams[name] = NewStream(item)
}
api.HandleFunc("api/streams", streamsHandler)
if cfg.Publish == nil {
return
}
time.AfterFunc(time.Second, func() {
for name, dst := range cfg.Publish {
if stream := Get(name); stream != nil {
Publish(stream, dst)
}
}
})
}
func Get(name string) *Stream {
@@ -172,6 +186,10 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
} else {
api.ResponseJSON(w, stream)
}
} else if stream = Get(src); stream != nil {
if err := stream.Publish(dst); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else {
http.Error(w, "", http.StatusNotFound)
}
+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
}
+27 -2
View File
@@ -1,6 +1,7 @@
package webrtc
import (
"encoding/base64"
"errors"
"io"
"net/http"
@@ -62,7 +63,7 @@ func streamsHandler(rawURL string) (core.Producer, error) {
// ex: ws://localhost:1984/api/ws?src=camera1
func go2rtcClient(url string) (core.Producer, error) {
// 1. Connect to signalign server
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
conn, _, err := Dial(url)
if err != nil {
return nil, err
}
@@ -87,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:
@@ -212,3 +213,27 @@ func whepClient(url string) (core.Producer, error) {
return prod, nil
}
// Dial - websocket.Dial with Basic auth support
func Dial(rawURL string) (*websocket.Conn, *http.Response, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, nil, err
}
if u.User == nil {
return websocket.DefaultDialer.Dial(rawURL, nil)
}
user := u.User.Username()
pass, _ := u.User.Password()
u.User = nil
header := http.Header{
"Authorization": []string{
"Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+pass)),
},
}
return websocket.DefaultDialer.Dial(u.String(), header)
}
+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
}
+1 -3
View File
@@ -195,9 +195,7 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) {
case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateClosed {
stream.RemoveProducer(prod)
if _, ok := sessions[id]; ok {
delete(sessions, id)
}
delete(sessions, id)
}
}
})
+16 -12
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()
}
+9 -1
View File
@@ -21,12 +21,20 @@ func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
if len(packet.Payload) < int(2+headersSize) {
return
}
headers := packet.Payload[2 : 2+headersSize]
units := packet.Payload[2+headersSize:]
for len(headers) > 0 {
for len(headers) >= 2 {
unitSize := binary.BigEndian.Uint16(headers) >> 3
if len(units) < int(unitSize) {
return
}
unit := units[:unitSize]
headers = headers[2:]
+1 -1
View File
@@ -11,7 +11,7 @@ import (
const (
BufferSize = 64 * 1024 // 64K
ConnDialTimeout = time.Second * 3
ConnDeadline = time.Second * 3
ConnDeadline = time.Second * 5
ProbeTimeout = time.Second * 3
)
+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
+3 -5
View File
@@ -117,9 +117,9 @@ func (s *Sender) HandleRTP(track *Receiver) {
if GetKind(track.Codec.Name) == KindVideo {
if track.Codec.IsRTP() {
// H.264 2560x1440 4096kbs can have 700+ packets between 25 frames
// H.265 5120x1440 can have 700+ packets between two keyframes
bufferSize = 1000
// in my tests 40Mbit/s 4K-video can generate up to 1500 items
// for the h264.RTPDepay => RTPPay queue
bufferSize = 5000
} else {
bufferSize = 50
}
@@ -140,9 +140,7 @@ func (s *Sender) HandleRTP(track *Receiver) {
go func() {
// read packets from buffer channel until it will be closed
for packet := range buffer {
s.mu.Lock()
s.bytes += len(packet.Payload)
s.mu.Unlock()
s.Handler(packet)
}
+3
View File
@@ -3,6 +3,7 @@ package core
import (
"bytes"
"io"
"net/http"
"sync"
)
@@ -32,6 +33,8 @@ func (w *WriteBuffer) Write(p []byte) (n int, err error) {
} else if n, err = w.Writer.Write(p); err != nil {
w.err = err
w.done()
} else if f, ok := w.Writer.(http.Flusher); ok {
f.Flush()
}
w.mu.Unlock()
return
+131 -322
View File
@@ -2,8 +2,8 @@ package dvrip
import (
"bufio"
"bytes"
"crypto/md5"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
@@ -12,49 +12,29 @@ import (
"net"
"net/url"
"time"
)
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/pion/rtp"
const (
Login = 1000
OPMonitorClaim = 1413
OPMonitorStart = 1410
OPTalkClaim = 1434
OPTalkStart = 1430
OPTalkData = 1432
)
type Client struct {
core.Listener
uri string
conn net.Conn
reader *bufio.Reader
session uint32
seq uint32
stream string
medias []*core.Media
receivers []*core.Receiver
videoTrack *core.Receiver
audioTrack *core.Receiver
videoTS uint32
videoDT uint32
audioTS uint32
audioSeq uint16
recv uint32
rd io.Reader
buf []byte
}
type Response map[string]any
const Login = uint16(1000)
const OPMonitorClaim = uint16(1413)
const OPMonitorStart = uint16(1410)
func NewClient(url string) *Client {
return &Client{uri: url}
}
func (c *Client) Dial() (err error) {
u, err := url.Parse(c.uri)
func (c *Client) Dial(rawURL string) (err error) {
u, err := url.Parse(rawURL)
if err != nil {
return
}
@@ -69,26 +49,27 @@ func (c *Client) Dial() (err error) {
return
}
c.reader = bufio.NewReader(c.conn)
if query := u.Query(); query.Get("backchannel") != "1" {
channel := query.Get("channel")
if channel == "" {
channel = "0"
}
query := u.Query()
channel := query.Get("channel")
if channel == "" {
channel = "0"
subtype := query.Get("subtype")
switch subtype {
case "", "0":
subtype = "Main"
case "1":
subtype = "Extra1"
}
c.stream = fmt.Sprintf(
`{"Channel":%s,"CombinMode":"NONE","StreamType":"%s","TransMode":"TCP"}`,
channel, subtype,
)
}
subtype := query.Get("subtype")
switch subtype {
case "", "0":
subtype = "Main"
case "1":
subtype = "Extra1"
}
c.stream = fmt.Sprintf(
`{"Channel":%s,"CombinMode":"NONE","StreamType":"%s","TransMode":"TCP"}`,
channel, subtype,
)
c.rd = bufio.NewReader(c.conn)
if u.User != nil {
pass, _ := u.User.Password()
@@ -98,210 +79,84 @@ func (c *Client) Dial() (err error) {
}
}
func (c *Client) Login(user, pass string) (err error) {
data := fmt.Sprintf(
`{"EncryptType":"MD5","LoginType":"DVRIP-Web","PassWord":"%s","UserName":"%s"}`,
SofiaHash(pass), user,
)
if err = c.Request(Login, data); err != nil {
return
}
_, err = c.ResponseJSON()
return
}
func (c *Client) Play() (err error) {
format := `{"Name":"OPMonitor","SessionID":"0x%08X","OPMonitor":{"Action":"%s","Parameter":%s}}`
data := fmt.Sprintf(format, c.session, "Claim", c.stream)
if err = c.Request(OPMonitorClaim, data); err != nil {
return
}
if _, err = c.ResponseJSON(); err != nil {
return
}
data = fmt.Sprintf(format, c.session, "Start", c.stream)
return c.Request(OPMonitorStart, data)
}
func (c *Client) Handle() error {
var buf []byte
var size int
var probe byte
if c.medias == nil {
probe = 1
}
for {
b, err := c.Response()
if err != nil {
return err
}
// collect data from multiple packets
if size > 0 {
buf = append(buf, b...)
if len(buf) < size {
continue
}
if len(buf) > size {
return errors.New("wrong size")
}
b = buf
}
dataType := binary.BigEndian.Uint32(b)
switch dataType {
case 0x1FC, 0x1FE:
size = int(binary.LittleEndian.Uint32(b[12:])) + 16
case 0x1FD: // PFrame
size = int(binary.LittleEndian.Uint32(b[4:])) + 8
case 0x1FA, 0x1F9:
size = int(binary.LittleEndian.Uint16(b[6:])) + 8
default:
return fmt.Errorf("unknown type: %X", dataType)
}
if len(b) < size {
buf = b
continue // need to collect data from next packets
}
//log.Printf("[DVR] type: %d, len: %d", dataType, len(b))
switch dataType {
case 0x1FC, 0x1FE: // video IFrame
payload := annexb.EncodeToAVCC(b[16:], false)
if c.videoTrack == nil {
fps := b[5]
//width := uint16(b[6]) * 8
//height := uint16(b[7]) * 8
//println(width, height)
ts := b[8:]
// the exact value of the start TS does not matter
c.videoTS = binary.LittleEndian.Uint32(ts)
c.videoDT = 90000 / uint32(fps)
c.AddVideoTrack(b[4], payload)
}
if c.videoTrack != nil {
c.videoTS += c.videoDT
packet := &rtp.Packet{
Header: rtp.Header{Timestamp: c.videoTS},
Payload: payload,
}
//log.Printf("[AVC] %v, len: %d, ts: %10d", h265.Types(payload), len(payload), packet.Timestamp)
c.videoTrack.WriteRTP(packet)
}
case 0x1FD: // PFrame
if c.videoTrack != nil {
c.videoTS += c.videoDT
packet := &rtp.Packet{
Header: rtp.Header{Timestamp: c.videoTS},
Payload: annexb.EncodeToAVCC(b[8:], false),
}
//log.Printf("[DVR] %v, len: %d, ts: %10d", h265.Types(packet.Payload), len(packet.Payload), packet.Timestamp)
c.videoTrack.WriteRTP(packet)
}
case 0x1FA, 0x1F9: // audio
if c.audioTrack == nil {
// the exact value of the start TS does not matter
c.audioTS = c.videoTS
c.AddAudioTrack(b[4], b[5])
}
if c.audioTrack != nil {
for b != nil {
payload := b[8:size]
if len(b) > size {
b = b[size:]
} else {
b = nil
}
c.audioTS += uint32(len(payload))
c.audioSeq++
packet := &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
SequenceNumber: c.audioSeq,
Timestamp: c.audioTS,
},
Payload: payload,
}
//log.Printf("[DVR] len: %d, ts: %10d", len(packet.Payload), packet.Timestamp)
c.audioTrack.WriteRTP(packet)
}
}
}
if probe != 0 {
probe++
if (c.videoTS > 0 && c.audioTS > 0) || probe == 20 {
return nil
}
}
size = 0
}
}
func (c *Client) Close() error {
return c.conn.Close()
}
func (c *Client) Request(cmd uint16, data string) (err error) {
func (c *Client) Login(user, pass string) (err error) {
data := fmt.Sprintf(
`{"EncryptType":"MD5","LoginType":"DVRIP-Web","PassWord":"%s","UserName":"%s"}`+"\x0A\x00",
SofiaHash(pass), user,
)
if _, err = c.WriteCmd(Login, []byte(data)); err != nil {
return
}
_, err = c.ReadJSON()
return
}
func (c *Client) Play() error {
format := `{"Name":"OPMonitor","SessionID":"0x%08X","OPMonitor":{"Action":"%s","Parameter":%s}}` + "\x0A\x00"
data := fmt.Sprintf(format, c.session, "Claim", c.stream)
if _, err := c.WriteCmd(OPMonitorClaim, []byte(data)); err != nil {
return err
}
if _, err := c.ReadJSON(); err != nil {
return err
}
data = fmt.Sprintf(format, c.session, "Start", c.stream)
_, err := c.WriteCmd(OPMonitorStart, []byte(data))
return err
}
func (c *Client) Talk() error {
format := `{"Name":"OPTalk","SessionID":"0x%08X","OPTalk":{"Action":"%s"}}` + "\x0A\x00"
data := fmt.Sprintf(format, c.session, "Claim")
if _, err := c.WriteCmd(OPTalkClaim, []byte(data)); err != nil {
return err
}
if _, err := c.ReadJSON(); err != nil {
return err
}
data = fmt.Sprintf(format, c.session, "Start")
_, err := c.WriteCmd(OPTalkStart, []byte(data))
return err
}
func (c *Client) WriteCmd(cmd uint16, payload []byte) (n int, err error) {
b := make([]byte, 20, 128)
b[0] = 255
binary.LittleEndian.PutUint32(b[4:], c.session)
binary.LittleEndian.PutUint32(b[8:], c.seq)
binary.LittleEndian.PutUint16(b[14:], cmd)
binary.LittleEndian.PutUint32(b[16:], uint32(len(data))+2)
b = append(b, data...)
b = append(b, 0x0A, 0x00)
binary.LittleEndian.PutUint32(b[16:], uint32(len(payload)))
b = append(b, payload...)
c.seq++
if err = c.conn.SetWriteDeadline(time.Now().Add(time.Second * 5)); err != nil {
return
return 0, err
}
_, err = c.conn.Write(b)
return
return c.conn.Write(b)
}
func (c *Client) Response() (b []byte, err error) {
func (c *Client) ReadChunk() (b []byte, err error) {
if err = c.conn.SetReadDeadline(time.Now().Add(time.Second * 5)); err != nil {
return
}
b = make([]byte, 20)
if _, err = io.ReadFull(c.reader, b); err != nil {
if _, err = io.ReadFull(c.rd, b); err != nil {
return
}
c.recv += 20
if b[0] != 255 {
return nil, errors.New("read error")
}
@@ -310,17 +165,59 @@ func (c *Client) Response() (b []byte, err error) {
size := binary.LittleEndian.Uint32(b[16:])
b = make([]byte, size)
if _, err = io.ReadFull(c.reader, b); err != nil {
if _, err = io.ReadFull(c.rd, b); err != nil {
return
}
c.recv += size
return
}
func (c *Client) ResponseJSON() (res Response, err error) {
b, err := c.Response()
func (c *Client) ReadPacket() (pType byte, payload []byte, err error) {
var b []byte
// many cameras may split packet to multiple chunks
// some rare cameras may put multiple packets to single chunk
for len(c.buf) < 16 {
if b, err = c.ReadChunk(); err != nil {
return 0, nil, err
}
c.buf = append(c.buf, b...)
}
if !bytes.HasPrefix(c.buf, []byte{0, 0, 1}) {
return 0, nil, fmt.Errorf("dvrip: wrong packet: %0.16x", c.buf)
}
var size int
switch pType = c.buf[3]; pType {
case 0xFC, 0xFE:
size = int(binary.LittleEndian.Uint32(c.buf[12:])) + 16
case 0xFD: // PFrame
size = int(binary.LittleEndian.Uint32(c.buf[4:])) + 8
case 0xFA, 0xF9:
size = int(binary.LittleEndian.Uint16(c.buf[6:])) + 8
default:
return 0, nil, fmt.Errorf("dvrip: unknown packet type: %X", pType)
}
for len(c.buf) < size {
if b, err = c.ReadChunk(); err != nil {
return 0, nil, err
}
c.buf = append(c.buf, b...)
}
payload = c.buf[:size]
c.buf = c.buf[size:]
return
}
type Response map[string]any
func (c *Client) ReadJSON() (res Response, err error) {
b, err := c.ReadChunk()
if err != nil {
return
}
@@ -336,94 +233,6 @@ func (c *Client) ResponseJSON() (res Response, err error) {
return
}
func (c *Client) AddVideoTrack(mediaCode byte, payload []byte) {
var codec *core.Codec
switch mediaCode {
case 0x02, 0x12:
codec = &core.Codec{
Name: core.CodecH264,
ClockRate: 90000,
PayloadType: core.PayloadTypeRAW,
FmtpLine: h264.GetFmtpLine(payload),
}
case 0x03, 0x13, 0x43, 0x53:
codec = &core.Codec{
Name: core.CodecH265,
ClockRate: 90000,
PayloadType: core.PayloadTypeRAW,
FmtpLine: "profile-id=1",
}
for {
size := 4 + int(binary.BigEndian.Uint32(payload))
switch h265.NALUType(payload) {
case h265.NALUTypeVPS:
codec.FmtpLine += ";sprop-vps=" + base64.StdEncoding.EncodeToString(payload[4:size])
case h265.NALUTypeSPS:
codec.FmtpLine += ";sprop-sps=" + base64.StdEncoding.EncodeToString(payload[4:size])
case h265.NALUTypePPS:
codec.FmtpLine += ";sprop-pps=" + base64.StdEncoding.EncodeToString(payload[4:size])
}
if size < len(payload) {
payload = payload[size:]
} else {
break
}
}
default:
println("[DVRIP] unsupported video codec:", mediaCode)
return
}
media := &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
}
c.medias = append(c.medias, media)
c.videoTrack = core.NewReceiver(media, codec)
c.receivers = append(c.receivers, c.videoTrack)
}
var sampleRates = []uint32{4000, 8000, 11025, 16000, 20000, 22050, 32000, 44100, 48000}
func (c *Client) AddAudioTrack(mediaCode byte, sampleRate byte) {
// https://github.com/vigoss30611/buildroot-ltc/blob/master/system/qm/ipc/ProtocolService/src/ZhiNuo/inc/zn_dh_base_type.h
// PCM8 = 7, G729, IMA_ADPCM, G711U, G721, PCM8_VWIS, MS_ADPCM, G711A, PCM16
var codec *core.Codec
switch mediaCode {
case 10: // G711U
codec = &core.Codec{
Name: core.CodecPCMU,
}
case 14: // G711A
codec = &core.Codec{
Name: core.CodecPCMA,
}
default:
println("[DVRIP] unsupported audio codec:", mediaCode)
return
}
if sampleRate <= byte(len(sampleRates)) {
codec.ClockRate = sampleRates[sampleRate-1]
}
media := &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
}
c.medias = append(c.medias, media)
c.audioTrack = core.NewReceiver(media, codec)
c.receivers = append(c.receivers, c.audioTrack)
}
func SofiaHash(password string) string {
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+84
View File
@@ -0,0 +1,84 @@
package dvrip
import (
"encoding/binary"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
)
type Consumer struct {
core.SuperConsumer
client *Client
}
func (c *Consumer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
return nil, core.ErrCantGetTrack
}
func (c *Consumer) Start() error {
if err := c.client.conn.SetReadDeadline(time.Time{}); err != nil {
return err
}
b := make([]byte, 4096)
for {
if _, err := c.client.rd.Read(b); err != nil {
return err
}
}
}
func (c *Consumer) Stop() error {
_ = c.SuperConsumer.Close()
return c.client.Close()
}
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
if err := c.client.Talk(); err != nil {
return err
}
const PacketSize = 320
buf := make([]byte, 8+PacketSize)
binary.BigEndian.PutUint32(buf, 0x1FA)
switch track.Codec.Name {
case core.CodecPCMU:
buf[4] = 10
case core.CodecPCMA:
buf[4] = 14
}
//for i, rate := range sampleRates {
// if rate == track.Codec.ClockRate {
// buf[5] = byte(i) + 1
// break
// }
//}
buf[5] = 2 // ClockRate=8000
binary.LittleEndian.PutUint16(buf[6:], PacketSize)
var payload []byte
sender := core.NewSender(media, track.Codec)
sender.Handler = func(packet *rtp.Packet) {
payload = append(payload, packet.Payload...)
for len(payload) >= PacketSize {
buf = append(buf[:8], payload[:PacketSize]...)
if n, err := c.client.WriteCmd(OPTalkData, buf); err != nil {
c.Send += n
}
payload = payload[PacketSize:]
}
}
sender.HandleRTP(track)
c.Senders = append(c.Senders, sender)
return nil
}
+33
View File
@@ -0,0 +1,33 @@
package dvrip
import "github.com/AlexxIT/go2rtc/pkg/core"
func Dial(url string) (core.Producer, error) {
client := &Client{}
if err := client.Dial(url); err != nil {
return nil, err
}
if client.stream != "" {
prod := &Producer{client: client}
prod.Type = "DVRIP active producer"
if err := prod.probe(); err != nil {
return nil, err
}
return prod, nil
} else {
cons := &Consumer{client: client}
cons.Type = "DVRIP active consumer"
cons.Medias = []*core.Media{
{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},
{Name: core.CodecPCMU, ClockRate: 8000, PayloadType: 0},
},
},
}
return cons, nil
}
}
+247 -22
View File
@@ -1,41 +1,266 @@
package dvrip
import (
"encoding/json"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/pion/rtp"
)
func (c *Client) GetMedias() []*core.Media {
return c.medias
type Producer struct {
core.SuperProducer
client *Client
video, audio *core.Receiver
videoTS uint32
videoDT uint32
audioTS uint32
audioSeq uint16
}
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
for _, track := range c.receivers {
if track.Codec == codec {
return track, nil
func (c *Producer) Start() error {
for {
pType, b, err := c.client.ReadPacket()
if err != nil {
return err
}
//log.Printf("[DVR] type: %d, len: %d", dataType, len(b))
switch pType {
case 0xFC, 0xFE, 0xFD:
if c.video == nil {
continue
}
var payload []byte
if pType != 0xFD {
payload = b[16:] // iframe
} else {
payload = b[8:] // pframe
}
c.videoTS += c.videoDT
packet := &rtp.Packet{
Header: rtp.Header{Timestamp: c.videoTS},
Payload: annexb.EncodeToAVCC(payload, false),
}
//log.Printf("[AVC] %v, len: %d, ts: %10d", h265.Types(payload), len(payload), packet.Timestamp)
c.video.WriteRTP(packet)
case 0xFA: // audio
if c.audio == nil {
continue
}
payload := b[8:]
c.audioTS += uint32(len(payload))
c.audioSeq++
packet := &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
SequenceNumber: c.audioSeq,
Timestamp: c.audioTS,
},
Payload: payload,
}
//log.Printf("[DVR] len: %d, ts: %10d", len(packet.Payload), packet.Timestamp)
c.audio.WriteRTP(packet)
case 0xF9: // unknown
default:
println(fmt.Sprintf("dvrip: unknown packet type: %d", pType))
}
}
return nil, core.ErrCantGetTrack
}
func (c *Client) Start() error {
return c.Handle()
func (c *Producer) Stop() error {
return c.client.Close()
}
func (c *Client) Stop() error {
for _, receiver := range c.receivers {
receiver.Close()
func (c *Producer) probe() error {
if err := c.client.Play(); err != nil {
return err
}
rd := core.NewReadBuffer(c.client.rd)
rd.BufferSize = core.ProbeSize
defer func() {
c.client.buf = nil
rd.Reset()
}()
c.client.rd = rd
// some awful cameras has VERY rare keyframes
// so we wait video+audio for default probe time
// and wait anything for 15 seconds
timeoutBoth := time.Now().Add(core.ProbeTimeout)
timeoutAny := time.Now().Add(time.Second * 15)
for {
if now := time.Now(); now.Before(timeoutBoth) {
if c.video != nil && c.audio != nil {
return nil
}
} else if now.Before(timeoutAny) {
if c.video != nil || c.audio != nil {
return nil
}
} else {
return errors.New("dvrip: can't probe medias")
}
tag, b, err := c.client.ReadPacket()
if err != nil {
return err
}
switch tag {
case 0xFC, 0xFE: // video
if c.video != nil {
continue
}
fps := b[5]
//width := uint16(b[6]) * 8
//height := uint16(b[7]) * 8
//println(width, height)
ts := b[8:]
// the exact value of the start TS does not matter
c.videoTS = binary.LittleEndian.Uint32(ts)
c.videoDT = 90000 / uint32(fps)
payload := annexb.EncodeToAVCC(b[16:], false)
c.addVideoTrack(b[4], payload)
case 0xFA: // audio
if c.audio != nil {
continue
}
// the exact value of the start TS does not matter
c.audioTS = c.videoTS
c.addAudioTrack(b[4], b[5])
}
}
return c.Close()
}
func (c *Client) MarshalJSON() ([]byte, error) {
info := &core.Info{
Type: "DVRIP active producer",
RemoteAddr: c.conn.RemoteAddr().String(),
Medias: c.medias,
Receivers: c.receivers,
Recv: int(c.recv),
func (c *Producer) addVideoTrack(mediaCode byte, payload []byte) {
var codec *core.Codec
switch mediaCode {
case 0x02, 0x12:
codec = &core.Codec{
Name: core.CodecH264,
ClockRate: 90000,
PayloadType: core.PayloadTypeRAW,
FmtpLine: h264.GetFmtpLine(payload),
}
case 0x03, 0x13, 0x43, 0x53:
codec = &core.Codec{
Name: core.CodecH265,
ClockRate: 90000,
PayloadType: core.PayloadTypeRAW,
FmtpLine: "profile-id=1",
}
for {
size := 4 + int(binary.BigEndian.Uint32(payload))
switch h265.NALUType(payload) {
case h265.NALUTypeVPS:
codec.FmtpLine += ";sprop-vps=" + base64.StdEncoding.EncodeToString(payload[4:size])
case h265.NALUTypeSPS:
codec.FmtpLine += ";sprop-sps=" + base64.StdEncoding.EncodeToString(payload[4:size])
case h265.NALUTypePPS:
codec.FmtpLine += ";sprop-pps=" + base64.StdEncoding.EncodeToString(payload[4:size])
}
if size < len(payload) {
payload = payload[size:]
} else {
break
}
}
default:
println("[DVRIP] unsupported video codec:", mediaCode)
return
}
return json.Marshal(info)
media := &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
}
c.Medias = append(c.Medias, media)
c.video = core.NewReceiver(media, codec)
c.Receivers = append(c.Receivers, c.video)
}
var sampleRates = []uint32{4000, 8000, 11025, 16000, 20000, 22050, 32000, 44100, 48000}
func (c *Producer) addAudioTrack(mediaCode byte, sampleRate byte) {
// https://github.com/vigoss30611/buildroot-ltc/blob/master/system/qm/ipc/ProtocolService/src/ZhiNuo/inc/zn_dh_base_type.h
// PCM8 = 7, G729, IMA_ADPCM, G711U, G721, PCM8_VWIS, MS_ADPCM, G711A, PCM16
var codec *core.Codec
switch mediaCode {
case 10: // G711U
codec = &core.Codec{
Name: core.CodecPCMU,
}
case 14: // G711A
codec = &core.Codec{
Name: core.CodecPCMA,
}
default:
println("[DVRIP] unsupported audio codec:", mediaCode)
return
}
if sampleRate <= byte(len(sampleRates)) {
codec.ClockRate = sampleRates[sampleRate-1]
}
media := &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
}
c.Medias = append(c.Medias, media)
c.audio = core.NewReceiver(media, codec)
c.Receivers = append(c.Receivers, c.audio)
}
//func (c *Client) MarshalJSON() ([]byte, error) {
// info := &core.Info{
// Type: "DVRIP active producer",
// RemoteAddr: c.conn.RemoteAddr().String(),
// Medias: c.Medias,
// Receivers: c.Receivers,
// Recv: c.Recv,
// }
// return json.Marshal(info)
//}
+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/antonmedv/expr"
)
func newRequest(method, url string, headers map[string]any) (*http.Request, error) {
if method == "" {
method = "GET"
}
req, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, err
}
for k, v := range headers {
req.Header.Set(k, fmt.Sprintf("%v", v))
}
return req, nil
}
func regExp(params ...any) (*regexp.Regexp, error) {
exp := params[0].(string)
if len(params) >= 2 {
// support:
// i case-insensitive (default false)
// m multi-line mode: ^ and $ match begin/end line (default false)
// s let . match \n (default false)
// https://pkg.go.dev/regexp/syntax
flags := params[1].(string)
exp = "(?" + flags + ")" + exp
}
return regexp.Compile(exp)
}
var Options = []expr.Option{
expr.Function(
"fetch",
func(params ...any) (any, error) {
var req *http.Request
var err error
url := params[0].(string)
if len(params) == 2 {
options := params[1].(map[string]any)
method, _ := options["method"].(string)
headers, _ := options["headers"].(map[string]any)
req, err = newRequest(method, url, headers)
} else {
req, err = http.NewRequest("GET", url, nil)
}
if err != nil {
return nil, err
}
res, err := tcp.Do(req)
if err != nil {
return nil, err
}
b, _ := io.ReadAll(res.Body)
return map[string]any{
"ok": res.StatusCode < 400,
"status": res.Status,
"text": string(b),
"json": func() (v any) {
_ = json.Unmarshal(b, &v)
return
},
}, nil
},
//new(func(url string) map[string]any),
//new(func(url string, options map[string]any) map[string]any),
),
expr.Function(
"match",
func(params ...any) (any, error) {
re, err := regExp(params[1:]...)
if err != nil {
return nil, err
}
str := params[0].(string)
return re.FindStringSubmatch(str), nil
},
//new(func(str, expr string) []string),
//new(func(str, expr, flags string) []string),
),
expr.Function(
"RegExp",
func(params ...any) (any, error) {
return regExp(params)
},
),
}
func Run(input string) (any, error) {
program, err := expr.Compile(input, Options...)
if err != nil {
return nil, err
}
return expr.Run(program, nil)
}
+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)
}
+41 -2
View File
@@ -60,6 +60,9 @@ func (a *AMF) ReadItem() (any, error) {
case TypeObject:
return a.ReadObject()
case TypeEcmaArray:
return a.ReadEcmaArray()
case TypeNull:
return nil, nil
@@ -174,7 +177,18 @@ func (a *AMF) WriteString(s string) {
func (a *AMF) WriteObject(obj map[string]any) {
a.buf = append(a.buf, TypeObject)
a.writeKV(obj)
a.buf = append(a.buf, 0, 0, TypeObjectEnd)
}
func (a *AMF) WriteEcmaArray(obj map[string]any) {
n := len(obj)
a.buf = append(a.buf, TypeEcmaArray, byte(n>>24), byte(n>>16), byte(n>>8), byte(n))
a.writeKV(obj)
a.buf = append(a.buf, 0, 0, TypeObjectEnd)
}
func (a *AMF) writeKV(obj map[string]any) {
for k, v := range obj {
n := len(k)
a.buf = append(a.buf, byte(n>>8), byte(n))
@@ -185,16 +199,41 @@ func (a *AMF) WriteObject(obj map[string]any) {
a.WriteString(v)
case int:
a.WriteNumber(float64(v))
case uint16:
a.WriteNumber(float64(v))
case uint32:
a.WriteNumber(float64(v))
case float64:
a.WriteNumber(v)
case bool:
a.WriteBool(v)
default:
panic(v)
}
}
a.buf = append(a.buf, 0, 0, TypeObjectEnd)
}
func (a *AMF) WriteNull() {
a.buf = append(a.buf, TypeNull)
}
func EncodeItems(items ...any) []byte {
a := &AMF{}
for _, item := range items {
switch v := item.(type) {
case float64:
a.WriteNumber(v)
case int:
a.WriteNumber(float64(v))
case string:
a.WriteString(v)
case map[string]any:
a.WriteObject(v)
case nil:
a.WriteNull()
default:
panic(v)
}
}
return a.Bytes()
}
+217
View File
@@ -0,0 +1,217 @@
package amf
import (
"encoding/hex"
"testing"
"github.com/stretchr/testify/require"
)
func TestNewReader(t *testing.T) {
tests := []struct {
name string
actual string
expect []any
}{
{
name: "ffmpeg-http",
actual: "02000a6f6e4d65746144617461080000001000086475726174696f6e000000000000000000000577696474680040940000000000000006686569676874004086800000000000000d766964656f646174617261746500409e62770000000000096672616d6572617465004038000000000000000c766964656f636f646563696400401c000000000000000d617564696f646174617261746500405ea93000000000000f617564696f73616d706c65726174650040e5888000000000000f617564696f73616d706c6573697a65004030000000000000000673746572656f0101000c617564696f636f6465636964004024000000000000000b6d616a6f725f6272616e640200046d703432000d6d696e6f725f76657273696f6e020001300011636f6d70617469626c655f6272616e647302000c69736f6d617663316d7034320007656e636f64657202000c4c61766636302e352e313030000866696c6573697a65000000000000000000000009",
expect: []any{
"onMetaData",
map[string]any{
"compatible_brands": "isomavc1mp42",
"major_brand": "mp42",
"minor_version": "0",
"encoder": "Lavf60.5.100",
"filesize": float64(0),
"duration": float64(0),
"videocodecid": float64(7),
"width": float64(1280),
"height": float64(720),
"framerate": float64(24),
"videodatarate": 1944.6162109375,
"audiocodecid": float64(10),
"audiosamplerate": float64(44100),
"stereo": true,
"audiosamplesize": float64(16),
"audiodatarate": 122.6435546875,
},
},
},
{
name: "ffmpeg-file",
actual: "02000a6f6e4d65746144617461080000000800086475726174696f6e004000000000000000000577696474680040940000000000000006686569676874004086800000000000000d766964656f646174617261746500000000000000000000096672616d6572617465004039000000000000000c766964656f636f646563696400401c0000000000000007656e636f64657202000c4c61766636302e352e313030000866696c6573697a6500411f541400000000000009",
expect: []any{
"onMetaData",
map[string]any{
"encoder": "Lavf60.5.100",
"filesize": float64(513285),
"duration": float64(2),
"videocodecid": float64(7),
"width": float64(1280),
"height": float64(720),
"framerate": float64(25),
"videodatarate": float64(0),
},
},
},
{
name: "reolink-1",
actual: "0200075f726573756c74003ff0000000000000030006666d7356657202000d464d532f332c302c312c313233000c6361706162696c697469657300403f0000000000000000090300056c6576656c0200067374617475730004636f646502001d4e6574436f6e6e656374696f6e2e436f6e6e6563742e53756363657373000b6465736372697074696f6e020015436f6e6e656374696f6e207375636365656465642e000e6f626a656374456e636f64696e67000000000000000000000009",
expect: []any{
"_result", float64(1),
map[string]any{
"capabilities": float64(31),
"fmsVer": "FMS/3,0,1,123",
},
map[string]any{
"code": "NetConnection.Connect.Success",
"description": "Connection succeeded.",
"level": "status",
"objectEncoding": float64(0),
},
},
},
{
name: "reolink-2",
actual: "0200075f726573756c7400400000000000000005003ff0000000000000",
expect: []any{
"_result", float64(2), nil, float64(1),
},
},
{
name: "reolink-3",
actual: "0200086f6e537461747573000000000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e506c61792e5374617274000b6465736372697074696f6e020015537461727420766964656f206f6e2064656d616e64000009",
expect: []any{
"onStatus", float64(0), nil,
map[string]any{
"code": "NetStream.Play.Start",
"description": "Start video on demand",
"level": "status",
},
},
},
{
name: "reolink-4",
actual: "0200117c52746d7053616d706c6541636365737301010101",
expect: []any{
"|RtmpSampleAccess", true, true,
},
},
{
name: "reolink-5",
actual: "02000a6f6e4d6574614461746103000577696474680040a4000000000000000668656967687400409e000000000000000c646973706c617957696474680040a4000000000000000d646973706c617948656967687400409e00000000000000086475726174696f6e000000000000000000000c766964656f636f646563696400401c000000000000000c617564696f636f6465636964004024000000000000000f617564696f73616d706c65726174650040cf40000000000000096672616d657261746500403e000000000000000009",
expect: []any{
"onMetaData",
map[string]any{
"duration": float64(0),
"videocodecid": float64(7),
"width": float64(2560),
"height": float64(1920),
"displayWidth": float64(2560),
"displayHeight": float64(1920),
"framerate": float64(30),
"audiocodecid": float64(10),
"audiosamplerate": float64(16000),
},
},
},
{
name: "mediamtx",
actual: "02000d40736574446174614672616d6502000a6f6e4d6574614461746103000d766964656f6461746172617465000000000000000000000c766964656f636f646563696400401c000000000000000d617564696f6461746172617465000000000000000000000c617564696f636f6465636964004024000000000000000009",
expect: []any{
"@setDataFrame",
"onMetaData",
map[string]any{
"videocodecid": float64(7),
"videodatarate": float64(0),
"audiocodecid": float64(10),
"audiodatarate": float64(0),
},
},
},
{
name: "obs-connect",
actual: "020007636f6e6e656374003ff000000000000003000361707002000c617070312f73747265616d3100047479706502000a6e6f6e70726976617465000e737570706f727473476f4177617901010008666c61736856657202001f464d4c452f332e302028636f6d70617469626c653b20464d53632f312e3029000673776655726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d310005746355726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d31000009",
expect: []any{
"connect", 1,
map[string]any{
"app": "app1/stream1",
"flashVer": "FMLE/3.0 (compatible; FMSc/1.0)",
"supportsGoAway": true,
"swfUrl": "rtmp://192.168.10.101/app1/stream1",
"tcUrl": "rtmp://192.168.10.101/app1/stream1",
"type": "nonprivate",
},
},
},
{
name: "obs-key",
actual: "02000d72656c6561736553747265616d004000000000000000050200046b657931",
expect: []any{
"releaseStream", float64(2), nil, "key1",
},
},
{
name: "obs",
actual: "02000d40736574446174614672616d6502000a6f6e4d65746144617461080000001400086475726174696f6e000000000000000000000866696c6553697a65000000000000000000000577696474680040840000000000000006686569676874004076800000000000000c766964656f636f646563696400401c000000000000000d766964656f64617461726174650040a388000000000000096672616d6572617465004039000000000000000c617564696f636f6465636964004024000000000000000d617564696f6461746172617465004064000000000000000f617564696f73616d706c65726174650040e5888000000000000f617564696f73616d706c6573697a65004030000000000000000d617564696f6368616e6e656c73004000000000000000000673746572656f01010003322e3101000003332e3101000003342e3001000003342e3101000003352e3101000003372e3101000007656e636f6465720200376f62732d6f7574707574206d6f64756c6520286c69626f62732076657273696f6e2032392e302e302d36322d6739303031323131663829000009",
expect: []any{
"@setDataFrame", "onMetaData", map[string]any{
"2.1": false,
"3.1": false,
"4.0": false,
"4.1": false,
"5.1": false,
"7.1": false,
"audiochannels": float64(2),
"audiocodecid": float64(10),
"audiodatarate": float64(160),
"audiosamplerate": float64(44100),
"audiosamplesize": float64(16),
"duration": float64(0),
"encoder": "obs-output module (libobs version 29.0.0-62-g9001211f8)",
"fileSize": float64(0),
"framerate": float64(25),
"height": float64(360),
"stereo": true,
"videocodecid": float64(7),
"videodatarate": float64(2500),
"width": float64(640),
},
},
},
{
name: "telegram-2",
actual: "0200075f726573756c7400400000000000000005",
expect: []any{
"_result", float64(2), nil,
},
},
{
name: "telegram-4",
actual: "0200075f726573756c7400401000000000000005003ff0000000000000",
expect: []any{
"_result", float64(4), nil, float64(1),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
b, err := hex.DecodeString(test.actual)
require.Nil(t, err)
rd := NewReader(b)
v, err := rd.ReadItems()
require.Nil(t, err)
require.Equal(t, test.expect, v)
})
}
}
+93
View File
@@ -0,0 +1,93 @@
package flv
import (
"io"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/pion/rtp"
)
type Consumer struct {
core.SuperConsumer
wr *core.WriteBuffer
muxer *Muxer
}
func NewConsumer() *Consumer {
c := &Consumer{
wr: core.NewWriteBuffer(nil),
muxer: &Muxer{},
}
c.Medias = []*core.Media{
{
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecH264},
},
},
{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecAAC},
},
},
}
return c
}
func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
sender := core.NewSender(media, track.Codec)
switch track.Codec.Name {
case core.CodecH264:
payload := c.muxer.GetPayloader(track.Codec)
sender.Handler = func(pkt *rtp.Packet) {
b := payload(pkt)
if n, err := c.wr.Write(b); err == nil {
c.Send += n
}
}
if track.Codec.IsRTP() {
sender.Handler = h264.RTPDepay(track.Codec, sender.Handler)
} else {
sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)
}
case core.CodecAAC:
payload := c.muxer.GetPayloader(track.Codec)
sender.Handler = func(pkt *rtp.Packet) {
b := payload(pkt)
if n, err := c.wr.Write(b); err == nil {
c.Send += n
}
}
if track.Codec.IsRTP() {
sender.Handler = aac.RTPDepay(sender.Handler)
}
}
sender.HandleRTP(track)
c.Senders = append(c.Senders, sender)
return nil
}
func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
b := c.muxer.GetInit()
if _, err := wr.Write(b); err != nil {
return 0, err
}
return c.wr.WriteTo(wr)
}
func (c *Consumer) Stop() error {
_ = c.SuperConsumer.Close()
return c.wr.Close()
}
+172
View File
@@ -0,0 +1,172 @@
package flv
import (
"encoding/binary"
"encoding/hex"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/pion/rtp"
)
type Muxer struct {
codecs []*core.Codec
}
const (
FlagsVideo = 0b001
FlagsAudio = 0b100
)
func (m *Muxer) GetInit() []byte {
b := []byte{
'F', 'L', 'V', // signature
1, // version
0, // flags (has video/audio)
0, 0, 0, 9, // header size
0, 0, 0, 0, // tag 0 size
}
obj := map[string]any{}
for _, codec := range m.codecs {
switch codec.Name {
case core.CodecH264:
b[4] |= FlagsVideo
obj["videocodecid"] = CodecAVC
case core.CodecAAC:
b[4] |= FlagsAudio
obj["audiocodecid"] = CodecAAC
obj["audiosamplerate"] = codec.ClockRate
obj["audiosamplesize"] = 16
obj["stereo"] = codec.Channels == 2
}
}
data := amf.EncodeItems("@setDataFrame", "onMetaData", obj)
b = append(b, EncodeTag(TagData, 0, data)...)
for _, codec := range m.codecs {
switch codec.Name {
case core.CodecH264:
sps, pps := h264.GetParameterSet(codec.FmtpLine)
if len(sps) == 0 {
sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
}
if len(pps) == 0 {
pps = []byte{0x68, 0xce, 0x38, 0x80}
}
config := h264.EncodeConfig(sps, pps)
video := append(encodeAVData(codec, 0), config...)
b = append(b, EncodeTag(TagVideo, 0, video)...)
case core.CodecAAC:
s := core.Between(codec.FmtpLine, "config=", ";")
config, _ := hex.DecodeString(s)
audio := append(encodeAVData(codec, 0), config...)
b = append(b, EncodeTag(TagAudio, 0, audio)...)
}
}
return b
}
func (m *Muxer) GetPayloader(codec *core.Codec) func(packet *rtp.Packet) []byte {
m.codecs = append(m.codecs, codec)
var ts0 uint32
var k = codec.ClockRate / 1000
switch codec.Name {
case core.CodecH264:
buf := encodeAVData(codec, 1)
return func(packet *rtp.Packet) []byte {
if h264.IsKeyframe(packet.Payload) {
buf[0] = 1<<4 | 7
} else {
buf[0] = 2<<4 | 7
}
buf = append(buf[:5], packet.Payload...) // reset buffer to previous place
if ts0 == 0 {
ts0 = packet.Timestamp
}
timeMS := (packet.Timestamp - ts0) / k
return EncodeTag(TagVideo, timeMS, buf)
}
case core.CodecAAC:
buf := encodeAVData(codec, 1)
return func(packet *rtp.Packet) []byte {
buf = append(buf[:2], packet.Payload...)
if ts0 == 0 {
ts0 = packet.Timestamp
}
timeMS := (packet.Timestamp - ts0) / k
return EncodeTag(TagAudio, timeMS, buf)
}
}
return nil
}
func EncodeTag(tagType byte, timeMS uint32, payload []byte) []byte {
payloadSize := uint32(len(payload))
tagSize := payloadSize + 11
b := make([]byte, tagSize+4)
b[0] = tagType
b[1] = byte(payloadSize >> 16)
b[2] = byte(payloadSize >> 8)
b[3] = byte(payloadSize)
b[4] = byte(timeMS >> 16)
b[5] = byte(timeMS >> 8)
b[6] = byte(timeMS)
b[7] = byte(timeMS >> 24)
copy(b[11:], payload)
binary.BigEndian.PutUint32(b[tagSize:], tagSize)
return b
}
func encodeAVData(codec *core.Codec, isFrame byte) []byte {
switch codec.Name {
case core.CodecH264:
return []byte{
1<<4 | 7, // keyframe + AVC
isFrame, // 0 - config, 1 - frame
0, 0, 0, // composition time = 0
}
case core.CodecAAC:
var b0 byte = 10 << 4 // AAC
switch codec.ClockRate {
case 11025:
b0 |= 1 << 2
case 22050:
b0 |= 2 << 2
case 44100:
b0 |= 3 << 2
}
b0 |= 1 << 1 // 16 bits
if codec.Channels == 2 {
b0 |= 1
}
return []byte{b0, isFrame} // 0 - config, 1 - frame
}
return nil
}
+4 -1
View File
@@ -170,7 +170,10 @@ func (c *Producer) probe() error {
if !bytes.Contains(pkt.Payload, []byte("onMetaData")) {
waitType = append(waitType, TagData)
}
if bytes.Contains(pkt.Payload, []byte("videocodecid")) {
// Dahua cameras doesn't send videocodecid
if bytes.Contains(pkt.Payload, []byte("videocodecid")) ||
bytes.Contains(pkt.Payload, []byte("width")) ||
bytes.Contains(pkt.Payload, []byte("framerate")) {
waitType = append(waitType, TagVideo)
}
if bytes.Contains(pkt.Payload, []byte("audiocodecid")) {
+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")
}
+19
View File
@@ -139,3 +139,22 @@ func IndexFrame(b []byte) int {
return -1
}
func FixAnnexBInAVCC(b []byte) []byte {
for i := 0; i < len(b); {
if i+4 >= len(b) {
break
}
size := bytes.Index(b[i+4:], []byte{0, 0, 0, 1})
if size < 0 {
size = len(b) - (i + 4)
}
binary.BigEndian.PutUint32(b[i:], uint32(size))
i += size + 4
}
return b
}
+10
View File
@@ -5,6 +5,7 @@ import (
"encoding/hex"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -83,3 +84,12 @@ func TestGetProfileLevelID(t *testing.T) {
profile = GetProfileLevelID(s)
require.Equal(t, "640029", profile)
}
func TestDecodeSPS2(t *testing.T) {
s := "6764001fad84010c20086100430802184010c200843b50740932"
b, err := hex.DecodeString(s)
require.Nil(t, err)
sps := DecodeSPS(b)
assert.Nil(t, sps) // broken SPS?
}
+8 -2
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 {
@@ -83,10 +89,10 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
// some Chinese buggy cameras has single packet with SPS+PPS+IFrame separated by 00 00 00 01
// https://github.com/AlexxIT/WebRTC/issues/391
// https://github.com/AlexxIT/WebRTC/issues/392
payload = annexb.EncodeToAVCC(payload, false)
payload = annexb.FixAnnexBInAVCC(payload)
}
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber)
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", NALUTypes(payload), len(payload), packet.Timestamp, packet.SequenceNumber)
clone := *packet
clone.Version = RTPPacketVersionAVC
+23 -3
View File
@@ -115,9 +115,14 @@ func DecodeSPS(sps []byte) *SPS {
s.seq_scaling_matrix_present_flag = r.ReadBit()
if s.seq_scaling_matrix_present_flag != 0 {
for i := byte(0); i < n; i++ {
ssl := r.ReadBit() // seq_scaling_list_present_flag[i]
if ssl != 0 {
return nil // not implemented
//goland:noinspection GoSnakeCaseUsage
seq_scaling_list_present_flag := r.ReadBit()
if seq_scaling_list_present_flag != 0 {
if i < 6 {
s.scaling_list(r, 16)
} else {
s.scaling_list(r, 64)
}
}
}
}
@@ -209,3 +214,18 @@ func DecodeSPS(sps []byte) *SPS {
return s
}
//goland:noinspection GoSnakeCaseUsage
func (s *SPS) scaling_list(r *bits.Reader, sizeOfScalingList int) {
lastScale := int32(8)
nextScale := int32(8)
for j := 0; j < sizeOfScalingList; j++ {
if nextScale != 0 {
delta_scale := r.ReadSEGolomb()
nextScale = (lastScale + delta_scale + 256) % 256
}
if nextScale != 0 {
lastScale = nextScale
}
}
}
+2
View File
@@ -93,6 +93,8 @@ func (a *Accessory) GetCharacterByID(iid uint64) *Character {
}
type Service struct {
Desc string `json:"description,omitempty"`
Type string `json:"type"`
IID uint64 `json:"iid"`
Primary bool `json:"primary,omitempty"`
+2 -1
View File
@@ -13,13 +13,14 @@ import (
// Value should be omit for PW
// Value may be empty for PR
type Character struct {
Desc string `json:"description,omitempty"`
IID uint64 `json:"iid"`
Type string `json:"type"`
Format string `json:"format"`
Value any `json:"value,omitempty"`
Perms []string `json:"perms"`
//Descr string `json:"description,omitempty"`
//MaxLen int `json:"maxLen,omitempty"`
//Unit string `json:"unit,omitempty"`
//MinValue any `json:"minValue,omitempty"`
+38 -6
View File
@@ -41,6 +41,9 @@ type Client struct {
Conn net.Conn
reader *bufio.Reader
res chan *http.Response
err error
}
func NewClient(rawURL string) (*Client, error) {
@@ -214,7 +217,7 @@ func (c *Client) Dial() (err error) {
return
}
// new reader for new conn
c.reader = bufio.NewReaderSize(c.Conn, 32*1024) // 32K like default request body
c.reader = bufio.NewReader(c.Conn)
return
}
@@ -223,9 +226,33 @@ func (c *Client) Close() error {
if c.Conn == nil {
return nil
}
conn := c.Conn
c.Conn = nil
return conn.Close()
return c.Conn.Close()
}
func (c *Client) eventsReader() {
c.res = make(chan *http.Response)
for {
var res *http.Response
if res, c.err = ReadResponse(c.reader, nil); c.err != nil {
break
}
var body []byte
if body, c.err = io.ReadAll(res.Body); c.err != nil {
break
}
res.Body = io.NopCloser(bytes.NewReader(body))
if res.Proto != ProtoEvent {
c.res <- res
} else if c.OnEvent != nil {
c.OnEvent(res)
}
}
close(c.res)
}
func (c *Client) GetAccessories() ([]*Accessory, error) {
@@ -296,11 +323,13 @@ func (c *Client) PutCharacters(characters ...*Character) error {
return err
}
_, err = c.Put(PathCharacteristics, MimeJSON, bytes.NewReader(body))
res, err := c.Put(PathCharacteristics, MimeJSON, bytes.NewReader(body))
if err != nil {
return err
}
_, _ = io.ReadAll(res.Body) // important to "clear" body
return nil
}
@@ -317,8 +346,11 @@ func (c *Client) GetImage(width, height int) ([]byte, error) {
}
func (c *Client) LocalIP() string {
if c.Conn == nil {
return ""
}
addr := c.Conn.LocalAddr().(*net.TCPAddr)
return addr.IP.To4().String()
return addr.IP.String()
}
func DecodeKey(s string) []byte {
+28
View File
@@ -1,6 +1,7 @@
package hap
import (
"bufio"
"errors"
"io"
"net/http"
@@ -22,6 +23,9 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
if err := req.Write(c.Conn); err != nil {
return nil, err
}
if c.res != nil {
return <-c.res, c.err
}
return http.ReadResponse(c.reader, req)
}
@@ -54,3 +58,27 @@ func (c *Client) Post(path, contentType string, body io.Reader) (*http.Response,
func (c *Client) Put(path, contentType string, body io.Reader) (*http.Response, error) {
return c.Request("PUT", path, contentType, body)
}
const ProtoEvent = "EVENT/1.0"
func ReadResponse(r *bufio.Reader, req *http.Request) (*http.Response, error) {
b, err := r.Peek(9)
if err != nil {
return nil, err
}
if string(b) != ProtoEvent {
return http.ReadResponse(r, req)
}
copy(b, "HTTP/1.1 ")
res, err := http.ReadResponse(r, req)
if err != nil {
return nil, err
}
res.Proto = ProtoEvent
return res, nil
}
+2
View File
@@ -156,6 +156,8 @@ func (c *Client) Pair(feature, pin string) (err error) {
Proof string `tlv8:"4"` // server proof
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
EncryptedData string `tlv8:"5"` // skip EncryptedData validation (for MFi devices)
}
if err = tlv8.UnmarshalReader(res.Body, &plainM4); err != nil {
return
-68
View File
@@ -1,68 +0,0 @@
package hap
import (
"io"
"os"
"time"
)
type EventReader struct {
r io.Reader
ch chan []byte
err error
left []byte
}
func NewEventReader(r io.Reader) *EventReader {
e := &EventReader{r: r, ch: make(chan []byte, 1)}
go e.background()
return e
}
func (e *EventReader) background() {
b := make([]byte, 32*1024)
for {
n, err := e.r.Read(b)
if err != nil {
e.err = err
return
}
if n >= 6 && string(b[:6]) == "EVENT " {
panic("TODO")
}
// copy because will be overwriten
buf := make([]byte, n)
copy(buf, b)
e.ch <- buf
}
}
func (e *EventReader) Read(p []byte) (n int, err error) {
if e.err != nil {
return 0, e.err
}
// if something left after previous reading
if e.left != nil {
// if still something left
if n = copy(p, e.left); n < len(e.left) {
e.left = e.left[n:]
} else {
e.left = nil
}
return
}
select {
case <-time.After(time.Second * 5):
return 0, os.ErrDeadlineExceeded
case b := <-e.ch:
if n = copy(p, b); n < len(b) {
e.left = b[n:]
}
}
return
}
+5 -3
View File
@@ -31,8 +31,9 @@ const (
StatusPaired = "0"
StatusNotPaired = "1"
CategoryBridge = "2"
CategoryCamera = "17"
CategoryBridge = "2"
CategoryCamera = "17"
CategoryDoorbell = "18"
StateM1 = 1
StateM2 = 2
@@ -65,7 +66,8 @@ type JSONCharacters struct {
type JSONCharacter struct {
AID uint8 `json:"aid"`
IID uint64 `json:"iid"`
Value any `json:"value"`
Value any `json:"value,omitempty"`
Event any `json:"ev,omitempty"`
}
func SanitizePin(pin string) (string, error) {
+49 -57
View File
@@ -1,7 +1,9 @@
package secure
import (
"bufio"
"encoding/binary"
"errors"
"io"
"net"
"sync"
@@ -14,6 +16,9 @@ import (
type Conn struct {
conn net.Conn
rd *bufio.Reader
wr *bufio.Writer
encryptKey []byte
decryptKey []byte
encryptCnt uint64
@@ -33,11 +38,19 @@ func Client(conn net.Conn, sharedKey []byte, isClient bool) (net.Conn, error) {
return nil, err
}
if isClient {
return &Conn{conn: conn, encryptKey: key2, decryptKey: key1}, nil
} else {
return &Conn{conn: conn, encryptKey: key1, decryptKey: key2}, nil
c := &Conn{
conn: conn,
rd: bufio.NewReaderSize(conn, 32*1024),
wr: bufio.NewWriterSize(conn, 32*1024),
}
if isClient {
c.encryptKey, c.decryptKey = key2, key1
} else {
c.encryptKey, c.decryptKey = key1, key2
}
return c, nil
}
const (
@@ -50,84 +63,63 @@ const (
)
func (c *Conn) Read(b []byte) (n int, err error) {
verify := make([]byte, VerifySize) // = packet length
buf := make([]byte, PacketSizeMax+Overhead)
nonce := make([]byte, NonceSize)
for {
if len(b) < PacketSizeMax {
return
}
if _, err = io.ReadFull(c.conn, verify); err != nil {
return
}
size := binary.LittleEndian.Uint16(verify)
ciphertext := buf[:size+Overhead]
if _, err = io.ReadFull(c.conn, ciphertext); err != nil {
return
}
binary.LittleEndian.PutUint64(nonce, c.decryptCnt)
c.decryptCnt++
// put decrypted text to b's end
_, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, b[:0], nonce, ciphertext, verify)
if err != nil {
return
}
n += int(size) // plaintext size
// Finish when all bytes fit in b
if size < PacketSizeMax {
return
}
b = b[size:]
if cap(b) < PacketSizeMax {
return 0, errors.New("hap: read buffer is too small")
}
verify := make([]byte, 2) // verify = plain message size
if _, err = io.ReadFull(c.rd, verify); err != nil {
return
}
n = int(binary.LittleEndian.Uint16(verify))
ciphertext := make([]byte, n+Overhead)
if _, err = io.ReadFull(c.rd, ciphertext); err != nil {
return
}
nonce := make([]byte, NonceSize)
binary.LittleEndian.PutUint64(nonce, c.decryptCnt)
c.decryptCnt++
_, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, b[:0], nonce, ciphertext, verify)
return
}
func (c *Conn) Write(b []byte) (n int, err error) {
c.mx.Lock()
defer c.mx.Unlock()
buf := make([]byte, 0, PacketSizeMax+Overhead)
nonce := make([]byte, NonceSize)
buf := make([]byte, NonceSize+PacketSizeMax+Overhead)
verify := buf[:VerifySize] // part of write buffer
verify := make([]byte, VerifySize)
for {
for len(b) > 0 {
size := len(b)
if size > PacketSizeMax {
size = PacketSizeMax
}
binary.LittleEndian.PutUint16(verify, uint16(size))
if _, err = c.wr.Write(verify); err != nil {
return
}
binary.LittleEndian.PutUint64(nonce, c.encryptCnt)
c.encryptCnt++
// put encrypted text to writing buffer just after size (2 bytes)
_, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf[2:2], nonce, b[:size], verify)
_, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf, nonce, b[:size], verify)
if err != nil {
return
}
if _, err = c.conn.Write(buf[:VerifySize+size+Overhead]); err != nil {
if _, err = c.wr.Write(buf[:size+Overhead]); err != nil {
return
}
n += size // plaintext size
if size < PacketSizeMax {
break
}
b = b[PacketSizeMax:]
b = b[size:]
n += size
}
err = c.wr.Flush()
return
}
+1 -1
View File
@@ -146,7 +146,7 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co
clientPublic := s.GetPair(conn, plainM3.Identifier)
if clientPublic == nil {
return fmt.Errorf("hap: PairVerify from: %s, with unknown client_id: %s", plainM3.Identifier)
return fmt.Errorf("hap: PairVerify from: %s, with unknown client_id: %s", conn.RemoteAddr(), plainM3.Identifier)
}
b = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic)
+3 -2
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
}
+2 -2
View File
@@ -175,10 +175,10 @@ func (c *Client) Start() error {
func (c *Client) Stop() error {
_ = c.SuperProducer.Close()
if c.videoSession != nil {
if c.videoSession != nil && c.videoSession.Remote != nil {
c.srtp.DelSession(c.videoSession)
}
if c.audioSession != nil {
if c.audioSession != nil && c.audioSession.Remote != nil {
c.srtp.DelSession(c.audioSession)
}
+17 -13
View File
@@ -64,20 +64,24 @@ func (c *Producer) Start() error {
buf = append(buf, b[:n]...)
i := annexb.IndexFrame(buf)
if i < 0 {
continue
for {
i := annexb.IndexFrame(buf)
if i < 0 {
break
}
if len(c.Receivers) > 0 {
pkt := &rtp.Packet{
Header: rtp.Header{Timestamp: core.Now90000()},
Payload: annexb.EncodeToAVCC(buf[:i], true),
}
c.Receivers[0].WriteRTP(pkt)
//log.Printf("[AVC] %v, len: %d", h264.Types(pkt.Payload), len(pkt.Payload))
}
buf = buf[i:]
}
pkt := &rtp.Packet{
Header: rtp.Header{Timestamp: core.Now90000()},
Payload: annexb.EncodeToAVCC(buf[:i], true),
}
c.Receivers[0].WriteRTP(pkt)
//log.Printf("[AVC] %v, len: %d", h264.Types(pkt.Payload), len(pkt.Payload))
buf = buf[i:]
}
}
+3 -3
View File
@@ -52,6 +52,8 @@ func (k *Keyframe) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
if track.Codec.IsRTP() {
sender.Handler = h264.RTPDepay(track.Codec, sender.Handler)
} else {
sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)
}
case core.CodecH265:
@@ -66,9 +68,7 @@ func (k *Keyframe) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
}
if track.Codec.IsRTP() {
sender.Handler = h264.RTPDepay(track.Codec, sender.Handler)
} else {
sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)
sender.Handler = h265.RTPDepay(track.Codec, sender.Handler)
}
case core.CodecJPEG:
+16 -1
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
}
+24
View File
@@ -0,0 +1,24 @@
package mdns
import (
"syscall"
)
func SetsockoptInt(fd uintptr, level, opt int, value int) (err error) {
// change SO_REUSEADDR and REUSEPORT flags simultaneously for BSD-like OS
// https://github.com/AlexxIT/go2rtc/issues/626
// https://stackoverflow.com/questions/14388706/how-do-so-reuseaddr-and-so-reuseport-differ/14388707
if opt == syscall.SO_REUSEADDR {
if err = syscall.SetsockoptInt(int(fd), level, opt, value); err != nil {
return
}
opt = syscall.SO_REUSEPORT
}
return syscall.SetsockoptInt(int(fd), level, opt, value)
}
func SetsockoptIPMreq(fd uintptr, level, opt int, mreq *syscall.IPMreq) (err error) {
return syscall.SetsockoptIPMreq(int(fd), level, opt, mreq)
}
@@ -1,8 +1,8 @@
//go:build darwin || linux
package mdns
import "syscall"
import (
"syscall"
)
func SetsockoptInt(fd uintptr, level, opt int, value int) (err error) {
return syscall.SetsockoptInt(int(fd), level, opt, value)
+16 -9
View File
@@ -2,7 +2,6 @@ package mp4
import (
"encoding/hex"
"errors"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
@@ -43,13 +42,17 @@ func (m *Muxer) GetInit() ([]byte, error) {
pps = []byte{0x68, 0xce, 0x38, 0x80}
}
s := h264.DecodeSPS(sps)
if s == nil {
return nil, errors.New("mp4: can't parse SPS")
var width, height uint16
if s := h264.DecodeSPS(sps); s != nil {
width = s.Width()
height = s.Height()
} else {
width = 1920
height = 1080
}
mv.WriteVideoTrack(
uint32(i+1), codec.Name, codec.ClockRate, s.Width(), s.Height(), h264.EncodeConfig(sps, pps),
uint32(i+1), codec.Name, codec.ClockRate, width, height, h264.EncodeConfig(sps, pps),
)
case core.CodecH265:
@@ -65,13 +68,17 @@ func (m *Muxer) GetInit() ([]byte, error) {
pps = []byte{0x44, 0x01, 0xc0, 0x73, 0xc0, 0x4c, 0x90}
}
s := h265.DecodeSPS(sps)
if s == nil {
return nil, errors.New("mp4: can't parse SPS")
var width, height uint16
if s := h265.DecodeSPS(sps); s != nil {
width = s.Width()
height = s.Height()
} else {
width = 1920
height = 1080
}
mv.WriteVideoTrack(
uint32(i+1), codec.Name, codec.ClockRate, s.Width(), s.Height(), h265.EncodeConfig(vps, sps, pps),
uint32(i+1), codec.Name, codec.ClockRate, width, height, h265.EncodeConfig(vps, sps, pps),
)
case core.CodecAAC:
+3 -2
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 -4
View File
@@ -5,7 +5,6 @@ import (
"crypto/sha1"
"encoding/base64"
"errors"
"github.com/AlexxIT/go2rtc/pkg/core"
"html"
"io"
"net/http"
@@ -13,6 +12,8 @@ import (
"regexp"
"strings"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
)
const PathDevice = "/onvif/device_service"
@@ -78,10 +79,10 @@ func (c *Client) GetURI() (string, error) {
return "", err
}
uri := FindTagValue(b, "Uri")
uri = html.UnescapeString(uri)
rawURL := FindTagValue(b, "Uri")
rawURL = strings.TrimSpace(html.UnescapeString(rawURL))
u, err := url.Parse(uri)
u, err := url.Parse(rawURL)
if err != nil {
return "", err
}
+3 -2
View File
@@ -1,16 +1,17 @@
package onvif
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"net"
"regexp"
"strconv"
"strings"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func FindTagValue(b []byte, tag string) string {
re := regexp.MustCompile(`<[^/>]*` + tag + `[^>]*>([^<]+)`)
re := regexp.MustCompile(`(?s)[:<]` + tag + `>([^<]+)`)
m := re.FindSubmatch(b)
if len(m) != 2 {
return ""
+199
View File
@@ -0,0 +1,199 @@
package onvif
import (
"html"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestGetStreamUri(t *testing.T) {
tests := []struct {
name string
xml string
url string
}{
{
name: "Dahua stream default",
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes" ?><s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:trt="http://www.onvif.org/ver10/media/wsdl"><s:Header/><s:Body><trt:GetStreamUriResponse><trt:MediaUri><tt:Uri>rtsp://192.168.1.123:554/cam/realmonitor?channel=1&amp;subtype=1&amp;unicast=true&amp;proto=Onvif</tt:Uri><tt:InvalidAfterConnect>true</tt:InvalidAfterConnect><tt:InvalidAfterReboot>true</tt:InvalidAfterReboot><tt:Timeout>PT0S</tt:Timeout></trt:MediaUri></trt:GetStreamUriResponse></s:Body></s:Envelope>`,
url: "rtsp://192.168.1.123:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif",
},
{
name: "Dahua snapshot default",
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes" ?><s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:trt="http://www.onvif.org/ver10/media/wsdl"><s:Header/><s:Body><trt:GetSnapshotUriResponse><trt:MediaUri><tt:Uri>http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&amp;subtype=1</tt:Uri><tt:InvalidAfterConnect>false</tt:InvalidAfterConnect><tt:InvalidAfterReboot>false</tt:InvalidAfterReboot><tt:Timeout>PT0S</tt:Timeout></trt:MediaUri></trt:GetSnapshotUriResponse></s:Body></s:Envelope>`,
url: "http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&subtype=1",
},
{
name: "Dahua stream formatted",
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding"
xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema"
xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
<s:Header />
<s:Body>
<trt:GetStreamUriResponse>
<trt:MediaUri>
<tt:Uri>
rtsp://192.168.1.123:554/cam/realmonitor?channel=1&amp;subtype=1&amp;unicast=true&amp;proto=Onvif</tt:Uri>
<tt:InvalidAfterConnect>true</tt:InvalidAfterConnect>
<tt:InvalidAfterReboot>true</tt:InvalidAfterReboot>
<tt:Timeout>PT0S</tt:Timeout>
</trt:MediaUri>
</trt:GetStreamUriResponse>
</s:Body>
</s:Envelope>`,
url: "rtsp://192.168.1.123:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif",
},
{
name: "Dahua snapshot formatted",
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding"
xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema"
xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
<s:Header />
<s:Body>
<trt:GetSnapshotUriResponse>
<trt:MediaUri>
<tt:Uri>
http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&amp;subtype=1</tt:Uri>
<tt:InvalidAfterConnect>false</tt:InvalidAfterConnect>
<tt:InvalidAfterReboot>false</tt:InvalidAfterReboot>
<tt:Timeout>PT0S</tt:Timeout>
</trt:MediaUri>
</trt:GetSnapshotUriResponse>
</s:Body>
</s:Envelope>`,
url: "http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&subtype=1",
},
{
name: "Unknown",
xml: `<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope ...>
<SOAP-ENV:Header></SOAP-ENV:Header>
<SOAP-ENV:Body>
<MC1:GetStreamUriResponse>
<MC1:MediaUri>
<MC2:Uri>
rtsp://192.168.5.53:8090/profile1=r
</MC2:Uri>
</MC1:MediaUri>
</MC1:GetStreamUriResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>`,
url: "rtsp://192.168.5.53:8090/profile1=r",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
uri := FindTagValue([]byte(test.xml), "Uri")
uri = strings.TrimSpace(html.UnescapeString(uri))
u, err := url.Parse(uri)
require.Nil(t, err)
require.Equal(t, test.url, u.String())
})
}
}
func TestGetCapabilities(t *testing.T) {
tests := []struct {
name string
xml string
}{
{
name: "Dahua default",
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes" ?><s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:tds="http://www.onvif.org/ver10/device/wsdl"><s:Header/><s:Body><tds:GetCapabilitiesResponse><tds:Capabilities><tt:Analytics><tt:XAddr>http://192.168.1.123/onvif/analytics_service</tt:XAddr><tt:RuleSupport>true</tt:RuleSupport><tt:AnalyticsModuleSupport>true</tt:AnalyticsModuleSupport></tt:Analytics><tt:Device><tt:XAddr>http://192.168.1.123/onvif/device_service</tt:XAddr><tt:Network><tt:IPFilter>false</tt:IPFilter><tt:ZeroConfiguration>false</tt:ZeroConfiguration><tt:IPVersion6>false</tt:IPVersion6><tt:DynDNS>false</tt:DynDNS><tt:Extension><tt:Dot11Configuration>false</tt:Dot11Configuration></tt:Extension></tt:Network><tt:System><tt:DiscoveryResolve>false</tt:DiscoveryResolve><tt:DiscoveryBye>true</tt:DiscoveryBye><tt:RemoteDiscovery>false</tt:RemoteDiscovery><tt:SystemBackup>false</tt:SystemBackup><tt:SystemLogging>true</tt:SystemLogging><tt:FirmwareUpgrade>true</tt:FirmwareUpgrade><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>00</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>10</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>20</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>30</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>40</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>42</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>16</tt:Major><tt:Minor>12</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>18</tt:Major><tt:Minor>06</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>18</tt:Major><tt:Minor>12</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>19</tt:Major><tt:Minor>06</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>19</tt:Major><tt:Minor>12</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>20</tt:Major><tt:Minor>06</tt:Minor></tt:SupportedVersions><tt:Extension><tt:HttpFirmwareUpgrade>true</tt:HttpFirmwareUpgrade><tt:HttpSystemBackup>false</tt:HttpSystemBackup><tt:HttpSystemLogging>false</tt:HttpSystemLogging><tt:HttpSupportInformation>false</tt:HttpSupportInformation></tt:Extension></tt:System><tt:IO><tt:InputConnectors>2</tt:InputConnectors><tt:RelayOutputs>1</tt:RelayOutputs><tt:Extension><tt:Auxiliary>false</tt:Auxiliary><tt:AuxiliaryCommands></tt:AuxiliaryCommands><tt:Extension></tt:Extension></tt:Extension></tt:IO><tt:Security><tt:TLS1.1>false</tt:TLS1.1><tt:TLS1.2>false</tt:TLS1.2><tt:OnboardKeyGeneration>false</tt:OnboardKeyGeneration><tt:AccessPolicyConfig>false</tt:AccessPolicyConfig><tt:X.509Token>false</tt:X.509Token><tt:SAMLToken>false</tt:SAMLToken><tt:KerberosToken>false</tt:KerberosToken><tt:RELToken>false</tt:RELToken><tt:Extension><tt:TLS1.0>false</tt:TLS1.0><tt:Extension><tt:Dot1X>false</tt:Dot1X><tt:SupportedEAPMethod>0</tt:SupportedEAPMethod><tt:RemoteUserHandling>false</tt:RemoteUserHandling></tt:Extension></tt:Extension></tt:Security></tt:Device><tt:Events><tt:XAddr>http://192.168.1.123/onvif/event_service</tt:XAddr><tt:WSSubscriptionPolicySupport>true</tt:WSSubscriptionPolicySupport><tt:WSPullPointSupport>true</tt:WSPullPointSupport><tt:WSPausableSubscriptionManagerInterfaceSupport>false</tt:WSPausableSubscriptionManagerInterfaceSupport></tt:Events><tt:Imaging><tt:XAddr>http://192.168.1.123/onvif/imaging_service</tt:XAddr></tt:Imaging><tt:Media><tt:XAddr>http://192.168.1.123/onvif/media_service</tt:XAddr><tt:StreamingCapabilities><tt:RTPMulticast>true</tt:RTPMulticast><tt:RTP_TCP>true</tt:RTP_TCP><tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP></tt:StreamingCapabilities><tt:Extension><tt:ProfileCapabilities><tt:MaximumNumberOfProfiles>6</tt:MaximumNumberOfProfiles></tt:ProfileCapabilities></tt:Extension></tt:Media><tt:Extension><tt:DeviceIO><tt:XAddr>http://192.168.1.123/onvif/deviceIO_service</tt:XAddr><tt:VideoSources>1</tt:VideoSources><tt:VideoOutputs>0</tt:VideoOutputs><tt:AudioSources>1</tt:AudioSources><tt:AudioOutputs>1</tt:AudioOutputs><tt:RelayOutputs>1</tt:RelayOutputs></tt:DeviceIO></tt:Extension></tds:Capabilities></tds:GetCapabilitiesResponse></s:Body></s:Envelope>`,
},
{
name: "Dahua formatted",
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding"
xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema"
xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<s:Header />
<s:Body>
<tds:GetCapabilitiesResponse>
<tds:Capabilities>
<tt:Analytics>
<tt:XAddr>http://192.168.1.123/onvif/analytics_service</tt:XAddr>
<tt:RuleSupport>true</tt:RuleSupport>
<tt:AnalyticsModuleSupport>true</tt:AnalyticsModuleSupport>
</tt:Analytics>
<tt:Device>
<tt:XAddr>http://192.168.1.123/onvif/device_service</tt:XAddr>
<tt:Network>
<tt:IPFilter>false</tt:IPFilter>
<tt:ZeroConfiguration>false</tt:ZeroConfiguration>
<tt:IPVersion6>false</tt:IPVersion6>
<tt:DynDNS>false</tt:DynDNS>
<tt:Extension>
<tt:Dot11Configuration>false</tt:Dot11Configuration>
</tt:Extension>
</tt:Network>
<tt:System>
...
</tt:System>
<tt:IO>
<tt:InputConnectors>2</tt:InputConnectors>
<tt:RelayOutputs>1</tt:RelayOutputs>
<tt:Extension>
<tt:Auxiliary>false</tt:Auxiliary>
<tt:AuxiliaryCommands></tt:AuxiliaryCommands>
<tt:Extension></tt:Extension>
</tt:Extension>
</tt:IO>
<tt:Security>
...
</tt:Security>
</tt:Device>
<tt:Events>
<tt:XAddr>http://192.168.1.123/onvif/event_service</tt:XAddr>
<tt:WSSubscriptionPolicySupport>true</tt:WSSubscriptionPolicySupport>
<tt:WSPullPointSupport>true</tt:WSPullPointSupport>
<tt:WSPausableSubscriptionManagerInterfaceSupport>false</tt:WSPausableSubscriptionManagerInterfaceSupport>
</tt:Events>
<tt:Imaging>
<tt:XAddr>http://192.168.1.123/onvif/imaging_service</tt:XAddr>
</tt:Imaging>
<tt:Media>
<tt:XAddr>http://192.168.1.123/onvif/media_service</tt:XAddr>
<tt:StreamingCapabilities>
<tt:RTPMulticast>true</tt:RTPMulticast>
<tt:RTP_TCP>true</tt:RTP_TCP>
<tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP>
</tt:StreamingCapabilities>
<tt:Extension>
<tt:ProfileCapabilities>
<tt:MaximumNumberOfProfiles>6</tt:MaximumNumberOfProfiles>
</tt:ProfileCapabilities>
</tt:Extension>
</tt:Media>
<tt:Extension>
<tt:DeviceIO>
<tt:XAddr>http://192.168.1.123/onvif/deviceIO_service</tt:XAddr>
<tt:VideoSources>1</tt:VideoSources>
<tt:VideoOutputs>0</tt:VideoOutputs>
<tt:AudioSources>1</tt:AudioSources>
<tt:AudioOutputs>1</tt:AudioOutputs>
<tt:RelayOutputs>1</tt:RelayOutputs>
</tt:DeviceIO>
</tt:Extension>
</tds:Capabilities>
</tds:GetCapabilitiesResponse>
</s:Body>
</s:Envelope>`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
rawURL := FindTagValue([]byte(test.xml), "Media.+?XAddr")
require.Equal(t, "http://192.168.1.123/onvif/media_service", rawURL)
rawURL = FindTagValue([]byte(test.xml), "Imaging.+?XAddr")
require.Equal(t, "http://192.168.1.123/onvif/imaging_service", rawURL)
})
}
}
+11 -8
View File
@@ -6,16 +6,17 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/roborock/iot"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
"log"
"net/rpc"
"net/url"
"strconv"
"sync"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/roborock/iot"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
)
type Client struct {
@@ -38,13 +39,15 @@ func NewClient(url string) *Client {
return &Client{url: url}
}
func (c *Client) Dial() (err error) {
func (c *Client) Dial() error {
u, err := url.Parse(c.url)
if err != nil {
return
return err
}
c.iot, err = iot.Dial(c.url)
if c.iot, err = iot.Dial(c.url); err != nil {
return err
}
c.pin = u.Query().Get("pin")
if c.pin != "" {
@@ -87,7 +90,7 @@ func (c *Client) Connect() error {
}
// 4. Create Peer Connection
api, err := webrtc.NewAPI("")
api, err := webrtc.NewAPI()
if err != nil {
return err
}
+19
View File
@@ -0,0 +1,19 @@
## Logs
```
request []interface {}{"connect", 1, map[string]interface {}{"app":"s", "flashVer":"FMLE/3.0 (compatible; FMSc/1.0)", "tcUrl":"rtmps://xxx.rtmp.t.me/s/xxxxx"}}
response []interface {}{"_result", 1, map[string]interface {}{"capabilities":31, "fmsVer":"FMS/3,0,1,123"}, map[string]interface {}{"code":"NetConnection.Connect.Success", "description":"Connection succeeded.", "level":"status", "objectEncoding":0}}
request []interface {}{"releaseStream", 2, interface {}(nil), "xxxxx"}
request []interface {}{"FCPublish", 3, interface {}(nil), "xxxxx"}
request []interface {}{"createStream", 4, interface {}(nil)}
response []interface {}{"_result", 2, interface {}(nil)}
response []interface {}{"_result", 4, interface {}(nil), 1}
request []interface {}{"publish", 5, interface {}(nil), "xxxxx", "live"}
response []interface {}{"onStatus", 0, interface {}(nil), map[string]interface {}{"code":"NetStream.Publish.Start", "description":"xxxxx is now published", "detail":"xxxxx", "level":"status"}}
```
## Useful links
- https://en.wikipedia.org/wiki/Flash_Video
- https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol
- https://rtmp.veriskope.com/pdf/rtmp_specification_1.0.pdf
+155
View File
@@ -0,0 +1,155 @@
package rtmp
import (
"bufio"
"io"
"net"
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/tcp"
)
func DialPlay(rawURL string) (core.Producer, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
conn, err := tcp.Dial(u, core.ConnDialTimeout)
if err != nil {
return nil, err
}
rtmpConn, err := NewClient(conn, u)
if err != nil {
return nil, err
}
if err = rtmpConn.play(); err != nil {
return nil, err
}
return rtmpConn.Producer()
}
func DialPublish(rawURL string) (io.Writer, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
conn, err := tcp.Dial(u, core.ConnDialTimeout)
if err != nil {
return nil, err
}
client, err := NewClient(conn, u)
if err != nil {
return nil, err
}
if err = client.publish(); err != nil {
return nil, err
}
return client, nil
}
func NewClient(conn net.Conn, u *url.URL) (*Conn, error) {
c := &Conn{
url: u.String(),
conn: conn,
rd: bufio.NewReaderSize(conn, core.BufferSize),
wr: conn,
chunks: map[uint8]*header{},
rdPacketSize: 128,
wrPacketSize: 4096, // OBS - 4096, Reolink - 4096
}
if args := strings.Split(u.Path, "/"); len(args) >= 2 {
c.App = args[1]
if len(args) >= 3 {
c.Stream = args[2]
if u.RawQuery != "" {
c.Stream += "?" + u.RawQuery
}
}
}
if err := c.clienHandshake(); err != nil {
return nil, err
}
if err := c.writePacketSize(); err != nil {
return nil, err
}
return c, nil
}
func (c *Conn) clienHandshake() error {
// simple handshake without real random and check response
b := make([]byte, 1+1536)
b[0] = 0x03
// write C0+C1
if _, err := c.conn.Write(b); err != nil {
return err
}
// read S0+S1
if _, err := io.ReadFull(c.rd, b); err != nil {
return err
}
// write S1
if _, err := c.conn.Write(b[1:]); err != nil {
return err
}
// read C1, skip check
if _, err := io.ReadFull(c.rd, b[1:]); err != nil {
return err
}
return nil
}
func (c *Conn) play() error {
if err := c.writeConnect(); err != nil {
return err
}
if err := c.writeCreateStream(); err != nil {
return err
}
if err := c.writePlay(); err != nil {
return err
}
return nil
}
func (c *Conn) publish() error {
if err := c.writeConnect(); err != nil {
return err
}
if err := c.writeReleaseStream(); err != nil {
return err
}
if err := c.writeCreateStream(); err != nil {
return err
}
if err := c.writePublish(); err != nil {
return err
}
go func() {
for {
_, _, _, err := c.readMessage()
//log.Printf("!!! %d %d %.30x", msgType, timeMS, b)
if err != nil {
return
}
}
}()
return nil
}
+353
View File
@@ -0,0 +1,353 @@
package rtmp
import (
"encoding/binary"
"fmt"
"io"
"net"
"strings"
"sync"
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
)
const (
TypeSetPacketSize = 1
TypeServerBandwidth = 5
TypeClientBandwidth = 6
TypeAudio = 8
TypeVideo = 9
TypeData = 18
TypeCommand = 20
)
type Conn struct {
App string
Stream string
Intent string
rdPacketSize uint32
wrPacketSize uint32
chunks map[byte]*header
streamID byte
url string
conn net.Conn
rd io.Reader
wr io.Writer
rdBuf []byte
wrBuf []byte
mu sync.Mutex
}
func (c *Conn) Close() error {
return c.conn.Close()
}
func (c *Conn) readResponse(transID float64) ([]any, error) {
for {
msgType, _, b, err := c.readMessage()
if err != nil {
return nil, err
}
switch msgType {
case TypeSetPacketSize:
c.rdPacketSize = binary.BigEndian.Uint32(b)
case TypeCommand:
items, _ := amf.NewReader(b).ReadItems()
if len(items) >= 3 && items[1] == transID {
return items, nil
}
}
}
}
type header struct {
timeMS uint32
dataSize uint32
tagType byte
streamID uint32
}
//var ErrNotImplemented = errors.New("rtmp: not implemented")
func (c *Conn) readMessage() (byte, uint32, []byte, error) {
b, err := c.readSize(1) // doesn't support big chunkID!!!
if err != nil {
return 0, 0, nil, err
}
hdrType := b[0] >> 6
chunkID := b[0] & 0b111111
// storing header information for support header type 3
hdr, ok := c.chunks[chunkID]
if !ok {
hdr = &header{}
c.chunks[chunkID] = hdr
}
switch hdrType {
case 0: // 12 byte header (full header)
if b, err = c.readSize(11); err != nil {
return 0, 0, nil, err
}
_ = b[7]
hdr.timeMS = Uint24(b)
hdr.dataSize = Uint24(b[3:])
hdr.tagType = b[6]
hdr.streamID = binary.LittleEndian.Uint32(b[7:])
case 1: // 8 bytes - like type b00, not including message ID (4 last bytes)
if b, err = c.readSize(7); err != nil {
return 0, 0, nil, err
}
_ = b[6]
hdr.timeMS = Uint24(b) // timestamp
hdr.dataSize = Uint24(b[3:]) // msgdatalen
hdr.tagType = b[6] // msgtypeid
case 2: // 4 bytes - Basic Header and timestamp (3 bytes) are included
if b, err = c.readSize(3); err != nil {
return 0, 0, nil, err
}
hdr.timeMS = Uint24(b) // timestamp
case 3: // 1 byte - only the Basic Header is included
// use here hdr from previous msg with same session ID (sid)
}
timeMS := hdr.timeMS
if timeMS == 0xFFFFFF {
if b, err = c.readSize(4); err != nil {
return 0, 0, nil, err
}
timeMS = binary.BigEndian.Uint32(b)
}
//log.Printf("[rtmp] hdr=%d chunkID=%d timeMS=%d size=%d tagType=%d streamID=%d", hdrType, chunkID, hdr.timeMS, hdr.dataSize, hdr.tagType, hdr.streamID)
// 1. Response zero size
if hdr.dataSize == 0 {
return hdr.tagType, timeMS, nil, nil
}
b = make([]byte, hdr.dataSize)
// 2. Response small packet
if hdr.dataSize <= c.rdPacketSize {
if _, err = io.ReadFull(c.rd, b); err != nil {
return 0, 0, nil, err
}
return hdr.tagType, timeMS, b, nil
}
// 3. Response big packet
var i0 uint32
for i1 := c.rdPacketSize; i1 < hdr.dataSize; i1 += c.rdPacketSize {
if _, err = io.ReadFull(c.rd, b[i0:i1]); err != nil {
return 0, 0, nil, err
}
if _, err = c.readSize(1); err != nil {
return 0, 0, nil, err
}
if hdr.timeMS == 0xFFFFFF {
if _, err = c.readSize(4); err != nil {
return 0, 0, nil, err
}
}
i0 = i1
}
if _, err = io.ReadFull(c.rd, b[i0:]); err != nil {
return 0, 0, nil, err
}
return hdr.tagType, timeMS, b, nil
}
func (c *Conn) writeMessage(chunkID, tagType byte, timeMS uint32, payload []byte) error {
c.mu.Lock()
c.resetBuffer()
b := payload
size := uint32(len(b))
if size > c.wrPacketSize {
c.appendType0(chunkID, tagType, timeMS, size, b[:c.wrPacketSize])
for {
b = b[c.wrPacketSize:]
if uint32(len(b)) > c.wrPacketSize {
c.appendType3(chunkID, b[:c.wrPacketSize])
} else {
c.appendType3(chunkID, b)
break
}
}
} else {
c.appendType0(chunkID, tagType, timeMS, size, b)
}
//log.Printf("%d %2d %5d %6d %.32x", chunkID, tagType, timeMS, size, payload)
_, err := c.wr.Write(c.wrBuf)
c.mu.Unlock()
return err
}
func (c *Conn) resetBuffer() {
c.wrBuf = c.wrBuf[:0]
}
func (c *Conn) appendType0(chunkID, tagType byte, timeMS, size uint32, payload []byte) {
// TODO: timeMS more than 24 bit
c.wrBuf = append(c.wrBuf,
chunkID,
byte(timeMS>>16), byte(timeMS>>8), byte(timeMS),
byte(size>>16), byte(size>>8), byte(size),
tagType,
c.streamID, 0, 0, 0, // little endian streamID
)
c.wrBuf = append(c.wrBuf, payload...)
}
func (c *Conn) appendType3(chunkID byte, payload []byte) {
c.wrBuf = append(c.wrBuf, 3<<6|chunkID)
c.wrBuf = append(c.wrBuf, payload...)
}
func (c *Conn) writePacketSize() error {
b := binary.BigEndian.AppendUint32(nil, c.wrPacketSize)
return c.writeMessage(2, TypeSetPacketSize, 0, b)
}
func (c *Conn) writeConnect() error {
b := amf.EncodeItems("connect", 1, map[string]any{
"app": c.App,
"flashVer": "FMLE/3.0 (compatible; FMSc/1.0)",
"tcUrl": c.url,
})
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
return err
}
v, err := c.readResponse(1)
if err != nil {
return err
}
code := getString(v, 3, "code")
if code != "NetConnection.Connect.Success" {
return fmt.Errorf("rtmp: wrong response %#v", v)
}
return nil
}
func (c *Conn) writeReleaseStream() error {
b := amf.EncodeItems("releaseStream", 2, nil, c.Stream)
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
return err
}
b = amf.EncodeItems("FCPublish", 3, nil, c.Stream)
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
return err
}
return nil
}
func (c *Conn) writeCreateStream() error {
b := amf.EncodeItems("createStream", 4, nil)
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
return err
}
v, err := c.readResponse(4)
if err != nil {
return err
}
if len(v) == 4 {
if f, ok := v[3].(float64); ok {
c.streamID = byte(f)
return nil
}
}
return fmt.Errorf("rtmp: wrong response %#v", v)
}
func (c *Conn) writePublish() error {
b := amf.EncodeItems("publish", 5, nil, c.Stream, "live")
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
return err
}
v, err := c.readResponse(0)
if err != nil {
return nil
}
code := getString(v, 3, "code")
if code != "NetStream.Publish.Start" {
return fmt.Errorf("rtmp: wrong response %#v", v)
}
return nil
}
func (c *Conn) writePlay() error {
b := amf.EncodeItems("play", 5, nil, c.Stream)
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
return err
}
v, err := c.readResponse(0)
if err != nil {
return nil
}
code := getString(v, 3, "code")
if !strings.HasPrefix(code, "NetStream.Play.") {
return fmt.Errorf("rtmp: wrong response %#v", v)
}
return nil
}
func (c *Conn) readSize(n uint32) ([]byte, error) {
b := make([]byte, n)
if _, err := io.ReadAtLeast(c.rd, b, int(n)); err != nil {
return nil, err
}
return b, nil
}
func PutUint24(b []byte, v uint32) {
_ = b[2]
b[0] = byte(v >> 16)
b[1] = byte(v >> 8)
b[2] = byte(v)
}
func Uint24(b []byte) uint32 {
_ = b[2]
return uint32(b[0])<<16 | uint32(b[1])<<8 | uint32(b[2])
}
func getString(v []any, i int, key string) string {
if len(v) <= i {
return ""
}
if v, ok := v[i].(map[string]any); ok {
if s, ok := v[key].(string); ok {
return s
}
}
return ""
}
+87
View File
@@ -0,0 +1,87 @@
package rtmp
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/flv"
)
func (c *Conn) Producer() (core.Producer, error) {
c.rdBuf = []byte{
'F', 'L', 'V', // signature
1, // version
0, // flags (has video/audio)
0, 0, 0, 9, // header size
}
return flv.Open(c)
}
// Read - convert RTMP to FLV format
func (c *Conn) Read(p []byte) (n int, err error) {
// 1. Check temporary tempbuffer
if len(c.rdBuf) == 0 {
msgType, timeMS, payload, err2 := c.readMessage()
if err2 != nil {
return 0, err2
}
// previous tag size (4 byte) + header (11 byte) + payload
n = 4 + 11 + len(payload)
// 2. Check if the message fits in the buffer
if n <= len(p) {
encodeFLV(p, msgType, timeMS, payload)
return
}
// 3. Put the message into a temporary buffer
c.rdBuf = make([]byte, n)
encodeFLV(c.rdBuf, msgType, timeMS, payload)
}
// 4. Send temporary buffer
n = copy(p, c.rdBuf)
c.rdBuf = c.rdBuf[n:]
return
}
func encodeFLV(b []byte, msgType byte, time uint32, payload []byte) {
_ = b[4+11]
b[0] = 0
b[1] = 0
b[2] = 0
b[3] = 0
b[4+0] = msgType
PutUint24(b[4+1:], uint32(len(payload)))
PutUint24(b[4+4:], time)
b[4+7] = byte(time >> 24)
copy(b[4+11:], payload)
}
// Write - convert FLV format to RTMP format
func (c *Conn) Write(p []byte) (n int, err error) {
n = len(p)
if p[0] == 'F' {
p = p[9+4:] // skip first msg with FLV header
for len(p) > 0 {
size := 11 + uint16(p[2])<<8 + uint16(p[3]) + 4
if _, err = c.Write(p[:size]); err != nil {
return 0, err
}
p = p[size:]
}
return
}
// decode FLV: 11 bytes header + payload + 4 byte size
tagType := p[0]
timeMS := uint32(p[4])<<16 | uint32(p[5])<<8 | uint32(p[6]) | uint32(p[7])<<24
payload := p[11 : len(p)-4]
err = c.writeMessage(4, tagType, timeMS, payload)
return
}
-28
View File
@@ -1,28 +0,0 @@
package rtmp
import (
"net/url"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/flv"
"github.com/AlexxIT/go2rtc/pkg/tcp"
)
func Dial(rawURL string) (core.Producer, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
conn, err := tcp.Dial(u, "1935", core.ConnDialTimeout)
if err != nil {
return nil, err
}
rd, err := NewReader(u, conn)
if err != nil {
return nil, err
}
return flv.Open(rd)
}
-488
View File
@@ -1,488 +0,0 @@
package rtmp
import (
"bufio"
"encoding/binary"
"errors"
"io"
"net"
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
)
const (
MsgSetPacketSize = 1
MsgServerBandwidth = 5
MsgClientBandwidth = 6
MsgCommand = 20
//MsgAck = 3
//MsgControl = 4
//MsgAudioPacket = 8
//MsgVideoPacket = 9
//MsgDataExt = 15
//MsgCommandExt = 17
//MsgData = 18
)
var ErrResponse = errors.New("rtmp: wrong response")
type Reader struct {
url string
app string
stream string
pktSize uint32
headers map[uint32]*header
conn net.Conn
rd io.Reader
buf []byte
}
func NewReader(u *url.URL, conn net.Conn) (*Reader, error) {
rd := &Reader{
url: u.String(),
headers: map[uint32]*header{},
conn: conn,
rd: bufio.NewReaderSize(conn, core.BufferSize),
}
if args := strings.Split(u.Path, "/"); len(args) >= 2 {
rd.app = args[1]
if len(args) >= 3 {
rd.stream = args[2]
if u.RawQuery != "" {
rd.stream += "?" + u.RawQuery
}
}
}
if err := rd.handshake(); err != nil {
return nil, err
}
if err := rd.sendConfig(); err != nil {
return nil, err
}
if err := rd.sendConnect(); err != nil {
return nil, err
}
if err := rd.sendPlay(); err != nil {
return nil, err
}
rd.buf = []byte{
'F', 'L', 'V', // signature
1, // version
0, // flags (has video/audio)
0, 0, 0, 9, // header size
}
return rd, nil
}
func (c *Reader) Read(p []byte) (n int, err error) {
// 1. Check temporary tempbuffer
if len(c.buf) == 0 {
msgType, timeMS, payload, err2 := c.readMessage()
if err2 != nil {
return 0, err2
}
payloadSize := len(payload)
// previous tag size (4 byte) + header (11 byte) + payload
n = 4 + 11 + payloadSize
// 2. Check if the message fits in the buffer
if n <= len(p) {
encodeFLV(p, msgType, timeMS, uint32(payloadSize), payload)
return
}
// 3. Put the message into a temporary buffer
c.buf = make([]byte, n)
encodeFLV(c.buf, msgType, timeMS, uint32(payloadSize), payload)
}
// 4. Send temporary buffer
n = copy(p, c.buf)
c.buf = c.buf[n:]
return
}
func (c *Reader) Close() error {
return c.conn.Close()
}
func encodeFLV(b []byte, msgType byte, time, size uint32, payload []byte) {
b[0] = 0
b[1] = 0
b[2] = 0
b[3] = 0
b[4+0] = msgType
PutUint24(b[4+1:], size)
PutUint24(b[4+4:], time)
b[4+7] = byte(time >> 24)
copy(b[4+11:], payload)
}
type header struct {
msgTime uint32
msgSize uint32
msgType byte
}
func (c *Reader) readMessage() (byte, uint32, []byte, error) {
hdrType, sid, err := c.readHeader()
if err != nil {
return 0, 0, nil, err
}
// storing header information for support header type 3
hdr, ok := c.headers[sid]
if !ok {
hdr = &header{}
c.headers[sid] = hdr
}
var b []byte
// https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol#Packet_structure
switch hdrType {
case 0: // 12 byte header (full header)
if b, err = c.readSize(11); err != nil {
return 0, 0, nil, err
}
_ = b[7]
hdr.msgTime = Uint24(b) // timestamp
hdr.msgSize = Uint24(b[3:]) // msgdatalen
hdr.msgType = b[6] // msgtypeid
_ = binary.BigEndian.Uint32(b[7:]) // msgsid
case 1: // 8 bytes - like type b00, not including message ID (4 last bytes)
if b, err = c.readSize(7); err != nil {
return 0, 0, nil, err
}
_ = b[6]
hdr.msgTime = Uint24(b) // timestamp
hdr.msgSize = Uint24(b[3:]) // msgdatalen
hdr.msgType = b[6] // msgtypeid
case 2: // 4 bytes - Basic Header and timestamp (3 bytes) are included
if b, err = c.readSize(3); err != nil {
return 0, 0, nil, err
}
hdr.msgTime = Uint24(b) // timestamp
case 3: // 1 byte - only the Basic Header is included
// use here hdr from previous msg with same session ID (sid)
}
timeMS := hdr.msgTime
if timeMS == 0xFFFFFF {
if b, err = c.readSize(4); err != nil {
return 0, 0, nil, err
}
timeMS = binary.BigEndian.Uint32(b)
}
//log.Printf("[Reader] hdrType=%d sid=%d msdTime=%d msgSize=%d msgType=%d", hdrType, sid, hdr.msgTime, hdr.msgSize, hdr.msgType)
// 1. Response zero size
if hdr.msgSize == 0 {
return hdr.msgType, timeMS, nil, nil
}
b = make([]byte, hdr.msgSize)
// 2. Response small packet
if c.pktSize == 0 || hdr.msgSize < c.pktSize {
if _, err = io.ReadFull(c.rd, b); err != nil {
return 0, 0, nil, err
}
return hdr.msgType, timeMS, b, nil
}
// 3. Response big packet
var i0 uint32
for i1 := c.pktSize; i1 < hdr.msgSize; i1 += c.pktSize {
if _, err = io.ReadFull(c.rd, b[i0:i1]); err != nil {
return 0, 0, nil, err
}
if _, _, err = c.readHeader(); err != nil {
return 0, 0, nil, err
}
if hdr.msgTime == 0xFFFFFF {
if _, err = c.readSize(4); err != nil {
return 0, 0, nil, err
}
}
i0 = i1
}
if _, err = io.ReadFull(c.rd, b[i0:]); err != nil {
return 0, 0, nil, err
}
return hdr.msgType, timeMS, b, nil
}
func (c *Reader) handshake() error {
// simple handshake without real random and check response
const randomSize = 4 + 4 + 1528
b := make([]byte, 1+randomSize)
b[0] = 0x03
if _, err := c.conn.Write(b); err != nil {
return err
}
if _, err := io.ReadFull(c.rd, b); err != nil {
return err
}
if b[0] != 3 {
return errors.New("Reader: wrong handshake")
}
if _, err := c.conn.Write(b[1:]); err != nil {
return err
}
if _, err := io.ReadFull(c.rd, b[1:]); err != nil {
return err
}
return nil
}
func (c *Reader) sendConfig() error {
b := make([]byte, 5)
binary.BigEndian.PutUint32(b, 65536)
if err := c.sendRequest(MsgSetPacketSize, 0, b[:4]); err != nil {
return err
}
binary.BigEndian.PutUint32(b, 2500000)
if err := c.sendRequest(MsgServerBandwidth, 0, b[:4]); err != nil {
return err
}
binary.BigEndian.PutUint32(b, 10000000) // ack size
b[4] = 2 // limit type
if err := c.sendRequest(MsgClientBandwidth, 0, b); err != nil {
return err
}
return nil
}
func (c *Reader) sendConnect() error {
msg := amf.AMF{}
msg.WriteString("connect")
msg.WriteNumber(1)
msg.WriteObject(map[string]any{
"app": c.app,
"flashVer": "MAC 32,0,0,0",
"tcUrl": c.url,
"fpad": false, // ?
"capabilities": 15, // ?
"audioCodecs": 4071, // ?
"videoCodecs": 252, // ?
"videoFunction": 1, // ?
})
if err := c.sendRequest(MsgCommand, 0, msg.Bytes()); err != nil {
return err
}
s, err := c.waitCode("_result", float64(1)) // result with same ID
if err != nil {
return err
}
if s != "NetConnection.Connect.Success" {
return errors.New("Reader: wrong code: " + s)
}
return nil
}
func (c *Reader) sendPlay() error {
msg := amf.NewWriter()
msg.WriteString("createStream")
msg.WriteNumber(2)
msg.WriteNull()
if err := c.sendRequest(MsgCommand, 0, msg.Bytes()); err != nil {
return err
}
args, err := c.waitResponse("_result", float64(2)) // result with same ID
if err != nil {
return err
}
if len(args) < 4 {
return ErrResponse
}
sid, _ := args[3].(float64)
msg = amf.NewWriter()
msg.WriteString("play")
msg.WriteNumber(0)
msg.WriteNull()
msg.WriteString(c.stream)
if err = c.sendRequest(MsgCommand, uint32(sid), msg.Bytes()); err != nil {
return err
}
s, err := c.waitCode("onStatus", float64(0)) // events has zero transaction ID
if err != nil {
return err
}
switch s {
case "NetStream.Play.Start", "NetStream.Play.Reset":
return nil
}
return errors.New("Reader: wrong code: " + s)
}
func (c *Reader) sendRequest(msgType byte, streamID uint32, payload []byte) error {
n := len(payload)
b := make([]byte, 12+n)
_ = b[12]
switch msgType {
case MsgSetPacketSize, MsgServerBandwidth, MsgClientBandwidth:
b[0] = 0x02 // chunk ID
case MsgCommand:
if streamID == 0 {
b[0] = 0x03 // chunk ID
} else {
b[0] = 0x08 // chunk ID
}
}
//PutUint24(b[1:], 0) // timestamp
PutUint24(b[4:], uint32(n)) // payload size
b[7] = msgType // message type
binary.BigEndian.PutUint32(b[8:], streamID) // message stream ID
copy(b[12:], payload)
if _, err := c.conn.Write(b); err != nil {
return err
}
return nil
}
func (c *Reader) readHeader() (byte, uint32, error) {
b, err := c.readSize(1)
if err != nil {
return 0, 0, err
}
hdrType := b[0] >> 6
sid := uint32(b[0] & 0b111111)
switch sid {
case 0:
if b, err = c.readSize(1); err != nil {
return 0, 0, err
}
sid = 64 + uint32(b[0])
case 1:
if b, err = c.readSize(2); err != nil {
return 0, 0, err
}
sid = 64 + uint32(binary.BigEndian.Uint16(b))
}
return hdrType, sid, nil
}
func (c *Reader) readSize(n uint32) ([]byte, error) {
b := make([]byte, n)
if _, err := io.ReadAtLeast(c.rd, b, int(n)); err != nil {
return nil, err
}
return b, nil
}
func (c *Reader) waitResponse(cmd any, tid any) ([]any, error) {
for {
msgType, _, b, err := c.readMessage()
if err != nil {
return nil, err
}
switch msgType {
case MsgSetPacketSize:
c.pktSize = binary.BigEndian.Uint32(b)
case MsgCommand:
var v []any
if v, err = amf.NewReader(b).ReadItems(); err != nil {
return nil, err
}
if len(v) < 4 {
return nil, ErrResponse
}
if v[0] == cmd && v[1] == tid {
return v, nil
}
}
}
}
func (c *Reader) waitCode(cmd any, tid any) (string, error) {
args, err := c.waitResponse(cmd, tid)
if err != nil {
return "", err
}
if len(args) < 4 {
return "", ErrResponse
}
m, _ := args[3].(map[string]any)
if m == nil {
return "", ErrResponse
}
v, _ := m["code"]
if v == nil {
return "", ErrResponse
}
s, _ := v.(string)
return s, nil
}
func PutUint24(b []byte, v uint32) {
_ = b[2]
b[0] = byte(v >> 16)
b[1] = byte(v >> 8)
b[2] = byte(v)
}
func Uint24(b []byte) uint32 {
_ = b[2]
return uint32(b[0])<<16 | uint32(b[1])<<8 | uint32(b[2])
}
+162
View File
@@ -0,0 +1,162 @@
package rtmp
import (
"bufio"
"encoding/binary"
"fmt"
"io"
"net"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
)
func NewServer(conn net.Conn) (*Conn, error) {
c := &Conn{
conn: conn,
rd: bufio.NewReaderSize(conn, core.BufferSize),
wr: conn,
chunks: map[uint8]*header{},
rdPacketSize: 128,
wrPacketSize: 4096,
}
if err := c.serverHandshake(); err != nil {
return nil, err
}
if err := c.writePacketSize(); err != nil {
return nil, err
}
return c, nil
}
func (c *Conn) serverHandshake() error {
b := make([]byte, 1+1536)
// read C0+C1
if _, err := io.ReadFull(c.rd, b); err != nil {
return err
}
// write S0+S1, skip random
if _, err := c.conn.Write(b); err != nil {
return err
}
// read S1, skip check
if _, err := io.ReadFull(c.rd, make([]byte, 1536)); err != nil {
return err
}
// write C1
if _, err := c.conn.Write(b[1:]); err != nil {
return err
}
return nil
}
func (c *Conn) ReadCommands() error {
for {
msgType, _, b, err := c.readMessage()
if err != nil {
return err
}
//log.Printf("%d %.256x", msgType, b)
switch msgType {
case TypeSetPacketSize:
c.rdPacketSize = binary.BigEndian.Uint32(b)
case TypeCommand:
if err = c.acceptCommand(b); err != nil {
return err
}
if c.Intent != "" {
return nil
}
}
}
}
const (
CommandConnect = "connect"
CommandReleaseStream = "releaseStream"
CommandFCPublish = "FCPublish"
CommandCreateStream = "createStream"
CommandPublish = "publish"
CommandPlay = "play"
)
func (c *Conn) acceptCommand(b []byte) error {
items, err := amf.NewReader(b).ReadItems()
if err != nil {
return nil
}
//log.Printf("%#v", items)
if len(items) < 2 {
return fmt.Errorf("rtmp: read command %x", b)
}
cmd, ok := items[0].(string)
if !ok {
return fmt.Errorf("rtmp: read command %x", b)
}
tID, ok := items[1].(float64) // transaction ID
if !ok {
return fmt.Errorf("rtmp: read command %x", b)
}
switch cmd {
case CommandConnect:
if len(items) == 3 {
if v, ok := items[2].(map[string]any); ok {
c.App, _ = v["app"].(string)
}
}
if c.App == "" {
return fmt.Errorf("rtmp: read command %x", b)
}
payload := amf.EncodeItems(
"_result", tID,
map[string]any{"fmsVer": "FMS/3,0,1,123"},
map[string]any{"code": "NetConnection.Connect.Success"},
)
return c.writeMessage(3, TypeCommand, 0, payload)
case CommandReleaseStream:
payload := amf.EncodeItems("_result", tID, nil)
return c.writeMessage(3, TypeCommand, 0, payload)
case CommandCreateStream:
payload := amf.EncodeItems("_result", tID, nil, 1)
return c.writeMessage(3, TypeCommand, 0, payload)
case CommandPublish, CommandPlay: // response later
c.Intent = cmd
c.streamID = 1
case CommandFCPublish: // no response
default:
println("rtmp: unknown command: " + cmd)
}
return nil
}
func (c *Conn) WriteStart() error {
var code string
if c.Intent == CommandPublish {
code = "NetStream.Publish.Start"
} else {
code = "NetStream.Play.Start"
}
payload := amf.EncodeItems("onStatus", 0, nil, map[string]any{"code": code})
return c.writeMessage(3, TypeCommand, 0, payload)
}
+1 -1
View File
@@ -35,7 +35,7 @@ func (c *Conn) Dial() (err error) {
if c.Timeout != 0 {
timeout = time.Second * time.Duration(c.Timeout)
}
conn, err = tcp.Dial(c.URL, "554", timeout)
conn, err = tcp.Dial(c.URL, timeout)
} else {
conn, err = websocket.Dial(c.Transport)
}
+5 -5
View File
@@ -111,12 +111,12 @@ func (c *Conn) Handle() (err error) {
if c.Timeout == 0 {
// polling frames from remote RTSP Server (ex Camera)
if len(c.receivers) > 0 {
// if we receiving video/audio from camera
timeout = time.Second * 5
} else {
timeout = time.Second * 5
if len(c.receivers) == 0 {
// if we only send audio to camera
timeout = time.Second * 30
// https://github.com/AlexxIT/go2rtc/issues/659
timeout += keepaliveDT
}
} else {
timeout = time.Second * time.Duration(c.Timeout)
+2 -3
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')
+26 -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,27 @@ a=sendonly`
assert.Nil(t, err)
assert.Len(t, medias, 3)
}
func TestBugSDP4(t *testing.T) {
s := `v=0
o=- 14665860 31787219 1 IN IP4 10.0.0.94
s=Session streamed by "MERCURY RTSP Server"
t=0 0
m=video 0 RTP/AVP 96
c=IN IP4 0.0.0.0
b=AS:4096
a=range:npt=0-
a=control:track1
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1; profile-level-id=640016; sprop-parameter-sets=Z2QAFqzGoCgPaEAAAAMAQAAAB6E=,aOqPLA==
m=audio 0 RTP/AVP 8
a=rtpmap:8 PCMA/8000
a=control:track2
m=application/MERCURY 0 RTP/AVP smart/1/90000
a=rtpmap:95 MERCURY/90000
a=control:track3
`
medias, err := UnmarshalSDP([]byte(s))
assert.Nil(t, err)
assert.Len(t, medias, 3)
}
+37 -25
View File
@@ -2,7 +2,9 @@ package shell
import (
"os"
"os/exec"
"os/signal"
"path/filepath"
"regexp"
"strings"
"syscall"
@@ -12,35 +14,28 @@ func QuoteSplit(s string) []string {
var a []string
for len(s) > 0 {
is := strings.IndexByte(s, ' ')
if is >= 0 {
// skip prefix and double spaces
if is == 0 {
// goto next symbol
s = s[1:]
continue
switch c := s[0]; c {
case '\t', '\n', '\r', ' ': // unicode.IsSpace
s = s[1:]
case '"', '\'': // quote chars
if i := strings.IndexByte(s[1:], c); i > 0 {
a = append(a, s[1:i+1])
s = s[i+2:]
} else {
return nil // error
}
// check if quote in word
if i := strings.IndexByte(s[:is], '"'); i >= 0 {
// search quote end
if is = strings.Index(s, `" `); is > 0 {
is += 1
} else {
is = -1
}
default:
i := strings.IndexAny(s, "\t\n\r ")
if i > 0 {
a = append(a, s[:i])
s = s[i:]
} else {
a = append(a, s)
s = ""
}
}
if is >= 0 {
a = append(a, strings.ReplaceAll(s[:is], `"`, ""))
s = s[is+1:]
} else {
//add last word
a = append(a, s)
break
}
}
return a
}
@@ -75,3 +70,20 @@ func RunUntilSignal() {
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
println("exit with signal:", (<-sigs).String())
}
// Restart idea taken from https://github.com/tillberg/autorestart
// Copyright (c) 2015, Dan Tillberg
func Restart() {
path, err := exec.LookPath(os.Args[0])
if err != nil {
return
}
path, err = filepath.Abs(path)
if err != nil {
return
}
path = filepath.Clean(path)
if err = syscall.Exec(path, os.Args, os.Environ()); err != nil {
panic(err)
}
}
+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="0" -i "DeckLink SDI (2)"`
require.Equal(t, []string{"ffmpeg", "-i", "video=\"0\"", "-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()
}
+79 -20
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"
@@ -62,33 +65,19 @@ func (c *Client) newConn() (net.Conn, error) {
return nil, err
}
// support raw username/password
username := u.User.Username()
password, _ := u.User.Password()
// or cloud password in place of username
if password == "" {
password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
username = "admin"
u.User = url.UserPassword(username, password)
}
u.Scheme = "http"
u.Path = "/stream"
if u.Port() == "" {
u.Host += ":8800"
}
// TODO: fix closing connection
ctx, pconn := tcp.WithConn()
req, err := http.NewRequestWithContext(ctx, "POST", u.String(), nil)
req, err := http.NewRequest("POST", "http://"+u.Host+"/stream", nil)
if err != nil {
return nil, err
}
req.URL.User = u.User
req.Header.Set("Content-Type", "multipart/mixed; boundary=--client-stream-boundary--")
res, err := tcp.Do(req)
conn, res, err := dial(req)
if err != nil {
return nil, err
}
@@ -98,13 +87,16 @@ func (c *Client) newConn() (net.Conn, error) {
}
if c.decrypt == nil {
c.newDectypter(res, username, password)
c.newDectypter(res)
}
return *pconn, nil
return conn, nil
}
func (c *Client) newDectypter(res *http.Response, username, password string) {
func (c *Client) newDectypter(res *http.Response) {
username := res.Request.URL.User.Username()
password, _ := res.Request.URL.User.Password()
// extract nonce from response
// cipher="AES_128_CBC" username="admin" padding="PKCS7_16" algorithm="MD5" nonce="***"
nonce := res.Header.Get("Key-Exchange")
@@ -244,3 +236,70 @@ func (c *Client) Request(conn net.Conn, body []byte) (string, error) {
return v.Params.SessionID, nil
}
}
func dial(req *http.Request) (net.Conn, *http.Response, error) {
conn, err := net.DialTimeout("tcp", req.URL.Host, core.ConnDialTimeout)
if err != nil {
return nil, nil, err
}
username := req.URL.User.Username()
password, _ := req.URL.User.Password()
req.URL.User = nil
if err = req.Write(conn); err != nil {
return nil, nil, err
}
r := bufio.NewReader(conn)
res, err := http.ReadResponse(r, req)
if err != nil {
return nil, nil, err
}
auth := res.Header.Get("WWW-Authenticate")
if res.StatusCode != http.StatusUnauthorized || !strings.HasPrefix(auth, "Digest") {
return nil, nil, err
}
if password == "" {
// support cloud password in place of username
if strings.Contains(auth, `encrypt_type="3"`) {
password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username)))
} else {
password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
}
username = "admin"
}
realm := tcp.Between(auth, `realm="`, `"`)
nonce := tcp.Between(auth, `nonce="`, `"`)
qop := tcp.Between(auth, `qop="`, `"`)
uri := req.URL.RequestURI()
ha1 := tcp.HexMD5(username, realm, password)
ha2 := tcp.HexMD5(req.Method, uri)
nc := "00000001"
cnonce := "00000001"
response := tcp.HexMD5(ha1, nonce, nc, cnonce, qop, ha2)
header := fmt.Sprintf(
`Digest username="%s", realm="%s", nonce="%s", uri="%s", qop=%s, nc=%s, cnonce="%s", response="%s"`,
username, realm, nonce, uri, qop, nc, cnonce, response,
)
req.Header.Set("Authorization", header)
if err = req.Write(conn); err != nil {
return nil, nil, err
}
if res, err = http.ReadResponse(r, req); err != nil {
return nil, nil, err
}
req.URL.User = url.UserPassword(username, password)
return conn, res, nil
}
+1 -1
View File
@@ -12,7 +12,7 @@ import (
func (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
if c.sender == nil {
if err := c.SetupBackchannel(); err != nil {
return nil
return err
}
muxer := mpegts.NewMuxer()
+12 -3
View File
@@ -10,13 +10,22 @@ import (
)
// Dial - for RTSP(S|X) and RTMP(S|X)
func Dial(u *url.URL, port string, timeout time.Duration) (net.Conn, error) {
func Dial(u *url.URL, timeout time.Duration) (net.Conn, error) {
var address string
var hostname string // without port
if i := strings.IndexByte(u.Host, ':'); i > 0 {
address = u.Host
hostname = u.Host[:i]
} else {
switch u.Scheme {
case "rtsp", "rtsps", "rtspx":
address = u.Host + ":554"
case "rtmp":
address = u.Host + ":1935"
case "rtmps", "rtmpx":
address = u.Host + ":443"
}
hostname = u.Host
u.Host += ":" + port
}
var secure *tls.Config
@@ -33,7 +42,7 @@ func Dial(u *url.URL, port string, timeout time.Duration) (net.Conn, error) {
return nil, errors.New("unsupported scheme: " + u.Scheme)
}
conn, err := net.DialTimeout("tcp", u.Host, timeout)
conn, err := net.DialTimeout("tcp", address, timeout)
if err != nil {
return nil, err
}

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