Compare commits

...

358 Commits

Author SHA1 Message Date
Alexey Khit
9ddbb326b4 Update version to 1.0.0 2023-01-20 17:07:43 +03:00
Alexey Khit
a2e58d928e Fix timezone in logs 2023-01-20 13:45:01 +03:00
Alexey Khit
3c48fb8bea Simplify Dockerfile 2023-01-20 11:23:28 +03:00
Alexey Khit
4b0cbb5a73 Add support basic auth for API 2023-01-20 10:54:26 +03:00
Alexey Khit
e28b49ea86 Ignore errors for RTCP packets 2023-01-20 10:26:57 +03:00
Alexey Khit
5c17d8fcb6 Add support AAC audio for HTTP-FLV 2023-01-19 21:44:15 +03:00
Alexey Khit
e040fb591f Disable CGO for git releases 2023-01-18 15:07:42 +03:00
Alexey Khit
140014f2a6 Fix info for WS/MP4 2023-01-18 15:04:06 +03:00
Alexey Khit
23f72d111e Add Teardown handler for RTSP server (untested) 2023-01-18 12:21:54 +03:00
Alexey Khit
f9d5ab9d0a Fix RTSP server SDP for some clients 2023-01-18 11:45:39 +03:00
Alexey Khit
8628c48db8 Add no-cache for all GET API requests 2023-01-18 10:01:00 +03:00
Alexey Khit
6e49d51c33 Update GET config API when config file not set 2023-01-18 10:00:20 +03:00
Alexey Khit
6a61b5234e Fix HTTP-FLV support for Reolink cameras 2023-01-18 09:36:32 +03:00
Alexey Khit
7a0091777d Fix relative config path #171 2023-01-16 11:00:04 +03:00
Alexey Khit
d23d2a7eff Fix release binaries for mac 2023-01-16 00:40:02 +03:00
Alexey Khit
cecbe4166c Update version to 0.1-rc.9 2023-01-16 00:06:55 +03:00
Alexey Khit
dcb457235c Rewrite stream info API 2023-01-15 23:51:20 +03:00
Alexey Khit
bc4e032830 Update readme 2023-01-15 11:13:38 +03:00
Alexey Khit
8218cda149 Add version, config_path to web UI and fix RTSP link 2023-01-15 09:57:15 +03:00
Alexey Khit
d1e56feeb6 Update full path to config file 2023-01-15 09:55:32 +03:00
Alexey Khit
463d05dfd3 Update readme 2023-01-15 00:28:48 +03:00
Alexey Khit
a1a73f7b45 Rewrite WS+MP4 format to keyframes stream 2023-01-15 00:12:26 +03:00
Alexey Khit
39662e10af Fix errors in JS player 2023-01-15 00:11:31 +03:00
Alexey Khit
1c830d6e60 Code refactoring 2023-01-14 22:49:12 +03:00
Alex X
2039aa60b3 Merge pull request #170 from skrashevich/config-api-patch-method
PATH api/config method for merge configuration
2023-01-14 21:57:34 +03:00
Sergey Krashevich
b7016e798f Update config.go 2023-01-14 21:27:23 +03:00
Alexey Khit
0b291f5185 Support multiple configs and config in raw yaml form 2023-01-14 21:12:17 +03:00
Alexey Khit
395304654a Code refactoring 2023-01-14 19:15:13 +03:00
Alexey Khit
e472397705 Add general info API 2023-01-14 18:00:43 +03:00
Alexey Khit
7c1f48e0ad Support empty default environment value 2023-01-14 17:25:05 +03:00
Alexey Khit
f4346a104f Add support env variables in config file #143 2023-01-14 17:19:51 +03:00
Alexey Khit
030972b436 Auto build binaries on release #158 2023-01-14 14:14:23 +03:00
Alexey Khit
efddefa123 Add web config editor #153 2023-01-14 13:47:34 +03:00
Alexey Khit
3c1bdd0dab Fix WebRTC candidate type 2023-01-14 09:45:03 +03:00
Alexey Khit
7e7e15d7c8 Update readme 2023-01-14 09:22:22 +03:00
Alex X
a1a9f77535 Merge pull request #167 from felipecrs/master
Match docs with new webrtc udp fixed port
2023-01-14 09:10:46 +03:00
Alexey Khit
a06462729d Code refactoring 2023-01-14 09:04:54 +03:00
Alex X
331c5bbcad Merge pull request #166 from tsightler/udp-candidate-fix
Fix invalid tcpType for UDP candidate
2023-01-14 08:59:25 +03:00
Felipe Santos
58a76efc8a Match docs with new webrtc udp fixed port 2023-01-13 23:15:04 -03:00
tsightler
5e0f010885 Update helper.go 2023-01-13 18:18:39 -05:00
Alexey Khit
4ae733aa11 Update version to 0.1-rc.8 2023-01-13 22:39:24 +03:00
Alexey Khit
27d8b33b62 Fix concurrency in ivideon 2023-01-13 21:52:29 +03:00
Alexey Khit
ff8b0fbb9c Set default 8555 port for WebRTC (UDP+TCP) 2023-01-13 21:51:48 +03:00
Alexey Khit
c6ad7ac39f Add single UDP port for WebRTC Server 2023-01-13 21:51:48 +03:00
Alexey Khit
7a3adf17be Fix mp4f consumer (unused) 2023-01-13 21:51:24 +03:00
Alexey Khit
94f6c07b28 Fix mjpeg client network connection 2023-01-13 18:03:54 +03:00
Alexey Khit
7b326d4753 Fix simultaneous stream reconnect and start 2023-01-13 18:03:17 +03:00
Alexey Khit
5407a3bc4b Fix multiple requests from different consumers 2023-01-13 18:02:03 +03:00
Alexey Khit
6b24421722 Fix unblocking exec error 2023-01-13 18:01:01 +03:00
Alexey Khit
d12775a2d7 Fix unblocking exec waiter 2023-01-13 18:00:48 +03:00
Alexey Khit
6151593c08 Fix ws lock on write and close 2023-01-13 17:28:01 +03:00
Alexey Khit
dba0989c54 Fix empty streams json on stream lock 2023-01-13 13:37:36 +03:00
Alexey Khit
ba0c7d911d Fix ffmpeg link to same stream 2023-01-13 13:36:43 +03:00
Alexey Khit
09fefca712 Remove backchannel codec from add consumer error 2023-01-13 13:35:58 +03:00
Alexey Khit
b3f177e2ec Handle closed state for ws connection 2023-01-13 13:34:41 +03:00
Alexey Khit
228abb8fbe Change logs msg from WRN to DBG for fail on add consumer 2023-01-13 13:33:55 +03:00
Alexey Khit
eee70c07b7 Fix closer for ivideon source 2023-01-13 13:32:48 +03:00
Alexey Khit
d92b0f29af Fix states handle for RTSP 2023-01-13 13:32:09 +03:00
Alexey Khit
fca6c87b2c Fix RTSP tracks list in info json 2023-01-13 13:31:22 +03:00
Alexey Khit
0601091772 Fix closer for RTSP server #163 2023-01-13 13:30:41 +03:00
Alexey Khit
89eb653d67 Update version to 0.1-rc.7 2023-01-08 23:18:52 +03:00
Alexey Khit
0e49ffdfff Fix GetMedias for producer in reconnect state 2023-01-08 21:42:13 +03:00
Alexey Khit
bd2fc1252d Update last error for reconnect stream 2023-01-08 21:36:28 +03:00
Alexey Khit
78ac88448c Fix close problem ivideon client 2023-01-08 21:35:45 +03:00
Alexey Khit
4cd9757e53 Fix status info in JS player 2023-01-08 21:05:50 +03:00
Alexey Khit
f9cb6fd670 Fix wrong RTSP H264 profile for some cameras 2023-01-08 21:05:17 +03:00
Alexey Khit
57fa6a5530 Add support for simultaneous requests from different consumers 2023-01-08 20:31:00 +03:00
Alexey Khit
6906b56524 Fix double start for RTSP source 2023-01-08 20:01:38 +03:00
Alexey Khit
c9b0806c84 Add producer url to logs 2023-01-08 20:00:48 +03:00
Alexey Khit
a9d1e64f88 Fix STUN candidate in IPv6 format 2023-01-08 15:45:11 +03:00
Alex X
9e9f07f3f7 Merge pull request #150 from skrashevich/dockerfile-crossbuild
Speedup container building using Golang cross-building
2023-01-06 14:06:50 +03:00
Sergey Krashevich
b51aabd3d9 Update Dockerfile 2023-01-06 11:52:09 +03:00
Alexey Khit
368562c540 Update version to 0.1-rc.6 2023-01-02 20:53:04 +03:00
Alexey Khit
6d6e7010b4 Rewrite JS player for better integration 2023-01-02 16:33:00 +03:00
Alexey Khit
4157a53dd8 Response with error on codec negotiation 2023-01-02 16:32:08 +03:00
Alexey Khit
bdf5654c01 Change WS default buffer 2023-01-02 16:31:11 +03:00
Alexey Khit
66f729aa0e Send WS response on MJPEG or MP4 stream starts 2023-01-02 16:30:54 +03:00
Alexey Khit
96d1ef2d2c Adds about RTSPtoWebRTC STUN server to readme 2022-12-27 15:59:08 +03:00
Alexey Khit
9739f7f416 Add auto create new stream for async webrtc 2022-12-25 11:17:14 +03:00
Alexey Khit
654fa32b3a Fix packet size for MSE 2022-12-25 11:09:19 +03:00
Alexey Khit
db2263c7fe Fix stream page for raw urls in src 2022-12-25 08:55:58 +03:00
Alexey Khit
e6c36f1cf7 Rename Hardware Dockerfile 2022-12-19 12:16:38 +03:00
Alexey Khit
110f90cb34 Disable JS stream background by default 2022-12-18 22:39:52 +03:00
Alexey Khit
aca3bab238 Fix Firefox WebRTC support 2022-12-18 22:38:44 +03:00
Alexey Khit
4df44645d7 Fix lags for Intel HW transcoding 2022-12-18 21:33:38 +03:00
Alexey Khit
097fdfbbb8 Rename HW engine for Raspberry 2022-12-18 10:25:04 +03:00
Alexey Khit
dc21a04da7 Fix freezing with VAAPI HW 2022-12-18 10:24:30 +03:00
Alexey Khit
db255b476a Add hardware acceleration support to FFmpeg 2022-12-18 01:02:12 +03:00
Alexey Khit
464ea417ef Add docker image with Hardware drivers 2022-12-18 01:00:39 +03:00
Alexey Khit
c1fac66329 Refactoring CI 2022-12-17 23:40:56 +03:00
Alex X
a6057a2eca Merge pull request #72 from felipecrs/refactor-docker
Refactor docker image and ci
2022-12-17 23:07:55 +03:00
Alexey Khit
7c69ba13b0 Fix RTP H264 with two SEI in packet 2022-12-17 22:59:06 +03:00
Alexey Khit
2b8bfe8bd9 Add support width and height params for FFmpeg 2022-12-09 22:07:57 +03:00
Alexey Khit
0bd54da456 Increase compression level for 7zip 2022-12-09 00:25:38 +03:00
Alexey Khit
9f6af1c9e4 Update connection method in JS player so it can be extended 2022-12-09 00:24:29 +03:00
Alexey Khit
c9dd0e37e4 Fix RTSP JPEG processing 2022-12-09 00:22:25 +03:00
Felipe Santos
562872beb8 Add jq 2022-12-06 10:38:18 -03:00
Felipe Santos
46a278c067 Add curl and exit on error run.sh 2022-12-06 10:38:18 -03:00
Felipe Santos
270fc7c1b6 Allow to run container without mounting /config 2022-12-06 10:38:18 -03:00
Felipe Santos
6feb635522 Delete config.yaml not used anymore 2022-12-06 10:38:18 -03:00
Felipe Santos
6f48131e4d Remove armv6 which was never supported 2022-12-06 10:38:18 -03:00
Felipe Santos
f120db71a3 Add all platforms 2022-12-06 10:38:18 -03:00
Felipe Santos
72823af9d0 Refactor docker workflow 2022-12-06 10:38:18 -03:00
Felipe Santos
15d9d4ebf4 Use official python as base and add tini 2022-12-06 10:38:18 -03:00
Felipe Santos
b09bbd79c4 Fix VERSION 2022-12-06 10:38:18 -03:00
Felipe Santos
1830273f02 Refactor docker image 2022-12-06 10:38:18 -03:00
Alexey Khit
07f3972794 Update readme 2022-12-06 16:17:28 +03:00
Alexey Khit
4c2ebd20bc Update version to 0.1-rc.5 2022-12-06 12:19:29 +03:00
Alexey Khit
440c7bd6e1 Recover link to 2-way audio on Web UI 2022-12-06 12:15:22 +03:00
Alexey Khit
74c3510a10 Trace to log supported MP4 codecs 2022-12-06 10:51:42 +03:00
Alexey Khit
c2748fc77b Add check H264 High@5.1 for JS player 2022-12-06 10:48:42 +03:00
Alexey Khit
d334551591 Update main Web UI page 2022-12-06 01:34:24 +03:00
Alexey Khit
cfe20925ac Big rewrite JS player WebSocket processing 2022-12-05 23:54:40 +03:00
Alexey Khit
5b39f78ace Update error msg for fail codecs negotiation 2022-12-05 23:52:28 +03:00
Alexey Khit
b965c191b7 Adds errors output to API 2022-12-05 20:03:26 +03:00
Alexey Khit
7057b4846f Code refactoring 2022-12-05 00:47:46 +03:00
Alexey Khit
a746b96adc Remove old video.html file 2022-12-04 23:24:44 +03:00
Alexey Khit
b7718b33b8 Rewrite WS transport handler 2022-12-04 23:24:20 +03:00
Alexey Khit
69b17230f3 Collect producers codecs during negotiation 2022-12-04 23:16:34 +03:00
Alexey Khit
e2ecd909ab Update stream HTML page 2022-12-04 22:38:44 +03:00
Alexey Khit
ea79da0d53 Rename modes to mode in JS player 2022-12-04 22:38:21 +03:00
Alexey Khit
e64919838c Update MP4 over WebSocket support 2022-12-04 22:37:15 +03:00
Alexey Khit
162b11213d Allow JS player URL to http 2022-12-04 20:09:46 +03:00
Alexey Khit
d27acbd7e3 Fix MSE buffering 2022-12-04 18:38:15 +03:00
Alexey Khit
a692ecd7c1 Fix H265 for WebRTC in Safari 2022-12-03 11:44:26 +03:00
Alexey Khit
98c5366ba9 Fix supported codecs check in JS player 2022-12-03 09:39:45 +03:00
Alexey Khit
3eaaa3fcfa Add support URL as JS player src 2022-12-03 09:39:23 +03:00
Alexey Khit
7409b32836 Fix support old iOS version 2022-12-02 23:12:37 +03:00
Alexey Khit
e2cfdf8419 Support WebRTC, MSE, MSE2, MP4, MJPEG in JS player 2022-12-02 21:37:17 +03:00
Alexey Khit
4c0929d854 Support MP4 over WebSocket 2022-12-02 21:33:51 +03:00
Alexey Khit
258a0ffb91 Support MJPEG over WebSocket 2022-12-02 21:31:41 +03:00
Alexey Khit
999e81c2dd Code refactoring for webrtc candidates 2022-12-01 23:41:12 +03:00
Alexey Khit
8c6729027b Add feature to SETUP new RTSP tracks after PLAY 2022-12-01 23:37:11 +03:00
Alexey Khit
d3bd5eeab5 Added a blank payloader for MJPEG RTSP 2022-12-01 23:20:31 +03:00
Alexey Khit
dbbf2ea310 Update readme about Dahua bug 2022-12-01 14:43:13 +03:00
Alexey Khit
b8234e0c76 Update codecs table in readme 2022-12-01 13:46:59 +03:00
Alexey Khit
96cd753e27 Adds about RTSP server password to readme 2022-12-01 13:35:46 +03:00
Alexey Khit
c522e5bb08 Update docs about new sources 2022-12-01 13:02:29 +03:00
Alexey Khit
a16d8acc30 Add support HTTP JPEG and MJPEG sources 2022-12-01 13:01:48 +03:00
Alexey Khit
684878b4b1 Skip check RTSP auth for localhost requests 2022-11-29 21:04:38 +03:00
Alexey Khit
140a742cee Fix MSE2 check in JS 2022-11-27 10:22:58 +03:00
Alexey Khit
1b518b94fd Add support auth for RTSP server 2022-11-27 10:08:37 +03:00
Alexey Khit
5678121c50 Fix build for mac arm64 2022-11-27 09:57:27 +03:00
Alexey Khit
4915f12bde Add build for win arm64 2022-11-27 09:57:04 +03:00
Alexey Khit
bb91240b95 Remove unnecessary build scripts 2022-11-27 09:54:46 +03:00
Alexey Khit
b1d5d53832 Update to go 1.19 2022-11-27 09:53:11 +03:00
Alexey Khit
31fbbf91bb Remove websocket error on disconnect 2022-11-25 10:04:51 +03:00
Alexey Khit
a3f72fbab9 Fix websocket origin check with wrong port 2022-11-25 10:04:13 +03:00
Alexey Khit
fae59c7992 Update version to 0.1-rc.4 2022-11-24 02:00:31 +03:00
Alexey Khit
aff34f1d21 Totally rewritten MSE player 2022-11-24 01:59:48 +03:00
Alexey Khit
65e7efa775 Support codecs negotiation for MSE 2022-11-23 21:45:10 +03:00
Alexey Khit
3c3e9d282b Update about H265 support in readme 2022-11-23 20:35:11 +03:00
Alexey Khit
bd51069086 Add support CORS for API 2022-11-23 20:34:06 +03:00
Alexey Khit
1ddf7f1a6c Change trace log for stream.mp4 2022-11-23 12:57:11 +03:00
Alexey Khit
0e281e36d3 Fix race (concurency) for Track 2022-11-22 20:03:36 +03:00
Alexey Khit
3d6472cfb1 Update H265 preset for FFmpeg 2022-11-22 17:22:54 +03:00
Alexey Khit
7c31fa2ffd Fix empty SPS for MSE H265 2022-11-22 17:22:26 +03:00
Alexey Khit
0ed9d2410a Fix H265 support for MSE in Safari 2022-11-22 17:21:58 +03:00
Alexey Khit
1c89e7945e Remove printf for webrtc ontrack 2022-11-18 09:13:24 +03:00
Alexey Khit
48635ae341 Add two locks for Track 2022-11-18 09:12:48 +03:00
Alexey Khit
fdb316910f Fix WebRTC async connection 2022-11-16 11:26:56 +03:00
Alexey Khit
e29f2594fa Fix multiple transcoding when track not exists 2022-11-15 16:16:22 +03:00
Alexey Khit
c3da7584b0 Add check on RTSP server requers with empty url path 2022-11-14 19:06:43 +03:00
Alexey Khit
1e247cba92 Igroner app media for WebRTC from Hass 2022-11-14 17:26:59 +03:00
Alexey Khit
01631d9eb0 Update readme 2022-11-14 14:57:43 +03:00
Alexey Khit
4b27d119f0 Update version to 0.1-rc.3 2022-11-14 09:50:47 +03:00
Alexey Khit
dd55c03dc2 Add support multiple transcoding for ffmpeg 2022-11-14 02:26:14 +03:00
Alexey Khit
a4eab1944a Add ffmpeg async option for HomeKit audio 2022-11-14 01:29:45 +03:00
Alexey Khit
eea413a36c Support stream name as ffmpeg input 2022-11-14 01:22:07 +03:00
Alexey Khit
cdd42a8ed2 Change HomeKit codec from AAC to ELD 2022-11-14 00:58:34 +03:00
Alexey Khit
4815ce1baf Fix stop ffmpeg without matching tracks 2022-11-14 00:58:34 +03:00
Alexey Khit
e6d3939c78 Fix external producers 2022-11-13 21:33:09 +03:00
Alexey Khit
220b9ca318 Remove old example file 2022-11-13 20:54:19 +03:00
Alexey Khit
d625620dfd Fix ffmpeg profile warning 2022-11-13 20:09:18 +03:00
Alexey Khit
dd503f3410 Adds rotate template for ffmpeg 2022-11-13 20:05:54 +03:00
Alexey Khit
3e8e87bfcc Fix RTSP unknown response handler 2022-11-13 19:26:22 +03:00
Alexey Khit
64d218886e Add exec early exit handler 2022-11-13 19:24:26 +03:00
Alexey Khit
e91ccc211e Change ffmpeg verbose level to error 2022-11-13 19:18:53 +03:00
Alexey Khit
9f8a219483 Exec stderr will show with debug log 2022-11-13 19:15:12 +03:00
Alexey Khit
b617796941 Improve RTCP for HomeKit 2022-11-13 14:35:53 +03:00
Alexey Khit
77888fe086 Refactoring for HomeKit client 2022-11-11 22:47:14 +03:00
Alexey Khit
7bc3534bcb Add useful links to readmes 2022-11-11 22:44:54 +03:00
Alexey Khit
77bc0630d6 Add teaps to main readme 2022-11-11 22:44:49 +03:00
Alexey Khit
2f68711405 Fix MP4f test consumer 2022-11-11 22:44:34 +03:00
Alexey Khit
b8cab5db60 Remove aacparser from MP4 muxer 2022-11-11 22:44:05 +03:00
Alexey Khit
eae01be71f Add User-Agent to FFmpeg input and output 2022-11-11 18:04:31 +03:00
Alexey Khit
0127115180 Add User-Agent to go2rtc RTSP requests 2022-11-11 18:02:08 +03:00
Alexey Khit
aef84cef6b Add go2rtc version info 2022-11-11 18:01:38 +03:00
Alexey Khit
d478436758 Set video track for WebRTC always first 2022-11-11 16:33:08 +03:00
Alexey Khit
f77db44529 Refactoring for HomeKit client 2022-11-11 13:24:09 +03:00
Alex X
149d1bf235 Merge pull request #101 from felipecrs/patch-2
Mention alternative method to import hass cameras
2022-11-09 20:34:31 +03:00
Felipe Santos
b650475b10 Mention alternative method to import hass cameras 2022-11-09 13:00:30 -03:00
Alexey Khit
16e5406156 Update readme 2022-11-08 09:57:17 +03:00
Alexey Khit
49f6233bde Update RTSP server filters 2022-11-08 01:50:28 +03:00
Alexey Khit
78c5c70c73 Add duration API for MP4 file 2022-11-08 01:29:58 +03:00
Alexey Khit
32651c74ab Fix frame.mp4 support 2022-11-08 01:13:38 +03:00
Alexey Khit
5c64d1f847 Update MSE procession on JS side 2022-11-08 00:37:32 +03:00
Alexey Khit
717af29630 Refactoring 2022-11-08 00:37:13 +03:00
Alexey Khit
ea18475d31 Split MSE data on packets 2022-11-07 23:35:36 +03:00
Alexey Khit
701a9c69ec Update write websocket func 2022-11-07 23:35:08 +03:00
Alexey Khit
c06253c8b2 Fix producer request new track after start 2022-11-07 17:48:45 +03:00
Alexey Khit
3a07e9fa03 Fix lock on mp4 restarts 2022-11-07 13:32:27 +03:00
Alexey Khit
e1bc30fab3 Add support AAC for RTSP 2022-11-07 11:02:26 +03:00
Alexey Khit
d16ae0972f Code refactoring 2022-11-07 11:01:19 +03:00
Alexey Khit
8b93c97e69 Add support AAC for RTMP to MP4 2022-11-06 22:44:48 +03:00
Alexey Khit
d8158bc1e3 Update stream log message 2022-11-04 22:27:11 +03:00
Alexey Khit
f4f588d2c6 Add mutex to stream 2022-11-04 22:20:52 +03:00
Alexey Khit
e287b52808 Add check for RTSPtoWeb API 2022-11-04 22:12:00 +03:00
Alexey Khit
ff96257252 Add backchannel=0 option to readme 2022-11-04 21:49:36 +03:00
Alexey Khit
909f21b7e4 Update docs about TURN server 2022-11-04 21:44:12 +03:00
Alexey Khit
7d6a5b44f8 Add frame.jpeg api for MJPEG stream 2022-11-04 21:22:33 +03:00
Alexey Khit
278f7696b6 Make sink private for Track 2022-11-04 20:54:35 +03:00
Alexey Khit
3cbf2465ae Fix loopback producer 2022-11-04 17:52:26 +03:00
Alexey Khit
e9ea7a0b1f Add reconnection feature 2022-11-04 17:23:42 +03:00
Alexey Khit
0231fc3a90 Code refactoring 2022-11-04 17:16:42 +03:00
Alexey Khit
9ef2633840 Add 5 sec timeout to ffmpeg rtsp 2022-11-04 17:06:24 +03:00
Alexey Khit
5a8df3e90a Change RTSP dial timeout to 5 sec 2022-11-04 17:05:57 +03:00
Alexey Khit
a31cbec3eb Fix check RTSP transport prefix 2022-11-04 17:05:30 +03:00
Alexey Khit
54f547977e Add mutext to streams handlers 2022-11-04 17:04:47 +03:00
Alexey Khit
65d91e02bd Move NewLogger to function 2022-11-04 17:03:56 +03:00
Alexey Khit
7fc3f0f641 Ignore srtp init in stack list func 2022-11-04 10:07:50 +03:00
Alexey Khit
7725d5ed31 Rewrite get handlers code 2022-11-04 06:24:39 +03:00
Alexey Khit
6c1b9daa8b Update logs for RTSP packets (disabled) 2022-11-03 11:25:47 +03:00
Alexey Khit
6d432574bf Make main logger global 2022-11-03 10:26:26 +03:00
Alexey Khit
616f69c88b Cache public IP for 5 min 2022-11-02 12:48:36 +03:00
Alexey Khit
f72440712b Add timeout to GetPublicIP func 2022-11-02 12:47:26 +03:00
Alexey Khit
ceed146fb8 Add webrtc sync API 2022-11-02 12:46:39 +03:00
Alexey Khit
f17dadbbbf Rewrite keepalive and add timeouts for RTSP 2022-11-02 10:50:11 +03:00
Alexey Khit
3d4514eab9 Fix loopback for stream 2022-11-02 08:51:54 +03:00
Alexey Khit
2629dccb81 Rename HTTP-FLV 2022-10-31 20:05:28 +03:00
Alexey Khit
04f1aa2900 Fix trash in webrtc.html 2022-10-31 08:34:32 +03:00
Alexey Khit
0dacdea1c3 Add support RTMPT (flv over HTTP) 2022-10-30 17:17:42 +03:00
Alexey Khit
24082b1616 Fix backchannel reconnection issue 2022-10-29 11:33:01 +03:00
Alexey Khit
7964b1743b Fix RTSP processing for Amcrest IP4M-1051 2022-10-29 11:29:53 +03:00
Alexey Khit
49773a1ece Add mjpeg link to stream to main page 2022-10-21 12:01:00 +03:00
Alexey Khit
c97a48a73f Fix mjpeg for 2K cameras 2022-10-21 12:00:00 +03:00
Alexey Khit
e03231ebb4 fix ffmpeg transcoding for Reolink RLC-510A 2022-10-21 10:58:56 +03:00
Alexey Khit
649525a842 Merge remote-tracking branch 'origin/master' 2022-10-21 10:54:54 +03:00
Alex X
d411c1a25c Merge pull request #76 from NickM-27/name_api_stream
Add ability for API to set name of stream
2022-10-14 06:59:31 +03:00
Nicolas Mowen
2f0bcf4ae0 Add ability for API to set name of stream 2022-10-13 14:58:26 -06:00
Alexey Khit
831c504cab Fix memory usage for RTSP processing 2022-10-05 21:15:59 +03:00
Alexey Khit
12925a6bc5 Fix TP-Link Tapo TC70 support 2022-10-05 19:43:36 +03:00
Alexey Khit
e50e929150 Fix empty SPS for mp4 format 2022-10-05 15:35:30 +03:00
Alexey Khit
d0c87e0379 Support SEI NAL from ffmpeg transcoding 2022-10-05 15:35:04 +03:00
Alexey Khit
247b61790e Update EncodeAVC for empty NALs 2022-10-05 15:34:34 +03:00
Alexey Khit
2ec618334a Adds NALs types logger 2022-10-05 15:33:51 +03:00
Alexey Khit
6f9976c806 Rework RTSP and RTMP processing 2022-10-05 13:25:29 +03:00
Alexey Khit
17b3a4cf3a Code refactoring 2022-10-05 13:23:31 +03:00
Alexey Khit
ba30f46c02 Fix FmtpLine for RTMP 2022-10-05 10:50:00 +03:00
Alexey Khit
4134f2a89c Fix timestamp for RTMP 2022-10-05 10:48:37 +03:00
Alexey Khit
a81160bea1 Fix support Escam Q6 camera 2022-10-03 21:12:27 +03:00
Alexey Khit
80392acb78 Fix audio copy #46 2022-09-24 08:24:52 +03:00
Alexey Khit
5afac513b4 Adds codecs section to readme 2022-09-22 00:22:44 +03:00
Alexey Khit
2243110e08 Fix H265 support for different sources 2022-09-21 23:43:52 +03:00
Alexey Khit
04a6e64650 Adds support WebRTC + H265 to Safari 2022-09-21 22:28:59 +03:00
Alexey Khit
62c13f016b Remove broken Safari codec from WebRTC API 2022-09-21 22:27:46 +03:00
Alexey Khit
9596c6139f Adds support H265 for MP4 2022-09-18 20:24:21 +03:00
Alexey Khit
34f5b99126 Update codecs.html 2022-09-18 17:37:44 +03:00
Alexey Khit
b562392d45 Remove unnecessary imports 2022-09-18 17:14:59 +03:00
Alexey Khit
eb8a4919a2 Adds fix on RemoveConsumer panic 2022-09-18 17:14:18 +03:00
Alexey Khit
237fbf23a1 FIx backward support for RTSPtoWebRTC API 2022-09-18 02:01:43 +03:00
Alexey Khit
12a73b00cb Fix readme 2022-09-17 20:32:17 +03:00
Alexey Khit
ce0fac959f Adds module MJPEG 2022-09-17 19:13:25 +03:00
Alexey Khit
1b14be7033 Update readme about Hass module 2022-09-17 06:41:56 +03:00
Alexey Khit
bbbade4097 Adds rtsp link to index.html 2022-09-16 17:59:54 +03:00
Alexey Khit
8f43ad2a35 Adds pretty print to info 2022-09-16 17:39:29 +03:00
Alexey Khit
105331d50f Fix track async access 2022-09-16 17:22:48 +03:00
Alexey Khit
a45d0b507b Code refactoring 2022-09-16 17:04:00 +03:00
Alexey Khit
407ccc45bc Adds URL templates to integration with Hass 2022-09-16 17:03:03 +03:00
Alexey Khit
428628fcce Code refactoring 2022-09-16 17:00:56 +03:00
Alexey Khit
fa23bb6899 Handle FFmpeg RTMP as HTTP source 2022-09-16 17:00:24 +03:00
Alexey Khit
71e1c840a7 Fix base_path for integration with Hass 2022-09-16 14:19:23 +03:00
Alexey Khit
63b9639e86 Adds trace logs for API 2022-09-16 12:11:40 +03:00
Alexey Khit
ae3e1372c8 Adds support RTSPtoWeb API (entity_id for zero config from Hass) 2022-09-16 12:10:58 +03:00
Alexey Khit
800ebb39be Adds canditates from domain resolver 2022-09-15 09:07:53 +03:00
Alexey Khit
3a10cb25bb Fix green video from RTSP H264 2022-09-15 06:55:05 +03:00
Alexey Khit
7784b0e64c Adds about ivideon to readme 2022-09-14 17:59:12 +03:00
Alexey Khit
945b486fe0 Update readme about new source Echo 2022-09-14 15:53:46 +03:00
Alexey Khit
d72d7b089c Reduce docker container size and add python inside 2022-09-14 14:12:43 +03:00
Alexey Khit
d339fbe712 Fix backchannel option for Dahua VTO2111D 2022-09-13 21:57:07 +03:00
Alexey Khit
3aeb278c47 Option to disable backchannel for RTSP 2022-09-13 21:54:49 +03:00
Alexey Khit
c92c1fc3e9 Adds echo source 2022-09-13 15:42:39 +03:00
Alexey Khit
def57119f4 Move shell QuoteSplit to separate pkg 2022-09-13 15:42:23 +03:00
Alexey Khit
b20275d2b5 Adds support ivideon source 2022-09-13 14:41:28 +03:00
Alexey Khit
a11ca1da6e Adds error output to producer start function 2022-09-13 14:40:58 +03:00
Alexey Khit
0fb7132947 Move SplitAVC to public function 2022-09-13 14:39:55 +03:00
Alexey Khit
0f9e3c97c5 Update mp4 entry duration 2022-09-13 14:39:19 +03:00
Alexey Khit
e049a17216 Adds error handler for mp4 init 2022-09-13 14:38:54 +03:00
Alexey Khit
217c8c2bf6 Update readme about MP4 module 2022-09-10 06:26:15 +03:00
Alexey Khit
9f0153e2a8 Adds skip SEI frame 2022-09-09 19:32:56 +03:00
Alexey Khit
b2eaf03914 Adds match any clockrate or channels 2022-09-09 19:32:36 +03:00
Alexey Khit
8b54444c89 Adds mp4 module 2022-09-09 19:31:52 +03:00
Alexey Khit
76b352d67f Add exec launch time to debug 2022-09-07 12:21:36 +03:00
Alexey Khit
e8edb65a31 Adds ignoring unnecessary ffmpeg rtsp input tracks 2022-09-07 11:31:15 +03:00
Alexey Khit
88a6208912 Update ffmpeg output param name 2022-09-07 11:29:59 +03:00
Alexey Khit
14b6df68ce Adds support nginx with wrong port 2022-09-06 18:09:44 +03:00
Alexey Khit
77080663ee Add the feature for update to any version 2022-09-06 14:10:08 +03:00
Alexey Khit
d25d27a0ee Fix SDP parsing for noname camera 2022-09-06 12:43:10 +03:00
Alexey Khit
5460e194e8 Update loggers 2022-09-06 07:08:35 +03:00
Alexey Khit
e4f565f343 Fix H264 in RTSP processing 2022-09-06 07:00:22 +03:00
Alexey Khit
6b274f2a37 Adds Hass stream info to log 2022-09-06 06:36:13 +03:00
Alexey Khit
f442aab176 Fix RTSP from RTMP stream 2022-09-05 20:04:30 +03:00
Alexey Khit
0e71bd4dcb Adds low delay for any ffmpeg source 2022-09-04 23:02:54 +03:00
Alexey Khit
e3618d70c3 Adds support MPA codec 2022-09-04 22:34:39 +03:00
Alexey Khit
99c4a3e34a Update RTSP Setup link logic 2022-09-04 21:43:32 +03:00
Alexey Khit
b78de349ab Hide streams from Hass by default 2022-09-02 20:52:39 +03:00
Alexey Khit
b4990b1e90 Fix support RTSPtoWebRTC API 2022-09-02 20:52:22 +03:00
Alexey Khit
687bdadba6 Update docs about HomeKit 2022-09-01 16:11:47 +03:00
Alexey Khit
c0c96cfcdb Adds support HomeKit cameras! 2022-09-01 16:11:34 +03:00
Alexey Khit
4f92608f33 Fix webrtc close when it starts from ws 2022-08-31 23:09:01 +03:00
Alexey Khit
ac49bbef4d Update readme 2022-08-28 06:44:18 +03:00
Alexey Khit
f2aedbaf04 Adds build script for linux amd 2022-08-28 06:29:37 +03:00
Alexey Khit
1654ac8c82 Move ffmpeg device to separate module 2022-08-28 06:29:16 +03:00
Alexey Khit
38a18cab62 Clear webrtc resources on failed connection 2022-08-27 15:57:47 +03:00
Alexey Khit
a006394e5f Fix empty webrtc remote 2022-08-27 15:57:16 +03:00
Alexey Khit
1b3024b055 Adds license 2022-08-27 06:35:30 +03:00
Alexey Khit
9101cd4458 Adds warn about reading config 2022-08-26 20:14:26 +03:00
Alexey Khit
62c0fcd1ed Adds about video rotation to readme 2022-08-26 17:14:54 +03:00
Alexey Khit
ff810d3394 Return support skip RTSPS verification 2022-08-26 17:14:29 +03:00
Alexey Khit
14dae12ce2 Adds webcam support to readme 2022-08-26 11:54:26 +03:00
Alexey Khit
64247fc90f Adds support local USB cameras 2022-08-26 10:43:35 +03:00
Alexey Khit
c019dc58b1 Update panic message for RTMP 2022-08-26 10:10:45 +03:00
Alexey Khit
d3adaf05b1 Update build scripts 2022-08-26 10:09:52 +03:00
Alexey Khit
e6cfd1818b Adds on the fly add producer feature 2022-08-26 10:07:35 +03:00
Alexey Khit
fae4398d21 Adds FAQ to readme 2022-08-26 07:22:12 +03:00
Alexey Khit
5daf043937 Update readme 2022-08-25 14:21:34 +03:00
Alexey Khit
18d7b9075b Autoadd cameras from Hass config 2022-08-25 06:41:39 +03:00
Alexey Khit
7c4497f856 Fix incoming RTSP without OPTIONS 2022-08-25 06:38:08 +03:00
Alexey Khit
befa4ca1e6 Remove wrong RTSP channel panic 2022-08-25 06:37:47 +03:00
Alexey Khit
dd3b326f7a Update readme 2022-08-24 14:41:00 +03:00
Alexey Khit
e36123bb19 Update build docker 2022-08-24 14:30:02 +03:00
Alexey Khit
9310343ad3 Update docker cwd to /config 2022-08-24 12:49:59 +03:00
Alexey Khit
e2d4fa3393 Advanced debug on app start 2022-08-24 12:49:48 +03:00
Alexey Khit
5fea2932c1 Error on wrong config 2022-08-24 12:49:40 +03:00
Alexey Khit
1fd110b70d Update readme 2022-08-24 09:55:34 +03:00
Alexey Khit
8377cf2655 Change url param to src in Web API 2022-08-24 09:55:16 +03:00
Alexey Khit
8f01b08d42 Code refactoring 2022-08-24 09:54:28 +03:00
Alexey Khit
97ce4c3114 Adds Security section to readme 2022-08-23 09:34:06 +03:00
Alexey Khit
4813a64d9d Adds build script for mips 2022-08-23 05:43:15 +03:00
Alexey Khit
7923ec74a8 Adds network filter for webrtc 2022-08-23 05:43:01 +03:00
Alexey Khit
1f0a5fb880 Stop WebRTC conn on AddConsumer error 2022-08-22 22:46:08 +03:00
Alexey Khit
c6a3ee65b8 Remove UPX from Windows builds because antiviruses 2022-08-22 22:32:23 +03:00
Alexey Khit
12b712426d Fix busy RTSP backchannel 2022-08-22 15:41:25 +03:00
Alexey Khit
a9af245ef8 Fix async requests to Producer 2022-08-22 15:40:28 +03:00
Alexey Khit
f251129a2f Fix RTSP Transport header parsing 2022-08-22 14:46:39 +03:00
Alexey Khit
d28debabe9 Update fix for parsing RTSP SDP 2022-08-22 14:44:33 +03:00
Alexey Khit
07bf00f9f6 Update readme 2022-08-22 13:40:58 +03:00
Alexey Khit
be6ec7dbb9 Fix RTSP requests for some cameras 2022-08-22 13:38:26 +03:00
Alexey Khit
4e575d1356 Adds build file for win64 2022-08-22 11:43:42 +03:00
Alexey Khit
4cbacfec0c Adds empty response on RTSP error 2022-08-22 11:43:26 +03:00
Alexey Khit
31e24c6e03 Adds stop with empty producer warning 2022-08-22 11:33:38 +03:00
Alexey Khit
401bf85a10 Update RTSP error output 2022-08-22 09:09:18 +03:00
Alexey Khit
f36851f83a Fix response with empty producer 2022-08-22 09:06:40 +03:00
Alexey Khit
67522dbb19 Update readme 2022-08-22 08:44:27 +03:00
Alexey Khit
26b5745f0a Adds keep-alive to RTSP connection 2022-08-22 06:54:58 +03:00
Alexey Khit
46f6a5d8e1 Return unmodified errors from RTSP 2022-08-22 06:54:42 +03:00
Alexey Khit
48f58d0669 Fix wrong stream name request 2022-08-22 06:54:08 +03:00
Alexey Khit
fd0b8f3c39 Fix RTMP with audio 2022-08-22 05:46:22 +03:00
138 changed files with 11271 additions and 1839 deletions

View File

@@ -1,59 +0,0 @@
# https://github.com/home-assistant/builder
name: 'Builder'
on:
push:
tags: [ 'v*' ]
workflow_dispatch:
jobs:
hassio:
name: Hassio Addon
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v3
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Branch name
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
echo "REPO=alexxit/go2rtc" >> $GITHUB_ENV
echo "TAG=${VERSION}" >> $GITHUB_ENV
echo "IMAGE=alexxit/go2rtc:${VERSION}" >> $GITHUB_ENV
- name: Build amd64
uses: home-assistant/builder@master
with:
args: --amd64 --target build/hassio --version $TAG-amd64 --no-latest --docker-hub-check
- name: Build i386
uses: home-assistant/builder@master
with:
args: --i386 --target build/hassio --version $TAG-i386 --no-latest --docker-hub-check
- name: Build aarch64
uses: home-assistant/builder@master
with:
args: --aarch64 --target build/hassio --version $TAG-aarch64 --no-latest --docker-hub-check
- name: Build armv7
uses: home-assistant/builder@master
with:
args: --armv7 --target build/hassio --version $TAG-armv7 --no-latest --docker-hub-check
- name: Docker manifest
run: |
# thanks to https://github.com/aler9/rtsp-simple-server/blob/main/Makefile
docker manifest create "${IMAGE}" \
"${IMAGE}-amd64" "${IMAGE}-i386" "${IMAGE}-aarch64" "${IMAGE}-armv7"
docker manifest push "${IMAGE}"
docker manifest create "${REPO}:latest" \
"${IMAGE}-amd64" "${IMAGE}-i386" "${IMAGE}-aarch64" "${IMAGE}-armv7"
docker manifest push "${REPO}:latest"

75
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,75 @@
name: docker
on:
workflow_dispatch:
push:
branches:
- 'master'
tags:
- 'v*'
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
type=match,pattern=v(.*),group=1
- name: Docker meta Hardware
id: meta-hw
uses: docker/metadata-action@v4
with:
images: ${{ github.repository }}
flavor: |
suffix=-hardware
latest=false
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
type=match,pattern=v(.*),group=1
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
platforms: |
linux/amd64
linux/386
linux/arm/v7
linux/arm64/v8
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Build and push Hardware
uses: docker/build-push-action@v3
with:
context: .
file: hardware.Dockerfile
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta-hw.outputs.tags }}
labels: ${{ steps.meta-hw.outputs.labels }}

92
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,92 @@
name: release
on:
workflow_dispatch:
push:
tags:
- 'v*'
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Generate changelog
run: |
echo -e "$(git log $(git describe --tags --abbrev=0)..HEAD --oneline | awk '{print "- "$0}')" > CHANGELOG.md
- name: Build Go binaries
run: |
#!/bin/bash
esport CGO_ENABLED=0
mkdir artifacts
export GOOS=windows
export GOARCH=amd64
export FILENAME=artifacts/go2rtc_win64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
export GOOS=windows
export GOARCH=386
export FILENAME=artifacts/go2rtc_win32.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
export GOOS=windows
export GOARCH=arm64
export FILENAME=artifacts/go2rtc_win_arm64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
export GOOS=linux
export GOARCH=amd64
export FILENAME=artifacts/go2rtc_linux_amd64
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=386
export FILENAME=artifacts/go2rtc_linux_i386
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=arm64
export FILENAME=artifacts/go2rtc_linux_arm64
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=arm
export GOARM=7
export FILENAME=artifacts/go2rtc_linux_arm
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=mipsle
export FILENAME=artifacts/go2rtc_linux_mipsel
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=darwin
export GOARCH=amd64
export FILENAME=artifacts/go2rtc_mac_amd64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc
export GOOS=darwin
export GOARCH=arm64
export FILENAME=artifacts/go2rtc_mac_arm64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc
parallel --jobs $(nproc) "upx {}" ::: artifacts/go2rtc_linux_*
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
if: ${{ failure() }}
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Create GitHub release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: artifacts/*
generate_release_notes: true
name: Release ${{ env.RELEASE_VERSION }}
body_path: CHANGELOG.md
draft: false
prerelease: false

2
.gitignore vendored
View File

@@ -4,3 +4,5 @@
.tmp/
go2rtc.yaml
go2rtc.json

61
Dockerfile Normal file
View File

@@ -0,0 +1,61 @@
# syntax=docker/dockerfile:labs
# 0. Prepare images
ARG PYTHON_VERSION="3.11"
ARG GO_VERSION="1.19"
ARG NGROK_VERSION="3"
FROM python:${PYTHON_VERSION}-alpine AS base
FROM ngrok/ngrok:${NGROK_VERSION}-alpine AS ngrok
# 1. Build go2rtc binary
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build
ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH
ENV GOOS=${TARGETOS}
ENV GOARCH=${TARGETARCH}
WORKDIR /build
# Cache dependencies
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
# 2. Collect all files
FROM scratch AS rootfs
COPY --from=build /build/go2rtc /usr/local/bin/
COPY --from=ngrok /bin/ngrok /usr/local/bin/
# 3. Final image
FROM base
# Install ffmpeg, tini (for signal handling),
# and other common tools for the echo source.
RUN apk add --no-cache tini ffmpeg bash curl jq
# Hardware Acceleration for Intel CPU (+50MB)
ARG TARGETARCH
RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver intel-media-driver; fi
# Hardware: AMD and NVidia VAAPI (not sure about this)
# RUN libva-glx mesa-va-gallium
# Hardware: AMD and NVidia VDPAU (not sure about this)
# RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)
COPY --from=rootfs / /
ENTRYPOINT ["/sbin/tini", "--"]
VOLUME /config
WORKDIR /config
CMD ["go2rtc", "-config", "/config/go2rtc.yaml"]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Alexey Khit
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

540
README.md
View File

@@ -1,61 +1,42 @@
# go2rtc
**go2rtc** - ultimate camera streaming application with support RTSP, WebRTC, FFmpeg, RTMP, etc.
Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg, RTMP, etc.
- zero-dependency and zero-config small [app for all OS](#installation) (Windows, macOS, Linux, ARM)
- zero-delay for all supported protocols (lowest possible streaming latency)
- zero-load on CPU for supported codecs
![](assets/go2rtc.png)
- zero-dependency and zero-config [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, ARM)
- zero-delay for many supported protocols (lowest possible streaming latency)
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [HTTP](#source-http) (FLV/MJPEG/JPEG), [FFmpeg](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4) or [MJPEG](#module-mjpeg)
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
- first project in the World with 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)
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
- streaming from private networks via [Ngrok](#module-webrtc)
- mixing tracks from different sources to single stream
- auto match client supported codecs
- 2-way audio for `ONVIF Profile T` Cameras
- 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:**
- [webrtc](https://github.com/pion/webrtc) go library and whole [@pion](https://github.com/pion) team
- series of streaming projects from [@deepch](https://github.com/deepch)
- [webrtc](https://github.com/pion/webrtc) go library and whole [@pion](https://github.com/pion) team
- [rtsp-simple-server](https://github.com/aler9/rtsp-simple-server) idea from [@aler9](https://github.com/aler9)
- [GStreamer](https://gstreamer.freedesktop.org/) multimedia framework pipeline idea
- [GStreamer](https://gstreamer.freedesktop.org/) framework pipeline idea
- [MediaSoup](https://mediasoup.org/) framework routing idea
## Codecs negotiation
For example, you want to watch RTSP-stream from [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your Chrome browser.
- this camera support 2-way audio standard **ONVIF Profile T**
- this camera support codecs **H264, H265** for send video, and you select `H264` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for send audio (from mic), and you select `AAC/16000` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for receive audio (to speaker), you don't need to select them
- your browser support codecs **H264, VP8, VP9, AV1** for receive video, you don't need to select them
- your browser support codecs **OPUS, PCMU, PCMA** for send and receive audio, you don't need to select them
- you can't get camera audio directly, because its audio codecs doesn't match with your browser codecs
- so you decide to use transcoding via FFmpeg and add this setting to config YAML file
- you have chosen `OPUS/48000/2` codec, because it is higher quality than the `PCMU/8000` or `PCMA/8000`
- now you have stream with two sources - **RTSP and FFmpeg**
**go2rtc** automatically match codecs for you browser and all your stream sources. This called **multi-source 2-way codecs negotiation**. And this is one of the main features of this app.
**PS.** You can select `PCMU` or `PCMA` codec in camera setting and don't use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.
```yaml
streams:
dahua:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
- ffmpeg:rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif#audio=opus
```
![](codecs.svg)
- HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap)
## Fast start
1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on)
2. Open web interface [http://localhost:1984/](http://localhost:1984/)
2. Open web interface: `http://localhost:1984/`
**Optionally:**
- add your [streams](#module-streams) to [config](#configuration) file
- setup [external access](#module-webrtc) to webrtc
- setup [external access](#module-ngrok) to web interface
- install [ffmpeg](#source-ffmpeg) for transcoding
**Developers:**
@@ -66,17 +47,17 @@ streams:
Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/):
- `go2rtc_win64.exe` - Windows 64-bit
- `go2rtc_win32.exe` - Windows 32-bit
- `go2rtc_win64.zip` - Windows 64-bit
- `go2rtc_win32.zip` - Windows 32-bit
- `go2rtc_linux_amd64` - Linux 64-bit
- `go2rtc_linux_i386` - Linux 32-bit
- `go2rtc_linux_arm64` - Linux ARM 64-bit (ex. Raspberry 64-bit OS)
- `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS)
- `go2rtc_linux_mipsel` - Linux on MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3))
- `go2rtc_mac_amd64` - Mac with Intel
- `go2rtc_mac_arm64` - Mac with M1
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3))
- `go2rtc_mac_amd64.zip` - Mac Intel 64-bit
- `go2rtc_mac_arm64.zip` - Mac ARM 64-bit
Don't forget to fix the rights `chmod +x go2rtc_linux_xxx` on Linux and Mac.
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
### go2rtc: Home Assistant Add-on
@@ -87,23 +68,19 @@ Don't forget to fix the rights `chmod +x go2rtc_linux_xxx` on Linux and Mac.
- go2rtc > Install > Start
2. Setup [Integration](#module-hass)
**Optionally:**
- create `go2rtc.yaml` in your Home Assistant [config](https://www.home-assistant.io/docs/configuration) folder
### go2rtc: Docker
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from the Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg) and [Ngrok](#module-ngrok) applications.
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).
## Configuration
Create file `go2rtc.yaml` next to the app.
Create file `go2rtc.yaml`. go2rtc will search this file in current work dirrectory by default.
- by default, you need to config only your `streams` links
- `api` server will start on default **1984 port**
- `rtsp` server will start on default **8554 port**
- `webrtc` will use random UDP port for each connection
- `ffmpeg` will use default transcoding options (you need to install it [manually](https://ffmpeg.org/))
- `api` server will start on default **1984 port** (TCP)
- `rtsp` server will start on default **8554 port** (TCP)
- `webrtc` will use port **8555** (TCP/UDP) for connections
- `ffmpeg` will use default transcoding options
Available modules:
@@ -111,21 +88,30 @@ Available modules:
- [api](#module-api) - HTTP API (important for WebRTC support)
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
- [webrtc](#module-webrtc) - WebRTC Server
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot Server
- [mjpeg](#module-mjpeg) - MJPEG Server
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
- [hass](#module-hass) - Home Assistant integration
- [log](#module-log) - logs config
Full default config [example](https://github.com/AlexxIT/go2rtc/wiki/Configuration).
### Module: Streams
**go2rtc** support different stream source types. You can config only one link as stream source or multiple.
**go2rtc** support different stream source types. You can config one or multiple links of any type as stream source.
Available source types:
- [rtsp](#source-rtsp) - most cameras on market
- [rtmp](#source-rtmp)
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
- [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras
- [rtmp](#source-rtmp) - `RTMP` streams
- [http](#source-http) - `HTTP-FLV`, `JPEG` (snapshots), `MJPEG` streams
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`HLS`, `files` and many others)
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
- [echo](#source-echo) - get stream link from bash or python
- [homekit](#source-homekit) - streaming from HomeKit Camera
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
- [hass](#source-hass) - Home Assistant integration
#### Source: RTSP
@@ -133,7 +119,7 @@ Available source types:
- Support **RTSP and RTSPS** links with multiple video and audio tracks
- Support **2-way audio** ONLY for [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) cameras (back channel connection)
**Attention:** proprietary 2-way audio standards are not supported!
**Attention:** other 2-way audio standards are not supported! ONVIF without Profile T is not supported!
```yaml
streams:
@@ -151,64 +137,103 @@ streams:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1
```
**PS.** For disable bachannel just add `#backchannel=0` to end of RTSP link.
#### Source: RTMP
You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp). Support ONLY `H264` video codec without audio.
You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp).
```yaml
streams:
rtmp_stream: rtmp://192.168.1.123/live/camera1
```
#### Source: HTTP
Support Content-Type:
- **HTTP-FLV** (`video/x-flv`) - same as RTMP, but over HTTP
- **HTTP-JPEG** (`image/jpeg`) - camera snapshot link, can be converted by go2rtc to MJPEG stream
- **HTTP-MJPEG** (`multipart/x`) - simple MJPEG stream over HTTP
```yaml
streams:
# [HTTP-FLV] stream in video/x-flv format
http_flv: http://192.168.1.123:20880/api/camera/stream/780900131155/657617
# [JPEG] snapshots from Dahua camera, will be converted to MJPEG stream
dahua_snap: http://admin:password@192.168.1.123/cgi-bin/snapshot.cgi?channel=1
# [MJPEG] stream will be proxied without modification
http_mjpeg: https://mjpeg.sanford.io/count.mjpeg
```
**PS.** Dahua camera has bug: if you select MJPEG codec for RTSP second stream - snapshot won't work.
#### Source: FFmpeg
You can get any stream or file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream.
- FFmpeg preistalled for **Docker** and **Hass Add-on** users
- **Hass Add-on** users can target files from [/media](https://www.home-assistant.io/more-info/local-media/setup-media/) folder
Format: `ffmpeg:{input}#{param1}#{param2}#{param3}`. Examples:
```yaml
streams:
# [FILE] all tracks will be copied without transcoding codecs
file1: ffmpeg:~/media/BigBuckBunny.mp4
file1: ffmpeg:/media/BigBuckBunny.mp4
# [FILE] video will be transcoded to H264, audio will be skipped
file2: ffmpeg:~/media/BigBuckBunny.mp4#video=h264
file2: ffmpeg:/media/BigBuckBunny.mp4#video=h264
# [FILE] video will be copied, audio will be transcoded to pcmu
file3: ffmpeg:~/media/BigBuckBunny.mp4#video=copy#audio=pcmu
file3: ffmpeg:/media/BigBuckBunny.mp4#video=copy#audio=pcmu
# [HLS] video will be copied, audio will be skipped
hls: ffmpeg:https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear5/prog_index.m3u8#video=copy
# [MJPEG] video will be transcoded to H264
mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg?stream=half&fps=15#video=h264
mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg#video=h264
# [RTSP] video and audio will be copied
rtsp: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=copy#audio=copy
# [RTSP] video with rotation, should be transcoded, so select H264
rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
```
All trascoding formats has built-in templates. But you can override them via YAML config. You can also add your own formats to config and use them with source params.
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/cmd/ffmpeg/ffmpeg.go): `h264`, `h264/ultra`, `h264/high`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
But you can override them via YAML config. You can also add your own formats to config and use them with source params.
```yaml
ffmpeg:
bin: ffmpeg # path to ffmpeg binary
link: -hide_banner -i {input} # if input is link
file: -hide_banner -re -stream_loop -1 -i {input} # if input not link
rtsp: -hide_banner -fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input} # if input is RTSP link
output: -rtsp_transport tcp -f rtsp {output} # output
bin: ffmpeg # path to ffmpeg binary
h264: "-codec:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1"
mycodec: "-any args that support ffmpeg..."
```
h264: "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1"
h264/ultra: "-codec:v libx264 -g 30 -preset ultrafast -tune zerolatency"
h264/high: "-codec:v libx264 -g 30 -preset superfast -tune zerolatency"
h265: "-codec:v libx265 -g 30 -preset ultrafast -tune zerolatency"
opus: "-codec:a libopus -ar 48000 -ac 2"
pcmu: "-codec:a pcm_mulaw -ar 8000 -ac 1"
pcmu/16000: "-codec:a pcm_mulaw -ar 16000 -ac 1"
pcmu/48000: "-codec:a pcm_mulaw -ar 48000 -ac 1"
pcma: "-codec:a pcm_alaw -ar 8000 -ac 1"
pcma/16000: "-codec:a pcm_alaw -ar 16000 -ac 1"
pcma/48000: "-codec:a pcm_alaw -ar 48000 -ac 1"
aac/16000: "-codec:a aac -ar 16000 -ac 1"
- You can use `video` and `audio` params multiple times (ex. `#video=copy#audio=copy#audio=pcmu`)
- You can use go2rtc stream name as ffmpeg input (ex. `ffmpeg:camera1#video=h264`)
- You can use `rotate` params with `90`, `180`, `270` or `-90` values, important with transcoding (ex. `#video=h264#rotate=90`)
- You can use `width` and/or `height` params, important with transcoding (ex. `#video=h264#width=1280`)
- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`).
Read more about encoding [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration).
#### Source: FFmpeg Device
You can get video from any USB-camera or Webcam as RTSP or WebRTC stream. This is part of FFmpeg integration.
- check available devices in Web interface
- `resolution` and `framerate` must be supported by your camera!
- for Linux supported only video for now
- for macOS you can stream Facetime camera or whole Desktop!
- for macOS important to set right framerate
```yaml
streams:
linux_usbcam: ffmpeg:device?video=0&resolution=1280x720#video=h264
windows_webcam: ffmpeg:device?video=0#video=h264
macos_facetime: ffmpeg:device?video=0&audio=1&resolution=1280x720&framerate=30#video=h264#audio=pcma
```
#### Source: Exec
@@ -217,28 +242,115 @@ FFmpeg source just a shortcut to exec source. You can get any stream or file or
```yaml
streams:
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i ~/media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
```
#### Source: Echo
Some sources may have a dynamic link. And you will need to get it using a bash or python script. Your script should echo a link to the source. RTSP, FFmpeg or any of the [supported sources](#module-streams).
**Docker** and **Hass Add-on** users has preinstalled `python3`, `curl`, `jq`.
Check examples in [wiki](https://github.com/AlexxIT/go2rtc/wiki/Source-Echo-examples).
```yaml
streams:
apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html
```
#### Source: HomeKit
**Important:**
- You can use HomeKit Cameras **without Apple devices** (iPhone, iPad, etc.), it's just a yet another protocol
- HomeKit device can be paired with only one ecosystem. So, if you have paired it to an iPhone (Apple Home) - you can't pair it with Home Assistant or go2rtc. Or if you have paired it to go2rtc - you can't pair it with iPhone
- HomeKit device should be in same network with working [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS) between device and go2rtc
go2rtc support import paired HomeKit devices from [Home Assistant](#source-hass). So you can use HomeKit camera with Hass and go2rtc simultaneously. If you using Hass, I recommend pairing devices with it, it will give you more options.
You can pair device with go2rtc on the HomeKit page. If you can't see your devices - reload the page. Also try reboot your HomeKit device (power off). If you still can't see it - you have a problems with mDNS.
If you see a device but it does not have a pair button - it is paired to some ecosystem (Apple Home, Home Assistant, HomeBridge etc). You need to delete device from that ecosystem, and it will be available for pairing. If you cannot unpair device, you will have to reset it.
**Important:**
- HomeKit audio uses very non-standard **AAC-ELD** codec with very non-standard params and specification violation
- Audio can be transcoded by [ffmpeg](#source-ffmpeg) source with `#async` option
- Audio can be played by `ffplay` with `-use_wallclock_as_timestamps 1 -async 1` options
- Audio can't be played in `VLC` and probably any other player
Recommended settings for using HomeKit Camera with WebRTC, MSE, MP4, RTSP:
```
streams:
aqara_g3:
- hass:Camera-Hub-G3-AB12
- ffmpeg:aqara_g3#audio=aac#audio=opus#async
```
RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g3?video&audio=aac`
**This source is in active development!** Tested only with [Aqara Camera Hub G3](https://www.aqara.com/eu/product/camera-hub-g3) (both EU and CN versions).
#### Source: Ivideon
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
```yaml
streams:
quailcam: ivideon:100-tu5dkUPct39cTp9oNEN2B6/0
```
#### Source: Hass
Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files:
- support ONLY [Generic Camera](https://www.home-assistant.io/integrations/generic/), setup via GUI
- support [Generic Camera](https://www.home-assistant.io/integrations/generic/), setup via GUI
- support [HomeKit Camera](https://www.home-assistant.io/integrations/homekit_controller/)
```yaml
hass:
config: "~/.homeassistant"
config: "/config" # skip this setting if you Hass Add-on user
streams:
generic_camera: hass:Camera1 # Settings > Integrations > Integration Name
aqara_g3: hass:Camera-Hub-G3-AB12
```
More cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), [ONVIF](https://www.home-assistant.io/integrations/onvif/), and possibly others can also be imported by using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-home-assistant-cameras-to-go2rtc-andor-frigate).
### Module: API
The HTTP API is the main part for interacting with the application. Default address: `http://127.0.0.1:1984/`.
- you can use WebRTC only when HTTP API enabled
go2rtc has its own JS video player (`video-rtc.js`) with:
- support technologies:
- WebRTC over UDP or TCP
- MSE or MP4 or MJPEG over WebSocket
- automatic selection best technology according on:
- codecs inside your stream
- current browser capabilities
- current network configuration
- automatic stop stream while browser or page not active
- automatic stop stream while player not inside page viewport
- automatic reconnection
Technology selection based on priorities:
1. Video and Audio better than just Video
2. H265 better than H264
3. WebRTC better than MSE, than MP4, than MJPEG
go2rtc has simple HTML page (`stream.html`) with support params in URL:
- multiple streams on page `src=camera1&src=camera2...`
- stream technology autoselection `mode=webrtc,mse,mp4,mjpeg`
- stream technology comparison `src=camera1&mode=webrtc&mode=mse&mode=mp4`
- player width setting in pixels `width=320px` or percents `width=50%`
**Module config**
- you can disable HTTP API with `listen: ""` and use, for example, only RTSP client/server protocol
- 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
@@ -246,86 +358,89 @@ The HTTP API is the main part for interacting with the application. Default addr
```yaml
api:
listen: ":1984" # HTTP API port ("" - disabled)
base_path: "" # API prefix for serve on suburl
static_dir: "" # folder for static files (custom web interface)
listen: ":1984" # default ":1984", HTTP API port ("" - disabled)
base_path: "/rtc" # default "", API prefix for serve on suburl (/api => /rtc/api)
static_dir: "www" # default "", folder for static files (custom web interface)
origin: "*" # default "", allow CORS requests (only * supported)
```
**PS. go2rtc** don't provide HTTPS or password protection. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks.
**PS:**
**PS2.** You can access microphone (for 2-way audio) only with HTTPS
- go2rtc doesn't provide HTTPS or password protection. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks
- you can access microphone (for 2-way audio) only with HTTPS ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https))
- MJPEG over WebSocket plays better than native MJPEG because Chrome [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=527446)
- MP4 over WebSocket was created only for Apple iOS because it doesn't support MSE and native MP4
### Module: RTSP
You can get any stream as RTSP-stream with codecs filter:
You can get any stream as RTSP-stream: `rtsp://192.168.1.123:8554/{stream_name}`
```
rtsp://192.168.1.123/{stream_name}?video={codec}&audio={codec1}&audio={codec2}
```
- you can omit the codecs, so one first video and one first audio will be selected
- you can omit the codec filters, so one first video and one first audio will be selected
- you can set `?video=copy` or just `?video`, so only one first video without audio will be selected
- you can set multiple video or audio, so all of them will be selected
- you can enable external password protection for your RTSP streams
Password protection always disabled for localhost calls (ex. FFmpeg or Hass on same server)
```yaml
rtsp:
listen: ":8554"
listen: ":8554" # RTSP Server TCP port, default - 8554
username: admin # optional, default - disabled
password: pass # optional, default - disabled
```
### Module: WebRTC
WebRTC usually works without problems in the local network. But external access may require additional settings. It depends on what type of internet do you have.
WebRTC usually works without problems in the local network. But external access may require additional settings. It depends on what type of Internet do you have.
- by default, WebRTC use two random UDP ports for each connection (for video and audio)
- you can enable one additional TCP port for all connections and use it for external access
- by default, WebRTC uses both TCP and UDP on port 8555 for connections
- you can use this port for external access
- you can change the port in YAML config:
```yaml
webrtc:
listen: ":8555" # address of your local server and port (TCP/UDP)
```
**Static public IP**
- add some TCP port to YAML config (ex. 8555)
- forward this port on your router (you can use same 8555 port or any other)
- forward the port 8555 on your router (you can use same 8555 port or any other as external port)
- add your external IP-address and external port to YAML config
```yaml
webrtc:
listen: ":8555" # address of your local server (TCP)
candidates:
- 216.58.210.174:8555 # if you have static public IP-address
```
**Dynamic public IP**
- add some TCP port to YAML config (ex. 8555)
- forward this port on your router (you can use same 8555 port or any other)
- forward the port 8555 on your router (you can use same 8555 port or any other as the external port)
- add `stun` word and external port to YAML config
- go2rtc automatically detects your external address with STUN-server
```yaml
webrtc:
listen: ":8555" # address of your local server (TCP)
candidates:
- stun:8555 # if you have dynamic public IP-address
```
**Private IP**
- add some TCP port to YAML config (ex. 8555)
- setup integration with [Ngrok service](#module-ngrok)
```yaml
webrtc:
listen: ":8555" # address of your local server (TCP)
ngrok:
command: ...
```
**Own TCP-tunnel**
**Hard tech way 1. Own TCP-tunnel**
If you have personal VPS, you can create TCP-tunnel and setup in the same way as "Static public IP". But use your VPS IP-address in YAML config.
If you have personal [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), you can create TCP-tunnel and setup in the same way as "Static public IP". But use your VPS IP-address in YAML config.
**Using TURN-server**
**Hard tech way 2. Using TURN-server**
TODO...
If you have personal [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), you can install TURN server (e.g. [coturn](https://github.com/coturn/coturn), config [example](https://github.com/AlexxIT/WebRTC/wiki/Coturn-Example)).
```yaml
webrtc:
@@ -340,6 +455,7 @@ webrtc:
With Ngrok integration you can get external access to your streams in situation when you have Internet with private IP-address.
- Ngrok preistalled 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)
@@ -391,26 +507,72 @@ tunnels:
### Module: Hass
**go2rtc** compatible with Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration.
If you install **go2rtc** as [Hass Add-on](#go2rtc-home-assistant-add-on) - you need to use localhost IP-address. In other cases you need to use IP-address of server with **go2rtc** application.
If you install **go2rtc** as [Hass Add-on](#go2rtc-home-assistant-add-on) - you need to use localhost IP-address, example:
#### From go2rtc to Hass
- `http://127.0.0.1:1984/` to web interface
- `rtsp://127.0.0.1:8554/camera1` to RTSP streams
Add any supported [stream source](#module-streams) as [Generic Camera](https://www.home-assistant.io/integrations/generic/) and view stream with built-in [Stream](https://www.home-assistant.io/integrations/stream/) integration. Technology `HLS`, supported codecs: `H264`, poor latency.
In other cases you need to use IP-address of server with **go2rtc** application.
1. Add your stream to [go2rtc config](#configuration)
2. 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`
1. Add integration with link to go2rtc HTTP API:
- Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/`
2. Add generic camera with RTSP link:
- Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://...` or `rtmp://...`
#### From Hass to go2rtc
View almost any Hass camera using `WebRTC` technology, supported codecs `H264`/`PCMU`/`PCMA`/`OPUS`, best latency.
When the stream starts - the camera `entity_id` will be added to go2rtc "on the fly". You don't need to add cameras manually to [go2rtc config](#configuration). Some cameras (like [Nest](https://www.home-assistant.io/integrations/nest/)) have a dynamic link to the stream, it will be updated each time a stream is started from the Hass interface.
1. Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/`
2. RTSPtoWebRTC > Configure > STUN server: `stun.l.google.com:19302`
3. Use Picture Entity or Picture Glance lovelace card
4. Open full screen card - this is should be WebRTC stream
- you can use either direct RTSP links to cameras or take RTSP streams from **go2rtc**
You can add camera `entity_id` to [go2rtc config](#configuration) if you need transcoding:
```yaml
streams:
"camera.hall": ffmpeg:{input}#video=copy#audio=opus
```
PS. Default Home Assistant lovelace cards don't support 2-way audio. You can use 2-way audio from [Add-on Web UI](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons). But you need use HTTPS to access the microphone. This is a browser restriction and cannot be avoided.
### Module: MP4
Provides several features:
1. MSE stream (fMP4 over WebSocket)
2. Camera snapshots in MP4 format (single frame), can be sent to [Telegram](https://github.com/AlexxIT/go2rtc/wiki/Snapshot-to-Telegram)
3. MP4 "file stream" - bad format for streaming because of high start delay, doesn't work in Safari
API examples:
- MP4 stream: `http://192.168.1.123:1984/api/stream.mp4?src=camera1`
- MP4 snapshot: `http://192.168.1.123:1984/api/frame.mp4?src=camera1`
### Module: MJPEG
**Important.** For stream as MJPEG format, your source MUST contain the MJPEG codec. If your stream has a MJPEG codec - you can receive **MJPEG stream** or **JPEG snapshots** via API.
You can receive an MJPEG stream in several ways:
- some cameras support MJPEG codec inside [RTSP stream](#source-rtsp) (ex. second stream for Dahua cameras)
- some cameras has HTTP link with [MJPEG stream](#source-http)
- some cameras has HTTP link with snapshots - go2rtc can convert them to [MJPEG stream](#source-http)
- you can convert H264/H265 stream from your camera via [FFmpeg integraion](#source-ffmpeg)
With this example, your stream will have both H264 and MJPEG codecs:
```yaml
streams:
camera1:
- rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
- ffmpeg:camera1#video=mjpeg
```
API examples:
- MJPEG stream: `http://192.168.1.123:1984/api/stream.mjpeg?src=camera1`
- JPEG snapshots: `http://192.168.1.123:1984/api/frame.jpeg?src=camera1`
### Module: Log
You can set different log levels for different modules.
@@ -425,3 +587,125 @@ log:
streams: error
webrtc: fatal
```
## Security
By default `go2rtc` starts the Web interface on port `1984` and RTSP on port `8554`, as well as use port `8555` for WebRTC connections. The three ports are accessible from your local network. So anyone on your local network can watch video from your cameras without authorization. The same rule applies to the Home Assistant Add-on.
This is not a problem if you trust your local network as much as I do. But you can change this behaviour with a `go2rtc.yaml` config:
```yaml
api:
listen: "127.0.0.1:1984" # localhost
rtsp:
listen: "127.0.0.1:8554" # localhost
webrtc:
listen: ":8555" # external TCP/UDP port
```
- local access to RTSP is not a problem for [FFmpeg](#source-ffmpeg) integration, because it runs locally on your server
- local access to API is not a problem for [Home Assistant Add-on](#go2rtc-home-assistant-add-on), because Hass runs locally on same server and Add-on Web UI protected with Hass authorization ([Ingress feature](https://www.home-assistant.io/blog/2019/04/15/hassio-ingress/))
- 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.
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.
## Codecs madness
`AVC/H.264` codec 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 | MP4 |
|---------------------|-------------|-------------|-------------|
| *latency* | best | medium | bad |
| Desktop Chrome 107+ | H264 | H264, H265* | H264, H265* |
| Desktop Edge | H264 | H264, H265* | H264, H265* |
| Desktop Safari | H264, H265* | H264, H265 | **no!** |
| Desktop Firefox | H264 | H264 | H264 |
| Android Chrome 107+ | H264 | H264, H265* | H264 |
| iPad Safari 13+ | H264, H265* | H264, H265 | **no!** |
| iPhone Safari 13+ | H264, H265* | **no!** | **no!** |
| masOS Hass App | no | no | no |
- Chrome H265: [read this](https://chromestatus.com/feature/5186511939567616) and [read this](https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding)
- Edge H265: [read this](https://www.reddit.com/r/MicrosoftEdge/comments/v9iw8k/enable_hevc_support_in_edge/)
- Desktop Safari H265: Menu > Develop > Experimental > WebRTC H265
- iOS Safari H265: Settings > Safari > Advanced > Experimental > WebRTC H265
**Audio**
- WebRTC audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2`
- MSE/MP4 audio codecs: `AAC`
**Apple devices**
- all Apple devices don't support MP4 stream (they only support progressive loading of static files)
- iPhones don't support MSE technology because it competes with the HLS technology, invented by Apple
- HLS is the worst technology for **live** streaming, it still exists only because of iPhones
## Codecs negotiation
For example, you want to watch RTSP-stream from [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your Chrome browser.
- this camera support 2-way audio standard **ONVIF Profile T**
- this camera support codecs **H264, H265** for send video, and you select `H264` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for send audio (from mic), and you select `AAC/16000` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for receive audio (to speaker), you don't need to select them
- your browser support codecs **H264, VP8, VP9, AV1** for receive video, you don't need to select them
- your browser support codecs **OPUS, PCMU, PCMA** for send and receive audio, you don't need to select them
- you can't get camera audio directly, because its audio codecs doesn't match with your browser codecs
- so you decide to use transcoding via FFmpeg and add this setting to config YAML file
- you have chosen `OPUS/48000/2` codec, because it is higher quality than the `PCMU/8000` or `PCMA/8000`
Now you have stream with two sources - **RTSP and FFmpeg**:
```yaml
streams:
dahua:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
- ffmpeg:rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0#audio=opus
```
**go2rtc** automatically match codecs for you browser and all your stream sources. This called **multi-source 2-way codecs negotiation**. And this is one of the main features of this app.
![](assets/codecs.svg)
**PS.** You can select `PCMU` or `PCMA` codec in camera setting and don't use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.
## TIPS
**Using apps for low RTSP delay**
- `ffplay -fflags nobuffer -flags low_delay "rtsp://192.168.1.123:8554/camera1"`
- VLC > Preferences > Input / Codecs > Default Caching Level: Lowest Latency
**Snapshots to Telegram**
[read more](https://github.com/AlexxIT/go2rtc/wiki/Snapshot-to-Telegram)
## FAQ
**Q. What's the difference between go2rtc, WebRTC Camera and RTSPtoWebRTC?**
**go2rtc** is a new version of the server-side [WebRTC Camera](https://github.com/AlexxIT/WebRTC) integration, completely rewritten from scratch, with a number of fixes and a huge number of new features. It is compatible with native Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration. So you [can use](#module-hass) default lovelace Picture Entity or Picture Glance.
**Q. Why go2rtc is an addon and not an integration?**
Because **go2rtc** is more than just viewing your stream online with WebRTC. You can use it all the time for your various tasks. But every time the Hass is rebooted - all integrations are also rebooted. So your streams may be interrupted if you use them in additional tasks.
When **go2rtc** is released, the **WebRTC Camera** integration will be updated. And you can decide whether to use the integration or the addon.
**Q. Which RTSP link should I use inside Hass?**
You can use direct link to your cameras there (as you always do). **go2rtc** support zero-config feature. You may leave `streams` config section empty. And your streams will be created on the fly on first start from Hass. And your cameras will have multiple connections. Some from Hass directly and one from **go2rtc**.
Also you can specify your streams in **go2rtc** [config file](#configuration) and use RTSP links to this addon. With additional features: multi-source [codecs negotiation](#codecs-negotiation) or FFmpeg [transcoding](#source-ffmpeg) for unsupported codecs. Or use them as source for Frigate. And your cameras will have one connection from **go2rtc**. And **go2rtc** will have multiple connection - some from Hass via RTSP protocol, some from your browser via WebRTC protocol.
Use any config what you like.
**Q. What about lovelace card with support 2-way audio?**
At this moment I am focused on improving stability and adding new features to **go2rtc**. Maybe someone could write such a card themselves. It's not difficult, I have [some sketches](https://github.com/AlexxIT/go2rtc/blob/master/www/webrtc.html).

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

BIN
assets/go2rtc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

19
build/docker/run.sh Normal file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
set -euo pipefail
echo "Starting go2rtc..." >&2
readonly config_path="/config"
if [[ -x "${config_path}/go2rtc" ]]; then
readonly binary_path="${config_path}/go2rtc"
echo "Using go2rtc binary from '${binary_path}' instead of the embedded one" >&2
else
readonly binary_path="/usr/local/bin/go2rtc"
fi
# set cwd for go2rtc (for config file, Hass integration, etc)
cd "${config_path}" || echo "Could not change working directory to '${config_path}'" >&2
exec "${binary_path}"

View File

@@ -1,22 +0,0 @@
ARG BUILD_FROM
FROM $BUILD_FROM
RUN apk add --no-cache git go ffmpeg
ARG BUILD_ARCH
WORKDIR app
RUN git clone https://github.com/AlexxIT/go2rtc .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
# https://github.com/home-assistant/docker-base/blob/master/alpine/Dockerfile
RUN if [ "${BUILD_ARCH}" = "aarch64" ]; then BUILD_ARCH="arm64"; \
elif [ "${BUILD_ARCH}" = "armv7" ]; then BUILD_ARCH="arm"; fi \
&& curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \
&& unzip ngrok
COPY run.sh /
RUN chmod a+x /run.sh
CMD [ "/run.sh" ]

View File

@@ -1,6 +0,0 @@
# https://github.com/home-assistant/builder/blob/master/builder.sh
name: go2rtc
description: Ultimate camera streaming application
url: https://github.com/AlexxIT/go2rtc
image: alexxit/go2rtc
arch: [ amd64, aarch64, i386, armv7 ]

View File

@@ -1,13 +0,0 @@
#!/usr/bin/with-contenv bashio
set +e
while true; do
if [ -x /config/go2rtc ]; then
/config/go2rtc -config /config/go2rtc.yaml
else
/app/go2rtc -config /config/go2rtc.yaml
fi
sleep 5
done

View File

@@ -3,22 +3,24 @@ package api
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/gorilla/websocket"
"github.com/rs/zerolog"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync"
)
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"`
} `yaml:"api"`
}
@@ -36,97 +38,109 @@ func Init() {
log = app.GetLogger("api")
initStatic(cfg.Mod.StaticDir)
initWS(cfg.Mod.Origin)
HandleFunc("/api/frame.mp4", frameHandler)
HandleFunc("/api/frame.raw", frameHandler)
HandleFunc("/api/stack", stackHandler)
HandleFunc("/api/streams", streamsHandler)
HandleFunc("/api/exit", exitHandler)
HandleFunc("/api/ws", apiWS)
HandleFunc("api", apiHandler)
HandleFunc("api/config", configHandler)
HandleFunc("api/exit", exitHandler)
HandleFunc("api/ws", apiWS)
// ensure we can listen without errors
listener, 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")
s := http.Server{}
s.Handler = http.DefaultServeMux // 4th
if cfg.Mod.Origin == "*" {
s.Handler = middlewareCORS(s.Handler) // 3rd
}
if cfg.Mod.Username != "" {
s.Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, s.Handler) // 2nd
}
if log.Trace().Enabled() {
s.Handler = middlewareLog(s.Handler) // 1st
}
go func() {
s := http.Server{}
if err = s.Serve(listener); err != nil {
log.Fatal().Err(err).Msg("[api] Serve")
log.Fatal().Err(err).Msg("[api] serve")
}
}()
}
// HandleFunc handle pattern with relative path:
// - "api/streams" => "{basepath}/api/streams"
// - "/streams" => "/streams"
func HandleFunc(pattern string, handler http.HandlerFunc) {
http.HandleFunc(basePath+pattern, handler)
if len(pattern) == 0 || pattern[0] != '/' {
pattern = basePath + "/" + pattern
}
log.Trace().Str("path", pattern).Msg("[api] register path")
http.HandleFunc(pattern, handler)
}
func HandleWS(msgType string, handler WSHandler) {
wsHandlers[msgType] = handler
}
const StreamNotFound = "stream not found"
var basePath string
var log zerolog.Logger
var wsHandlers = make(map[string]WSHandler)
func streamsHandler(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
func middlewareLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Trace().Msgf("[api] %s %s %s", r.Method, r.URL, r.RemoteAddr)
next.ServeHTTP(w, r)
})
}
switch r.Method {
case "PUT":
streams.Get(src)
return
case "DELETE":
streams.Delete(src)
return
}
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]") {
user, pass, ok := r.BasicAuth()
if !ok || user != username || pass != password {
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
}
var v interface{}
if src != "" {
v = streams.Get(src)
} else {
v = streams.All()
}
data, err := json.Marshal(v)
if err != nil {
log.Error().Err(err).Msg("[api.streams] marshal")
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.streams] write")
next.ServeHTTP(w, r)
})
}
func middlewareCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
next.ServeHTTP(w, r)
})
}
var mu sync.Mutex
func apiHandler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
app.Info["host"] = r.Host
mu.Unlock()
if err := json.NewEncoder(w).Encode(app.Info); err != nil {
log.Warn().Err(err).Caller().Send()
}
}
func exitHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "", http.StatusBadRequest)
return
}
s := r.URL.Query().Get("code")
code, _ := strconv.Atoi(s)
os.Exit(code)
}
func apiWS(w http.ResponseWriter, r *http.Request) {
ctx := new(Context)
if err := ctx.Upgrade(w, r); err != nil {
log.Error().Err(err).Msg("[api.ws] upgrade")
return
}
defer ctx.Close()
for {
msg := new(streamer.Message)
if err := ctx.Conn.ReadJSON(msg); err != nil {
if websocket.IsUnexpectedCloseError(
err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure,
) {
log.Error().Err(err).Msg("[api.ws] readJSON")
}
return
}
handler := wsHandlers[msg.Type]
if handler != nil {
handler(ctx, msg)
}
}
}

102
cmd/api/config.go Normal file
View File

@@ -0,0 +1,102 @@
package api
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"gopkg.in/yaml.v3"
"io"
"net/http"
"os"
)
func configHandler(w http.ResponseWriter, r *http.Request) {
if app.ConfigPath == "" {
http.Error(w, "", http.StatusGone)
return
}
switch r.Method {
case "GET":
data, err := os.ReadFile(app.ConfigPath)
if err != nil {
http.Error(w, "", http.StatusNotFound)
return
}
if _, err = w.Write(data); err != nil {
log.Warn().Err(err).Caller().Send()
}
case "POST", "PATCH":
data, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if r.Method == "PATCH" {
// no need to validate after merge
data, err = mergeYAML(app.ConfigPath, data)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
} else {
// validate config
var tmp struct{}
if err = yaml.Unmarshal(data, &tmp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
if err = os.WriteFile(app.ConfigPath, data, 0644); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func mergeYAML(file1 string, yaml2 []byte) ([]byte, error) {
// Read the contents of the first YAML file
data1, err := os.ReadFile(file1)
if err != nil {
return nil, err
}
// Unmarshal the first YAML file into a map
var config1 map[string]interface{}
if err = yaml.Unmarshal(data1, &config1); err != nil {
return nil, err
}
// Unmarshal the second YAML document into a map
var config2 map[string]interface{}
if err = yaml.Unmarshal(yaml2, &config2); err != nil {
return nil, err
}
// Merge the two maps
config1 = merge(config1, config2)
// Marshal the merged map into YAML
return yaml.Marshal(&config1)
}
func merge(dst, src map[string]interface{}) map[string]interface{} {
for k, v := range src {
if vv, ok := dst[k]; ok {
switch vv := vv.(type) {
case map[string]interface{}:
v := v.(map[string]interface{})
dst[k] = merge(vv, v)
case []interface{}:
v := v.([]interface{})
dst[k] = v
default:
dst[k] = v
}
} else {
dst[k] = v
}
}
return dst
}

View File

@@ -1,40 +0,0 @@
package api
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/keyframe"
"net/http"
"strings"
)
func frameHandler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("url")
stream := streams.Get(url)
if stream == nil {
return
}
var ch = make(chan []byte)
cons := new(keyframe.Consumer)
cons.IsMP4 = strings.HasSuffix(r.URL.Path, ".mp4")
cons.Listen(func(msg interface{}) {
switch msg.(type) {
case []byte:
ch <- msg.([]byte)
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Warn().Err(err).Msg("[api.frame] add consumer")
return
}
data := <-ch
stream.RemoveConsumer(cons)
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.frame] write")
}
}

View File

@@ -14,13 +14,13 @@ func initStatic(staticDir string) {
root = http.FS(www.Static)
}
base := len(basePath)
fileServer := http.FileServer(root)
HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if basePath != "" {
r.URL.Path = r.URL.Path[len(basePath):]
HandleFunc("", func(w http.ResponseWriter, r *http.Request) {
if base > 0 {
r.URL.Path = r.URL.Path[base:]
}
fileServer.ServeHTTP(w, r)
})
}

View File

@@ -1,67 +1,154 @@
package api
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/gorilla/websocket"
"net/http"
"net/url"
"strings"
"sync"
"time"
)
type WSHandler func(ctx *Context, msg *streamer.Message)
var apiWsUp = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 512000,
// Message - struct for data exchange in Web API
type Message struct {
Type string `json:"type"`
Value interface{} `json:"value,omitempty"`
}
type Context struct {
Conn *websocket.Conn
Request *http.Request
Consumer interface{} // TODO: rewrite
type WSHandler func(tr *Transport, msg *Message) error
onClose []func()
mu sync.Mutex
func HandleWS(msgType string, handler WSHandler) {
wsHandlers[msgType] = handler
}
func (ctx *Context) Upgrade(w http.ResponseWriter, r *http.Request) (err error) {
ctx.Conn, err = apiWsUp.Upgrade(w, r, nil)
ctx.Request = r
return
}
var wsHandlers = make(map[string]WSHandler)
func (ctx *Context) Close() {
for _, f := range ctx.onClose {
f()
func initWS(origin string) {
wsUp = &websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 2028,
}
switch origin {
case "":
// same origin + ignore port
wsUp.CheckOrigin = func(r *http.Request) bool {
origin := r.Header["Origin"]
if len(origin) == 0 {
return true
}
o, err := url.Parse(origin[0])
if err != nil {
return false
}
if o.Host == r.Host {
return true
}
log.Trace().Msgf("[api.ws] origin=%s, host=%s", o.Host, r.Host)
// https://github.com/AlexxIT/go2rtc/issues/118
if i := strings.IndexByte(o.Host, ':'); i > 0 {
return o.Host[:i] == r.Host
}
return false
}
case "*":
// any origin
wsUp.CheckOrigin = func(r *http.Request) bool {
return true
}
}
_ = ctx.Conn.Close()
}
func (ctx *Context) Write(msg interface{}) {
ctx.mu.Lock()
defer ctx.mu.Unlock()
var err error
switch msg := msg.(type) {
case *streamer.Message:
err = ctx.Conn.WriteJSON(msg)
case []byte:
err = ctx.Conn.WriteMessage(websocket.BinaryMessage, msg)
default:
func apiWS(w http.ResponseWriter, r *http.Request) {
ws, err := wsUp.Upgrade(w, r, nil)
if err != nil {
origin := r.Header.Get("Origin")
log.Error().Err(err).Caller().Msgf("host=%s origin=%s", r.Host, origin)
return
}
if err != nil {
//panic(err) // TODO: fix panic
}
}
tr := &Transport{Request: r}
tr.OnWrite(func(msg interface{}) {
_ = ws.SetWriteDeadline(time.Now().Add(time.Second * 5))
func (ctx *Context) Error(err error) {
ctx.Write(&streamer.Message{
Type: "error", Value: err.Error(),
if data, ok := msg.([]byte); ok {
_ = ws.WriteMessage(websocket.BinaryMessage, data)
} else {
_ = ws.WriteJSON(msg)
}
})
for {
msg := new(Message)
if err = ws.ReadJSON(msg); err != nil {
log.Trace().Err(err).Caller().Send()
_ = ws.Close()
break
}
if handler := wsHandlers[msg.Type]; handler != nil {
go func() {
if err = handler(tr, msg); err != nil {
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + err.Error()})
}
}()
}
}
tr.Close()
}
func (ctx *Context) OnClose(f func()) {
ctx.onClose = append(ctx.onClose, f)
var wsUp *websocket.Upgrader
type Transport struct {
Request *http.Request
Consumer interface{} // TODO: rewrite
closed bool
mx sync.Mutex
wrmx sync.Mutex
onChange func()
onWrite func(msg interface{})
onClose []func()
}
func (t *Transport) OnWrite(f func(msg interface{})) {
t.mx.Lock()
if t.onChange != nil {
t.onChange()
}
t.onWrite = f
t.mx.Unlock()
}
func (t *Transport) Write(msg interface{}) {
t.wrmx.Lock()
t.onWrite(msg)
t.wrmx.Unlock()
}
func (t *Transport) Close() {
t.mx.Lock()
for _, f := range t.onClose {
f()
}
t.closed = true
t.mx.Unlock()
}
func (t *Transport) OnChange(f func()) {
t.mx.Lock()
t.onChange = f
t.mx.Unlock()
}
func (t *Transport) OnClose(f func()) {
t.mx.Lock()
if t.closed {
f()
} else {
t.onClose = append(t.onClose, f)
}
t.mx.Unlock()
}

View File

@@ -2,23 +2,64 @@ package app
import (
"flag"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"time"
)
func Init() {
config := flag.String(
"config",
"go2rtc.yaml",
"Path to go2rtc configuration file",
)
var Version = "1.0.0"
var UserAgent = "go2rtc/" + Version
var ConfigPath string
var Info = map[string]interface{}{
"version": Version,
}
func Init() {
var confs Config
flag.Var(&confs, "config", "go2rtc config (path to file or raw text), support multiple")
flag.Parse()
data, _ = os.ReadFile(*config)
if confs == nil {
confs = []string{"go2rtc.yaml"}
}
for _, conf := range confs {
if conf[0] != '{' {
// config as file
if ConfigPath == "" {
ConfigPath = conf
}
data, _ := os.ReadFile(conf)
if data == nil {
continue
}
data = []byte(shell.ReplaceEnvVars(string(data)))
configs = append(configs, data)
} else {
// config as raw YAML
configs = append(configs, []byte(conf))
}
}
if ConfigPath != "" {
if !filepath.IsAbs(ConfigPath) {
if cwd, err := os.Getwd(); err == nil {
ConfigPath = filepath.Join(cwd, ConfigPath)
}
}
Info["config_path"] = ConfigPath
}
var cfg struct {
Mod map[string]string `yaml:"log"`
@@ -26,10 +67,16 @@ func Init() {
LoadConfig(&cfg)
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"])
modules = cfg.Mod
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
}
func NewLogger(format string, level string) zerolog.Logger {
var writer io.Writer = os.Stdout
// styles
format := cfg.Mod["format"]
if format != "json" {
writer = zerolog.ConsoleWriter{
Out: writer, TimeFormat: "15:04:05.000",
@@ -37,47 +84,50 @@ func Init() {
}
}
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
zerolog.TimeFieldFormat = time.RFC3339Nano
lvl, err := zerolog.ParseLevel(cfg.Mod["level"])
lvl, err := zerolog.ParseLevel(level)
if err != nil || lvl == zerolog.NoLevel {
lvl = zerolog.InfoLevel
}
log = zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
modules = cfg.Mod
log.Info().Msgf("go2rtc %s/%s", runtime.GOOS, runtime.GOARCH)
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
}
func LoadConfig(v interface{}) {
if data != nil {
_ = yaml.Unmarshal(data, v)
for _, data := range configs {
if err := yaml.Unmarshal(data, v); err != nil {
log.Warn().Err(err).Msg("[app] read config")
}
}
}
func GetLogger(module string) zerolog.Logger {
if s, ok := modules[module]; ok {
lvl, err := zerolog.ParseLevel(s)
if err != nil {
log.Warn().Err(err).Msg("[log]")
return log
if err == nil {
return log.Level(lvl)
}
return log.Level(lvl)
log.Warn().Err(err).Caller().Send()
}
return log
return log.Logger
}
// internal
// data - config content
var data []byte
type Config []string
// log - main logger
var log zerolog.Logger
func (c *Config) String() string {
return strings.Join(*c, " ")
}
func (c *Config) Set(value string) error {
*c = append(*c, value)
return nil
}
var configs [][]byte
// modules log levels
var modules map[string]string

61
cmd/app/store/store.go Normal file
View File

@@ -0,0 +1,61 @@
package store
import (
"encoding/json"
"github.com/rs/zerolog/log"
"os"
)
const name = "go2rtc.json"
var store map[string]interface{}
func load() {
data, _ := os.ReadFile(name)
if data != nil {
if err := json.Unmarshal(data, &store); err != nil {
// TODO: log
log.Warn().Err(err).Msg("[app] read storage")
}
}
if store == nil {
store = make(map[string]interface{})
}
}
func save() error {
data, err := json.Marshal(store)
if err != nil {
return err
}
return os.WriteFile(name, data, 0644)
}
func GetRaw(key string) interface{} {
if store == nil {
load()
}
return store[key]
}
func GetDict(key string) map[string]interface{} {
raw := GetRaw(key)
if raw != nil {
return raw.(map[string]interface{})
}
return make(map[string]interface{})
}
func Set(key string, v interface{}) error {
if store == nil {
load()
}
store[key] = v
return save()
}

17
cmd/debug/debug.go Normal file
View File

@@ -0,0 +1,17 @@
package debug
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
func Init() {
api.HandleFunc("api/stack", stackHandler)
streams.HandleFunc("null", nullHandler)
}
func nullHandler(string) (streamer.Producer, error) {
return nil, nil
}

View File

@@ -1,4 +1,4 @@
package api
package debug
import (
"bytes"
@@ -21,9 +21,11 @@ var stackSkip = [][]byte{
[]byte("created by net/http.(*Server).Serve"), // TODO: why two?
[]byte("created by github.com/AlexxIT/go2rtc/cmd/rtsp.Init"),
[]byte("created by github.com/AlexxIT/go2rtc/cmd/srtp.Init"),
// webrtc/api.go
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),
[]byte("created by github.com/pion/ice/v2.NewUDPMuxDefault"),
}
func stackHandler(w http.ResponseWriter, r *http.Request) {

29
cmd/echo/echo.go Normal file
View File

@@ -0,0 +1,29 @@
package echo
import (
"bytes"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"os/exec"
)
func Init() {
log := app.GetLogger("echo")
streams.HandleFunc("echo", func(url string) (streamer.Producer, error) {
args := shell.QuoteSplit(url[5:])
b, err := exec.Command(args[0], args[1:]...).Output()
if err != nil {
return nil, err
}
b = bytes.TrimSpace(b)
log.Debug().Str("url", url).Msgf("[echo] %s", b)
return streams.GetProducer(string(b))
})
}

View File

@@ -4,15 +4,18 @@ import (
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
"os"
"os/exec"
"strings"
"sync"
"time"
)
@@ -22,22 +25,27 @@ func Init() {
return
}
rtsp.OnProducer = func(prod streamer.Producer) bool {
if conn := prod.(*pkg.Conn); conn != nil {
if waiter := waiters[conn.URL.Path]; waiter != nil {
waiter <- prod
return true
}
rtsp.HandleFunc(func(conn *pkg.Conn) bool {
waitersMu.Lock()
waiter := waiters[conn.URL.Path]
waitersMu.Unlock()
if waiter == nil {
return false
}
return false
}
// unblocking write to channel
select {
case waiter <- conn:
return true
default:
return false
}
})
streams.HandleFunc("exec", Handle)
log = app.GetLogger("exec")
// TODO: add sync.Mutex
waiters = map[string]chan streamer.Producer{}
}
func Handle(url string) (streamer.Producer, error) {
@@ -49,32 +57,58 @@ func Handle(url string) (streamer.Producer, error) {
)
// remove `exec:`
args := strings.Split(url[5:], " ")
args := shell.QuoteSplit(url[5:])
cmd := exec.Command(args[0], args[1:]...)
if log.Trace().Enabled() {
cmd.Stdout = os.Stdout
}
if log.Debug().Enabled() {
cmd.Stderr = os.Stderr
}
ch := make(chan streamer.Producer)
waitersMu.Lock()
waiters[path] = ch
defer delete(waiters, path)
waitersMu.Unlock()
defer func() {
waitersMu.Lock()
delete(waiters, path)
waitersMu.Unlock()
}()
log.Debug().Str("url", url).Msg("[exec] run")
ts := time.Now()
if err := cmd.Start(); err != nil {
log.Error().Err(err).Str("url", url).Msg("[exec]")
return nil, err
}
chErr := make(chan error)
go func() {
err := cmd.Wait()
// unblocking write to channel
select {
case chErr <- err:
default:
log.Trace().Str("url", url).Msg("[exec] close")
}
}()
select {
case <-time.After(time.Second * 15):
case <-time.After(time.Second * 60):
_ = cmd.Process.Kill()
log.Error().Str("url", url).Msg("[exec] timeout")
return nil, errors.New("timeout")
case err := <-chErr:
return nil, fmt.Errorf("exec: %s", err)
case prod := <-ch:
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run")
return prod, nil
}
}
@@ -82,4 +116,5 @@ func Handle(url string) (streamer.Producer, error) {
// internal
var log zerolog.Logger
var waiters map[string]chan streamer.Producer
var waiters = map[string]chan streamer.Producer{}
var waitersMu sync.Mutex

View File

@@ -1,6 +1,61 @@
## FFplay output
[FFplay](https://stackoverflow.com/questions/27778678/what-are-mv-fd-aq-vq-sq-and-f-in-a-video-stream) `7.11 A-V: 0.003 fd= 1 aq= 21KB vq= 321KB sq= 0B f=0/0`:
- `7.11` - master clock, is the time from start of the stream/video
- `A-V` - av_diff, difference between audio and video timestamps
- `fd` - frames dropped
- `aq` - audio queue (0 - no delay)
- `vq` - video queue (0 - no delay)
- `sq` - subtitle queue
- `f` - timestamp error correction rate (Not 100% sure)
`M-V`, `M-A` means video stream only, audio stream only respectively.
## Devices Windows
```
>ffmpeg -hide_banner -f dshow -list_options true -i video="VMware Virtual USB Video Device"
[dshow @ 0000025695e52900] DirectShow video device options (from video devices)
[dshow @ 0000025695e52900] Pin "Record" (alternative pin name "0")
[dshow @ 0000025695e52900] pixel_format=yuyv422 min s=1280x720 fps=1 max s=1280x720 fps=10
[dshow @ 0000025695e52900] pixel_format=yuyv422 min s=1280x720 fps=1 max s=1280x720 fps=10 (tv, bt470bg/bt709/unknown, topleft)
[dshow @ 0000025695e52900] pixel_format=nv12 min s=1280x720 fps=1 max s=1280x720 fps=23
[dshow @ 0000025695e52900] pixel_format=nv12 min s=1280x720 fps=1 max s=1280x720 fps=23 (tv, bt470bg/bt709/unknown, topleft)
```
## Devices Mac
```
% ./ffmpeg -hide_banner -f avfoundation -list_devices true -i ""
[AVFoundation indev @ 0x7f8b1f504d80] AVFoundation video devices:
[AVFoundation indev @ 0x7f8b1f504d80] [0] FaceTime HD Camera
[AVFoundation indev @ 0x7f8b1f504d80] [1] Capture screen 0
[AVFoundation indev @ 0x7f8b1f504d80] AVFoundation audio devices:
[AVFoundation indev @ 0x7f8b1f504d80] [0] Soundflower (2ch)
[AVFoundation indev @ 0x7f8b1f504d80] [1] Built-in Microphone
[AVFoundation indev @ 0x7f8b1f504d80] [2] Soundflower (64ch)
```
## Devices Linux
```
# ffmpeg -hide_banner -f v4l2 -list_formats all -i /dev/video0
[video4linux2,v4l2 @ 0x7f7de7c58bc0] Raw : yuyv422 : YUYV 4:2:2 : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
[video4linux2,v4l2 @ 0x7f7de7c58bc0] Compressed: mjpeg : Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
```
## Useful links
- https://superuser.com/questions/564402/explanation-of-x264-tune
- https://stackoverflow.com/questions/33624016/why-sliced-thread-affect-so-much-on-realtime-encoding-using-ffmpeg-x264
- https://codec.fandom.com/ru/wiki/X264_-_%D0%BE%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5_%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%B9_%D0%BA%D0%BE%D0%B4%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F
- https://codec.fandom.com/ru/wiki/X264_-_описание_ключей_кодирования
- https://html5test.com/
- https://trac.ffmpeg.org/wiki/Capture/Webcam
- https://trac.ffmpeg.org/wiki/DirectShow
- https://stackoverflow.com/questions/53207692/libav-mjpeg-encoding-and-huffman-table
- https://github.com/tuupola/esp_video/blob/master/README.md
- https://github.com/leandromoreira/ffmpeg-libav-tutorial
- https://www.reddit.com/user/VeritablePornocopium/comments/okw130/ffmpeg_with_libfdk_aac_for_windows_x64/
- https://slhck.info/video/2017/02/24/vbr-settings.html
- [HomeKit audio samples problem](https://superuser.com/questions/1290996/non-monotonous-dts-with-igndts-flag)

View File

@@ -0,0 +1,63 @@
package device
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"os/exec"
"strings"
)
// https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f avfoundation"
func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(streamer.KindVideo, videoIdx)
audio := findMedia(streamer.KindAudio, audioIdx)
switch {
case video != nil && audio != nil:
return `"` + video.Title + `:` + audio.Title + `"`
case video != nil:
return `"` + video.Title + `"`
case audio != nil:
return `"` + audio.Title + `"`
}
return ""
}
func loadMedias() {
cmd := exec.Command(
Bin, "-hide_banner", "-list_devices", "true", "-f", "avfoundation", "-i", "dummy",
)
var buf bytes.Buffer
cmd.Stderr = &buf
_ = cmd.Run()
var kind string
lines := strings.Split(buf.String(), "\n")
process:
for _, line := range lines {
switch {
case strings.HasSuffix(line, "video devices:"):
kind = streamer.KindVideo
continue
case strings.HasSuffix(line, "audio devices:"):
kind = streamer.KindAudio
continue
case strings.HasPrefix(line, "dummy"):
break process
}
// [AVFoundation indev @ 0x7fad54604380] [0] FaceTime HD Camera
name := line[42:]
media := loadMedia(kind, name)
medias = append(medias, media)
}
}
func loadMedia(kind, name string) *streamer.Media {
return &streamer.Media{
Kind: kind, Title: name,
}
}

View File

@@ -0,0 +1,50 @@
package device
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"io/ioutil"
"os/exec"
"strings"
)
// https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f v4l2"
func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(streamer.KindVideo, videoIdx)
return video.Title
}
func loadMedias() {
files, err := ioutil.ReadDir("/dev")
if err != nil {
return
}
for _, file := range files {
log.Trace().Msg("[ffmpeg] " + file.Name())
if strings.HasPrefix(file.Name(), streamer.KindVideo) {
media := loadMedia(streamer.KindVideo, "/dev/"+file.Name())
if media != nil {
medias = append(medias, media)
}
}
}
}
func loadMedia(kind, name string) *streamer.Media {
cmd := exec.Command(
Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name,
)
var buf bytes.Buffer
cmd.Stderr = &buf
_ = cmd.Run()
if !bytes.Contains(buf.Bytes(), []byte("Raw")) {
return nil
}
return &streamer.Media{
Kind: kind, Title: name,
}
}

View File

@@ -0,0 +1,59 @@
package device
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"os/exec"
"strings"
)
// https://trac.ffmpeg.org/wiki/DirectShow
const deviceInputPrefix = "-f dshow"
func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(streamer.KindVideo, videoIdx)
audio := findMedia(streamer.KindAudio, audioIdx)
switch {
case video != nil && audio != nil:
return `video="` + video.Title + `":audio=` + audio.Title + `"`
case video != nil:
return `video="` + video.Title + `"`
case audio != nil:
return `audio="` + audio.Title + `"`
}
return ""
}
func loadMedias() {
cmd := exec.Command(
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
)
var buf bytes.Buffer
cmd.Stderr = &buf
_ = cmd.Run()
lines := strings.Split(buf.String(), "\r\n")
for _, line := range lines {
var kind string
if strings.HasSuffix(line, "(video)") {
kind = streamer.KindVideo
} else if strings.HasSuffix(line, "(audio)") {
kind = streamer.KindAudio
} else {
continue
}
// hope we have constant prefix and suffix sizes
// [dshow @ 00000181e8d028c0] "VMware Virtual USB Video Device" (video)
name := line[28 : len(line)-9]
media := loadMedia(kind, name)
medias = append(medias, media)
}
}
func loadMedia(kind, name string) *streamer.Media {
return &streamer.Media{
Kind: kind, Title: name,
}
}

View File

@@ -0,0 +1,83 @@
package device
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
"net/http"
"net/url"
"strconv"
"strings"
)
func Init() {
log = app.GetLogger("exec")
api.HandleFunc("api/devices", handle)
}
func GetInput(src string) (string, error) {
if medias == nil {
loadMedias()
}
input := deviceInputPrefix
var videoIdx, audioIdx int
if i := strings.IndexByte(src, '?'); i > 0 {
query, err := url.ParseQuery(src[i+1:])
if err != nil {
return "", err
}
for key, value := range query {
switch key {
case "video":
videoIdx, _ = strconv.Atoi(value[0])
case "audio":
audioIdx, _ = strconv.Atoi(value[0])
case "framerate":
input += " -framerate " + value[0]
case "resolution":
input += " -video_size " + value[0]
}
}
}
input += " -i " + deviceInputSuffix(videoIdx, audioIdx)
return input, nil
}
var Bin string
var log zerolog.Logger
var medias []*streamer.Media
func findMedia(kind string, index int) *streamer.Media {
for _, media := range medias {
if media.Kind != kind {
continue
}
if index == 0 {
return media
}
index--
}
return nil
}
func handle(w http.ResponseWriter, r *http.Request) {
if medias == nil {
loadMedias()
}
data, err := json.Marshal(medias)
if err != nil {
log.Error().Err(err).Msg("[api.ffmpeg]")
return
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.ffmpeg]")
}
}

View File

@@ -1,11 +1,16 @@
package ffmpeg
import (
"bytes"
"errors"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"net/url"
"strconv"
"strings"
)
@@ -14,101 +19,224 @@ func Init() {
Mod map[string]string `yaml:"ffmpeg"`
}
// defaults
cfg.Mod = map[string]string{
"bin": "ffmpeg",
// inputs
"link": "-hide_banner -i {input}",
"rtsp": "-hide_banner -fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input}",
"file": "-hide_banner -re -stream_loop -1 -i {input}",
// output
"out": "-rtsp_transport tcp -f rtsp {output}",
// `-g 30` - group of picture, GOP, keyframe interval
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
// `-tune zerolatency` - for minimal latency
// `-profile main -level 4.1` - most used streaming profile
"h264": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1",
"h264/ultra": "-codec:v libx264 -g 30 -preset ultrafast -tune zerolatency",
"h264/high": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency",
"h265": "-codec:v libx265 -g 30 -preset ultrafast -tune zerolatency",
"opus": "-codec:a libopus -ar 48000 -ac 2",
"pcmu": "-codec:a pcm_mulaw -ar 8000 -ac 1",
"pcmu/16000": "-codec:a pcm_mulaw -ar 16000 -ac 1",
"pcmu/48000": "-codec:a pcm_mulaw -ar 48000 -ac 1",
"pcma": "-codec:a pcm_alaw -ar 8000 -ac 1",
"pcma/16000": "-codec:a pcm_alaw -ar 16000 -ac 1",
"pcma/48000": "-codec:a pcm_alaw -ar 48000 -ac 1",
"aac/16000": "-codec:a aac -ar 16000 -ac 1",
}
cfg.Mod = defaults // will be overriden from yaml
app.LoadConfig(&cfg)
tpl := cfg.Mod
if app.GetLogger("exec").GetLevel() >= 0 {
defaults["global"] += " -v error"
}
streams.HandleFunc("ffmpeg", func(s string) (streamer.Producer, error) {
s = s[7:] // remove `ffmpeg:`
var query url.Values
if i := strings.IndexByte(s, '#'); i > 0 {
query = parseQuery(s[i+1:])
s = s[:i]
streams.HandleFunc("ffmpeg", func(url string) (streamer.Producer, error) {
args := parseArgs(url[7:]) // remove `ffmpeg:`
if args == nil {
return nil, errors.New("can't generate ffmpeg command")
}
return exec.Handle("exec:" + args.String())
})
var template string
switch {
case strings.HasPrefix(s, "rtsp"):
template = tpl["rtsp"]
case strings.Contains(s, "://"):
template = tpl["link"]
device.Bin = defaults["bin"]
device.Init()
}
var defaults = map[string]string{
"bin": "ffmpeg",
"global": "-hide_banner",
// inputs
"file": "-re -stream_loop -1 -i {input}",
"http": "-fflags nobuffer -flags low_delay -i {input}",
"rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}",
// output
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
// `-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",
"h265": "-c:v libx265 -g 50 -profile:v high -level:v 5.1 -preset:v superfast -tune:v zerolatency",
"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
"opus": "-c:a libopus -ar:a 48000 -ac:a 2",
"pcmu": "-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/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",
// hardware Intel and AMD on Linux
// better not to set `-async_depth:v 1` like for QSV, because framedrops
// `-bf 0` - disable B-frames is very important
"h264/vaapi": "-c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0",
"h265/vaapi": "-c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0",
"mjpeg/vaapi": "-c:v mjpeg_vaapi",
// hardware Raspberry
"h264/v4l2m2m": "-c:v h264_v4l2m2m -g 50 -bf 0",
"h265/v4l2m2m": "-c:v hevc_v4l2m2m -g 50 -bf 0",
// hardware NVidia on Linux and Windows
// preset=p2 - faster, tune=ll - low latency
"h264/cuda": "-c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
"h265/cuda": "-c:v hevc_nvenc -g 50 -profile:v high -level:v auto",
// hardware Intel on Windows
"h264/dxva2": "-c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1",
"h265/dxva2": "-c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1",
"mjpeg/dxva2": "-c:v mjpeg_qsv -profile:v high -level:v 5.1",
// hardware macOS
"h264/videotoolbox": "-c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1",
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1",
}
func parseArgs(s string) *Args {
// init FFmpeg arguments
args := &Args{
bin: defaults["bin"],
global: defaults["global"],
output: defaults["output"],
}
var query url.Values
if i := strings.IndexByte(s, '#'); i > 0 {
query = parseQuery(s[i+1:])
args.video = len(query["video"])
args.audio = len(query["audio"])
s = s[:i]
}
// Parse input:
// 1. Input as xxxx:// link (http or rtsp or any other)
// 2. Input as stream name
// 3. Input as FFmpeg device (local USB camera)
if i := strings.Index(s, "://"); i > 0 {
switch s[:i] {
case "http", "https", "rtmp":
args.input = strings.Replace(defaults["http"], "{input}", s, 1)
case "rtsp", "rtsps":
// https://ffmpeg.org/ffmpeg-protocols.html#rtsp
// skip unnecessary input tracks
switch {
case (args.video > 0 && args.audio > 0) || (args.video == 0 && args.audio == 0):
args.input = "-allowed_media_types video+audio "
case args.video > 0:
args.input = "-allowed_media_types video "
case args.audio > 0:
args.input = "-allowed_media_types audio "
}
args.input += strings.Replace(defaults["rtsp"], "{input}", s, 1)
default:
template = tpl["file"]
args.input = "-i " + s
}
} else if streams.Get(s) != nil {
s = "rtsp://localhost:" + rtsp.Port + "/" + s
switch {
case args.video > 0 && args.audio == 0:
s += "?video"
case args.audio > 0 && args.video == 0:
s += "?audio"
}
args.input = strings.Replace(defaults["rtsp"], "{input}", s, 1)
} else if strings.HasPrefix(s, "device?") {
var err error
args.input, err = device.GetInput(s)
if err != nil {
return nil
}
} else {
args.input = strings.Replace(defaults["file"], "{input}", s, 1)
}
if query["async"] != nil {
args.input = "-use_wallclock_as_timestamps 1 -async 1 " + args.input
}
// Parse query params:
// 1. `width`/`height` params
// 2. `rotate` param
// 3. `video` params (support multiple)
// 4. `audio` params (support multiple)
// 5. `hardware` param
if query != nil {
// 1. Process raw params for FFmpeg
for _, raw := range query["raw"] {
args.AddCodec(raw)
}
s = "exec:" + tpl["bin"] + " " +
strings.Replace(template, "{input}", s, 1)
if query != nil {
for _, raw := range query["raw"] {
s += " " + raw
// 2. Process video filters (resize and rotation)
if query["width"] != nil || query["height"] != nil {
filter := "scale="
if query["width"] != nil {
filter += query["width"][0]
} else {
filter += "-1"
}
filter += ":"
if query["height"] != nil {
filter += query["height"][0]
} else {
filter += "-1"
}
args.AddFilter(filter)
}
// TODO: multiple codecs via -map
// s += fmt.Sprintf(" -map 0:v:0 -c:v:%d copy", i)
if query["rotate"] != nil {
var filter string
switch query["rotate"][0] {
case "90":
filter = "transpose=1" // 90 degrees clockwise
case "180":
filter = "transpose=1,transpose=1"
case "-90", "270":
filter = "transpose=2" // 90 degrees counterclockwise
}
if filter != "" {
args.AddFilter(filter)
}
}
// 3. Process video codecs
if args.video > 0 {
for _, video := range query["video"] {
if video == "copy" {
s += " -codec:v copy"
if video != "copy" {
args.AddCodec(defaults[video])
} else {
s += " " + tpl[video]
args.AddCodec("-c:v copy")
}
}
for _, audio := range query["audio"] {
if audio == "copy" {
s += " -codec:v copy"
} else {
s += " " + tpl[audio]
}
}
if query["video"] == nil {
s += " -vn"
}
if query["audio"] == nil {
s += " -an"
}
} else {
s += " -c copy"
args.AddCodec("-vn")
}
s += " " + tpl["out"]
// 4. Process audio codecs
if args.audio > 0 {
for _, audio := range query["audio"] {
if audio != "copy" {
args.AddCodec(defaults[audio])
} else {
args.AddCodec("-c:a copy")
}
}
} else {
args.AddCodec("-an")
}
return exec.Handle(s)
})
if query["hardware"] != nil {
MakeHardware(args, query["hardware"][0])
}
}
if args.codecs == nil {
args.AddCodec("-c copy")
}
return args
}
func parseQuery(s string) map[string][]string {
@@ -123,3 +251,76 @@ func parseQuery(s string) map[string][]string {
}
return query
}
type Args struct {
bin string // ffmpeg
global string // -hide_banner -v error
input string // -re -stream_loop -1 -i /media/bunny.mp4
codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency
filters []string // scale=1920:1080
output string // -f rtsp {output}
video, audio int // count of video and audio params
}
func (a *Args) AddCodec(codec string) {
a.codecs = append(a.codecs, codec)
}
func (a *Args) AddFilter(filter string) {
a.filters = append(a.filters, filter)
}
func (a *Args) InsertFilter(filter string) {
a.filters = append([]string{filter}, a.filters...)
}
func (a *Args) String() string {
b := bytes.NewBuffer(make([]byte, 0, 512))
b.WriteString(a.bin)
if a.global != "" {
b.WriteByte(' ')
b.WriteString(a.global)
}
b.WriteByte(' ')
b.WriteString(a.input)
multimode := a.video > 1 || a.audio > 1
var iv, ia int
for _, codec := range a.codecs {
// support multiple video and/or audio codecs
if multimode && len(codec) >= 5 {
switch codec[:5] {
case "-c:v ":
codec = "-map 0:v:0? " + strings.ReplaceAll(codec, ":v ", ":v:"+strconv.Itoa(iv)+" ")
iv++
case "-c:a ":
codec = "-map 0:a:0? " + strings.ReplaceAll(codec, ":a ", ":a:"+strconv.Itoa(ia)+" ")
ia++
}
}
b.WriteByte(' ')
b.WriteString(codec)
}
if a.filters != nil {
for i, filter := range a.filters {
if i == 0 {
b.WriteString(" -vf ")
} else {
b.WriteByte(',')
}
b.WriteString(filter)
}
}
b.WriteByte(' ')
b.WriteString(a.output)
return b.String()
}

112
cmd/ffmpeg/hardware.go Normal file
View File

@@ -0,0 +1,112 @@
package ffmpeg
import (
"github.com/rs/zerolog/log"
"os/exec"
"strings"
)
const (
EngineSoftware = "software"
EngineVAAPI = "vaapi" // Intel iGPU and AMD GPU
EngineV4L2M2M = "v4l2m2m" // Raspberry Pi 3 and 4
EngineCUDA = "cuda" // NVidia on Windows and Linux
EngineDXVA2 = "dxva2" // Intel on Windows
EngineVideoToolbox = "videotoolbox" // macOS
)
var cache = map[string]string{}
// MakeHardware converts software FFmpeg args to hardware args
// empty engine for autoselect
func MakeHardware(args *Args, engine string) {
for i, codec := range args.codecs {
if len(codec) < 12 {
continue // skip short line (-c:v libx264...)
}
// get current codec name
name := cut(codec, ' ', 1)
switch name {
case "libx264":
name = "h264"
case "libx265":
name = "h265"
case "mjpeg":
default:
continue // skip unsupported codec
}
// temporary disable probe for H265 and MJPEG
if engine == "" && name == "h264" {
if engine = cache[name]; engine == "" {
engine = ProbeHardware(name)
cache[name] = engine
}
}
switch engine {
case EngineVAAPI:
args.input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.input
args.codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.filters {
if strings.HasPrefix(filter, "scale=") {
args.filters[i] = "scale_vaapi=" + filter[6:]
}
}
// fix if input doesn't support hwaccel, do nothing when support
args.InsertFilter("format=vaapi|nv12,hwupload")
case EngineCUDA:
args.input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.input
args.codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.filters {
if strings.HasPrefix(filter, "scale=") {
args.filters[i] = "scale_cuda=" + filter[6:]
}
}
case EngineDXVA2:
args.input = "-hwaccel dxva2 -hwaccel_output_format dxva2_vld " + args.input
args.codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.filters {
if strings.HasPrefix(filter, "scale=") {
args.filters[i] = "scale_qsv=" + filter[6:]
}
}
args.InsertFilter("hwmap=derive_device=qsv,format=qsv")
case EngineVideoToolbox:
args.input = "-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld " + args.input
args.codecs[i] = defaults[name+"/"+engine]
case EngineV4L2M2M:
args.codecs[i] = defaults[name+"/"+engine]
}
}
}
func run(arg ...string) bool {
err := exec.Command(defaults["bin"], arg...).Run()
log.Printf("%v %v", arg, err)
return err == nil
}
func cut(s string, sep byte, pos int) string {
for n := 0; n < pos; n++ {
if i := strings.IndexByte(s, sep); i > 0 {
s = s[i+1:]
} else {
return ""
}
}
if i := strings.IndexByte(s, sep); i > 0 {
return s[:i]
}
return s
}

View File

@@ -0,0 +1,21 @@
package ffmpeg
func ProbeHardware(name string) string {
switch name {
case "h264":
if run(
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "h264_videotoolbox", "-f", "null", "-") {
return EngineVideoToolbox
}
case "h265":
if run(
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "hevc_videotoolbox", "-f", "null", "-") {
return EngineVideoToolbox
}
}
return EngineSoftware
}

View File

@@ -0,0 +1,67 @@
package ffmpeg
import (
"runtime"
)
func ProbeHardware(name string) string {
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
switch name {
case "h264":
if run(
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "h264_v4l2m2m", "-f", "null", "-") {
return EngineV4L2M2M
}
case "h265":
if run(
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "hevc_v4l2m2m", "-f", "null", "-") {
return EngineV4L2M2M
}
}
return EngineSoftware
}
switch name {
case "h264":
if run("-init_hw_device", "cuda",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "h264_nvenc", "-f", "null", "-") {
return EngineCUDA
}
if run("-init_hw_device", "vaapi",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-vf", "format=nv12,hwupload",
"-c", "h264_vaapi", "-f", "null", "-") {
return EngineVAAPI
}
case "h265":
if run("-init_hw_device", "cuda",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "hevc_nvenc", "-f", "null", "-") {
return EngineCUDA
}
if run("-init_hw_device", "vaapi",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-vf", "format=nv12,hwupload",
"-c", "hevc_vaapi", "-f", "null", "-") {
return EngineVAAPI
}
case "mjpeg":
if run("-init_hw_device", "vaapi",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-vf", "format=nv12,hwupload",
"-c", "mjpeg_vaapi", "-f", "null", "-") {
return EngineVAAPI
}
}
return EngineSoftware
}

View File

@@ -0,0 +1,40 @@
package ffmpeg
func ProbeHardware(name string) string {
switch name {
case "h264":
if run("-init_hw_device", "cuda",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "h264_nvenc", "-f", "null", "-") {
return EngineCUDA
}
if run("-init_hw_device", "dxva2",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "h264_qsv", "-f", "null", "-") {
return EngineDXVA2
}
case "h265":
if run("-init_hw_device", "cuda",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "hevc_nvenc", "-f", "null", "-") {
return EngineCUDA
}
if run("-init_hw_device", "dxva2",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "hevc_qsv", "-f", "null", "-") {
return EngineDXVA2
}
case "mjpeg":
if run("-init_hw_device", "dxva2",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "mjpeg_qsv", "-f", "null", "-") {
return EngineDXVA2
}
}
return EngineSoftware
}

153
cmd/hass/api.go Normal file
View File

@@ -0,0 +1,153 @@
package hass
import (
"encoding/base64"
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"net/http"
"net/url"
"strings"
)
func initAPI() {
ok := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"status":1,"payload":{}}`))
}
// support https://www.home-assistant.io/integrations/rtsp_to_webrtc/
api.HandleFunc("/static", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
api.HandleFunc("/streams", ok)
api.HandleFunc("/stream/", func(w http.ResponseWriter, r *http.Request) {
switch {
// /stream/{id}/add
case strings.HasSuffix(r.RequestURI, "/add"):
var v addJSON
if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
return
}
// we can get three types of links:
// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}
// 2. static link to Hass camera
// 3. dynamic link to Hass camera
stream := streams.Get(v.Name)
if stream == nil {
// check if it is rtsp link to go2rtc
stream = rtspStream(v.Channels.First.Url)
if stream != nil {
streams.New(v.Name, stream)
} else {
stream = streams.New(v.Name, "{input}")
}
}
stream.SetSource(v.Channels.First.Url)
ok(w, r)
// /stream/{id}/channel/0/webrtc
default:
i := strings.IndexByte(r.RequestURI[8:], '/')
if i <= 0 {
log.Warn().Msgf("wrong request: %s", r.RequestURI)
return
}
name := r.RequestURI[8 : 8+i]
stream := streams.Get(name)
if stream == nil {
w.WriteHeader(http.StatusNotFound)
return
}
if err := r.ParseForm(); err != nil {
log.Error().Err(err).Msg("[api.hass] parse form")
return
}
s := r.FormValue("data")
offer, err := base64.StdEncoding.DecodeString(s)
if err != nil {
log.Error().Err(err).Msg("[api.hass] sdp64 decode")
return
}
s, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
if err != nil {
log.Error().Err(err).Msg("[api.hass] exchange SDP")
return
}
s = base64.StdEncoding.EncodeToString([]byte(s))
_, _ = w.Write([]byte(s))
}
})
// api from RTSPtoWebRTC
api.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
return
}
str := r.FormValue("sdp64")
offer, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return
}
src := r.FormValue("url")
src, err = url.QueryUnescape(src)
if err != nil {
return
}
stream := streams.Get(src)
if stream == nil {
if stream = rtspStream(src); stream != nil {
streams.New(src, stream)
} else {
stream = streams.New(src, src)
}
}
str, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
if err != nil {
return
}
v := struct {
Answer string `json:"sdp64"`
}{
Answer: base64.StdEncoding.EncodeToString([]byte(str)),
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
})
}
func rtspStream(url string) *streams.Stream {
if strings.HasPrefix(url, "rtsp://") {
if i := strings.IndexByte(url[7:], '/'); i > 0 {
return streams.Get(url[8+i:])
}
}
return nil
}
type addJSON struct {
Name string `json:"name"`
Channels struct {
First struct {
//Name string `json:"name"`
Url string `json:"url"`
} `json:"0"`
} `json:"channels"`
}

View File

@@ -1,20 +1,14 @@
package hass
import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
"net/http"
"os"
"path"
"strings"
)
func Init() {
@@ -26,13 +20,9 @@ func Init() {
app.LoadConfig(&conf)
log = app.GetLogger("api")
log = app.GetLogger("hass")
// support https://www.home-assistant.io/integrations/rtsp_to_webrtc/
api.HandleFunc("/static", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
api.HandleFunc("/stream", handler)
initAPI()
// support load cameras from Hass config file
filename := path.Join(conf.Mod.Config, ".storage/core.config_entries")
@@ -41,82 +31,50 @@ func Init() {
return
}
ent := new(entries)
if err = json.Unmarshal(data, ent); err != nil {
storage := new(entries)
if err = json.Unmarshal(data, storage); err != nil {
return
}
urls := map[string]string{}
for _, entrie := range ent.Data.Entries {
switch entrie.Domain {
case "generic":
if entrie.Options.StreamSource != "" {
urls[entrie.Title] = entrie.Options.StreamSource
}
}
}
streams.HandleFunc("hass", func(url string) (streamer.Producer, error) {
if hurl := urls[url[5:]]; hurl != "" {
return streams.GetProducer(hurl)
}
return nil, fmt.Errorf("can't get url: %s", url)
})
for _, entrie := range storage.Data.Entries {
switch entrie.Domain {
case "generic":
if entrie.Options.StreamSource == "" {
continue
}
urls[entrie.Title] = entrie.Options.StreamSource
case "homekit_controller":
if entrie.Data.ClientID == "" {
continue
}
urls[entrie.Title] = fmt.Sprintf(
"homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s",
entrie.Data.DeviceHost, entrie.Data.DevicePort,
entrie.Data.ClientID, entrie.Data.ClientPrivate, entrie.Data.ClientPublic,
entrie.Data.DeviceID, entrie.Data.DevicePublic,
)
default:
continue
}
log.Info().Str("url", "hass:"+entrie.Title).Msg("[hass] load stream")
//streams.Get("hass:" + entrie.Title)
}
}
var log zerolog.Logger
func handler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
log.Error().Err(err).Msg("[api.hass] parse form")
return
}
url := r.FormValue("url")
str := r.FormValue("sdp64")
offer, err := base64.StdEncoding.DecodeString(str)
if err != nil {
log.Error().Err(err).Msg("[api.hass] sdp64 decode")
return
}
// TODO: fixme
if strings.HasPrefix(url, "rtsp://") {
port := ":" + rtsp.Port + "/"
i := strings.Index(url, port)
if i > 0 {
url = url[i+len(port):]
}
}
stream := streams.Get(url)
str, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
if err != nil {
log.Error().Err(err).Msg("[api.hass] exchange SDP")
return
}
resp := struct {
Answer string `json:"sdp64"`
}{
Answer: base64.StdEncoding.EncodeToString([]byte(str)),
}
data, err := json.Marshal(resp)
if err != nil {
log.Error().Err(err).Msg("[api.hass] marshal JSON")
return
}
w.Header().Set("Content-Type", "application/json")
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.hass] write")
return
}
}
type entries struct {
Data struct {
Entries []struct {

140
cmd/homekit/api.go Normal file
View File

@@ -0,0 +1,140 @@
package homekit
import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/app/store"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/mdns"
"net/http"
"net/url"
"strings"
)
func apiHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
items := make([]interface{}, 0)
for name, src := range store.GetDict("streams") {
if src := src.(string); strings.HasPrefix(src, "homekit") {
u, err := url.Parse(src)
if err != nil {
continue
}
device := Device{
Name: name,
Addr: u.Host,
Paired: true,
}
items = append(items, device)
}
}
for info := range mdns.GetAll() {
if !strings.HasSuffix(info.Name, mdns.Suffix) {
continue
}
name := info.Name[:len(info.Name)-len(mdns.Suffix)]
device := Device{
Name: strings.ReplaceAll(name, "\\", ""),
Addr: fmt.Sprintf("%s:%d", info.AddrV4, info.Port),
}
for _, field := range info.InfoFields {
switch field[:2] {
case "id":
device.ID = field[3:]
case "md":
device.Model = field[3:]
case "sf":
device.Paired = field[3] == '0'
}
}
items = append(items, device)
}
_ = json.NewEncoder(w).Encode(items)
case "POST":
// TODO: post params...
id := r.URL.Query().Get("id")
pin := r.URL.Query().Get("pin")
name := r.URL.Query().Get("name")
if err := hkPair(id, pin, name); err != nil {
log.Error().Err(err).Caller().Send()
_, err = w.Write([]byte(err.Error()))
}
case "DELETE":
src := r.URL.Query().Get("src")
if err := hkDelete(src); err != nil {
log.Error().Err(err).Caller().Send()
_, err = w.Write([]byte(err.Error()))
}
}
}
func hkPair(deviceID, pin, name string) (err error) {
var conn *hap.Conn
if conn, err = hap.Pair(deviceID, pin); err != nil {
return
}
streams.New(name, conn.URL())
dict := store.GetDict("streams")
dict[name] = conn.URL()
return store.Set("streams", dict)
}
func hkDelete(name string) (err error) {
dict := store.GetDict("streams")
for key, rawURL := range dict {
if key != name {
continue
}
var conn *hap.Conn
if conn, err = hap.NewConn(rawURL.(string)); err != nil {
return
}
if err = conn.Dial(); err != nil {
return
}
go func() {
if err = conn.Handle(); err != nil {
log.Warn().Err(err).Caller().Send()
}
}()
if err = conn.ListPairings(); err != nil {
return
}
if err = conn.DeletePairing(conn.ClientID); err != nil {
log.Error().Err(err).Caller().Send()
}
delete(dict, name)
return store.Set("streams", dict)
}
return nil
}
type Device struct {
ID string `json:"id"`
Name string `json:"name"`
Addr string `json:"addr"`
Model string `json:"model"`
Paired bool `json:"paired"`
//Type string `json:"type"`
}

32
cmd/homekit/homekit.go Normal file
View File

@@ -0,0 +1,32 @@
package homekit
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/srtp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
)
func Init() {
log = app.GetLogger("homekit")
streams.HandleFunc("homekit", streamHandler)
api.HandleFunc("api/homekit", apiHandler)
}
var log zerolog.Logger
func streamHandler(url string) (streamer.Producer, error) {
conn, err := homekit.NewClient(url, srtp.Server)
if err != nil {
return nil, err
}
if err = conn.Dial();err!=nil{
return nil, err
}
return conn, nil
}

56
cmd/http/http.go Normal file
View File

@@ -0,0 +1,56 @@
package http
import (
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"net/http"
"strings"
)
func Init() {
streams.HandleFunc("http", handle)
streams.HandleFunc("https", handle)
}
func handle(url string) (streamer.Producer, error) {
// first we get the Content-Type to define supported producer
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
res, err := tcp.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, errors.New(res.Status)
}
ct := res.Header.Get("Content-Type")
if i := strings.IndexByte(ct, ';'); i > 0 {
ct = ct[:i]
}
switch ct {
case "image/jpeg", "multipart/x-mixed-replace":
return mjpeg.NewClient(res), nil
case "video/x-flv":
var conn *rtmp.Client
if conn, err = rtmp.Accept(res); err != nil {
return nil, err
}
if err = conn.Describe(); err != nil {
return nil, err
}
return conn, nil
}
return nil, fmt.Errorf("unsupported Content-Type: %s", ct)
}

19
cmd/ivideon/ivideon.go Normal file
View File

@@ -0,0 +1,19 @@
package ivideon
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/ivideon"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
func Init() {
streams.HandleFunc("ivideon", func(url string) (streamer.Producer, error) {
id := strings.Replace(url[8:], "/", ":", 1)
prod := ivideon.NewClient(id)
if err := prod.Dial(); err != nil {
return nil, err
}
return prod, nil
})
}

140
cmd/mjpeg/mjpeg.go Normal file
View File

@@ -0,0 +1,140 @@
package mjpeg
import (
"errors"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/rs/zerolog/log"
"net/http"
"strconv"
)
func Init() {
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
api.HandleFunc("api/stream.mjpeg", handlerStream)
api.HandleWS("mjpeg", handlerWS)
}
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
exit := make(chan []byte)
cons := &mjpeg.Consumer{
RemoteAddr: r.RemoteAddr,
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case []byte:
exit <- msg
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
data := <-exit
stream.RemoveConsumer(cons)
h := w.Header()
h.Set("Content-Type", "image/jpeg")
h.Set("Content-Length", strconv.Itoa(len(data)))
h.Set("Cache-Control", "no-cache")
h.Set("Connection", "close")
h.Set("Pragma", "no-cache")
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}
}
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
func handlerStream(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
flusher := w.(http.Flusher)
cons := &mjpeg.Consumer{
RemoteAddr: r.RemoteAddr,
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case []byte:
data := []byte(header + strconv.Itoa(len(msg)))
data = append(data, '\r', '\n', '\r', '\n')
data = append(data, msg...)
data = append(data, '\r', '\n')
// Chrome bug: mjpeg image always shows the second to last image
// https://bugs.chromium.org/p/chromium/issues/detail?id=527446
_, _ = w.Write(data)
flusher.Flush()
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
return
}
h := w.Header()
h.Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
h.Set("Cache-Control", "no-cache")
h.Set("Connection", "close")
h.Set("Pragma", "no-cache")
<-r.Context().Done()
stream.RemoveConsumer(cons)
//log.Trace().Msg("[api.mjpeg] close")
}
func handlerWS(tr *api.Transport, _ *api.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return errors.New(api.StreamNotFound)
}
cons := &mjpeg.Consumer{
RemoteAddr: tr.Request.RemoteAddr,
UserAgent: tr.Request.UserAgent(),
}
cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok {
tr.Write(data)
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return err
}
tr.Write(&api.Message{Type: "mjpeg"})
tr.OnClose(func() {
stream.RemoveConsumer(cons)
})
return nil
}

157
cmd/mp4/mp4.go Normal file
View File

@@ -0,0 +1,157 @@
package mp4
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/rs/zerolog"
"net/http"
"strconv"
"strings"
"time"
)
func Init() {
log = app.GetLogger("mp4")
api.HandleWS("mse", handlerWSMSE)
api.HandleWS("mp4", handlerWSMP4)
api.HandleFunc("api/frame.mp4", handlerKeyframe)
api.HandleFunc("api/stream.mp4", handlerMP4)
}
var log zerolog.Logger
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
if isChromeFirst(w, r) {
return
}
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
exit := make(chan []byte)
cons := &mp4.Segment{OnlyKeyframe: true}
cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok && exit != nil {
exit <- data
exit = nil
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
data := <-exit
stream.RemoveConsumer(cons)
// Apple Safari won't show frame without length
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
w.Header().Set("Content-Type", cons.MimeType)
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}
}
func handlerMP4(w http.ResponseWriter, r *http.Request) {
log.Trace().Msgf("[mp4] %s %+v", r.Method, r.Header)
if isChromeFirst(w, r) || isSafari(w, r) {
return
}
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
exit := make(chan error)
cons := &mp4.Consumer{
RemoteAddr: r.RemoteAddr,
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok {
if _, err := w.Write(data); err != nil && exit != nil {
exit <- err
exit = nil
}
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
defer stream.RemoveConsumer(cons)
w.Header().Set("Content-Type", cons.MimeType())
data, err := cons.Init()
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
return
}
cons.Start()
var duration *time.Timer
if s := r.URL.Query().Get("duration"); s != "" {
if i, _ := strconv.Atoi(s); i > 0 {
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
if exit != nil {
exit <- nil
exit = nil
}
})
}
}
err = <-exit
log.Trace().Err(err).Caller().Send()
if duration != nil {
duration.Stop()
}
}
func isChromeFirst(w http.ResponseWriter, r *http.Request) bool {
// Chrome 105 does two requests: without Range and with `Range: bytes=0-`
if strings.Contains(r.UserAgent(), " Chrome/") {
if r.Header.Values("Range") == nil {
w.Header().Set("Content-Type", "video/mp4")
w.WriteHeader(http.StatusOK)
return true
}
}
return false
}
func isSafari(w http.ResponseWriter, r *http.Request) bool {
if r.Header.Get("Range") == "bytes=0-1" {
handlerKeyframe(w, r)
return true
}
return false
}

142
cmd/mp4/ws.go Normal file
View File

@@ -0,0 +1,142 @@
package mp4
import (
"errors"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
const packetSize = 1400
func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return errors.New(api.StreamNotFound)
}
cons := &mp4.Consumer{
RemoteAddr: tr.Request.RemoteAddr,
UserAgent: tr.Request.UserAgent(),
}
cons.UserAgent = tr.Request.UserAgent()
cons.RemoteAddr = tr.Request.RemoteAddr
if codecs, ok := msg.Value.(string); ok {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
cons.Medias = parseMedias(codecs, true)
}
cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok {
for len(data) > packetSize {
tr.Write(data[:packetSize])
data = data[packetSize:]
}
tr.Write(data)
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Debug().Err(err).Msg("[mp4] add consumer")
return err
}
tr.OnClose(func() {
stream.RemoveConsumer(cons)
})
tr.Write(&api.Message{Type: "mse", Value: cons.MimeType()})
data, err := cons.Init()
if err != nil {
log.Warn().Err(err).Caller().Send()
return err
}
tr.Write(data)
cons.Start()
return nil
}
func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return errors.New(api.StreamNotFound)
}
cons := &mp4.Segment{
RemoteAddr: tr.Request.RemoteAddr,
UserAgent: tr.Request.UserAgent(),
OnlyKeyframe: true,
}
if codecs, ok := msg.Value.(string); ok {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
cons.Medias = parseMedias(codecs, false)
}
cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok {
tr.Write(data)
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return err
}
tr.Write(&api.Message{Type: "mp4", Value: cons.MimeType})
tr.OnClose(func() {
stream.RemoveConsumer(cons)
})
return nil
}
func parseMedias(codecs string, parseAudio bool) (medias []*streamer.Media) {
var videos []*streamer.Codec
var audios []*streamer.Codec
for _, name := range strings.Split(codecs, ",") {
switch name {
case "avc1.640029":
codec := &streamer.Codec{Name: streamer.CodecH264}
videos = append(videos, codec)
case "hvc1.1.6.L153.B0":
codec := &streamer.Codec{Name: streamer.CodecH265}
videos = append(videos, codec)
case "mp4a.40.2":
codec := &streamer.Codec{Name: streamer.CodecAAC}
audios = append(audios, codec)
}
}
if videos != nil {
media := &streamer.Media{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: videos,
}
medias = append(medias, media)
}
if audios != nil && parseAudio {
media := &streamer.Media{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: audios,
}
medias = append(medias, media)
}
return
}

View File

@@ -1,42 +0,0 @@
package mse
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mse"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog/log"
)
func Init() {
api.HandleWS("mse", handler)
}
func handler(ctx *api.Context, msg *streamer.Message) {
url := ctx.Request.URL.Query().Get("url")
stream := streams.Get(url)
if stream == nil {
return
}
cons := new(mse.Consumer)
cons.UserAgent = ctx.Request.UserAgent()
cons.RemoteAddr = ctx.Request.RemoteAddr
cons.Listen(func(msg interface{}) {
switch msg.(type) {
case *streamer.Message, []byte:
ctx.Write(msg)
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Warn().Err(err).Msg("[api.mse] Add consumer")
ctx.Error(err)
return
}
ctx.OnClose(func() {
stream.RemoveConsumer(cons)
})
cons.Init()
}

View File

@@ -15,5 +15,8 @@ func handle(url string) (streamer.Producer, error) {
if err := conn.Dial(); err != nil {
return nil, err
}
if err := conn.Describe(); err != nil {
return nil, err
}
return conn, nil
}

View File

@@ -8,12 +8,15 @@ import (
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
"net"
"strings"
)
func Init() {
var conf struct {
Mod struct {
Listen string `yaml:"listen"`
Listen string `yaml:"listen" json:"listen"`
Username string `yaml:"username" json:"-"`
Password string `yaml:"password" json:"-"`
} `yaml:"rtsp"`
}
@@ -21,36 +24,78 @@ func Init() {
conf.Mod.Listen = ":8554"
app.LoadConfig(&conf)
app.Info["rtsp"] = conf.Mod
log = app.GetLogger("rtsp")
// RTSP client support
streams.HandleFunc("rtsp", rtspHandler)
streams.HandleFunc("rtsps", rtspHandler)
streams.HandleFunc("rtspx", rtspHandler)
// RTSP server support
address := conf.Mod.Listen
if address != "" {
_, Port, _ = net.SplitHostPort(address)
go worker(address)
if address == "" {
return
}
ln, err := net.Listen("tcp", address)
if err != nil {
log.Error().Err(err).Msg("[rtsp] listen")
return
}
_, Port, _ = net.SplitHostPort(address)
log.Info().Str("addr", address).Msg("[rtsp] listen")
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
c := rtsp.NewServer(conn)
// skip check auth for localhost
if conf.Mod.Username != "" && !conn.RemoteAddr().(*net.TCPAddr).IP.IsLoopback() {
c.Auth(conf.Mod.Username, conf.Mod.Password)
}
go tcpHandler(c)
}
}()
}
type Handler func(conn *rtsp.Conn) bool
func HandleFunc(handler Handler) {
handlers = append(handlers, handler)
}
var Port string
var OnProducer func(conn streamer.Producer) bool // TODO: maybe rewrite...
// internal
var log zerolog.Logger
var handlers []Handler
func rtspHandler(url string) (streamer.Producer, error) {
backchannel := true
if i := strings.IndexByte(url, '#'); i > 0 {
if url[i+1:] == "backchannel=0" {
backchannel = false
}
url = url[:i]
}
conn, err := rtsp.NewClient(url)
if err != nil {
return nil, err
}
conn.UserAgent = app.UserAgent
if log.Trace().Enabled() {
conn.Listen(func(msg interface{}) {
switch msg := msg.(type) {
@@ -65,108 +110,123 @@ func rtspHandler(url string) (streamer.Producer, error) {
if err = conn.Dial(); err != nil {
return nil, err
}
conn.Backchannel = backchannel
if err = conn.Describe(); err != nil {
return nil, err
if !backchannel {
return nil, err
}
// second try without backchannel, we need to reconnect
conn.Backchannel = false
if err = conn.Dial(); err != nil {
return nil, err
}
if err = conn.Describe(); err != nil {
return nil, err
}
}
return conn, nil
}
func worker(address string) {
srv, err := tcp.NewServer(address)
if err != nil {
log.Error().Err(err).Msg("[rtsp] listen")
return
}
func tcpHandler(conn *rtsp.Conn) {
var name string
var closer func()
log.Info().Str("addr", address).Msg("[rtsp] listen")
trace := log.Trace().Enabled()
srv.Listen(func(msg interface{}) {
switch msg.(type) {
case net.Conn:
var name string
var onDisconnect func()
conn.Listen(func(msg interface{}) {
if trace {
switch msg := msg.(type) {
case *tcp.Request:
log.Trace().Msgf("[rtsp] server request:\n%s", msg)
case *tcp.Response:
log.Trace().Msgf("[rtsp] server response:\n%s", msg)
}
}
trace := log.Trace().Enabled()
conn := rtsp.NewServer(msg.(net.Conn))
conn.Listen(func(msg interface{}) {
if trace {
switch msg := msg.(type) {
case *tcp.Request:
log.Trace().Msgf("[rtsp] server request:\n%s", msg)
case *tcp.Response:
log.Trace().Msgf("[rtsp] server response:\n%s", msg)
}
}
switch msg {
case rtsp.MethodDescribe:
name = conn.URL.Path[1:]
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
stream := streams.Get(name) // TODO: rewrite
if stream == nil {
return
}
initMedias(conn)
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
return
}
onDisconnect = func() {
stream.RemoveConsumer(conn)
}
case rtsp.MethodAnnounce:
if OnProducer != nil {
if OnProducer(conn) {
return
}
}
name = conn.URL.Path[1:]
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
str := streams.Get(conn.URL.Path[1:])
if str == nil {
return
}
str.AddProducer(conn)
onDisconnect = func() {
str.RemoveProducer(conn)
}
case streamer.StatePlaying:
log.Debug().Str("stream", name).Msg("[rtsp] start")
}
})
if err = conn.Accept(); err != nil {
log.Warn().Err(err).Msg("[rtsp] accept")
switch msg {
case rtsp.MethodDescribe:
if len(conn.URL.Path) == 0 {
log.Warn().Msg("[rtsp] server empty URL on DESCRIBE")
return
}
if err = conn.Handle(); err != nil {
//log.Warn().Err(err).Msg("[rtsp] handle server")
name = conn.URL.Path[1:]
stream := streams.Get(name)
if stream == nil {
return
}
if onDisconnect != nil {
onDisconnect()
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
conn.SessionName = app.UserAgent
initMedias(conn)
if err := stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
return
}
log.Debug().Str("stream", name).Msg("[rtsp] disconnect")
closer = func() {
stream.RemoveConsumer(conn)
}
case rtsp.MethodAnnounce:
if len(conn.URL.Path) == 0 {
log.Warn().Msg("[rtsp] server empty URL on ANNOUNCE")
return
}
name = conn.URL.Path[1:]
stream := streams.Get(name)
if stream == nil {
return
}
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
stream.AddProducer(conn)
closer = func() {
stream.RemoveProducer(conn)
}
case streamer.StatePlaying:
log.Debug().Str("stream", name).Msg("[rtsp] start")
}
})
srv.Serve()
if err := conn.Accept(); err != nil {
log.Warn().Err(err).Caller().Send()
if closer != nil {
closer()
}
_ = conn.Close()
return
}
for _, handler := range handlers {
if handler(conn) {
return
}
}
if closer != nil {
if err := conn.Handle(); err != nil {
log.Debug().Msgf("[rtsp] handle=%s", err)
}
closer()
log.Debug().Str("stream", name).Msg("[rtsp] disconnect")
}
_ = conn.Close()
}
func initMedias(conn *rtsp.Conn) {
@@ -174,16 +234,27 @@ func initMedias(conn *rtsp.Conn) {
for key, value := range conn.URL.Query() {
switch key {
case streamer.KindVideo, streamer.KindAudio:
for _, value := range value {
for _, name := range value {
name = strings.ToUpper(name)
// check aliases
switch name {
case "COPY":
name = "" // pass empty codecs list
case "MJPEG":
name = streamer.CodecJPEG
case "AAC":
name = streamer.CodecAAC
}
media := &streamer.Media{
Kind: key, Direction: streamer.DirectionRecvonly,
}
switch value {
case "", "copy": // pass empty codecs list
default:
codec := streamer.NewCodec(value)
media.Codecs = append(media.Codecs, codec)
// empty codecs match all codecs
if name != "" {
// empty clock rate and channels match any values
media.Codecs = []*streamer.Codec{{Name: name}}
}
conn.Medias = append(conn.Medias, media)

45
cmd/srtp/srtp.go Normal file
View File

@@ -0,0 +1,45 @@
package srtp
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/pkg/srtp"
"net"
)
func Init() {
var cfg struct {
Mod struct {
Listen string `yaml:"listen"`
} `yaml:"srtp"`
}
// default config
cfg.Mod.Listen = ":8443"
// load config from YAML
app.LoadConfig(&cfg)
if cfg.Mod.Listen == "" {
return
}
log := app.GetLogger("srtp")
// create SRTP server (endpoint) for receiving video from HomeKit camera
conn, err := net.ListenPacket("udp", cfg.Mod.Listen)
if err != nil {
log.Warn().Err(err).Caller().Send()
}
log.Info().Str("addr", cfg.Mod.Listen).Msg("[srtp] listen")
// run server
go func() {
Server = &srtp.Server{}
if err = Server.Serve(conn); err != nil {
log.Warn().Err(err).Caller().Send()
}
}()
}
var Server *srtp.Server

15
cmd/streams/consumer.go Normal file
View File

@@ -0,0 +1,15 @@
package streams
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
type Consumer struct {
element streamer.Consumer
tracks []*streamer.Track
}
func (c *Consumer) MarshalJSON() ([]byte, error) {
return json.Marshal(c.element)
}

View File

@@ -4,27 +4,36 @@ import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
"sync"
)
type Handler func(url string) (streamer.Producer, error)
var handlers map[string]Handler
var handlers = map[string]Handler{}
var handlersMu sync.Mutex
func HandleFunc(scheme string, handler Handler) {
if handlers == nil {
handlers = make(map[string]Handler)
}
handlersMu.Lock()
handlers[scheme] = handler
handlersMu.Unlock()
}
func getHandler(url string) Handler {
i := strings.IndexByte(url, ':')
if i <= 0 { // TODO: i < 4 ?
return nil
}
handlersMu.Lock()
defer handlersMu.Unlock()
return handlers[url[:i]]
}
func HasProducer(url string) bool {
i := strings.IndexByte(url, ':')
return handlers[url[:i]] != nil
return getHandler(url) != nil
}
func GetProducer(url string) (streamer.Producer, error) {
i := strings.IndexByte(url, ':')
handler := handlers[url[:i]]
handler := getHandler(url)
if handler == nil {
return nil, fmt.Errorf("unsupported scheme: %s", url)
}

View File

@@ -1,7 +1,11 @@
package streams
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
"sync"
"time"
)
type state byte
@@ -11,71 +15,192 @@ const (
stateMedias
stateTracks
stateStart
stateExternal
)
type Producer struct {
streamer.Element
url string
url string
template string
element streamer.Producer
lastErr error
tracks []*streamer.Track
state state
state state
mu sync.Mutex
workerID int
}
func (p *Producer) SetSource(s string) {
if p.template == "" {
p.template = p.url
}
p.url = strings.Replace(p.template, "{input}", s, 1)
}
func (p *Producer) GetMedias() []*streamer.Media {
if p.state == stateNone {
log.Debug().Str("url", p.url).Msg("[streams] probe producer")
p.mu.Lock()
defer p.mu.Unlock()
var err error
p.element, err = GetProducer(p.url)
if err != nil {
log.Error().Err(err).Str("url", p.url).Msg("[streams] probe producer")
if p.state == stateNone {
log.Debug().Msgf("[streams] probe producer url=%s", p.url)
p.element, p.lastErr = GetProducer(p.url)
if p.lastErr != nil || p.element == nil {
log.Error().Err(p.lastErr).Str("url", p.url).Caller().Send()
return nil
}
p.state = stateMedias
}
// if element in reconnect state
if p.element == nil {
return nil
}
return p.element.GetMedias()
}
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
if p.state == stateMedias {
p.state = stateTracks
p.mu.Lock()
defer p.mu.Unlock()
if p.state == stateNone {
return nil
}
track := p.element.GetTrack(media, codec)
for _, t := range p.tracks {
if track == t {
for _, track := range p.tracks {
if track.Codec == codec {
return track
}
}
track := p.element.GetTrack(media, codec)
if track == nil {
return nil
}
p.tracks = append(p.tracks, track)
if p.state == stateMedias {
p.state = stateTracks
}
return track
}
func (p *Producer) MarshalJSON() ([]byte, error) {
if p.element != nil {
return json.Marshal(p.element)
}
info := streamer.Info{URL: p.url}
return json.Marshal(info)
}
// internals
func (p *Producer) start() {
p.mu.Lock()
defer p.mu.Unlock()
if p.state != stateTracks {
return
}
log.Debug().Str("url", p.url).Msg("[streams] start producer")
log.Debug().Msgf("[streams] start producer url=%s", p.url)
p.state = stateStart
go p.element.Start()
p.workerID++
go p.worker(p.element, p.workerID)
}
func (p *Producer) worker(element streamer.Producer, workerID int) {
if err := element.Start(); err != nil {
p.mu.Lock()
closed := p.workerID != workerID
p.mu.Unlock()
if closed {
return
}
log.Warn().Err(err).Str("url", p.url).Caller().Send()
}
p.reconnect(workerID)
}
func (p *Producer) reconnect(workerID int) {
p.mu.Lock()
defer p.mu.Unlock()
if p.workerID != workerID {
log.Trace().Msgf("[streams] stop reconnect url=%s", p.url)
return
}
log.Debug().Msgf("[streams] reconnect to url=%s", p.url)
p.element, p.lastErr = GetProducer(p.url)
if p.lastErr != nil || p.element == nil {
log.Debug().Msgf("[streams] producer=%s", p.lastErr)
// TODO: dynamic timeout
time.AfterFunc(30*time.Second, func() {
p.reconnect(workerID)
})
return
}
medias := p.element.GetMedias()
// convert all old producer tracks to new tracks
for i, oldTrack := range p.tracks {
// match new element medias with old track codec
for _, media := range medias {
codec := media.MatchCodec(oldTrack.Codec)
if codec == nil {
continue
}
// move sink from old track to new track
newTrack := p.element.GetTrack(media, codec)
newTrack.GetSink(oldTrack)
p.tracks[i] = newTrack
break
}
}
go p.worker(p.element, workerID)
}
func (p *Producer) stop() {
log.Debug().Str("url", p.url).Msg("[streams] stop producer")
p.mu.Lock()
defer p.mu.Unlock()
switch p.state {
case stateExternal:
log.Debug().Msgf("[streams] can't stop external producer")
return
case stateNone:
log.Debug().Msgf("[streams] can't stop none producer")
return
case stateStart:
p.workerID++
}
log.Debug().Msgf("[streams] stop producer url=%s", p.url)
if p.element != nil {
_ = p.element.Stop()
p.element = nil
}
_ = p.element.Stop()
p.element = nil
p.tracks = nil
p.state = stateNone
p.tracks = nil
}

View File

@@ -2,56 +2,76 @@ package streams
import (
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
"sync"
"sync/atomic"
)
type Consumer struct {
element streamer.Consumer
tracks []*streamer.Track
}
type Stream struct {
producers []*Producer
consumers []*Consumer
mu sync.Mutex
requests int32
}
func NewStream(source interface{}) *Stream {
s := new(Stream)
switch source := source.(type) {
case string:
s := new(Stream)
prod := &Producer{url: source}
s.producers = append(s.producers, prod)
return s
case []interface{}:
s := new(Stream)
for _, source := range source {
prod := &Producer{url: source.(string)}
s.producers = append(s.producers, prod)
}
return s
case *Stream:
return source
case map[string]interface{}:
return NewStream(source["url"])
case nil:
return new(Stream)
default:
panic("wrong source type")
}
}
return s
func (s *Stream) SetSource(source string) {
for _, prod := range s.producers {
prod.SetSource(source)
}
}
func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
// support for multiple simultaneous requests from different consumers
atomic.AddInt32(&s.requests, 1)
ic := len(s.consumers)
consumer := &Consumer{element: cons}
var producers []*Producer // matched producers for consumer
var codecs string
// Step 1. Get consumer medias
for icc, consMedia := range cons.GetMedias() {
log.Trace().Stringer("media", consMedia).
Msgf("[streams] consumer:%d:%d candidate", ic, icc)
Msgf("[streams] consumer=%d candidate=%d", ic, icc)
producers:
for ip, prod := range s.producers {
// Step 2. Get producer medias (not tracks yet)
for ipc, prodMedia := range prod.GetMedias() {
log.Trace().Stringer("media", prodMedia).
Msgf("[streams] producer:%d:%d candidate", ip, ipc)
Msgf("[streams] producer=%d candidate=%d", ip, ipc)
collectCodecs(prodMedia, &codecs)
// Step 3. Match consumer/producer codecs list
prodCodec := prodMedia.MatchMedia(consMedia)
@@ -62,7 +82,7 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
// Step 4. Get producer track
prodTrack := prod.GetTrack(prodMedia, prodCodec)
if prodTrack == nil {
log.Warn().Msg("[stream] can't get track")
log.Warn().Str("url", prod.url).Msg("[streams] can't get track")
continue
}
@@ -70,20 +90,37 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
consTrack := consumer.element.AddTrack(consMedia, prodTrack)
consumer.tracks = append(consumer.tracks, consTrack)
producers = append(producers, prod)
break producers
}
}
}
}
// can't match tracks for consumer
if len(consumer.tracks) == 0 {
return nil
if atomic.AddInt32(&s.requests, -1) == 0 {
s.stopProducers()
}
s.consumers = append(s.consumers, consumer)
if len(producers) == 0 {
if len(codecs) > 0 {
return errors.New("codecs not match: " + codecs)
}
for _, prod := range s.producers {
for i, producer := range s.producers {
if producer.lastErr != nil {
return fmt.Errorf("source %d error: %w", i, producer.lastErr)
}
}
return fmt.Errorf("sources unavailable: %d", len(s.producers))
}
s.mu.Lock()
s.consumers = append(s.consumers, consumer)
s.mu.Unlock()
// there may be duplicates, but that's not a problem
for _, prod := range producers {
prod.start()
}
@@ -91,6 +128,7 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
}
func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
s.mu.Lock()
for i, consumer := range s.consumers {
if consumer.element == cons {
// remove consumer pads from all producers
@@ -102,57 +140,73 @@ func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
break
}
}
s.mu.Unlock()
for _, producer := range s.producers {
var sink bool
for _, track := range producer.tracks {
if len(track.Sink) > 0 {
sink = true
}
}
if !sink {
producer.stop()
}
}
s.stopProducers()
}
func (s *Stream) AddProducer(prod streamer.Producer) {
panic("not implemented")
producer := &Producer{element: prod, state: stateExternal}
s.mu.Lock()
s.producers = append(s.producers, producer)
s.mu.Unlock()
}
func (s *Stream) RemoveProducer(prod streamer.Producer) {
panic("not implemented")
}
func (s *Stream) Active() bool {
if len(s.consumers) > 0 {
return true
}
for _, prod := range s.producers {
if prod.element != nil {
return true
s.mu.Lock()
for i, producer := range s.producers {
if producer.element == prod {
s.removeProducer(i)
break
}
}
return false
s.mu.Unlock()
}
func (s *Stream) stopProducers() {
s.mu.Lock()
producers:
for _, producer := range s.producers {
for _, track := range producer.tracks {
if track.HasSink() {
continue producers
}
}
producer.stop()
}
s.mu.Unlock()
}
//func (s *Stream) Active() bool {
// if len(s.consumers) > 0 {
// return true
// }
//
// for _, prod := range s.producers {
// if prod.element != nil {
// return true
// }
// }
//
// return false
//}
func (s *Stream) MarshalJSON() ([]byte, error) {
var v []interface{}
for _, prod := range s.producers {
if prod.element != nil {
v = append(v, prod.element)
}
if !s.mu.TryLock() {
log.Warn().Msgf("[streams] json locked")
return json.Marshal(nil)
}
for _, cons := range s.consumers {
// cons.element always not nil
v = append(v, cons.element)
var info struct {
Producers []*Producer `json:"producers"`
Consumers []*Consumer `json:"consumers"`
}
if len(v) == 0 {
v = nil
}
return json.Marshal(v)
info.Producers = s.producers
info.Consumers = s.consumers
s.mu.Unlock()
return json.Marshal(info)
}
func (s *Stream) removeConsumer(i int) {
@@ -180,3 +234,23 @@ func (s *Stream) removeProducer(i int) {
s.producers = append(s.producers[:i], s.producers[i+1:]...)
}
}
func collectCodecs(media *streamer.Media, codecs *string) {
if media.Direction == streamer.DirectionRecvonly {
return
}
for _, codec := range media.Codecs {
name := codec.Name
if name == streamer.CodecAAC {
name = "AAC"
}
if strings.Contains(*codecs, name) {
continue
}
if len(*codecs) > 0 {
*codecs += ","
}
*codecs += name
}
}

View File

@@ -2,6 +2,7 @@ package streams
import (
"github.com/AlexxIT/go2rtc/pkg/fake"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/stretchr/testify/assert"
"testing"
@@ -103,7 +104,7 @@ a=control:streamid=0
func TestRouting(t *testing.T) {
prod := &fake.Producer{}
prod.Medias, _ = streamer.UnmarshalRTSPSDP([]byte(dahuaSimple))
prod.Medias, _ = rtsp.UnmarshalSDP([]byte(dahuaSimple))
assert.Len(t, prod.Medias, 3)
HandleFunc("fake", func(url string) (streamer.Producer, error) {

View File

@@ -1,8 +1,12 @@
package streams
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/app/store"
"github.com/rs/zerolog"
"net/http"
)
func Init() {
@@ -17,36 +21,61 @@ func Init() {
for name, item := range cfg.Mod {
streams[name] = NewStream(item)
}
for name, item := range store.GetDict("streams") {
streams[name] = NewStream(item)
}
api.HandleFunc("api/streams", streamsHandler)
}
func Get(name string) *Stream {
if stream, ok := streams[name]; ok {
return streams[name]
}
func New(name string, source interface{}) *Stream {
stream := NewStream(source)
streams[name] = stream
return stream
}
func GetOrNew(src string) *Stream {
if stream, ok := streams[src]; ok {
return stream
}
if HasProducer(name) {
log.Info().Str("url", name).Msg("[streams] create new stream")
stream := NewStream(name)
streams[name] = stream
return stream
if !HasProducer(src) {
return nil
}
return nil
log.Info().Str("url", src).Msg("[streams] create new stream")
return New(src, src)
}
func Delete(name string) {
delete(streams, name)
}
func streamsHandler(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
func All() map[string]interface{} {
all := map[string]interface{}{}
for name, stream := range streams {
all[name] = stream
//if stream.Active() {
// all[name] = stream
//}
switch r.Method {
case "PUT":
name := r.URL.Query().Get("name")
if name == "" {
name = src
}
New(name, src)
return
case "DELETE":
delete(streams, src)
return
}
if src != "" {
e := json.NewEncoder(w)
e.SetIndent("", " ")
_ = e.Encode(streams[src])
} else {
_ = json.NewEncoder(w).Encode(streams)
}
return all
}
var log zerolog.Logger

View File

@@ -2,19 +2,40 @@ package webrtc
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/pion/sdp/v3"
"strings"
)
var candidates []string
var networks = []string{"udp", "tcp"}
func AddCandidate(address string) {
candidates = append(candidates, address)
}
func addCanditates(answer string) (string, error) {
func asyncCandidates(tr *api.Transport) {
for _, address := range candidates {
address, err := webrtc.LookupIP(address)
if err != nil {
log.Warn().Err(err).Caller().Send()
continue
}
for _, network := range networks {
cand, err := webrtc.NewCandidate(network, address)
if err != nil {
log.Warn().Err(err).Caller().Send()
continue
}
log.Trace().Str("candidate", cand).Msg("[webrtc] config")
tr.Write(&api.Message{Type: "webrtc/candidate", Value: cand})
}
}
}
func syncCanditates(answer string) (string, error) {
if len(candidates) == 0 {
return answer, nil
}
@@ -32,24 +53,22 @@ func addCanditates(answer string) (string, error) {
}
for _, address := range candidates {
if strings.HasPrefix(address, "stun:") {
ip, err := webrtc.GetPublicIP()
if err != nil {
log.Warn().Err(err).Msg("[webrtc] public IP")
continue
}
address = ip.String() + address[4:]
log.Debug().Str("addr", address).Msg("[webrtc] stun public address")
}
cand, err := webrtc.NewCandidate(address)
var err error
address, err = webrtc.LookupIP(address)
if err != nil {
log.Warn().Err(err).Msg("[webrtc] candidate")
continue
}
md.WithPropertyAttribute(cand)
for _, network := range networks {
cand, err := webrtc.NewCandidate(network, address)
if err != nil {
log.Warn().Err(err).Msg("[webrtc] candidate")
continue
}
md.WithPropertyAttribute(cand)
}
}
if end {
@@ -64,12 +83,14 @@ func addCanditates(answer string) (string, error) {
return string(data), nil
}
func candidateHandler(ctx *api.Context, msg *streamer.Message) {
if ctx.Consumer == nil {
return
func candidateHandler(tr *api.Transport, msg *api.Message) error {
if tr.Consumer == nil {
return nil
}
if conn := ctx.Consumer.(*webrtc.Conn); conn != nil {
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] remote")
conn.Push(msg)
if conn := tr.Consumer.(*webrtc.Conn); conn != nil {
s := msg.Value.(string)
log.Trace().Str("candidate", s).Msg("[webrtc] remote")
conn.AddCandidate(s)
}
return nil
}

View File

@@ -1,14 +1,16 @@
package webrtc
import (
"errors"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
"github.com/rs/zerolog"
"io"
"net"
"net/http"
)
func Init() {
@@ -20,6 +22,7 @@ func Init() {
} `yaml:"webrtc"`
}
cfg.Mod.Listen = ":8555"
cfg.Mod.IceServers = []pion.ICEServer{
{URLs: []string{"stun:stun.l.google.com:19302"}},
}
@@ -31,7 +34,7 @@ func Init() {
address := cfg.Mod.Listen
pionAPI, err := webrtc.NewAPI(address)
if pionAPI == nil {
log.Error().Err(err).Msg("[webrtc] init API")
log.Error().Err(err).Caller().Msg("webrtc.NewAPI")
return
}
@@ -53,8 +56,10 @@ func Init() {
candidates = cfg.Mod.Candidates
api.HandleWS(webrtc.MsgTypeOffer, offerHandler)
api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler)
api.HandleWS("webrtc/offer", asyncHandler)
api.HandleWS("webrtc/candidate", candidateHandler)
api.HandleFunc("api/webrtc", syncHandler)
}
var Port string
@@ -62,14 +67,14 @@ var log zerolog.Logger
var NewPConn func() (*pion.PeerConnection, error)
func offerHandler(ctx *api.Context, msg *streamer.Message) {
name := ctx.Request.URL.Query().Get("url")
stream := streams.Get(name)
func asyncHandler(tr *api.Transport, msg *api.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return
return errors.New(api.StreamNotFound)
}
log.Debug().Str("stream", name).Msg("[webrtc] new consumer")
log.Debug().Str("url", src).Msg("[webrtc] new consumer")
var err error
@@ -77,21 +82,23 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
conn := new(webrtc.Conn)
conn.Conn, err = NewPConn()
if err != nil {
log.Error().Err(err).Msg("[webrtc] new conn")
return
log.Error().Err(err).Caller().Send()
return err
}
conn.UserAgent = ctx.Request.UserAgent()
conn.UserAgent = tr.Request.UserAgent()
conn.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case streamer.EventType:
if msg == streamer.StateNull {
case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateClosed {
stream.RemoveConsumer(conn)
}
case *streamer.Message:
// subscribe on webrtc server candidates
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] local")
ctx.Write(msg)
case *pion.ICECandidate:
if msg != nil {
s := msg.ToJSON().Candidate
log.Trace().Str("candidate", s).Msg("[webrtc] local")
tr.Write(&api.Message{Type: "webrtc/candidate", Value: s})
}
}
})
@@ -100,40 +107,61 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
if err = conn.SetOffer(offer); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] set offer")
ctx.Error(err)
return
log.Warn().Err(err).Caller().Send()
return err
}
// 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
ctx.Error(err)
return
log.Debug().Err(err).Msg("[webrtc] add consumer")
_ = conn.Conn.Close()
return err
}
conn.Init()
// exchange sdp without waiting all candidates
//answer, err := conn.ExchangeSDP(offer, false)
//answer, err := conn.GetAnswer()
answer, err := conn.GetCompleteAnswer()
if err == nil {
answer, err = addCanditates(answer)
}
// 3. Exchange SDP without waiting all candidates
answer, err := conn.GetAnswer()
log.Trace().Msgf("[webrtc] answer\n%s", answer)
if err != nil {
log.Error().Err(err).Msg("[webrtc] get answer")
ctx.Error(err)
log.Error().Err(err).Caller().Send()
return err
}
tr.Consumer = conn
tr.Write(&api.Message{Type: "webrtc/answer", Value: answer})
asyncCandidates(tr)
return nil
}
func syncHandler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("src")
stream := streams.Get(url)
if stream == nil {
return
}
ctx.Write(&streamer.Message{
Type: webrtc.MsgTypeAnswer, Value: answer,
})
// get offer
offer, err := io.ReadAll(r.Body)
if err != nil {
log.Error().Err(err).Caller().Msg("ioutil.ReadAll")
return
}
ctx.Consumer = conn
answer, err := ExchangeSDP(stream, string(offer), r.UserAgent())
if err != nil {
log.Error().Err(err).Caller().Msg("ExchangeSDP")
return
}
// send SDP to client
if _, err = w.Write([]byte(answer)); err != nil {
log.Error().Err(err).Caller().Msg("w.Write")
}
}
func ExchangeSDP(
@@ -143,15 +171,15 @@ func ExchangeSDP(
conn := new(webrtc.Conn)
conn.Conn, err = NewPConn()
if err != nil {
log.Error().Err(err).Msg("[webrtc] new conn")
log.Error().Err(err).Caller().Msg("NewPConn")
return
}
conn.UserAgent = userAgent
conn.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case streamer.EventType:
if msg == streamer.StateNull {
case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateClosed {
stream.RemoveConsumer(conn)
}
}
@@ -161,13 +189,14 @@ func ExchangeSDP(
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
if err = conn.SetOffer(offer); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] set offer")
log.Warn().Err(err).Caller().Msg("conn.SetOffer")
return
}
// 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
log.Warn().Err(err).Caller().Msg("stream.AddConsumer")
_ = conn.Conn.Close()
return
}
@@ -177,12 +206,12 @@ func ExchangeSDP(
//answer, err := conn.ExchangeSDP(offer, false)
answer, err = conn.GetCompleteAnswer()
if err == nil {
answer, err = addCanditates(answer)
answer, err = syncCanditates(answer)
}
log.Trace().Msgf("[webrtc] answer\n%s", answer)
if err != nil {
log.Error().Err(err).Msg("[webrtc] get answer")
log.Error().Err(err).Caller().Msg("conn.GetCompleteAnswer")
}
return

View File

@@ -1,58 +0,0 @@
package main
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/pion/rtp"
"os"
"time"
)
func main() {
client, err := rtsp.NewClient(os.Args[1])
if err != nil {
panic(err)
}
if err = client.Dial(); err != nil {
panic(err)
}
if err = client.Describe(); err != nil {
panic(err)
}
for _, media := range client.GetMedias() {
fmt.Printf("Media: %v\n", media)
if media.AV() {
track := client.GetTrack(media, media.Codecs[0])
fmt.Printf("Track: %v, %v\n", track, track.Codec)
track.Bind(func(packet *rtp.Packet) error {
nalUnitType := packet.Payload[0] & 0x1F
fmt.Printf(
"[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d\n",
track.Codec.Name, nalUnitType, len(packet.Payload), packet.Timestamp,
packet.PayloadType, packet.SSRC,
)
return nil
})
}
}
if err = client.Play(); err != nil {
panic(err)
}
time.AfterFunc(time.Second*5, func() {
if err = client.Close(); err != nil {
panic(err)
}
})
if err = client.Handle(); err != nil {
panic(err)
}
fmt.Println("The End")
}

26
go.mod
View File

@@ -1,41 +1,59 @@
module github.com/AlexxIT/go2rtc
go 1.17
go 1.19
require (
github.com/brutella/hap v0.0.17
github.com/deepch/vdk v0.0.19
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/mdns v1.0.5
github.com/pion/ice/v2 v2.2.6
github.com/pion/interceptor v0.1.11
github.com/pion/rtcp v1.2.9
github.com/pion/rtp v1.7.13
github.com/pion/sdp/v3 v3.0.5
github.com/pion/srtp/v2 v2.0.10
github.com/pion/stun v0.3.5
github.com/pion/webrtc/v3 v3.1.43
github.com/rs/zerolog v1.27.0
github.com/stretchr/testify v1.7.1
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/brutella/dnssd v1.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-chi/chi v1.5.4 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/miekg/dns v1.1.50 // indirect
github.com/pion/datachannel v1.5.2 // indirect
github.com/pion/dtls/v2 v2.1.5 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.5 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.2 // indirect
github.com/pion/srtp/v2 v2.0.10 // indirect
github.com/pion/transport v0.13.1 // indirect
github.com/pion/turn/v2 v2.0.8 // indirect
github.com/pion/udp v0.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 // indirect
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.11 // indirect
)
replace github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e
replace (
// windows support: https://github.com/brutella/dnssd/pull/35
github.com/brutella/dnssd v1.2.2 => github.com/rblenkinsopp/dnssd v1.2.3-0.20220516082132-0923f3c787a1
// RTP tlv8 fix
github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045
// fix reading AAC config bytes
github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92
)

85
go.sum
View File

@@ -1,15 +1,19 @@
github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e h1:NAgHHZB+JUN3/J4/yq1q1EAc8xwJ8bb/Qp0AcjkfzAA=
github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e/go.mod h1:KqQ/KU3hOc4a62l/jPRH5Hiz5fhTq5cGCl8IqeCxWQI=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045 h1:xJf3FxQJReJSDyYXJfI1NUWv8tUEAGNV9xigLqNtmrI=
github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045/go.mod h1:QNA3sm16zE5uUyC8+E/gNkMvQWjqQLuxQKkU5PMi8N4=
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92 h1:cIeYMGaAirSZnrKRDTb5VgZDDYqPLhYiczElMg4sQW0=
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
github.com/brutella/dnssd v1.2.3 h1:4fBLjZjPH7SbcHhEcIJhZcC9nOhIDZ0m3rn9bjl1/i0=
github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@@ -23,94 +27,72 @@ 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.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.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/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
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/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
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=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U=
github.com/pion/dtls/v2 v2.0.2/go.mod h1:27PEO3MDdaCfo21heT59/vsdmZc0zMt9wQPcSlLu/1I=
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c=
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
github.com/pion/ice v0.7.18 h1:KbAWlzWRUdX9SmehBh3gYpIFsirjhSQsCw6K2MjYMK0=
github.com/pion/ice v0.7.18/go.mod h1:+Bvnm3nYC6Nnp7VV6glUkuOfToB/AtMRZpOU8ihuf4c=
github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs=
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
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.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0=
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k=
github.com/pion/randutil v0.0.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
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.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I=
github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI=
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E=
github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw=
github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA=
github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
github.com/pion/srtp/v2 v2.0.10 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w=
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA=
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog=
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
github.com/pion/webrtc/v2 v2.2.26/go.mod h1:XMZbZRNHyPDe1gzTIHFcQu02283YO45CbiwFgKvXnmc=
github.com/pion/webrtc/v3 v3.1.41/go.mod h1:sUcW9SFPEWerDqGOBmdYEMfRvbdd7rgwo4bNzfsXww4=
github.com/pion/webrtc/v3 v3.1.43 h1:YT3ZTO94UT4kSBvZnRAH82+0jJPUruiKr9CEstdlQzk=
github.com/pion/webrtc/v3 v3.1.43/go.mod h1:G/J8k0+grVsjC/rjCZ24AKoCCxcFFODgh7zThNZGs0M=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -121,38 +103,41 @@ github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg3JUlQABcc0dH2VQ4Y+9s=
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 h1:y+mHpWoQJNAHt26Nhh6JP7hvM71IRZureyvZhoVALIs=
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
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=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
@@ -164,36 +149,44 @@ golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug
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-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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-20211216021012-1d35b9e2eb4e/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-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/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 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
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.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
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=
@@ -211,9 +204,9 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

52
hardware.Dockerfile Normal file
View File

@@ -0,0 +1,52 @@
# 0. Prepare images
# only debian 12 (bookworm) has latest ffmpeg
ARG DEBIAN_VERSION="bookworm-slim"
ARG GO_VERSION="1.19-buster"
ARG NGROK_VERSION="3"
FROM debian:${DEBIAN_VERSION} AS base
FROM golang:${GO_VERSION} AS go
FROM ngrok/ngrok:${NGROK_VERSION} AS ngrok
# 1. Build go2rtc binary
FROM go AS build
WORKDIR /build
# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
# 2. Collect all files
FROM scratch AS rootfs
COPY --from=build /build/go2rtc /usr/local/bin/
COPY --from=ngrok /bin/ngrok /usr/local/bin/
COPY ./build/docker/run.sh /
# 3. Final image
FROM base
# Install ffmpeg, bash (for run.sh), tini (for signal handling),
# and other common tools for the echo source.
# non-free for Intel QSV support (not used by go2rtc, just for tests)
RUN echo 'deb http://deb.debian.org/debian bookworm non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
apt-get -y update && apt-get -y install tini ffmpeg python3 curl jq intel-media-va-driver-non-free
COPY --from=rootfs / /
RUN chmod a+x /run.sh && mkdir -p /config
ENTRYPOINT ["/usr/bin/tini", "--"]
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
ENV NVIDIA_VISIBLE_DEVICES all
ENV NVIDIA_DRIVER_CAPABILITIES compute,video,utility
CMD ["/run.sh"]

25
main.go
View File

@@ -3,13 +3,20 @@ package main
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/debug"
"github.com/AlexxIT/go2rtc/cmd/echo"
"github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
"github.com/AlexxIT/go2rtc/cmd/hass"
"github.com/AlexxIT/go2rtc/cmd/mse"
"github.com/AlexxIT/go2rtc/cmd/homekit"
"github.com/AlexxIT/go2rtc/cmd/http"
"github.com/AlexxIT/go2rtc/cmd/ivideon"
"github.com/AlexxIT/go2rtc/cmd/mjpeg"
"github.com/AlexxIT/go2rtc/cmd/mp4"
"github.com/AlexxIT/go2rtc/cmd/ngrok"
"github.com/AlexxIT/go2rtc/cmd/rtmp"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/srtp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"os"
@@ -21,18 +28,28 @@ func main() {
app.Init() // init config and logs
streams.Init() // load streams list
api.Init() // init HTTP API server
echo.Init()
rtsp.Init() // add support RTSP client and RTSP server
rtmp.Init() // add support RTMP client
exec.Init() // add support exec scheme (depends on RTSP server)
ffmpeg.Init() // add support ffmpeg scheme (depends on exec scheme)
hass.Init() // add support hass scheme
api.Init() // init HTTP API server
webrtc.Init()
mse.Init()
mp4.Init()
mjpeg.Init()
http.Init()
srtp.Init()
homekit.Init()
ivideon.Init()
ngrok.Init()
debug.Init()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

8
pkg/README.md Normal file
View File

@@ -0,0 +1,8 @@
## Useful links
- https://www.wowza.com/blog/streaming-protocols
- https://vimeo.com/blog/post/rtmp-stream/
- https://sanjeev-pandey.medium.com/understanding-the-mpeg-4-moov-atom-pseudo-streaming-in-mp4-93935e1b9e9a
- [Android Supported media formats](https://developer.android.com/guide/topics/media/media-formats)
- [THEOplayer](https://www.theoplayer.com/test-your-stream-hls-dash-hesp)
- [How Generate DTS/PTS](https://www.ramugedia.com/how-generate-dts-pts-from-elementary-stream)

20
pkg/aac/README.md Normal file
View File

@@ -0,0 +1,20 @@
## AAC-LD and AAC-ELD
Codec | Rate | QuickTime | ffmpeg | VLC
------|------|-----------|--------|----
AAC-LD | 8000 | yes | no | no
AAC-LD | 16000 | yes | no | no
AAC-LD | 22050 | yes | yes | no
AAC-LD | 24000 | yes | yes | no
AAC-LD | 32000 | yes | yes | no
AAC-ELD | 8000 | yes | no | no
AAC-ELD | 16000 | yes | no | no
AAC-ELD | 22050 | yes | yes | yes
AAC-ELD | 24000 | yes | yes | yes
AAC-ELD | 32000 | yes | yes | yes
## Useful links
- [4.6.20 Enhanced Low Delay Codec](https://csclub.uwaterloo.ca/~ehashman/ISO14496-3-2009.pdf)
- https://stackoverflow.com/questions/40014508/aac-adts-for-aacobject-eld-packets
- https://code.videolan.org/videolan/vlc/-/blob/master/modules/packetizer/mpeg4audio.c

57
pkg/aac/rtp.go Normal file
View File

@@ -0,0 +1,57 @@
package aac
import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
const RTPPacketVersionAAC = 0
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
// support ONLY 2 bytes header size!
// streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
headersSize := binary.BigEndian.Uint16(packet.Payload) >> 3
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
clone := *packet
clone.Version = RTPPacketVersionAAC
clone.Payload = packet.Payload[2+headersSize:]
return push(&clone)
}
}
}
func RTPPay(mtu uint16) streamer.WrapperFunc {
sequencer := rtp.NewRandomSequencer()
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
if packet.Version != RTPPacketVersionAAC {
return push(packet)
}
// support ONLY one unit in payload
size := uint16(len(packet.Payload))
// 2 bytes header size + 2 bytes first payload size
payload := make([]byte, 2+2+size)
payload[1] = 16 // header size in bits
binary.BigEndian.PutUint16(payload[2:], size<<3)
copy(payload[4:], packet.Payload)
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: payload,
}
return push(&clone)
}
}
}

View File

@@ -24,7 +24,7 @@ func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *strea
panic("you shall not pass!")
}
track := &streamer.Track{Codec: codec, Direction: media.Direction}
track := streamer.NewTrack(codec, media.Direction)
switch media.Direction {
case streamer.DirectionSendonly:

View File

@@ -1,3 +1,22 @@
# H264
Access Unit (AU) can contain one or multiple NAL Unit:
1. [SEI,] SPS, PPS, IFrame, [IFrame...]
2. BFrame, [BFrame...]
3. IFrame, [IFrame...]
## RTP H264
Camera | NALu
-------|-----
EZVIZ C3S | 7f, 8f, 28:28:28 -> 5t, 28:28:28 -> 1t, 1t, 1t, 1t
Sonoff GK-200MP2-B | 28:28:28 -> 5t, 1t, 1t, 1t
Dahua IPC-K42 | 7f, 8f, 28:28:28 -> 5t, 28:28:28 -> 1t, 28:28:28 -> 1t
FFmpeg copy | 5t, 1t, 1t, 28:28:28 -> 1t, 28:28:28 -> 1t
FFmpeg h264 | 24 -> 6:5:5:5:5t, 24 -> 1:1:1:1t, 28:28:28 -> 5f, 28:28:28 -> 5f, 28:28:28 -> 5t
FFmpeg resize | 6f, 28:28:28 -> 5f, 28... -> 5t, 24 -> 1:1f, 24 -> 1:1t
## WebRTC
Video codec | Media string | Device
@@ -25,3 +44,4 @@ H.264/high | avc1.6400xx | FFmpeg superfast
- [AVC levels](https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels)
- [AVC profiles table](https://developer.mozilla.org/ru/docs/Web/Media/Formats/codecs_parameter)
- [Supported Media for Google Cast](https://developers.google.com/cast/docs/media)
- [Two stream formats, Annex-B, AVCC (H.264) and HVCC (H.265)](https://www.programmersought.com/article/3901815022/)

View File

@@ -6,55 +6,71 @@ import (
"github.com/pion/rtp"
)
const PayloadTypeAVC = 255
func EncodeAVC(nals ...[]byte) (avc []byte) {
var i, n int
func IsAVC(codec *streamer.Codec) bool {
return codec.PayloadType == PayloadTypeAVC
}
for _, nal := range nals {
if i = len(nal); i > 0 {
n += 4 + i
}
}
avc = make([]byte, n)
n = 0
for _, nal := range nals {
if i = len(nal); i > 0 {
binary.BigEndian.PutUint32(avc[n:], uint32(i))
n += 4 + copy(avc[n+4:], nal)
}
}
func EncodeAVC(raw []byte) (avc []byte) {
avc = make([]byte, len(raw)+4)
binary.BigEndian.PutUint32(avc, uint32(len(raw)))
copy(avc[4:], raw)
return
}
func RepairAVC(track *streamer.Track) streamer.WrapperFunc {
sps, pps := GetParameterSet(track.Codec.FmtpLine)
sps = EncodeAVC(sps)
pps = EncodeAVC(pps)
ps := EncodeAVC(sps, pps)
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) (err error) {
naluType := NALUType(packet.Payload)
switch naluType {
case NALUTypeSPS:
sps = packet.Payload
return
case NALUTypePPS:
pps = packet.Payload
return
if NALUType(packet.Payload) == NALUTypeIFrame {
packet.Payload = Join(ps, packet.Payload)
}
var clone rtp.Packet
if naluType == NALUTypeIFrame {
clone = *packet
clone.Payload = sps
if err = push(&clone); err != nil {
return
}
clone = *packet
clone.Payload = pps
if err = push(&clone); err != nil {
return
}
}
clone = *packet
clone.Payload = packet.Payload
return push(&clone)
return push(packet)
}
}
}
func SplitAVC(data []byte) [][]byte {
var nals [][]byte
for {
// get AVC length
size := int(binary.BigEndian.Uint32(data)) + 4
// check if multiple items in one packet
if size < len(data) {
nals = append(nals, data[:size])
data = data[size:]
} else {
nals = append(nals, data)
break
}
}
return nals
}
func Types(data []byte) []byte {
var types []byte
for {
types = append(types, NALUType(data))
size := 4 + int(binary.BigEndian.Uint32(data))
if size < len(data) {
data = data[size:]
} else {
break
}
}
return types
}

View File

@@ -2,21 +2,69 @@ package h264
import (
"encoding/base64"
"encoding/binary"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
const (
NALUTypePFrame = 1
NALUTypeIFrame = 5
NALUTypeSPS = 7
NALUTypePPS = 8
NALUTypePFrame = 1 // Coded slice of a non-IDR picture
NALUTypeIFrame = 5 // Coded slice of an IDR picture
NALUTypeSEI = 6 // Supplemental enhancement information (SEI)
NALUTypeSPS = 7 // Sequence parameter set
NALUTypePPS = 8 // Picture parameter set
NALUTypeAUD = 9 // Access unit delimiter
)
func NALUType(b []byte) byte {
return b[4] & 0x1F
}
// IsKeyframe - check if any NALU in one AU is Keyframe
func IsKeyframe(b []byte) bool {
for {
switch NALUType(b) {
case NALUTypePFrame:
return false
case NALUTypeIFrame:
return true
}
size := int(binary.BigEndian.Uint32(b)) + 4
if size < len(b) {
b = b[size:]
continue
} else {
return false
}
}
}
func Join(ps, iframe []byte) []byte {
b := make([]byte, len(ps)+len(iframe))
i := copy(b, ps)
copy(b[i:], iframe)
return b
}
func GetProfileLevelID(fmtp string) string {
if fmtp == "" {
return ""
}
// some cameras has wrong profile-level-id
// https://github.com/AlexxIT/go2rtc/issues/155
if s := streamer.Between(fmtp, "sprop-parameter-sets=", ","); s != "" {
sps, _ := base64.StdEncoding.DecodeString(s)
if len(sps) >= 4 {
return fmt.Sprintf("%06X", sps[1:4])
}
}
return streamer.Between(fmtp, "profile-level-id=", ";")
}
func GetParameterSet(fmtp string) (sps, pps []byte) {
if fmtp == "" {
return

View File

@@ -1,6 +1,7 @@
package h264
import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
@@ -12,68 +13,68 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
depack := &codecs.H264Packet{IsAVC: true}
sps, pps := GetParameterSet(track.Codec.FmtpLine)
sps = EncodeAVC(sps)
pps = EncodeAVC(pps)
ps := EncodeAVC(sps, pps)
var buffer []byte
buf := make([]byte, 0, 512*1024) // 512K
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
//println(packet.SequenceNumber, packet.Payload[0]&0x1F, packet.Payload[0], packet.Payload[1], packet.Marker, packet.Timestamp)
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
data, err := depack.Unmarshal(packet.Payload)
if len(data) == 0 || err != nil {
payload, err := depack.Unmarshal(packet.Payload)
if len(payload) == 0 || err != nil {
return nil
}
naluType := NALUType(data)
//println(naluType, len(data))
switch naluType {
case NALUTypeSPS:
//println("new SPS")
sps = data
return nil
case NALUTypePPS:
//println("new PPS")
pps = data
return nil
// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
if packet.Marker {
switch NALUType(payload) {
case NALUTypeSPS, NALUTypePPS:
buf = append(buf, payload...)
return nil
}
}
// ffmpeg with `-tune zerolatency` enable option `-x264opts sliced-threads=1`
// and every NALU will be sliced to multiple NALUs
if len(buf) == 0 {
for {
// Amcrest IP4M-1051: 9, 7, 8, 6, 28...
// Amcrest IP4M-1051: 9, 6, 1
switch NALUType(payload) {
case NALUTypeIFrame:
// fix IFrame without SPS,PPS
buf = append(buf, ps...)
case NALUTypeSEI, NALUTypeAUD:
// fix ffmpeg with transcoding first frame
i := int(4 + binary.BigEndian.Uint32(payload))
// check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A)
if i == len(payload) {
return nil
}
payload = payload[i:]
continue
}
break
}
}
// collect all NALs for Access Unit
if !packet.Marker {
buffer = append(buffer, data...)
buf = append(buf, payload...)
return nil
}
if buffer != nil {
buffer = append(buffer, data...)
data = buffer
buffer = nil
if len(buf) > 0 {
payload = append(buf, payload...)
buf = buf[:0]
}
var clone rtp.Packet
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber)
if naluType == NALUTypeIFrame {
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = sps
if err = push(&clone); err != nil {
return err
}
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = pps
if err = push(&clone); err != nil {
return err
}
}
clone = *packet
clone := *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = data
clone.Payload = payload
return push(&clone)
}
}
@@ -86,28 +87,28 @@ func RTPPay(mtu uint16) streamer.WrapperFunc {
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
if packet.Version == RTPPacketVersionAVC {
payloads := payloader.Payload(mtu, packet.Payload)
for i, payload := range payloads {
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: i == len(payloads)-1,
//PayloadType: packet.PayloadType,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
//SSRC: packet.SSRC,
},
Payload: payload,
}
if err := push(&clone); err != nil {
return err
}
}
return nil
if packet.Version != RTPPacketVersionAVC {
return push(packet)
}
return push(packet)
payloads := payloader.Payload(mtu, packet.Payload)
last := len(payloads) - 1
for i, payload := range payloads {
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: i == last,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: payload,
}
if err := push(&clone); err != nil {
return err
}
}
return nil
}
}
}

4
pkg/h265/README.md Normal file
View File

@@ -0,0 +1,4 @@
## Useful links
- https://datatracker.ietf.org/doc/html/rfc7798
- [Add initial support for WebRTC HEVC](https://trac.webkit.org/changeset/259452/webkit)

73
pkg/h265/helper.go Normal file
View File

@@ -0,0 +1,73 @@
package h265
import (
"encoding/base64"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
const (
NALUTypePFrame = 1
NALUTypeIFrame = 19
NALUTypeIFrame2 = 20
NALUTypeIFrame3 = 21
NALUTypeVPS = 32
NALUTypeSPS = 33
NALUTypePPS = 34
NALUTypeFU = 49
)
func NALUType(b []byte) byte {
return (b[4] >> 1) & 0x3F
}
func IsKeyframe(b []byte) bool {
for {
switch NALUType(b) {
case NALUTypePFrame:
return false
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
return true
}
size := int(binary.BigEndian.Uint32(b)) + 4
if size < len(b) {
b = b[size:]
continue
} else {
return false
}
}
}
func Types(data []byte) []byte {
var types []byte
for {
types = append(types, NALUType(data))
size := 4 + int(binary.BigEndian.Uint32(data))
if size < len(data) {
data = data[size:]
} else {
break
}
}
return types
}
func GetParameterSet(fmtp string) (vps, sps, pps []byte) {
if fmtp == "" {
return
}
s := streamer.Between(fmtp, "sprop-vps=", ";")
vps, _ = base64.StdEncoding.DecodeString(s)
s = streamer.Between(fmtp, "sprop-sps=", ";")
sps, _ = base64.StdEncoding.DecodeString(s)
s = streamer.Between(fmtp, "sprop-pps=", ";")
pps, _ = base64.StdEncoding.DecodeString(s)
return
}

147
pkg/h265/rtp.go Normal file
View File

@@ -0,0 +1,147 @@
package h265
import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
//vps, sps, pps := GetParameterSet(track.Codec.FmtpLine)
//ps := h264.EncodeAVC(vps, sps, pps)
buf := make([]byte, 0, 512*1024) // 512K
var nuStart int
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
data := packet.Payload
nuType := (data[0] >> 1) & 0x3F
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, nuType, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
if nuType == NALUTypeFU {
switch data[2] >> 6 {
case 2: // begin
nuType = data[2] & 0x3F
// push PS data before keyframe
//if len(buf) == 0 && nuType >= 19 && nuType <= 21 {
// buf = append(buf, ps...)
//}
nuStart = len(buf)
buf = append(buf, 0, 0, 0, 0) // NAL unit size
buf = append(buf, (data[0]&0x81)|(nuType<<1), data[1])
buf = append(buf, data[3:]...)
return nil
case 0: // continue
buf = append(buf, data[3:]...)
return nil
case 1: // end
buf = append(buf, data[3:]...)
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4))
}
} else {
nuStart = len(buf)
buf = append(buf, 0, 0, 0, 0) // NAL unit size
buf = append(buf, data...)
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(data)))
}
// collect all NAL Units for Access Unit
if !packet.Marker {
return nil
}
//log.Printf("[HEVC] %v, len: %d", Types(buf), len(buf))
clone := *packet
clone.Version = h264.RTPPacketVersionAVC
clone.Payload = buf
buf = buf[:0]
return push(&clone)
}
}
}
// SafariPay - generate Safari friendly payload for H265
// https://github.com/AlexxIT/Blog/issues/5
func SafariPay(mtu uint16) streamer.WrapperFunc {
sequencer := rtp.NewRandomSequencer()
size := int(mtu - 12) // rtp.Header size
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
return push(packet)
}
// protect original packets from modification
au := make([]byte, len(packet.Payload))
copy(au, packet.Payload)
var start byte
for i := 0; i < len(au); {
size := int(binary.BigEndian.Uint32(au[i:])) + 4
// convert AVC to Annex-B
au[i] = 0
au[i+1] = 0
au[i+2] = 0
au[i+3] = 1
switch NALUType(au[i:]) {
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
start = 3
default:
if start == 0 {
start = 2
}
}
i += size
}
// rtp.Packet payload
b := make([]byte, 1, size)
size-- // minus header byte
for au != nil {
b[0] = start
if start > 1 {
start -= 2
}
if len(au) > size {
b = append(b, au[:size]...)
au = au[size:]
} else {
b = append(b, au...)
au = nil
}
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: au == nil,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: b,
}
if err := push(&clone); err != nil {
return err
}
b = b[:1] // clear buffer
}
return nil
}
}
}

33
pkg/hap/README.md Normal file
View File

@@ -0,0 +1,33 @@
# Home Accessory Protocol
> PS. Character = Characteristic
**Device** - HomeKit end device (swith, camera, etc)
- mDNS name: `MyCamera._hap._tcp.local.`
- DeviceID - mac-like: `0E:AA:CE:2B:35:71`
- HomeKit device is described by:
- one or more `Accessories` - has `AID` and `Services`
- `Services` - has `IID`, `Type` and `Characters`
- `Characters` - has `IID`, `Type`, `Format` and `Value`
**Client** - HomeKit client (iPhone, iPad, MacBook or opensource library)
- ClientID - static random UUID
- ClientPublic/ClientPrivate - static random 32 byte keypair
- can pair with Device (exchange ClientID/ClientPublic, ServerID/ServerPublic using Pin)
- can auth to Device using ClientPrivate
- holding persistant Secure connection to device
- can read device Accessories
- can read and write device Characters
- can subscribe on device Characters change (Event)
**Server** - HomeKit server (soft on end device or opensource library)
- ServerID - same as DeviceID (using for Client auth)
- ServerPublic/ServerPrivate - static random 32 byte keypair
## Useful links
- [Extracting HomeKit Pairing Keys](https://pvieito.com/2019/12/extract-homekit-pairing-keys)
- [HAP in AirPlay2 receiver](https://github.com/openairplay/airplay2-receiver/blob/master/ap2/pairing/hap.py)

62
pkg/hap/accessory.go Normal file
View File

@@ -0,0 +1,62 @@
package hap
type Accessory struct {
AID int `json:"aid"`
Services []*Service `json:"services"`
}
type Accessories struct {
Accessories []*Accessory `json:"accessories"`
}
type Characters struct {
Characters []*Character `json:"characteristics"`
}
func (a *Accessory) GetService(servType string) *Service {
for _, serv := range a.Services {
if serv.Type == servType {
return serv
}
}
return nil
}
func (a *Accessory) GetCharacter(charType string) *Character {
for _, serv := range a.Services {
for _, char := range serv.Characters {
if char.Type == charType {
return char
}
}
}
return nil
}
func (a *Accessory) GetCharacterByID(iid int) *Character {
for _, serv := range a.Services {
for _, char := range serv.Characters {
if char.IID == iid {
return char
}
}
}
return nil
}
type Service struct {
IID int `json:"iid"`
Type string `json:"type"`
Primary bool `json:"primary,omitempty"`
Hidden bool `json:"hidden,omitempty"`
Characters []*Character `json:"characteristics"`
}
func (s *Service) GetCharacter(charType string) *Character {
for _, char := range s.Characters {
if char.Type == charType {
return char
}
}
return nil
}

100
pkg/hap/camera/client.go Normal file
View File

@@ -0,0 +1,100 @@
package camera
import (
"errors"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/brutella/hap/characteristic"
"github.com/brutella/hap/rtp"
)
type Client struct {
client *hap.Conn
}
func NewClient(client *hap.Conn) *Client {
return &Client{client: client}
}
func (c *Client) StartStream(ses *Session) (err error) {
// Step 1. Check if camera ready (free) to stream
var srv *hap.Service
if srv, err = c.GetFreeStream(); err != nil {
return err
}
if srv == nil {
return errors.New("no free streams")
}
if ses.Answer, err = c.SetupEndpoins(srv, ses.Offer); err != nil {
return
}
return c.SetConfig(srv, ses.Config)
}
// GetFreeStream search free streaming service.
// Usual every HomeKit camera can stream only to two clients simultaniosly.
// So it has two similar services for streaming.
func (c *Client) GetFreeStream() (srv *hap.Service, err error) {
var accs []*hap.Accessory
if accs, err = c.client.GetAccessories(); err != nil {
return
}
for _, srv = range accs[0].Services {
for _, char := range srv.Characters {
if char.Type == characteristic.TypeStreamingStatus {
status := rtp.StreamingStatus{}
if err = char.ReadTLV8(&status); err != nil {
return
}
if status.Status == rtp.SessionStatusSuccess {
return
}
}
}
}
return nil, nil
}
func (c *Client) SetupEndpoins(
srv *hap.Service, req *rtp.SetupEndpoints,
) (res *rtp.SetupEndpointsResponse, err error) {
// get setup endpoint character ID
char := srv.GetCharacter(characteristic.TypeSetupEndpoints)
char.Event = nil
// encode new character value
if err = char.Write(req); err != nil {
return
}
// write (put) new endpoint value to device
if err = c.client.PutCharacters(char); err != nil {
return
}
// get new endpoint value from device (response)
if err = c.client.GetCharacter(char); err != nil {
return
}
// decode new endpoint value
res = &rtp.SetupEndpointsResponse{}
if err = char.ReadTLV8(res); err != nil {
return
}
return
}
func (c *Client) SetConfig(srv *hap.Service, config *rtp.StreamConfiguration) (err error) {
// get setup endpoint character ID
char := srv.GetCharacter(characteristic.TypeSelectedStreamConfiguration)
char.Event = nil
// encode new character value
if err = char.Write(config); err != nil {
panic(err)
}
// write (put) new character value to device
return c.client.PutCharacters(char)
}

75
pkg/hap/camera/session.go Normal file
View File

@@ -0,0 +1,75 @@
package camera
import (
cryptorand "crypto/rand"
"encoding/binary"
"github.com/brutella/hap/rtp"
)
type Session struct {
Offer *rtp.SetupEndpoints
Answer *rtp.SetupEndpointsResponse
Config *rtp.StreamConfiguration
}
func NewSession(vp *rtp.VideoParameters, ap *rtp.AudioParameters) *Session {
vp.RTP = rtp.RTPParams{
PayloadType: 99,
Ssrc: RandomUint32(),
Bitrate: 2048,
Interval: 10,
MTU: 1200, // like WebRTC
}
ap.RTP = rtp.RTPParams{
PayloadType: 110,
Ssrc: RandomUint32(),
Bitrate: 32,
Interval: 10,
ComfortNoisePayloadType: 98,
MTU: 0,
}
sessionID := RandomBytes(16)
s := &Session{
Offer: &rtp.SetupEndpoints{
SessionId: sessionID,
Video: rtp.CryptoSuite{
MasterKey: RandomBytes(16),
MasterSalt: RandomBytes(14),
},
Audio: rtp.CryptoSuite{
MasterKey: RandomBytes(16),
MasterSalt: RandomBytes(14),
},
},
Config: &rtp.StreamConfiguration{
Command: rtp.SessionControlCommand{
Identifier: sessionID,
Type: rtp.SessionControlCommandTypeStart,
},
Video: *vp,
Audio: *ap,
},
}
return s
}
func (s *Session) SetLocalEndpoint(host string, port uint16) {
s.Offer.ControllerAddr = rtp.Addr{
IPAddr: host,
VideoRtpPort: port,
AudioRtpPort: port,
}
}
func RandomBytes(size int) []byte {
data := make([]byte, size)
_, _ = cryptorand.Read(data)
return data
}
func RandomUint32() uint32 {
data := make([]byte, 4)
_, _ = cryptorand.Read(data)
return binary.BigEndian.Uint32(data)
}

133
pkg/hap/character.go Normal file
View File

@@ -0,0 +1,133 @@
package hap
import (
"bytes"
"encoding/base64"
"encoding/json"
"github.com/brutella/hap/characteristic"
"github.com/brutella/hap/tlv8"
"io"
"net/http"
)
type Character struct {
AID int `json:"aid,omitempty"`
IID int `json:"iid"`
Type string `json:"type,omitempty"`
Format string `json:"format,omitempty"`
Value interface{} `json:"value,omitempty"`
Event interface{} `json:"ev,omitempty"`
Perms []string `json:"perms,omitempty"`
Description string `json:"description,omitempty"`
//MaxDataLen int `json:"maxDataLen"`
listeners map[io.Writer]bool
}
func (c *Character) AddListener(w io.Writer) {
// TODO: sync.Mutex
if c.listeners == nil {
c.listeners = map[io.Writer]bool{}
}
c.listeners[w] = true
}
func (c *Character) RemoveListener(w io.Writer) {
delete(c.listeners, w)
if len(c.listeners) == 0 {
c.listeners = nil
}
}
func (c *Character) NotifyListeners(ignore io.Writer) error {
if c.listeners == nil {
return nil
}
data, err := c.GenerateEvent()
if err != nil {
return err
}
for w, _ := range c.listeners {
if w == ignore {
continue
}
if _, err = w.Write(data); err != nil {
// error not a problem - just remove listener
c.RemoveListener(w)
}
}
return nil
}
// GenerateEvent with raw HTTP headers
func (c *Character) GenerateEvent() (data []byte, err error) {
chars := Characters{
Characters: []*Character{{AID: DeviceAID, IID: c.IID, Value: c.Value}},
}
if data, err = json.Marshal(chars); err != nil {
return
}
res := http.Response{
StatusCode: http.StatusOK,
ProtoMajor: 1,
ProtoMinor: 0,
Header: http.Header{"Content-Type": []string{MimeJSON}},
ContentLength: int64(len(data)),
Body: io.NopCloser(bytes.NewReader(data)),
}
buf := bytes.NewBuffer([]byte{0})
if err = res.Write(buf); err != nil {
return
}
copy(buf.Bytes(), "EVENT")
return buf.Bytes(), err
}
// Set new value and NotifyListeners
func (c *Character) Set(v interface{}) (err error) {
if err = c.Write(v); err != nil {
return
}
return c.NotifyListeners(nil)
}
// Write new value with right format
func (c *Character) Write(v interface{}) (err error) {
switch c.Format {
case characteristic.FormatTLV8:
var data []byte
if data, err = tlv8.Marshal(v); err != nil {
return
}
c.Value = base64.StdEncoding.EncodeToString(data)
case characteristic.FormatBool:
switch v.(type) {
case bool:
c.Value = v.(bool)
case float64:
c.Value = v.(float64) != 0
}
}
return
}
// ReadTLV8 value to right struct
func (c *Character) ReadTLV8(v interface{}) (err error) {
var data []byte
if data, err = base64.StdEncoding.DecodeString(c.Value.(string)); err != nil {
return
}
return tlv8.Unmarshal(data, v)
}
func (c *Character) ReadBool() bool {
return c.Value.(bool)
}

733
pkg/hap/conn.go Normal file
View File

@@ -0,0 +1,733 @@
package hap
import (
"bufio"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/hap/mdns"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/brutella/hap"
"github.com/brutella/hap/chacha20poly1305"
"github.com/brutella/hap/curve25519"
"github.com/brutella/hap/ed25519"
"github.com/brutella/hap/hkdf"
"github.com/brutella/hap/tlv8"
"github.com/tadglines/go-pkgs/crypto/srp"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
)
// Conn for HomeKit. DevicePublic can be null.
type Conn struct {
streamer.Element
DeviceAddress string // including port
DeviceID string
DevicePublic []byte
ClientID string
ClientPrivate []byte
OnEvent func(res *http.Response)
Output func(msg interface{})
conn net.Conn
secure *Secure
httpResponse chan *bufio.Reader
}
func NewConn(rawURL string) (*Conn, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
query := u.Query()
c := &Conn{
DeviceAddress: u.Host,
DeviceID: query.Get("device_id"),
DevicePublic: DecodeKey(query.Get("device_public")),
ClientID: query.Get("client_id"),
ClientPrivate: DecodeKey(query.Get("client_private")),
}
return c, nil
}
func Pair(deviceID, pin string) (*Conn, error) {
entry := mdns.GetEntry(deviceID)
if entry == nil {
return nil, errors.New("can't find device via mDNS")
}
c := &Conn{
DeviceAddress: fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port),
DeviceID: deviceID,
ClientID: GenerateUUID(),
ClientPrivate: GenerateKey(),
}
var mfi bool
for _, field := range entry.InfoFields {
if field[:2] == "ff" {
if field[3] == '1' {
mfi = true
}
break
}
}
return c, c.Pair(mfi, pin)
}
func (c *Conn) ClientPublic() []byte {
return c.ClientPrivate[32:]
}
func (c *Conn) URL() string {
return fmt.Sprintf(
"homekit://%s?device_id=%s&device_public=%16x&client_id=%s&client_private=%32x",
c.DeviceAddress, c.DeviceID, c.DevicePublic, c.ClientID, c.ClientPrivate,
)
}
func (c *Conn) DialAndServe() error {
if err := c.Dial(); err != nil {
return err
}
return c.Handle()
}
func (c *Conn) Dial() error {
// update device host before dial
if host := mdns.GetAddress(c.DeviceID); host != "" {
c.DeviceAddress = host
}
var err error
c.conn, err = net.DialTimeout("tcp", c.DeviceAddress, time.Second*5)
if err != nil {
return err
}
// STEP M1: send our session public to device
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
// 1. generate payload
// important not include other fields
requestM1 := struct {
State byte `tlv8:"6"`
PublicKey []byte `tlv8:"3"`
}{
State: hap.M1,
PublicKey: sessionPublic[:],
}
// 2. pack payload to TLV8
buf, err := tlv8.Marshal(requestM1)
if err != nil {
return err
}
// 3. send request
resp, err := c.Post(UriPairVerify, buf)
if err != nil {
return err
}
// STEP M2: unpack deviceID from response
responseM2 := PairVerifyPayload{}
if err = tlv8.UnmarshalReader(resp.Body, &responseM2); err != nil {
return err
}
// 1. generate session shared key
var deviceSessionPublic [32]byte
copy(deviceSessionPublic[:], responseM2.PublicKey)
sessionShared := curve25519.SharedSecret(sessionPrivate, deviceSessionPublic)
sessionKey, err := hkdf.Sha512(
sessionShared[:], []byte("Pair-Verify-Encrypt-Salt"),
[]byte("Pair-Verify-Encrypt-Info"),
)
// 2. decrypt M2 response with session key
msg := responseM2.EncryptedData[:len(responseM2.EncryptedData)-16]
var mac [16]byte
copy(mac[:], responseM2.EncryptedData[len(msg):]) // 16 byte (MAC)
buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PV-Msg02"), msg, mac, nil,
)
// 3. unpack payload from TLV8
payloadM2 := PairVerifyPayload{}
if err = tlv8.Unmarshal(buf, &payloadM2); err != nil {
return err
}
// 4. verify signature for M2 response with device public
// device session + device id + our session
if c.DevicePublic != nil {
buf = nil
buf = append(buf, responseM2.PublicKey[:]...)
buf = append(buf, []byte(payloadM2.Identifier)...)
buf = append(buf, sessionPublic[:]...)
if !ed25519.ValidateSignature(
c.DevicePublic[:], buf, payloadM2.Signature,
) {
return errors.New("device public signature invalid")
}
}
// STEP M3: send our clientID to device
// 1. generate signature with our private key
// (our session + our ID + device session)
buf = nil
buf = append(buf, sessionPublic[:]...)
buf = append(buf, []byte(c.ClientID)...)
buf = append(buf, responseM2.PublicKey[:]...)
signature, err := ed25519.Signature(c.ClientPrivate[:], buf)
if err != nil {
return err
}
// 2. generate payload
payloadM3 := struct {
Identifier string `tlv8:"1"`
Signature []byte `tlv8:"10"`
}{
Identifier: c.ClientID,
Signature: signature,
}
// 3. pack payload to TLV8
buf, err = tlv8.Marshal(payloadM3)
if err != nil {
return err
}
// 4. encrypt payload with session key
msg, mac, _ = chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PV-Msg03"), buf, nil,
)
// 4. generate request
requestM3 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
}{
State: hap.M3,
EncryptedData: append(msg, mac[:]...),
}
// 5. pack payload to TLV8
buf, err = tlv8.Marshal(requestM3)
if err != nil {
return err
}
resp, err = c.Post(UriPairVerify, buf)
if err != nil {
return err
}
// STEP M4. Read response
responseM4 := PairVerifyPayload{}
if err = tlv8.UnmarshalReader(resp.Body, &responseM4); err != nil {
return err
}
// 1. check response state
if responseM4.State != 4 || responseM4.Status != 0 {
return fmt.Errorf("wrong M4 response: %+v", responseM4)
}
c.secure, err = NewSecure(sessionShared, false)
//c.secure.Buffer = bytes.NewBuffer(nil)
c.secure.Conn = c.conn
c.httpResponse = make(chan *bufio.Reader, 10)
return err
}
// https://github.com/apple/HomeKitADK/blob/master/HAP/HAPPairingPairSetup.c
func (c *Conn) Pair(mfi bool, pin string) (err error) {
pin = strings.ReplaceAll(pin, "-", "")
if len(pin) != 8 {
return fmt.Errorf("wrong PIN format: %s", pin)
}
pin = pin[:3] + "-" + pin[3:5] + "-" + pin[5:]
c.conn, err = net.Dial("tcp", c.DeviceAddress)
if err != nil {
return
}
// STEP M1. Generate request
reqM1 := struct {
Method byte `tlv8:"0"`
State byte `tlv8:"6"`
}{
State: hap.M1,
}
if mfi {
reqM1.Method = 1 // ff=1 => method=1, ff=2 => method=0
}
buf, err := tlv8.Marshal(reqM1)
if err != nil {
return
}
// STEP M1. Send request
res, err := c.Post(UriPairSetup, buf)
if err != nil {
return
}
// STEP M2. Read response
resM2 := struct {
Salt []byte `tlv8:"2"`
PublicKey []byte `tlv8:"3"` // server public key, aka session.B
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
if err = tlv8.UnmarshalReader(res.Body, &resM2); err != nil {
return
}
if resM2.State != 2 || resM2.Error > 0 {
return fmt.Errorf("wrong M2: %+v", resM2)
}
// STEP M3. Generate session using pin
username := []byte("Pair-Setup")
SRP, err := srp.NewSRP(
"rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username),
)
if err != nil {
return
}
SRP.SaltLength = 16
// username: "Pair-Setup"
// password: PIN (with dashes)
session := SRP.NewClientSession(username, []byte(pin))
sessionShared, err := session.ComputeKey(resM2.Salt, resM2.PublicKey)
if err != nil {
return
}
// STEP M3. Generate request
reqM3 := struct {
PublicKey []byte `tlv8:"3"`
Proof []byte `tlv8:"4"`
State byte `tlv8:"6"`
}{
PublicKey: session.GetA(), // client public key, aka session.A
Proof: session.ComputeAuthenticator(),
State: hap.M3,
}
buf, err = tlv8.Marshal(reqM3)
if err != nil {
return err
}
// STEP M3. Send request
res, err = c.Post(UriPairSetup, buf)
if err != nil {
return
}
// STEP M4. Read response
resM4 := struct {
Proof []byte `tlv8:"4"` // server proof
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
if err = tlv8.UnmarshalReader(res.Body, &resM4); err != nil {
return
}
if resM4.Error == 2 {
return fmt.Errorf("wrong PIN: %s", pin)
}
if resM4.State != 4 || resM4.Error > 0 {
return fmt.Errorf("wrong M4: %+v", resM4)
}
// STEP M4. Verify response
if !session.VerifyServerAuthenticator(resM4.Proof) {
return errors.New("verify server auth fail")
}
// STEP M5. Generate signature
saltKey, err := hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"),
[]byte("Pair-Setup-Controller-Sign-Info"),
)
if err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, []byte(c.ClientID)...)
buf = append(buf, c.ClientPublic()...)
signature, err := ed25519.Signature(c.ClientPrivate, buf)
if err != nil {
return
}
// STEP M5. Generate payload
msgM5 := struct {
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{
Identifier: c.ClientID,
PublicKey: c.ClientPublic(),
Signature: signature,
}
buf, err = tlv8.Marshal(msgM5)
if err != nil {
return
}
// STEP M5. Encrypt payload
sessionKey, err := hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Encrypt-Salt"),
[]byte("Pair-Setup-Encrypt-Info"),
)
if err != nil {
return
}
buf, mac, _ := chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PS-Msg05"), buf, nil,
)
// STEP M5. Generate request
reqM5 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
}{
EncryptedData: append(buf, mac[:]...),
State: hap.M5,
}
buf, err = tlv8.Marshal(reqM5)
if err != nil {
return err
}
// STEP M5. Send request
res, err = c.Post(UriPairSetup, buf)
if err != nil {
return
}
// STEP M6. Read response
resM6 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
if err = tlv8.UnmarshalReader(res.Body, &resM6); err != nil {
return
}
if resM6.State != 6 || resM6.Error > 0 {
return fmt.Errorf("wrong M6: %+v", resM2)
}
// STEP M6. Decrypt payload
msg := resM6.EncryptedData[:len(resM6.EncryptedData)-16]
copy(mac[:], resM6.EncryptedData[len(msg):]) // 16 byte (MAC)
buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PS-Msg06"), msg, mac, nil,
)
if err != nil {
return
}
msgM6 := struct {
Identifier []byte `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{}
if err = tlv8.Unmarshal(buf, &msgM6); err != nil {
return
}
// STEP M6. Verify payload
if saltKey, err = hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Accessory-Sign-Salt"),
[]byte("Pair-Setup-Accessory-Sign-Info"),
); err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, msgM6.Identifier...)
buf = append(buf, msgM6.PublicKey...)
if !ed25519.ValidateSignature(
msgM6.PublicKey[:], buf, msgM6.Signature,
) {
return errors.New("wrong server signature")
}
if c.DeviceID != string(msgM6.Identifier) {
return fmt.Errorf("wrong Device ID: %s", msgM6.Identifier)
}
c.DevicePublic = msgM6.PublicKey
return nil
}
func (c *Conn) Close() error {
if c.conn == nil {
return nil
}
conn := c.conn
c.conn = nil
return conn.Close()
}
func (c *Conn) GetAccessories() ([]*Accessory, error) {
res, err := c.Get("/accessories")
if err != nil {
return nil, err
}
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
p := Accessories{}
if err = json.Unmarshal(data, &p); err != nil {
return nil, err
}
for _, accs := range p.Accessories {
for _, serv := range accs.Services {
for _, char := range serv.Characters {
char.AID = accs.AID
}
}
}
return p.Accessories, nil
}
func (c *Conn) GetCharacters(query string) ([]*Character, error) {
res, err := c.Get("/characteristics?id=" + query)
if err != nil {
return nil, err
}
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
ch := Characters{}
if err = json.Unmarshal(data, &ch); err != nil {
return nil, err
}
return ch.Characters, nil
}
func (c *Conn) GetCharacter(char *Character) error {
query := fmt.Sprintf("%d.%d", char.AID, char.IID)
chars, err := c.GetCharacters(query)
if err != nil {
return err
}
char.Value = chars[0].Value
return nil
}
func (c *Conn) PutCharacters(characters ...*Character) (err error) {
for i, char := range characters {
if char.Event != nil {
char = &Character{AID: char.AID, IID: char.IID, Event: char.Event}
} else {
char = &Character{AID: char.AID, IID: char.IID, Value: char.Value}
}
characters[i] = char
}
var data []byte
if data, err = json.Marshal(Characters{characters}); err != nil {
return
}
var res *http.Response
if res, err = c.Put("/characteristics", data); err != nil {
return
}
if res.StatusCode >= 400 {
return errors.New("wrong response status")
}
return
}
func (c *Conn) GetImage(width, height int) ([]byte, error) {
res, err := c.Post(
"/resource", []byte(fmt.Sprintf(
`{"image-width":%d,"image-height":%d,"resource-type":"image","reason":0}`,
width, height,
)),
)
if err != nil {
return nil, err
}
return io.ReadAll(res.Body)
}
//func (c *Client) onEventData(r io.Reader) error {
// if c.OnEvent == nil {
// return nil
// }
//
// data, err := io.ReadAll(r)
//
// ch := Characters{}
// if err = json.Unmarshal(data, &ch); err != nil {
// return err
// }
//
// c.OnEvent(ch.Characters)
//
// return nil
//}
func (c *Conn) ListPairings() error {
pReq := struct {
Method byte `tlv8:"0"`
State byte `tlv8:"6"`
}{
Method: hap.MethodListPairings,
State: hap.M1,
}
data, err := tlv8.Marshal(pReq)
if err != nil {
return err
}
res, err := c.Post("/pairings", data)
if err != nil {
return err
}
data, err = io.ReadAll(res.Body)
// TODO: don't know how to fix array of items
var pRes struct {
State byte `tlv8:"6"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Permission byte `tlv8:"11"`
}
if err = tlv8.Unmarshal(data, &pRes); err != nil {
return err
}
return nil
}
func (c *Conn) PairingsAdd(clientID string, clientPublic []byte, admin bool) error {
pReq := struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
State byte `tlv8:"6"`
Permission byte `tlv8:"11"`
}{
Method: hap.MethodAddPairing,
Identifier: clientID,
PublicKey: clientPublic,
State: hap.M1,
Permission: hap.PermissionUser,
}
if admin {
pReq.Permission = hap.PermissionAdmin
}
data, err := tlv8.Marshal(pReq)
if err != nil {
return err
}
res, err := c.Post("/pairings", data)
if err != nil {
return err
}
data, err = io.ReadAll(res.Body)
var pRes struct {
State byte `tlv8:"6"`
Unknown byte `tlv8:"7"`
}
if err = tlv8.Unmarshal(data, &pRes); err != nil {
return err
}
return nil
}
func (c *Conn) DeletePairing(id string) error {
reqM1 := struct {
State byte `tlv8:"6"`
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
}{
State: hap.M1,
Method: hap.MethodDeletePairing,
Identifier: id,
}
data, err := tlv8.Marshal(reqM1)
if err != nil {
return err
}
res, err := c.Post("/pairings", data)
if err != nil {
return err
}
data, err = io.ReadAll(res.Body)
var resM2 struct {
State byte `tlv8:"6"`
}
if err = tlv8.Unmarshal(data, &resM2); err != nil {
return err
}
if resM2.State != hap.M2 {
return errors.New("wrong state")
}
return nil
}
func (c *Conn) LocalAddr() string {
return c.conn.LocalAddr().String()
}
func DecodeKey(s string) []byte {
if s == "" {
return nil
}
data, err := hex.DecodeString(s)
if err != nil {
return nil
}
return data
}

90
pkg/hap/helpers.go Normal file
View File

@@ -0,0 +1,90 @@
package hap
import (
"crypto/rand"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
)
const DeviceAID = 1 // TODO: fix someday
func GenerateID(name string) string {
sum := sha512.Sum512([]byte(name))
return fmt.Sprintf(
"%02X:%02X:%02X:%02X:%02X:%02X",
sum[0], sum[1], sum[2], sum[3], sum[4], sum[5],
)
}
func GenerateUUID() string {
//12345678-9012-3456-7890-123456789012
data := make([]byte, 16)
_, _ = rand.Read(data)
s := hex.EncodeToString(data)
return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:]
}
type PairVerifyPayload struct {
Method byte `tlv8:"0,optional"`
Identifier string `tlv8:"1,optional"`
PublicKey []byte `tlv8:"3,optional"`
EncryptedData []byte `tlv8:"5,optional"`
State byte `tlv8:"6,optional"`
Status byte `tlv8:"7,optional"`
Signature []byte `tlv8:"10,optional"`
}
//func (c *Character) Unmarshal(value interface{}) error {
// switch c.Format {
// case characteristic.FormatTLV8:
// data, err := base64.StdEncoding.DecodeString(c.Value.(string))
// if err != nil {
// return err
// }
// return tlv8.Unmarshal(data, value)
// }
// return nil
//}
//func (c *Character) Marshal(value interface{}) error {
// switch c.Format {
// case characteristic.FormatTLV8:
// data, err := tlv8.Marshal(value)
// if err != nil {
// return err
// }
// c.Value = base64.StdEncoding.EncodeToString(data)
// }
// return nil
//}
func (c *Character) String() string {
data, err := json.Marshal(c)
if err != nil {
return "ERROR"
}
return string(data)
}
func UnmarshalEvent(res *http.Response) (char *Character, err error) {
var data []byte
if data, err = io.ReadAll(res.Body); err != nil {
return
}
ch := Characters{}
if err = json.Unmarshal(data, &ch); err != nil {
return
}
if len(ch.Characters) > 1 {
panic("not implemented")
}
char = ch.Characters[0]
return
}

246
pkg/hap/http.go Normal file
View File

@@ -0,0 +1,246 @@
package hap
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"net/http"
"net/textproto"
"strconv"
)
const (
MimeTLV8 = "application/pairing+tlv8"
MimeJSON = "application/hap+json"
UriPairSetup = "/pair-setup"
UriPairVerify = "/pair-verify"
UriPairings = "/pairings"
UriAccessories = "/accessories"
UriCharacteristics = "/characteristics"
UriResource = "/resource"
)
func (c *Conn) Write(p []byte) (r io.Reader, err error) {
if c.secure == nil {
if _, err = c.conn.Write(p); err == nil {
r = bufio.NewReader(c.conn)
}
} else {
if _, err = c.secure.Write(p); err == nil {
r = <-c.httpResponse
}
}
return
}
func (c *Conn) Do(req *http.Request) (*http.Response, error) {
if c.secure == nil {
// insecure requests
if err := req.Write(c.conn); err != nil {
return nil, err
}
return http.ReadResponse(bufio.NewReader(c.conn), req)
}
// secure support write interface to connection
if err := req.Write(c.secure); err != nil {
return nil, err
}
// get decrypted buffer from connection
buf := <-c.httpResponse
return http.ReadResponse(buf, req)
}
func (c *Conn) Get(uri string) (*http.Response, error) {
req, err := http.NewRequest(
"GET", "http://"+c.DeviceAddress+uri, nil,
)
if err != nil {
return nil, err
}
return c.Do(req)
}
func (c *Conn) Post(uri string, data []byte) (*http.Response, error) {
req, err := http.NewRequest(
"POST", "http://"+c.DeviceAddress+uri,
bytes.NewReader(data),
)
if err != nil {
return nil, err
}
switch uri {
case "/pair-verify", "/pairings":
req.Header.Set("Content-Type", MimeTLV8)
case UriResource:
req.Header.Set("Content-Type", MimeJSON)
}
return c.Do(req)
}
func (c *Conn) Put(uri string, data []byte) (*http.Response, error) {
req, err := http.NewRequest(
"PUT", "http://"+c.DeviceAddress+uri,
bytes.NewReader(data),
)
if err != nil {
return nil, err
}
switch uri {
case UriCharacteristics:
req.Header.Set("Content-Type", MimeJSON)
}
return c.Do(req)
}
func (c *Conn) Handle() (err error) {
defer func() {
if c.conn == nil {
err = nil
}
}()
b := make([]byte, 512000)
for {
var total, content int
header := -1
for {
var n1 int
n1, err = c.secure.Read(b[total:])
if err != nil {
return err
}
if n1 == 0 {
return io.EOF
}
total += n1
// TODO: rewrite
if header == -1 {
// step 1. wait whole header
header = bytes.Index(b[:total], []byte("\r\n\r\n"))
if header < 0 {
continue
}
header += 4
// step 2. check content-length
i1 := bytes.Index(b[:total], []byte("Content-Length: "))
if i1 < 0 {
break
}
i1 += 16
i2 := bytes.IndexByte(b[i1:total], '\r')
content, err = strconv.Atoi(string(b[i1 : i1+i2]))
if err != nil {
break
}
}
if total >= header+content {
break
}
}
// copy slice to buffer
buf := bytes.NewBuffer(make([]byte, 0, total))
buf.Write(b[:total])
r := bufio.NewReader(buf)
// EVENT/1.0 200 OK
if b[0] == 'E' {
if c.OnEvent == nil {
continue
}
tp := textproto.NewReader(r)
var s string
if s, err = tp.ReadLine(); err != nil {
return err
}
if s != "EVENT/1.0 200 OK" {
return errors.New("wrong response")
}
var mimeHeader textproto.MIMEHeader
if mimeHeader, err = tp.ReadMIMEHeader(); err != nil {
return err
}
var cl int
if cl, err = strconv.Atoi(
mimeHeader.Get("Content-Length"),
); err != nil {
return err
}
res := http.Response{
StatusCode: 200,
Proto: "EVENT/1.0",
ProtoMajor: 1,
ProtoMinor: 0,
Header: http.Header(mimeHeader),
ContentLength: int64(cl),
Body: io.NopCloser(r),
}
c.OnEvent(&res)
continue
}
//if bytes.Index(b, []byte("image/jpeg")) > 0 {
// if total, err = c.secure.Read(b); err != nil {
// return
// }
// buf.Write(b[:total])
//}
c.httpResponse <- r
}
}
func WriteStatusCode(w io.Writer, statusCode int) (err error) {
body := []byte(fmt.Sprintf(
"HTTP/1.1 %d %s\n\n", statusCode, http.StatusText(statusCode),
))
//print("<<<", string(body), "<<<\n")
_, err = w.Write(body)
return
}
func WriteResponse(
w io.Writer, statusCode int, contentType string, body []byte,
) (err error) {
header := fmt.Sprintf(
"HTTP/1.1 %d %s\nContent-Type: %s\nContent-Length: %d\n\n",
statusCode, http.StatusText(statusCode), contentType, len(body),
)
body = append([]byte(header), body...)
//print("<<<", string(body), "<<<\n")
_, err = w.Write(body)
return
}
func WriteChunked(w io.Writer, contentType string, body []byte) (err error) {
header := fmt.Sprintf(
"HTTP/1.1 200 OK\nContent-Type: %s\nTransfer-Encoding: chunked\n\n%x\n",
contentType, len(body),
)
body = append([]byte(header), body...)
body = append(body, "\n0\n\n"...)
//print("<<<", string(body), "<<<\n")
_, err = w.Write(body)
return
}

42
pkg/hap/mdns/client.go Normal file
View File

@@ -0,0 +1,42 @@
package mdns
import (
"fmt"
"github.com/hashicorp/mdns"
"strings"
)
const Suffix = "._hap._tcp.local."
func GetAll() chan *mdns.ServiceEntry {
entries := make(chan *mdns.ServiceEntry)
params := &mdns.QueryParam{
Service: "_hap._tcp", Entries: entries, DisableIPv6: true,
}
go func() {
_ = mdns.Query(params)
close(entries)
}()
return entries
}
func GetAddress(deviceID string) string {
for entry := range GetAll() {
if strings.Contains(entry.Info, deviceID) {
return fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port)
}
}
return ""
}
func GetEntry(deviceID string) *mdns.ServiceEntry {
for entry := range GetAll() {
if strings.Contains(entry.Info, deviceID) {
return entry
}
}
return nil
}

53
pkg/hap/mdns/server.go Normal file
View File

@@ -0,0 +1,53 @@
package mdns
import (
"github.com/hashicorp/mdns"
"net"
)
const HostHeaderTail = "._hap._tcp.local"
func NewServer(name string, port int, ips []net.IP, txt []string) (*mdns.Server, error) {
if ips == nil || ips[0] == nil {
ips = LocalIPs()
}
// important to set hostName manually with any value and `.local.` tail
// important to set ips manually
service, _ := mdns.NewMDNSService(
name, "_hap._tcp", "", name+".local.", port, ips, txt,
)
return mdns.NewServer(&mdns.Config{Zone: service})
}
func LocalIPs() []net.IP {
ifaces, err := net.Interfaces()
if err != nil {
return nil
}
var ips []net.IP
for _, iface := range ifaces {
if iface.Flags&net.FlagUp == 0 {
continue // interface down
}
if iface.Flags&net.FlagLoopback != 0 {
continue // loopback interface
}
var addrs []net.Addr
if addrs, err = iface.Addrs(); err != nil {
continue
}
for _, addr := range addrs {
switch addr := addr.(type) {
case *net.IPNet:
ips = append(ips, addr.IP)
case *net.IPAddr:
ips = append(ips, addr.IP)
}
}
}
return ips
}

410
pkg/hap/pairing.go Normal file
View File

@@ -0,0 +1,410 @@
package hap
import (
"bufio"
"crypto/sha512"
"errors"
"github.com/brutella/hap"
"github.com/brutella/hap/chacha20poly1305"
"github.com/brutella/hap/curve25519"
"github.com/brutella/hap/ed25519"
"github.com/brutella/hap/hkdf"
"github.com/brutella/hap/tlv8"
"github.com/tadglines/go-pkgs/crypto/srp"
"net"
"net/http"
)
type pairSetupPayload struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
Salt []byte `tlv8:"2"`
PublicKey []byte `tlv8:"3"`
Proof []byte `tlv8:"4"`
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
RetryDelay byte `tlv8:"8"`
Certificate []byte `tlv8:"9"`
Signature []byte `tlv8:"10"`
Permissions byte `tlv8:"11"`
FragmentData []byte `tlv8:"13"`
FragmentLast []byte `tlv8:"14"`
}
func (s *Server) PairSetupHandler(
conn net.Conn, req *http.Request,
) (clientID string, err error) {
// STEP 1. Request from iPhone
payloadM1 := pairSetupPayload{}
if err = tlv8.UnmarshalReader(req.Body, &payloadM1); err != nil {
return
}
if payloadM1.State != hap.M1 {
err = errors.New("wrong state")
return
}
// generate our session public and salt using PIN
username := []byte("Pair-Setup")
var SRP *srp.SRP
if SRP, err = srp.NewSRP(
"rfc5054.3072", sha512.New,
keyDerivativeFuncRFC2945(username),
); err != nil {
return
}
SRP.SaltLength = 16
var salt, verifier []byte
if salt, verifier, err = SRP.ComputeVerifier([]byte(s.Pin)); err != nil {
return
}
session := SRP.NewServerSession(username, salt, verifier)
// STEP 2. Response to iPhone
payloadM2 := struct {
Salt []byte `tlv8:"2"`
PublicKey []byte `tlv8:"3"`
State byte `tlv8:"6"`
}{
State: hap.M2,
PublicKey: session.GetB(),
Salt: salt,
}
var buf []byte
if buf, err = tlv8.Marshal(payloadM2); err != nil {
return
}
if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil {
return
}
// STEP 3. Request from iPhone
r := bufio.NewReader(conn)
if req, err = http.ReadRequest(r); err != nil {
return
}
payloadM3 := pairSetupPayload{}
if err = tlv8.UnmarshalReader(req.Body, &payloadM3); err != nil {
return
}
if payloadM3.State != hap.M3 {
err = errors.New("wrong state")
return
}
// important to compute key before verify client
var sessionShared []byte
if sessionShared, err = session.ComputeKey(payloadM3.PublicKey); err != nil {
return
}
// support skip pin verify (any pin accepted)
if s.Pin != "" && !session.VerifyClientAuthenticator(payloadM3.Proof) {
err = errors.New("client proof is invalid")
return
}
serverProof := session.ComputeAuthenticator(payloadM3.Proof)
// STEP 4. Response to iPhone
payloadM4 := struct {
Proof []byte `tlv8:"4"`
State byte `tlv8:"6"`
}{
State: hap.M4, Proof: serverProof,
}
if buf, err = tlv8.Marshal(payloadM4); err != nil {
return
}
if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil {
return
}
// STEP 5. Request from iPhone
if req, err = http.ReadRequest(r); err != nil {
return
}
encryptedM5 := pairSetupPayload{}
if err = tlv8.UnmarshalReader(req.Body, &encryptedM5); err != nil {
return
}
if encryptedM5.State != hap.M5 {
err = errors.New("wrong state")
return
}
msg := encryptedM5.EncryptedData[:len(encryptedM5.EncryptedData)-16]
var mac [16]byte
copy(mac[:], encryptedM5.EncryptedData[len(msg):]) // 16 byte (MAC)
// decrypt message using session shared
var sessionKey [32]byte
if sessionKey, err = hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Encrypt-Salt"),
[]byte("Pair-Setup-Encrypt-Info"),
); err != nil {
return
}
if buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PS-Msg05"), msg, mac, nil,
); err != nil {
return
}
// unpack message from TLV8
payloadM5 := struct {
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{}
if err = tlv8.Unmarshal(buf, &payloadM5); err != nil {
return
}
// 3. verify client ID and Public
var saltKey [32]byte
if saltKey, err = hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"),
[]byte("Pair-Setup-Controller-Sign-Info"),
); err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, payloadM5.Identifier...)
buf = append(buf, payloadM5.PublicKey[:]...)
if !ed25519.ValidateSignature(
payloadM5.PublicKey[:], buf, payloadM5.Signature,
) {
err = errors.New("wrong client signature")
return
}
// 4. generate signature to our ID adn Public
if saltKey, err = hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Accessory-Sign-Salt"),
[]byte("Pair-Setup-Accessory-Sign-Info"),
); err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, []byte(s.ServerID)...)
buf = append(buf, s.ServerPrivate[32:]...) // ServerPublic
var signature []byte
if signature, err = ed25519.Signature(s.ServerPrivate, buf); err != nil {
return
}
// 5. pack our ID and Public
payloadM6 := struct {
Identifier []byte `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{
Identifier: []byte(s.ServerID),
PublicKey: s.ServerPrivate[32:],
Signature: signature,
}
if buf, err = tlv8.Marshal(payloadM6); err != nil {
return
}
// 6. encrypt message
buf, mac, _ = chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PS-Msg06"), buf, nil,
)
// STEP 6. Response to iPhone
encryptedM6 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
}{
State: hap.M6,
EncryptedData: append(buf, mac[:]...),
}
if buf, err = tlv8.Marshal(encryptedM6); err != nil {
return
}
if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil {
return
}
if s.Pairings != nil {
s.Pairings[payloadM5.Identifier] = append(
payloadM5.PublicKey, 1, // adds admin (1) flag
)
}
clientID = payloadM5.Identifier
return
}
func keyDerivativeFuncRFC2945(username []byte) srp.KeyDerivationFunc {
return func(salt, pin []byte) []byte {
h := sha512.New()
h.Write(username)
h.Write([]byte(":"))
h.Write(pin)
t2 := h.Sum(nil)
h.Reset()
h.Write(salt)
h.Write(t2)
return h.Sum(nil)
}
}
type pairVerifyPayload struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
Signature []byte `tlv8:"10"`
}
func (s *Server) PairVerifyHandler(
conn net.Conn, req *http.Request,
) (secure *Secure, err error) {
// STEP M1. Request from iPhone
payloadM1 := pairVerifyPayload{}
if err = tlv8.UnmarshalReader(req.Body, &payloadM1); err != nil {
return
}
if payloadM1.State != hap.M1 {
err = errors.New("wrong state")
return
}
var clientPublic [32]byte
copy(clientPublic[:], payloadM1.PublicKey)
// Generate the key pair.
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
sessionShared := curve25519.SharedSecret(sessionPrivate, clientPublic)
var sessionKey [32]byte
if sessionKey, err = hkdf.Sha512(
sessionShared[:], []byte("Pair-Verify-Encrypt-Salt"),
[]byte("Pair-Verify-Encrypt-Info"),
); err != nil {
return
}
var buf []byte
buf = append(buf, sessionPublic[:]...)
buf = append(buf, s.ServerID...)
buf = append(buf, clientPublic[:]...)
var signature []byte
if signature, err = ed25519.Signature(s.ServerPrivate[:], buf); err != nil {
return
}
// STEP M2. Response to iPhone
payloadM2 := struct {
Identifier string `tlv8:"1"`
Signature []byte `tlv8:"10"`
}{
Identifier: s.ServerID,
Signature: signature,
}
if buf, err = tlv8.Marshal(payloadM2); err != nil {
return
}
var mac [16]byte
buf, mac, _ = chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PV-Msg02"), buf, nil,
)
encryptedM2 := struct {
State byte `tlv8:"6"`
PublicKey []byte `tlv8:"3"`
EncryptedData []byte `tlv8:"5"`
}{
State: hap.M2,
PublicKey: sessionPublic[:],
EncryptedData: append(buf, mac[:]...),
}
if buf, err = tlv8.Marshal(encryptedM2); err != nil {
return
}
if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil {
return
}
// STEP M3. Request from iPhone
r := bufio.NewReader(conn)
if req, err = http.ReadRequest(r); err != nil {
return
}
encryptedM3 := pairSetupPayload{}
if err = tlv8.UnmarshalReader(req.Body, &encryptedM3); err != nil {
return
}
if encryptedM3.State != hap.M3 {
err = errors.New("wrong state")
return
}
buf = encryptedM3.EncryptedData[:len(encryptedM3.EncryptedData)-16]
copy(mac[:], encryptedM3.EncryptedData[len(buf):]) // 16 byte (MAC)
if buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PV-Msg03"), buf, mac, nil,
); err != nil {
return
}
payloadM3 := pairVerifyPayload{}
if err = tlv8.Unmarshal(buf, &payloadM3); err != nil {
return
}
if s.Pairings != nil {
pairing := s.Pairings[payloadM3.Identifier]
if pairing == nil {
err = errors.New("not paired yet")
return
}
buf = nil
buf = append(buf, clientPublic[:]...)
buf = append(buf, []byte(payloadM3.Identifier)...)
buf = append(buf, sessionPublic[:]...)
if !ed25519.ValidateSignature(
pairing[:32], buf, payloadM3.Signature,
) {
err = errors.New("signature invalid")
return
}
}
// STEP M4. Response to iPhone
payloadM4 := struct {
State byte `tlv8:"6"`
}{
State: hap.M4,
}
if buf, err = tlv8.Marshal(payloadM4); err != nil {
return
}
err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf)
if secure, err = NewSecure(sessionShared, true); err != nil {
return
}
secure.Conn = conn
return
}

137
pkg/hap/secure.go Normal file
View File

@@ -0,0 +1,137 @@
package hap
import (
"encoding/binary"
"github.com/brutella/hap/chacha20poly1305"
"github.com/brutella/hap/hkdf"
"net"
"sync"
)
type Secure struct {
Conn net.Conn
encryptKey [32]byte
decryptKey [32]byte
encryptCount uint64
decryptCount uint64
mx sync.Mutex
}
func NewSecure(sharedKey [32]byte, isServer bool) (*Secure, error) {
salt := []byte("Control-Salt")
key1, err := hkdf.Sha512(
sharedKey[:], salt, []byte("Control-Read-Encryption-Key"),
)
if err != nil {
return nil, err
}
key2, err := hkdf.Sha512(
sharedKey[:], salt, []byte("Control-Write-Encryption-Key"),
)
if err != nil {
return nil, err
}
if isServer {
return &Secure{encryptKey: key1, decryptKey: key2}, nil
} else {
return &Secure{encryptKey: key2, decryptKey: key1}, nil
}
}
func (s *Secure) Read(b []byte) (n int, err error) {
for {
var length uint16
if err = binary.Read(s.Conn, binary.LittleEndian, &length); err != nil {
return
}
var enc = make([]byte, length)
if err = binary.Read(s.Conn, binary.LittleEndian, &enc); err != nil {
return
}
var mac [16]byte
if err = binary.Read(s.Conn, binary.LittleEndian, &mac); err != nil {
return
}
var nonce [8]byte
binary.LittleEndian.PutUint64(nonce[:], s.decryptCount)
s.decryptCount++
bLength := make([]byte, 2)
binary.LittleEndian.PutUint16(bLength, length)
var msg []byte
if msg, err = chacha20poly1305.DecryptAndVerify(
s.decryptKey[:], nonce[:], enc, mac, bLength,
); err != nil {
return
}
n += copy(b[n:], msg)
// Finish when all bytes fit in b
if length < packetLengthMax {
//fmt.Printf(">>>%s>>>\n", b[:n])
return
}
}
}
func (s *Secure) Write(b []byte) (n int, err error) {
s.mx.Lock()
defer s.mx.Unlock()
var packetLen = len(b)
for {
if packetLen > packetLengthMax {
packetLen = packetLengthMax
}
//fmt.Printf("<<<%s<<<\n", b[:packetLen])
var nonce [8]byte
binary.LittleEndian.PutUint64(nonce[:], s.encryptCount)
s.encryptCount++
bLength := make([]byte, 2)
binary.LittleEndian.PutUint16(bLength, uint16(packetLen))
var enc []byte
var mac [16]byte
enc, mac, err = chacha20poly1305.EncryptAndSeal(
s.encryptKey[:], nonce[:], b[:packetLen], bLength[:],
)
if err != nil {
return
}
enc = append(bLength, enc...)
enc = append(enc, mac[:]...)
if _, err = s.Conn.Write(enc); err != nil {
return
}
n += packetLen
if packetLen == packetLengthMax {
b = b[packetLengthMax:]
packetLen = len(b)
} else {
break
}
}
return
}
const (
// packetLengthMax is the max length of encrypted packets
packetLengthMax = 0x400
)

155
pkg/hap/server.go Normal file
View File

@@ -0,0 +1,155 @@
package hap
import (
"bufio"
"crypto/ed25519"
"github.com/brutella/hap"
"github.com/brutella/hap/tlv8"
"io"
"net"
"net/http"
)
type Server struct {
// Pin can't be null because server proof will be wrong
Pin string `json:"-"`
ServerID string `json:"server_id"`
// 32 bytes private key + 32 bytes public key
ServerPrivate []byte `json:"server_private"`
// Pairings can be nil for disable pair verify check
// ClientID: 32 bytes client public + 1 byte (isAdmin)
Pairings map[string][]byte `json:"pairings"`
DefaultPlainHandler func(w io.Writer, r *http.Request) error
DefaultSecureHandler func(w io.Writer, r *http.Request) error
OnPairChange func(clientID string, clientPublic []byte) `json:"-"`
OnRequest func(w io.Writer, r *http.Request) `json:"-"`
}
func GenerateKey() []byte {
_, key, _ := ed25519.GenerateKey(nil)
return key
}
func NewServer(name string) *Server {
return &Server{
ServerID: GenerateID(name),
ServerPrivate: GenerateKey(),
Pairings: map[string][]byte{},
}
}
func (s *Server) Serve(address string) (err error) {
var ln net.Listener
if ln, err = net.Listen("tcp", address); err != nil {
return
}
for {
var conn net.Conn
if conn, err = ln.Accept(); err != nil {
continue
}
go func() {
//fmt.Printf("[%s] new connection\n", conn.RemoteAddr().String())
s.Accept(conn)
//fmt.Printf("[%s] close connection\n", conn.RemoteAddr().String())
}()
}
}
func (s *Server) Accept(conn net.Conn) (err error) {
defer conn.Close()
var req *http.Request
r := bufio.NewReader(conn)
if req, err = http.ReadRequest(r); err != nil {
return
}
return s.HandleRequest(conn, req)
}
func (s *Server) HandleRequest(conn net.Conn, req *http.Request) (err error) {
if s.OnRequest != nil {
s.OnRequest(conn, req)
}
switch req.URL.Path {
case UriPairSetup:
if _, err = s.PairSetupHandler(conn, req); err != nil {
return
}
case UriPairVerify:
var secure *Secure
if secure, err = s.PairVerifyHandler(conn, req); err != nil {
return
}
err = s.HandleSecure(secure)
default:
if s.DefaultPlainHandler != nil {
err = s.DefaultPlainHandler(conn, req)
}
}
return
}
func (s *Server) HandleSecure(secure *Secure) (err error) {
r := bufio.NewReader(secure)
for {
var req *http.Request
if req, err = http.ReadRequest(r); err != nil {
return
}
if s.OnRequest != nil {
s.OnRequest(secure, req)
}
switch req.URL.Path {
case UriPairings:
s.HandlePairings(secure, req)
default:
if err = s.DefaultSecureHandler(secure, req); err != nil {
return
}
}
}
}
func (s *Server) HandlePairings(w io.Writer, r *http.Request) {
req := struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Permission byte `tlv8:"11"`
State byte `tlv8:"6"`
}{}
if err := tlv8.UnmarshalReader(r.Body, &req); err != nil {
panic(err)
}
switch req.Method {
case hap.MethodAddPairing, hap.MethodDeletePairing:
res := struct {
State byte `tlv8:"6"`
}{
State: hap.M2,
}
data, err := tlv8.Marshal(res)
if err != nil {
panic(err)
}
if err = WriteResponse(w, http.StatusOK, MimeJSON, data); err != nil {
panic(err)
}
}
}

283
pkg/homekit/client.go Normal file
View File

@@ -0,0 +1,283 @@
package homekit
import (
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
"github.com/AlexxIT/go2rtc/pkg/srtp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/brutella/hap/characteristic"
"github.com/brutella/hap/rtp"
"net"
"net/url"
"sync/atomic"
)
type Client struct {
streamer.Element
conn *hap.Conn
exit chan error
server *srtp.Server
url string
medias []*streamer.Media
tracks []*streamer.Track
sessions []*srtp.Session
}
func NewClient(rawURL string, server *srtp.Server) (*Client, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
query := u.Query()
c := &hap.Conn{
DeviceAddress: u.Host,
DeviceID: query.Get("device_id"),
DevicePublic: hap.DecodeKey(query.Get("device_public")),
ClientID: query.Get("client_id"),
ClientPrivate: hap.DecodeKey(query.Get("client_private")),
}
return &Client{conn: c, server: server}, nil
}
func (c *Client) Dial() error {
if err := c.conn.Dial(); err != nil {
return err
}
c.exit = make(chan error)
go func() {
//start goroutine for reading responses from camera
c.exit <- c.conn.Handle()
}()
return nil
}
func (c *Client) GetMedias() []*streamer.Media {
if c.medias == nil {
c.medias = c.getMedias()
}
return c.medias
}
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
for _, track := range c.tracks {
if track.Codec == codec {
return track
}
}
track := streamer.NewTrack(codec, media.Direction)
c.tracks = append(c.tracks, track)
return track
}
func (c *Client) Start() error {
if c.tracks == nil {
return errors.New("producer without tracks")
}
// get our server local IP-address
host, _, err := net.SplitHostPort(c.conn.LocalAddr())
if err != nil {
return err
}
// TODO: set right config
vp := &rtp.VideoParameters{
CodecType: rtp.VideoCodecType_H264,
CodecParams: rtp.VideoCodecParameters{
Profiles: []rtp.VideoCodecProfile{
{Id: rtp.VideoCodecProfileMain},
},
Levels: []rtp.VideoCodecLevel{
{Level: rtp.VideoCodecLevel4},
},
Packetizations: []rtp.VideoCodecPacketization{
{Mode: rtp.VideoCodecPacketizationModeNonInterleaved},
},
},
Attributes: rtp.VideoCodecAttributes{
Width: 1920, Height: 1080, Framerate: 30,
},
}
ap := &rtp.AudioParameters{
CodecType: rtp.AudioCodecType_AAC_ELD,
CodecParams: rtp.AudioCodecParameters{
Channels: 1,
Bitrate: rtp.AudioCodecBitrateVariable,
Samplerate: rtp.AudioCodecSampleRate16Khz,
// packet time=20 => AAC-ELD packet size=480
// packet time=30 => AAC-ELD packet size=480
// packet time=40 => AAC-ELD packet size=480
// packet time=60 => AAC-LD packet size=960
PacketTime: 40,
},
}
// setup HomeKit stream session
hkSession := camera.NewSession(vp, ap)
hkSession.SetLocalEndpoint(host, c.server.Port())
// create client for processing camera accessory
cam := camera.NewClient(c.conn)
// try to start HomeKit stream
if err = cam.StartStream(hkSession); err != nil {
return err
}
// SRTP Video Session
vs := &srtp.Session{
LocalSSRC: hkSession.Config.Video.RTP.Ssrc,
RemoteSSRC: hkSession.Answer.SsrcVideo,
}
if err = vs.SetKeys(
hkSession.Offer.Video.MasterKey, hkSession.Offer.Video.MasterSalt,
hkSession.Answer.Video.MasterKey, hkSession.Answer.Video.MasterSalt,
); err != nil {
return err
}
// SRTP Audio Session
as := &srtp.Session{
LocalSSRC: hkSession.Config.Audio.RTP.Ssrc,
RemoteSSRC: hkSession.Answer.SsrcAudio,
}
if err = as.SetKeys(
hkSession.Offer.Audio.MasterKey, hkSession.Offer.Audio.MasterSalt,
hkSession.Answer.Audio.MasterKey, hkSession.Answer.Audio.MasterSalt,
); err != nil {
return err
}
for _, track := range c.tracks {
switch track.Codec.Name {
case streamer.CodecH264:
vs.Track = track
case streamer.CodecELD:
as.Track = track
}
}
c.server.AddSession(vs)
c.server.AddSession(as)
c.sessions = []*srtp.Session{vs, as}
return <-c.exit
}
func (c *Client) Stop() error {
err := c.conn.Close()
for _, session := range c.sessions {
c.server.RemoveSession(session)
}
return err
}
func (c *Client) getMedias() []*streamer.Media {
var medias []*streamer.Media
accs, err := c.conn.GetAccessories()
if err != nil {
return nil
}
acc := accs[0]
// get supported video config (not really necessary)
char := acc.GetCharacter(characteristic.TypeSupportedVideoStreamConfiguration)
v1 := &rtp.VideoStreamConfiguration{}
if err = char.ReadTLV8(v1); err != nil {
return nil
}
for _, hkCodec := range v1.Codecs {
codec := &streamer.Codec{ClockRate: 90000}
switch hkCodec.Type {
case rtp.VideoCodecType_H264:
codec.Name = streamer.CodecH264
codec.FmtpLine = "profile-level-id=420029"
default:
fmt.Printf("unknown codec: %d", hkCodec.Type)
continue
}
media := &streamer.Media{
Kind: streamer.KindVideo, Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
}
medias = append(medias, media)
}
char = acc.GetCharacter(characteristic.TypeSupportedAudioStreamConfiguration)
v2 := &rtp.AudioStreamConfiguration{}
if err = char.ReadTLV8(v2); err != nil {
return nil
}
for _, hkCodec := range v2.Codecs {
codec := &streamer.Codec{
Channels: uint16(hkCodec.Parameters.Channels),
}
switch hkCodec.Parameters.Samplerate {
case rtp.AudioCodecSampleRate8Khz:
codec.ClockRate = 8000
case rtp.AudioCodecSampleRate16Khz:
codec.ClockRate = 16000
case rtp.AudioCodecSampleRate24Khz:
codec.ClockRate = 24000
default:
panic(fmt.Sprintf("unknown clockrate: %d", hkCodec.Parameters.Samplerate))
}
switch hkCodec.Type {
case rtp.AudioCodecType_AAC_ELD:
codec.Name = streamer.CodecELD
// only this value supported by FFmpeg
codec.FmtpLine = "profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=F8EC3000"
default:
fmt.Printf("unknown codec: %d", hkCodec.Type)
continue
}
media := &streamer.Media{
Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
}
medias = append(medias, media)
}
return medias
}
func (c *Client) MarshalJSON() ([]byte, error) {
var recv uint32
for _, session := range c.sessions {
recv += atomic.LoadUint32(&session.Recv)
}
info := &streamer.Info{
Type: "HomeKit source",
URL: c.conn.URL(),
Medias: c.medias,
Tracks: c.tracks,
Recv: recv,
}
return json.Marshal(info)
}

3
pkg/httpflv/README.md Normal file
View File

@@ -0,0 +1,3 @@
## Useful links
- https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779

165
pkg/httpflv/amf0.go Normal file
View File

@@ -0,0 +1,165 @@
package httpflv
import (
"encoding/binary"
"errors"
"math"
)
const (
TypeNumber byte = iota
TypeBoolean
TypeString
TypeObject
TypeEcmaArray = 8
TypeObjectEnd = 9
)
var Err = errors.New("amf0 read error")
// AMF0 spec: http://download.macromedia.com/pub/labs/amf/amf0_spec_121207.pdf
type AMF0 struct {
buf []byte
pos int
}
func NewReader(b []byte) *AMF0 {
return &AMF0{buf: b}
}
func (a *AMF0) ReadMetaData() map[string]interface{} {
if b, _ := a.ReadByte(); b != TypeString {
return nil
}
if s, _ := a.ReadString(); s != "onMetaData" {
return nil
}
b, _ := a.ReadByte()
switch b {
case TypeObject:
v, _ := a.ReadObject()
return v
case TypeEcmaArray:
v, _ := a.ReadEcmaArray()
return v
}
return nil
}
func (a *AMF0) ReadMap() (map[interface{}]interface{}, error) {
dict := make(map[interface{}]interface{})
for a.pos < len(a.buf) {
k, err := a.ReadItem()
if err != nil {
return nil, err
}
v, err := a.ReadItem()
if err != nil {
return nil, err
}
dict[k] = v
}
return dict, nil
}
func (a *AMF0) ReadItem() (interface{}, error) {
dataType, err := a.ReadByte()
if err != nil {
return nil, err
}
switch dataType {
case TypeNumber:
return a.ReadNumber()
case TypeBoolean:
v, err := a.ReadByte()
return v != 0, err
case TypeString:
return a.ReadString()
case TypeObject:
return a.ReadObject()
case TypeObjectEnd:
return nil, nil
}
return nil, Err
}
func (a *AMF0) ReadByte() (byte, error) {
if a.pos >= len(a.buf) {
return 0, Err
}
v := a.buf[a.pos]
a.pos++
return v, nil
}
func (a *AMF0) ReadNumber() (float64, error) {
if a.pos+8 >= len(a.buf) {
return 0, Err
}
v := binary.BigEndian.Uint64(a.buf[a.pos : a.pos+8])
a.pos += 8
return math.Float64frombits(v), nil
}
func (a *AMF0) ReadString() (string, error) {
if a.pos+2 >= len(a.buf) {
return "", Err
}
size := int(binary.BigEndian.Uint16(a.buf[a.pos:]))
a.pos += 2
if a.pos+size >= len(a.buf) {
return "", Err
}
s := string(a.buf[a.pos : a.pos+size])
a.pos += size
return s, nil
}
func (a *AMF0) ReadObject() (map[string]interface{}, error) {
obj := make(map[string]interface{})
for {
k, err := a.ReadString()
if err != nil {
return nil, err
}
v, err := a.ReadItem()
if err != nil {
return nil, err
}
if k == "" {
break
}
obj[k] = v
}
return obj, nil
}
func (a *AMF0) ReadEcmaArray() (map[string]interface{}, error) {
if a.pos+4 >= len(a.buf) {
return nil, Err
}
a.pos += 4 // skip size
return a.ReadObject()
}

210
pkg/httpflv/httpflv.go Normal file
View File

@@ -0,0 +1,210 @@
package httpflv
import (
"bufio"
"bytes"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/flv/flvio"
"github.com/deepch/vdk/utils/bits/pio"
"io"
"net/http"
)
func Dial(uri string) (*Conn, error) {
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
return Accept(res)
}
func Accept(res *http.Response) (*Conn, error) {
c := Conn{
conn: res.Body,
reader: bufio.NewReaderSize(res.Body, pio.RecommendBufioSize),
buf: make([]byte, 256),
}
if _, err := io.ReadFull(c.reader, c.buf[:flvio.FileHeaderLength]); err != nil {
return nil, err
}
flags, n, err := flvio.ParseFileHeader(c.buf)
if err != nil {
return nil, err
}
if flags&flvio.FILE_HAS_VIDEO != 0 {
c.videoIdx = -1
}
if flags&flvio.FILE_HAS_AUDIO != 0 {
c.audioIdx = -1
}
if _, err = c.reader.Discard(n); err != nil {
return nil, err
}
return &c, nil
}
type Conn struct {
conn io.ReadCloser
reader *bufio.Reader
buf []byte
videoIdx int8
audioIdx int8
}
func (c *Conn) Streams() ([]av.CodecData, error) {
var video, audio av.CodecData
// Normal software sends:
// 1. Video/audio flag in header
// 2. MetaData as first tag (with video/audio codec info)
// 3. Video/audio headers in 2nd and 3rd tag
// Reolink camera sends:
// 1. Empty video/audio flag
// 2. MedaData without stereo key for AAC
// 3. Audio header after Video keyframe tag
waitVideo := c.videoIdx != 0
waitAudio := c.audioIdx != 0
for i := 0; i < 20; i++ {
tag, _, err := flvio.ReadTag(c.reader, c.buf)
if err != nil {
return nil, err
}
//log.Printf("[FLV] type=%d avc=%d aac=%d video=%t audio=%t", tag.Type, tag.AVCPacketType, tag.AACPacketType, video != nil, audio != nil)
switch tag.Type {
case flvio.TAG_SCRIPTDATA:
if meta := NewReader(tag.Data).ReadMetaData(); meta != nil {
waitVideo = meta["videocodecid"] != nil
// don't wait audio tag because parse all info from MetaData
waitAudio = false
audio = parseAudioConfig(meta)
} else {
waitVideo = bytes.Contains(tag.Data, []byte("videocodecid"))
waitAudio = bytes.Contains(tag.Data, []byte("audiocodecid"))
}
case flvio.TAG_VIDEO:
if tag.AVCPacketType == flvio.AVC_SEQHDR {
video, _ = h264parser.NewCodecDataFromAVCDecoderConfRecord(tag.Data)
}
waitVideo = false
case flvio.TAG_AUDIO:
if tag.SoundFormat == flvio.SOUND_AAC && tag.AACPacketType == flvio.AAC_SEQHDR {
audio, _ = aacparser.NewCodecDataFromMPEG4AudioConfigBytes(tag.Data)
}
waitAudio = false
}
if !waitVideo && !waitAudio {
break
}
}
if video != nil && audio != nil {
c.videoIdx = 0
c.audioIdx = 1
return []av.CodecData{video, audio}, nil
} else if video != nil {
c.videoIdx = 0
return []av.CodecData{video}, nil
} else if audio != nil {
c.audioIdx = 0
return []av.CodecData{audio}, nil
}
return nil, nil
}
func (c *Conn) ReadPacket() (av.Packet, error) {
for {
tag, ts, err := flvio.ReadTag(c.reader, c.buf)
if err != nil {
return av.Packet{}, err
}
switch tag.Type {
case flvio.TAG_VIDEO:
if tag.AVCPacketType != flvio.AVC_NALU {
continue
}
return av.Packet{
Idx: c.videoIdx,
Data: tag.Data,
CompositionTime: flvio.TsToTime(tag.CompositionTime),
IsKeyFrame: tag.FrameType == flvio.FRAME_KEY,
Time: flvio.TsToTime(ts),
}, nil
case flvio.TAG_AUDIO:
if tag.SoundFormat != flvio.SOUND_AAC || tag.AACPacketType != flvio.AAC_RAW {
continue
}
return av.Packet{Idx: c.audioIdx, Data: tag.Data, Time: flvio.TsToTime(ts)}, nil
}
}
}
func (c *Conn) Close() (err error) {
return c.conn.Close()
}
func parseAudioConfig(meta map[string]interface{}) av.CodecData {
if meta["audiocodecid"] != float64(10) {
return nil
}
config := aacparser.MPEG4AudioConfig{
ObjectType: aacparser.AOT_AAC_LC,
}
switch v := meta["audiosamplerate"].(type) {
case float64:
config.SampleRate = int(v)
default:
return nil
}
switch meta["stereo"] {
case true:
config.ChannelConfig = 2
config.ChannelLayout = av.CH_STEREO
default:
// Reolink doesn't have this setting
config.ChannelConfig = 1
config.ChannelLayout = av.CH_MONO
}
buf := &bytes.Buffer{}
if err := aacparser.WriteMPEG4AudioConfig(buf, config); err != nil {
return nil
}
return aacparser.CodecData{
Config: config,
ConfigBytes: buf.Bytes(),
}
}

323
pkg/ivideon/client.go Normal file
View File

@@ -0,0 +1,323 @@
package ivideon
import (
"bytes"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/fmp4/fmp4io"
"github.com/gorilla/websocket"
"github.com/pion/rtp"
"io"
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
)
type State byte
const (
StateNone State = iota
StateConn
StateHandle
)
type Client struct {
streamer.Element
ID string
conn *websocket.Conn
medias []*streamer.Media
tracks map[byte]*streamer.Track
msg *message
t0 time.Time
buffer chan []byte
state State
mu sync.Mutex
recv uint32
}
func NewClient(id string) *Client {
return &Client{ID: id}
}
func (c *Client) Dial() (err error) {
resp, err := http.Get(
"https://openapi-alpha.ivideon.com/cameras/" + c.ID +
"/live_stream?op=GET&access_token=public&q=2&" +
"video_codecs=h264&format=ws-fmp4",
)
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var v liveResponse
if err = json.Unmarshal(data, &v); err != nil {
return err
}
if !v.Success {
return fmt.Errorf("wrong response: %s", data)
}
c.conn, _, err = websocket.DefaultDialer.Dial(v.Result.URL, nil)
if err != nil {
return err
}
if err = c.getTracks(); err != nil {
_ = c.conn.Close()
return err
}
c.state = StateConn
return nil
}
func (c *Client) Handle() error {
// add delay to the stream for smooth playing (not a best solution)
c.t0 = time.Now().Add(time.Second)
c.mu.Lock()
if c.state == StateConn {
c.buffer = make(chan []byte, 5)
c.state = StateHandle
// processing stream in separate thread for lower delay between packets
go c.worker(c.buffer)
}
c.mu.Unlock()
_, data, err := c.conn.ReadMessage()
if err != nil {
return err
}
track := c.tracks[c.msg.Track]
if track != nil {
c.mu.Lock()
if c.state == StateHandle {
c.buffer <- data
atomic.AddUint32(&c.recv, uint32(len(data)))
}
c.mu.Unlock()
}
// we have one unprocessed msg after getTracks
for {
_, data, err = c.conn.ReadMessage()
if err != nil {
return err
}
var msg message
if err = json.Unmarshal(data, &msg); err != nil {
return err
}
switch msg.Type {
case "stream-init":
continue
case "fragment":
_, data, err = c.conn.ReadMessage()
if err != nil {
return err
}
track = c.tracks[msg.Track]
if track != nil {
c.mu.Lock()
if c.state == StateHandle {
c.buffer <- data
atomic.AddUint32(&c.recv, uint32(len(data)))
}
c.mu.Unlock()
}
default:
return fmt.Errorf("wrong message type: %s", data)
}
}
}
func (c *Client) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
switch c.state {
case StateNone:
return nil
case StateConn:
case StateHandle:
close(c.buffer)
}
c.state = StateNone
return c.conn.Close()
}
func (c *Client) getTracks() error {
c.tracks = map[byte]*streamer.Track{}
for {
_, data, err := c.conn.ReadMessage()
if err != nil {
return err
}
var msg message
if err = json.Unmarshal(data, &msg); err != nil {
return err
}
switch msg.Type {
case "stream-init":
s := msg.CodecString
i := strings.IndexByte(s, '.')
if i > 0 {
s = s[:i]
}
switch s {
case "avc1": // avc1.4d0029
// skip multiple identical init
if c.tracks[msg.TrackID] != nil {
continue
}
codec := &streamer.Codec{
Name: streamer.CodecH264,
ClockRate: 90000,
FmtpLine: "profile-level-id=" + msg.CodecString[i+1:],
PayloadType: streamer.PayloadTypeRAW,
}
i = bytes.Index(msg.Data, []byte("avcC")) - 4
if i < 0 {
return fmt.Errorf("wrong AVC: %s", msg.Data)
}
avccLen := binary.BigEndian.Uint32(msg.Data[i:])
data = msg.Data[i+8 : i+int(avccLen)]
record := h264parser.AVCDecoderConfRecord{}
if _, err = record.Unmarshal(data); err != nil {
return err
}
codec.FmtpLine += ";sprop-parameter-sets=" +
base64.StdEncoding.EncodeToString(record.SPS[0]) + "," +
base64.StdEncoding.EncodeToString(record.PPS[0])
media := &streamer.Media{
Kind: streamer.KindVideo,
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
}
c.medias = append(c.medias, media)
track := streamer.NewTrack(codec, streamer.DirectionSendonly)
c.tracks[msg.TrackID] = track
case "mp4a": // mp4a.40.2
}
case "fragment":
c.msg = &msg
return nil
default:
return fmt.Errorf("wrong message type: %s", data)
}
}
}
func (c *Client) worker(buffer chan []byte) {
var track *streamer.Track
for _, track = range c.tracks {
break
}
for data := range buffer {
moof := &fmp4io.MovieFrag{}
if _, err := moof.Unmarshal(data, 0); err != nil {
continue
}
moofLen := binary.BigEndian.Uint32(data)
_ = moofLen
mdat := moof.Unknowns[0]
if mdat.Tag() != fmp4io.MDAT {
continue
}
i, _ := mdat.Pos() // offset, size
data = data[i+8:]
traf := moof.Tracks[0]
ts := uint32(traf.DecodeTime.Time)
//println("!!!", (time.Duration(ts) * time.Millisecond).String(), time.Since(c.t0).String())
for _, entry := range traf.Run.Entries {
// synchronize framerate for WebRTC and MSE
d := time.Duration(ts)*time.Millisecond - time.Since(c.t0)
if d < 0 {
d = time.Duration(entry.Duration) * time.Millisecond / 2
}
time.Sleep(d)
// can be SPS, PPS and IFrame in one packet
packet := &rtp.Packet{
// ivideon clockrate=1000, RTP clockrate=90000
Header: rtp.Header{Timestamp: ts * 90},
Payload: data[:entry.Size],
}
_ = track.WriteRTP(packet)
data = data[entry.Size:]
ts += entry.Duration
}
if len(data) != 0 {
continue
}
}
}
type liveResponse struct {
Result struct {
URL string `json:"url"`
} `json:"result"`
Success bool `json:"success"`
}
type message struct {
Type string `json:"type"`
CodecString string `json:"codec_string"`
Data []byte `json:"data"`
TrackID byte `json:"track_id"`
Track byte `json:"track"`
StartTime float32 `json:"start_time"`
Duration float32 `json:"duration"`
IsKey bool `json:"is_key"`
DataOffset uint32 `json:"data_offset"`
}

49
pkg/ivideon/streamer.go Normal file
View File

@@ -0,0 +1,49 @@
package ivideon
import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"sync/atomic"
)
func (c *Client) GetMedias() []*streamer.Media {
return c.medias
}
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
for _, track := range c.tracks {
if track.Codec == codec {
return track
}
}
panic(fmt.Sprintf("wrong media/codec: %+v %+v", media, codec))
}
func (c *Client) Start() error {
err := c.Handle()
if c.buffer == nil {
return nil
}
return err
}
func (c *Client) Stop() error {
return c.Close()
}
func (c *Client) MarshalJSON() ([]byte, error) {
var tracks []*streamer.Track
for _, track := range c.tracks {
tracks = append(tracks, track)
}
info := &streamer.Info{
Type: "Ivideon source",
URL: c.ID,
Medias: c.medias,
Tracks: tracks,
Recv: atomic.LoadUint32(&c.recv),
}
return json.Marshal(info)
}

View File

@@ -1,72 +0,0 @@
package keyframe
import (
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
var annexB = []byte{0, 0, 0, 1}
type Consumer struct {
streamer.Element
IsMP4 bool
}
func (k *Consumer) GetMedias() []*streamer.Media {
// support keyframe extraction only for one coded...
codec := streamer.NewCodec(streamer.CodecH264)
return []*streamer.Media{
{
Kind: streamer.KindVideo, Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{codec},
},
}
}
func (k *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
// sps and pps without AVC headers
sps, pps := h264.GetParameterSet(track.Codec.FmtpLine)
push := func(packet *rtp.Packet) error {
// TODO: remove it, unnecessary
if packet.Version != h264.RTPPacketVersionAVC {
panic("wrong packet type")
}
switch h264.NALUType(packet.Payload) {
case h264.NALUTypeSPS:
sps = packet.Payload[4:] // remove AVC header
case h264.NALUTypePPS:
pps = packet.Payload[4:] // remove AVC header
case h264.NALUTypeIFrame:
if sps == nil || pps == nil {
return nil
}
var data []byte
if k.IsMP4 {
data = mp4.MarshalMP4(sps, pps, packet.Payload)
} else {
data = append(data, annexB...)
data = append(data, sps...)
data = append(data, annexB...)
data = append(data, pps...)
data = append(data, annexB...)
data = append(data, packet.Payload[4:]...)
}
k.Fire(data)
}
return nil
}
if !h264.IsAVC(track.Codec) {
wrapper := h264.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}

5
pkg/mjpeg/README.md Normal file
View File

@@ -0,0 +1,5 @@
## Useful links
- https://www.rfc-editor.org/rfc/rfc2435
- https://github.com/GStreamer/gst-plugins-good/blob/master/gst/rtp/gstrtpjpegdepay.c
- https://mjpeg.sanford.io/

174
pkg/mjpeg/client.go Normal file
View File

@@ -0,0 +1,174 @@
package mjpeg
import (
"bufio"
"encoding/json"
"errors"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/pion/rtp"
"io"
"net/http"
"net/textproto"
"strconv"
"strings"
"sync/atomic"
"time"
)
type Client struct {
streamer.Element
UserAgent string
RemoteAddr string
closed bool
res *http.Response
track *streamer.Track
recv uint32
}
func NewClient(res *http.Response) *Client {
codec := &streamer.Codec{
Name: streamer.CodecJPEG, ClockRate: 90000, PayloadType: streamer.PayloadTypeRAW,
}
return &Client{
res: res,
track: streamer.NewTrack(codec, streamer.DirectionSendonly),
}
}
func (c *Client) GetMedias() []*streamer.Media {
return []*streamer.Media{{
Kind: streamer.KindVideo,
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{c.track.Codec},
}}
}
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
return c.track
}
func (c *Client) Start() error {
ct := c.res.Header.Get("Content-Type")
if ct == "image/jpeg" {
return c.startJPEG()
}
// added in go1.18
if _, s, ok := strings.Cut(ct, "boundary="); ok {
return c.startMJPEG(s)
}
return errors.New("wrong Content-Type: " + ct)
}
func (c *Client) Stop() error {
// important for close reader/writer gorutines
_ = c.res.Body.Close()
c.closed = true
return nil
}
func (c *Client) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
Type: "MJPEG source",
URL: c.res.Request.URL.String(),
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Recv: atomic.LoadUint32(&c.recv),
}
return json.Marshal(info)
}
func (c *Client) startJPEG() error {
buf, err := io.ReadAll(c.res.Body)
if err != nil {
return err
}
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
_ = c.track.WriteRTP(packet)
atomic.AddUint32(&c.recv, uint32(len(buf)))
req := c.res.Request
for !c.closed {
res, err := tcp.Do(req)
if err != nil {
return err
}
if res.StatusCode != http.StatusOK {
return errors.New("wrong status: " + res.Status)
}
buf, err = io.ReadAll(res.Body)
if err != nil {
return err
}
packet = &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
_ = c.track.WriteRTP(packet)
atomic.AddUint32(&c.recv, uint32(len(buf)))
}
return nil
}
func (c *Client) startMJPEG(boundary string) error {
boundary = "--" + boundary
r := bufio.NewReader(c.res.Body)
tp := textproto.NewReader(r)
for !c.closed {
s, err := tp.ReadLine()
if err != nil {
return err
}
if s != boundary {
return errors.New("wrong boundary: " + s)
}
header, err := tp.ReadMIMEHeader()
if err != nil {
return err
}
s = header.Get("Content-Length")
if s == "" {
return errors.New("no content length")
}
size, err := strconv.Atoi(s)
if err != nil {
return err
}
buf := make([]byte, size)
if _, err = io.ReadFull(r, buf); err != nil {
return err
}
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
_ = c.track.WriteRTP(packet)
atomic.AddUint32(&c.recv, uint32(len(buf)))
if _, err = r.Discard(2); err != nil {
return err
}
}
return nil
}
func now() uint32 {
return uint32(time.Now().UnixMilli() * 90)
}

53
pkg/mjpeg/consumer.go Normal file
View File

@@ -0,0 +1,53 @@
package mjpeg
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"sync/atomic"
)
type Consumer struct {
streamer.Element
UserAgent string
RemoteAddr string
codecs []*streamer.Codec
start bool
send uint32
}
func (c *Consumer) GetMedias() []*streamer.Media {
return []*streamer.Media{{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{{Name: streamer.CodecJPEG}},
}}
}
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
push := func(packet *rtp.Packet) error {
c.Fire(packet.Payload)
atomic.AddUint32(&c.send, uint32(len(packet.Payload)))
return nil
}
if track.Codec.IsRTP() {
wrapper := RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}
func (c *Consumer) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
Type: "MJPEG client",
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Send: atomic.LoadUint32(&c.send),
}
return json.Marshal(info)
}

182
pkg/mjpeg/rfc2435.go Normal file
View File

@@ -0,0 +1,182 @@
package mjpeg
// RFC 2435. Appendix A
var jpeg_luma_quantizer = []byte{
16, 11, 10, 16, 24, 40, 51, 61,
12, 12, 14, 19, 26, 58, 60, 55,
14, 13, 16, 24, 40, 57, 69, 56,
14, 17, 22, 29, 51, 87, 80, 62,
18, 22, 37, 56, 68, 109, 103, 77,
24, 35, 55, 64, 81, 104, 113, 92,
49, 64, 78, 87, 103, 121, 120, 101,
72, 92, 95, 98, 112, 100, 103, 99,
}
var jpeg_chroma_quantizer = []byte{
17, 18, 24, 47, 99, 99, 99, 99,
18, 21, 26, 66, 99, 99, 99, 99,
24, 26, 56, 99, 99, 99, 99, 99,
47, 66, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
}
func MakeTables(q byte) (lqt, cqt []byte) {
var factor int
switch {
case q < 1:
factor = 1
case q > 99:
factor = 99
default:
factor = int(q)
}
if q < 50 {
factor = 5000 / factor
} else if q > 99 {
factor = 200 - factor*2
}
lqt = make([]byte, 64)
cqt = make([]byte, 64)
for i := 0; i < 64; i++ {
lq := (int(jpeg_luma_quantizer[i])*factor + 50) / 100
cq := (int(jpeg_chroma_quantizer[i])*factor + 50) / 100
/* Limit the quantizers to 1 <= q <= 255 */
switch {
case lq < 1:
lqt[i] = 1
case lq > 255:
lqt[i] = 255
default:
lqt[i] = byte(lq)
}
switch {
case cq < 1:
cqt[i] = 1
case cq > 255:
cqt[i] = 255
default:
cqt[i] = byte(cq)
}
}
return
}
// RFC 2435. Appendix B
var lum_dc_codelens = []byte{
0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
}
var lum_dc_symbols = []byte{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
}
var lum_ac_codelens = []byte{
0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d,
}
var lum_ac_symbols = []byte{
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12,
0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08,
0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0,
0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16,
0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,
0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6,
0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5,
0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4,
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2,
0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0xfa,
}
var chm_dc_codelens = []byte{
0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
}
var chm_dc_symbols = []byte{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
}
var chm_ac_codelens = []byte{
0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77,
}
var chm_ac_symbols = []byte{
0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21,
0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71,
0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91,
0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0,
0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34,
0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26,
0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38,
0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,
0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96,
0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5,
0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4,
0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3,
0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2,
0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda,
0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9,
0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0xfa,
}
func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte {
// Appendix A from https://www.rfc-editor.org/rfc/rfc2435
p = append(p, 0xFF, 0xD8)
p = MakeQuantHeader(p, lqt, 0)
p = MakeQuantHeader(p, cqt, 1)
if t == 0 {
t = 0x21
} else {
t = 0x22
}
p = append(p,
0xFF, 0xC0, 0, 17, 8,
byte(h>>8), byte(h&0xFF),
byte(w>>8), byte(w&0xFF),
3, 0, t, 0, 1, 0x11, 1, 2, 0x11, 1,
)
p = MakeHuffmanHeader(p, lum_dc_codelens, lum_dc_symbols, 0, 0)
p = MakeHuffmanHeader(p, lum_ac_codelens, lum_ac_symbols, 0, 1)
p = MakeHuffmanHeader(p, chm_dc_codelens, chm_dc_symbols, 1, 0)
p = MakeHuffmanHeader(p, chm_ac_codelens, chm_ac_symbols, 1, 1)
return append(p, 0xFF, 0xDA, 0, 12, 3, 0, 0, 1, 0x11, 2, 0x11, 0, 63, 0)
}
func MakeQuantHeader(p []byte, qt []byte, tableNo byte) []byte {
p = append(p, 0xFF, 0xDB, 0, 67, tableNo)
return append(p, qt...)
}
func MakeHuffmanHeader(p, codelens, symbols []byte, tableNo, tableClass byte) []byte {
p = append(p,
0xFF, 0xC4, 0,
byte(3+len(codelens)+len(symbols)),
(tableClass<<4)|tableNo,
)
p = append(p, codelens...)
return append(p, symbols...)
}

212
pkg/mjpeg/rtp.go Normal file
View File

@@ -0,0 +1,212 @@
package mjpeg
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
buf := make([]byte, 0, 512*1024) // 512K
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
//log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", track.Codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1
b := packet.Payload
// 3.1. JPEG header
t := b[4]
// 3.1.7. Restart Marker header
if 64 <= t && t <= 127 {
b = b[12:] // skip it
} else {
b = b[8:]
}
if len(buf) == 0 {
var lqt, cqt []byte
// 3.1.8. Quantization Table header
q := packet.Payload[5]
if q >= 128 {
lqt = b[4:68]
cqt = b[68:132]
b = b[132:]
} else {
lqt, cqt = MakeTables(q)
}
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1.5
// The maximum width is 2040 pixels.
w := uint16(packet.Payload[6]) << 3
h := uint16(packet.Payload[7]) << 3
// fix 2560x1920 and 2560x1440
if w == 512 && (h == 1920 || h == 1440) {
w = 2560
}
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
buf = MakeHeaders(buf, t, w, h, lqt, cqt)
}
// 3.1.9. JPEG Payload
buf = append(buf, b...)
if !packet.Marker {
return nil
}
if end := buf[len(buf)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
buf = append(buf, 0xFF, 0xD9)
}
clone := *packet
clone.Payload = buf
buf = buf[:0] // clear buffer
return push(&clone)
}
}
}
func RTPPay() streamer.WrapperFunc {
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
return nil
}
}
}
//func RTPPay() streamer.WrapperFunc {
// const packetSize = 1436
//
// sequencer := rtp.NewRandomSequencer()
//
// return func(push streamer.WriterFunc) streamer.WriterFunc {
// return func(packet *rtp.Packet) error {
// // reincode image to more common form
// img, err := jpeg.Decode(bytes.NewReader(packet.Payload))
// if err != nil {
// return err
// }
//
// wh := img.Bounds().Size()
// w := wh.X
// h := wh.Y
//
// if w > 2040 {
// w = 2040
// } else if w&3 > 0 {
// w &= 3
// }
// if h > 2040 {
// h = 2040
// } else if h&3 > 0 {
// h &= 3
// }
//
// if w != wh.X || h != wh.Y {
// x0 := (wh.X - w) / 2
// y0 := (wh.Y - h) / 2
// rect := image.Rect(x0, y0, x0+w, y0+h)
// img = img.(*image.YCbCr).SubImage(rect)
// }
//
// buf := bytes.NewBuffer(nil)
// if err = jpeg.Encode(buf, img, nil); err != nil {
// return err
// }
//
// h1 := make([]byte, 8)
// h1[4] = 1 // Type
// h1[5] = 255 // Q
//
// // MBZ=0, Precision=0, Length=128
// h2 := make([]byte, 4, 132)
// h2[3] = 128
//
// var jpgData []byte
//
// p := buf.Bytes()
//
// for jpgData == nil {
// // 2 bytes h1
// if p[0] != 0xFF {
// return nil
// }
//
// size := binary.BigEndian.Uint16(p[2:]) + 2
//
// // 2 bytes payload size (include 2 bytes)
// switch p[1] {
// case 0xD8: // 0. Start Of Image (size=0)
// p = p[2:]
// continue
// case 0xDB: // 1. Define Quantization Table (size=130)
// for i := uint16(4 + 1); i < size; i += 1 + 64 {
// h2 = append(h2, p[i:i+64]...)
// }
// case 0xC0: // 2. Start Of Frame (size=15)
// if p[4] != 8 {
// return nil
// }
// h := binary.BigEndian.Uint16(p[5:])
// w := binary.BigEndian.Uint16(p[7:])
// h1[6] = uint8(w >> 3)
// h1[7] = uint8(h >> 3)
// case 0xC4: // 3. Define Huffman Table (size=416)
// case 0xDA: // 4. Start Of Scan (size=10)
// jpgData = p[size:]
// }
//
// p = p[size:]
// }
//
// offset := 0
// p = make([]byte, 0)
//
// for jpgData != nil {
// p = p[:0]
//
// if offset > 0 {
// h1[1] = byte(offset >> 16)
// h1[2] = byte(offset >> 8)
// h1[3] = byte(offset)
// p = append(p, h1...)
// } else {
// p = append(p, h1...)
// p = append(p, h2...)
// }
//
// dataLen := packetSize - len(p)
// if dataLen < len(jpgData) {
// p = append(p, jpgData[:dataLen]...)
// jpgData = jpgData[dataLen:]
// offset += dataLen
// } else {
// p = append(p, jpgData...)
// jpgData = nil
// }
//
// clone := rtp.Packet{
// Header: rtp.Header{
// Version: 2,
// Marker: jpgData == nil,
// SequenceNumber: sequencer.NextSequenceNumber(),
// Timestamp: packet.Timestamp,
// },
// Payload: p,
// }
// if err := push(&clone); err != nil {
// return err
// }
// }
//
// return nil
// }
// }
//}

24
pkg/mp4/README.md Normal file
View File

@@ -0,0 +1,24 @@
## HEVC
Browser | avc1 | hvc1 | hev1
------------|------|------|---
Mac Chrome | + | - | +
Mac Safari | + | + | -
iOS 15? | + | + | -
Mac Firefox | + | - | -
iOS 12 | + | - | -
Android 13 | + | - | -
```
ffmpeg -i input-hev1.mp4 -c:v copy -tag:v hvc1 -c:a copy output-hvc1.mp4
Stream #0:0(eng): Video: hevc (Main) (hev1 / 0x31766568), yuv420p(tv, progressive), 720x404, 164 kb/s, 29.97 fps,
Stream #0:0(eng): Video: hevc (Main) (hvc1 / 0x31637668), yuv420p(tv, progressive), 720x404, 164 kb/s, 29.97 fps,
```
## Useful links
- https://stackoverflow.com/questions/63468587/what-hevc-codec-tag-to-use-with-fmp4-hvc1-or-hev1
- https://stackoverflow.com/questions/32152090/encode-h265-to-hvc1-codec
- https://jellyfin.org/docs/general/clients/codec-support.html
- https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding
- https://developer.mozilla.org/ru/docs/Web/Media/Formats/codecs_parameter

100
pkg/mp4/const.go Normal file
View File

@@ -0,0 +1,100 @@
package mp4
import (
"encoding/binary"
"github.com/deepch/vdk/format/mp4/mp4io"
"github.com/deepch/vdk/format/mp4f"
"github.com/deepch/vdk/format/mp4f/mp4fio"
"time"
)
var matrix = [9]int32{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000}
var time0 = time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC)
func FTYP() []byte {
b := make([]byte, 0x18)
binary.BigEndian.PutUint32(b, 0x18)
copy(b[0x04:], "ftyp")
copy(b[0x08:], "iso5")
copy(b[0x10:], "iso5")
copy(b[0x14:], "avc1")
return b
}
func MOOV() *mp4io.Movie {
return &mp4io.Movie{
Header: &mp4io.MovieHeader{
PreferredRate: 1,
PreferredVolume: 1,
Matrix: matrix,
NextTrackId: -1,
Duration: 0,
TimeScale: 1000,
CreateTime: time0,
ModifyTime: time0,
PreviewTime: time0,
PreviewDuration: time0,
PosterTime: time0,
SelectionTime: time0,
SelectionDuration: time0,
CurrentTime: time0,
},
MovieExtend: &mp4io.MovieExtend{},
}
}
func TRAK(id int) *mp4io.Track {
return &mp4io.Track{
// trak > tkhd
Header: &mp4io.TrackHeader{
TrackId: int32(id),
Flags: 0x0007, // 7 ENABLED IN-MOVIE IN-PREVIEW
Duration: 0, // OK
Matrix: matrix,
CreateTime: time0,
ModifyTime: time0,
},
// trak > mdia
Media: &mp4io.Media{
// trak > mdia > mdhd
Header: &mp4io.MediaHeader{
TimeScale: 1000,
Duration: 0,
Language: 0x55C4,
CreateTime: time0,
ModifyTime: time0,
},
// trak > mdia > minf
Info: &mp4io.MediaInfo{
// trak > mdia > minf > dinf
Data: &mp4io.DataInfo{
Refer: &mp4io.DataRefer{
Url: &mp4io.DataReferUrl{
Flags: 0x000001, // self reference
},
},
},
// trak > mdia > minf > stbl
Sample: &mp4io.SampleTable{
SampleDesc: &mp4io.SampleDesc{},
TimeToSample: &mp4io.TimeToSample{},
SampleToChunk: &mp4io.SampleToChunk{},
SampleSize: &mp4io.SampleSize{},
ChunkOffset: &mp4io.ChunkOffset{},
},
},
},
}
}
func ESDS(conf []byte) *mp4f.FDummy {
esds := &mp4fio.ElemStreamDesc{DecConfig: conf}
b := make([]byte, esds.Len())
esds.Marshal(b)
return &mp4f.FDummy{
Data: b,
Tag_: mp4io.Tag(uint32(mp4io.ESDS)),
}
}

174
pkg/mp4/consumer.go Normal file
View File

@@ -0,0 +1,174 @@
package mp4
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"sync/atomic"
)
type Consumer struct {
streamer.Element
Medias []*streamer.Media
UserAgent string
RemoteAddr string
muxer *Muxer
codecs []*streamer.Codec
wait byte
send uint32
}
const (
waitNone byte = iota
waitKeyframe
waitInit
)
func (c *Consumer) GetMedias() []*streamer.Media {
if c.Medias != nil {
return c.Medias
}
// default medias
return []*streamer.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264},
{Name: streamer.CodecH265},
},
},
{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecAAC},
},
},
}
}
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
trackID := byte(len(c.codecs))
c.codecs = append(c.codecs, track.Codec)
codec := track.Codec
switch codec.Name {
case streamer.CodecH264:
c.wait = waitInit
push := func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
return nil
}
if c.wait != waitNone {
if c.wait == waitInit || !h264.IsKeyframe(packet.Payload) {
return nil
}
c.wait = waitNone
}
buf := c.muxer.Marshal(trackID, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
}
var wrapper streamer.WrapperFunc
if codec.IsRTP() {
wrapper = h264.RTPDepay(track)
} else {
wrapper = h264.RepairAVC(track)
}
push = wrapper(push)
return track.Bind(push)
case streamer.CodecH265:
c.wait = waitInit
push := func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
return nil
}
if c.wait != waitNone {
if c.wait == waitInit || !h265.IsKeyframe(packet.Payload) {
return nil
}
c.wait = waitNone
}
buf := c.muxer.Marshal(trackID, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
}
if codec.IsRTP() {
wrapper := h265.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
case streamer.CodecAAC:
push := func(packet *rtp.Packet) error {
if c.wait != waitNone {
return nil
}
buf := c.muxer.Marshal(trackID, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
}
if codec.IsRTP() {
wrapper := aac.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}
panic("unsupported codec")
}
func (c *Consumer) MimeType() string {
return c.muxer.MimeType(c.codecs)
}
func (c *Consumer) Init() ([]byte, error) {
c.muxer = &Muxer{}
return c.muxer.GetInit(c.codecs)
}
func (c *Consumer) Start() {
if c.wait == waitInit {
c.wait = waitKeyframe
}
}
//
func (c *Consumer) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
Type: "MP4 client",
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Send: atomic.LoadUint32(&c.send),
}
return json.Marshal(info)
}

View File

@@ -1,47 +0,0 @@
package mp4
import (
"errors"
"io"
)
type MemoryWriter struct {
buf []byte
pos int
}
func (m *MemoryWriter) Write(p []byte) (n int, err error) {
minCap := m.pos + len(p)
if minCap > cap(m.buf) { // Make sure buf has enough capacity:
buf2 := make([]byte, len(m.buf), minCap+len(p)) // add some extra
copy(buf2, m.buf)
m.buf = buf2
}
if minCap > len(m.buf) {
m.buf = m.buf[:minCap]
}
copy(m.buf[m.pos:], p)
m.pos += len(p)
return len(p), nil
}
func (m *MemoryWriter) Seek(offset int64, whence int) (int64, error) {
newPos, offs := 0, int(offset)
switch whence {
case io.SeekStart:
newPos = offs
case io.SeekCurrent:
newPos = m.pos + offs
case io.SeekEnd:
newPos = len(m.buf) + offs
}
if newPos < 0 {
return 0, errors.New("negative result pos")
}
m.pos = newPos
return int64(newPos), nil
}
func (m *MemoryWriter) Bytes() []byte {
return m.buf
}

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