Compare commits

..

434 Commits

Author SHA1 Message Date
Alex X
6f2af78392 Update version to 1.8.0 2023-10-14 17:19:19 +03:00
Alex X
548d8133eb Update readme with new features 2023-10-14 17:17:13 +03:00
Alex X
36ee2b29fb Update TLS files handling 2023-10-14 15:51:43 +03:00
Alex X
05accb4555 Add support media config param for JS player 2023-10-14 11:41:45 +03:00
Alex X
f949a278da Fix HLS JS error on latest iOS 2023-10-14 11:40:49 +03:00
Alex X
bfae16f3a0 Improve SPS parser 2023-10-14 08:11:03 +03:00
Alex X
d09d21434b Panic if shell can't restart process 2023-10-14 08:06:09 +03:00
Alex X
2b9926cedb Support broken SPS for MP4 2023-10-14 08:04:20 +03:00
Alex X
af24fd67aa Fix snapshots for some streams 2023-10-13 14:46:24 +03:00
Alex X
e2cd34ffe3 Fix hap secure connection 2023-10-13 11:25:17 +03:00
Alex X
ecdf5ba271 Fix homekit proxy after events handler 2023-10-13 11:23:00 +03:00
Alex X
995ef5bb36 Add support RTMP from Dahua cameras 2023-10-12 17:55:03 +03:00
Alex X
8165adcab1 Rewrite hap secure connection 2023-10-12 17:03:58 +03:00
Alex X
91c4a3e7b5 Add ffmpeg test for DeckLink 2023-10-11 22:35:53 +03:00
Alex X
cb710ea2be Update dvrip source processing 2023-10-11 22:23:22 +03:00
Alex X
843a3ae9c9 Total rework DVRIP source + add two way audio #633 2023-10-11 19:47:26 +03:00
Alex X
de040fb160 Fix panic for homekit source (nil conn) #628 2023-10-11 14:34:01 +03:00
Alex X
acec8a76aa Fix panic from roborock source (iot.Dial error) #601 2023-10-11 14:26:02 +03:00
Alex X
6c07c59454 Fix panic on aac.RTPDepay #635 2023-10-11 14:21:56 +03:00
Alex X
4d708b5385 Fix send audio to RTSP (cuts out after 30 seconds) #659 2023-10-11 13:56:40 +03:00
Alex X
2e9f3181d4 Fix onvif source: invalid control character in URL #662 2023-10-11 13:43:45 +03:00
Alex X
3ae15d8f80 Add support TLS cert/key as file path #680 2023-10-11 11:50:30 +03:00
Alex X
d016529030 Merge pull request #632 from skrashevich/230911-fix-hap-pairing-dups
Fix: duplicate pairing strings in config
2023-10-11 11:33:05 +03:00
Alex X
09f1553e40 Fix SO_REUSEPORT for macOS #626 2023-10-11 11:31:37 +03:00
Alex X
52e4bf1b35 Add retry for publish 2023-10-11 07:14:43 +03:00
Alex X
bbe6ae0059 Update publish RTMP examples 2023-10-11 06:58:17 +03:00
Alex X
c02117e626 Add support incoming RTMP 2023-10-11 06:54:50 +03:00
Alex X
b8fb3acbab Fix Tapo error on setup 2023-10-11 06:52:39 +03:00
Alex X
d4d0064220 Change publish start time from 5 to 1 second 2023-10-11 06:52:23 +03:00
Alex X
855bbdeb60 Update ffmpeg tests 2023-10-10 12:25:07 +03:00
Alex X
05893c9203 Add fix for YCbCr range on hardware transcoding 2023-10-10 11:29:22 +03:00
Alex X
c9c8e73587 Add feature auto publish on app start 2023-10-09 23:09:28 +03:00
Alex X
c7b6eb5d5b Fix ffmpeg pix_fmt for H264 transcoding 2023-10-09 23:08:18 +03:00
Alex X
96bc88d8ce Add copyright for restart func 2023-10-09 17:37:53 +03:00
Alex X
9a2e9dd6d1 Add support /api/restart #652 2023-10-09 17:12:25 +03:00
Alex X
b252fcaaa1 Code refactoring after #661 2023-10-05 17:31:59 +03:00
Alex X
c582b932c7 Merge pull request #661 from skrashevich/230930-unpkg 2023-10-05 17:31:52 +03:00
Alex X
c3f26c4db8 Fix logger for rtmp 2023-10-05 16:37:58 +03:00
Sergey Krashevich
f27f7d28bb Update editor.html 2023-09-30 12:38:31 +03:00
Alex X
0424b1a92a Merge pull request #656 from skrashevich/230927-fix-whep-link
fix broken link in README
2023-09-27 14:35:09 +03:00
Sergey Krashevich
81fb8fc238 Update README.md 2023-09-27 14:11:03 +03:00
Alex X
037970a4ea Add support ManagedMediaSource for Safari 17 2023-09-26 13:25:50 +03:00
Alex X
3f6e83e87c Merge pull request #653 from skrashevich/230925-fix-openapi-spec
fix openapi specs
2023-09-25 10:33:05 +03:00
Sergey Krashevich
aa5b23fa80 fix openapi specs 2023-09-25 07:41:55 +03:00
Alex X
02bde2c8b7 Add RTMP publish to WebUI 2023-09-17 20:39:31 +03:00
Alex X
cb5e90cc3b Add RTMP server and publish to RMTP logic 2023-09-17 20:31:36 +03:00
Alex X
209fe09806 Add active publish logic to streams 2023-09-17 20:29:28 +03:00
Alex X
dca8279e0c Update AMF tests 2023-09-17 20:28:09 +03:00
Alex X
8163c7a520 Update tcp.Dial func 2023-09-17 20:28:09 +03:00
Alex X
4dffceaf7e Update FLV muxer 2023-09-17 14:07:55 +03:00
Alex X
9f1e33e0c6 Add output to HTTP-FLV 2023-09-16 11:14:56 +03:00
Alex X
9a7d7e68e2 Add tests to AMF reader 2023-09-16 11:12:51 +03:00
Alex X
ab18d5d1ca Improve magic bitstream producer 2023-09-16 11:11:23 +03:00
Alex X
6e53e74742 Add WriteEcmaArray func for AMF proto 2023-09-16 11:10:32 +03:00
Alex X
f910bd4fce Add auto Flush to core WriteBuffer 2023-09-16 11:09:46 +03:00
Alexey Khit
93e475f3a4 Fix support ONVIF client with line breaks #638 2023-09-13 18:08:57 +03:00
Alexey Khit
e5d8170037 Add HomeKit accessories parser 2023-09-12 21:04:55 +03:00
Alexey Khit
861632f92b Add support events for HomeKit client 2023-09-12 21:04:19 +03:00
Alexey Khit
9cf75565b5 Fix error for HAP server 2023-09-12 21:02:40 +03:00
Sergey Krashevich
9368a6b85e Add conditional check before adding a new pair in the server's AddPair function 2023-09-11 10:34:31 +03:00
Alexey Khit
c8ac6b2271 Update version to 1.7.1 2023-09-10 20:19:20 +03:00
Alexey Khit
28f5c2b974 Update dependencies 2023-09-10 20:01:39 +03:00
Alexey Khit
daa2522a52 Fix panic for HomeKit source 2023-09-10 16:10:00 +03:00
Alexey Khit
863f8ec19b Fix malformed HTTP version for HomeKit source #620 2023-09-10 16:08:06 +03:00
Alexey Khit
8f98fc4547 Fix after #614 fix 2023-09-10 16:03:49 +03:00
Alexey Khit
398afbe49f Update default deadline from 3 to 5 seconds 2023-09-10 16:03:08 +03:00
Alexey Khit
ad8c0ab2fb Fix HomeKit pairing for some cameras 2023-09-10 14:56:00 +03:00
Alexey Khit
37130576e9 Add support webrtc go2rtc source with auth #539 2023-09-10 07:58:20 +03:00
Alex X
486fea2227 Merge pull request #611 from felipecrs/patch-1
Clarify import from go2rtc to hass generic camera
2023-09-10 07:18:17 +03:00
Felipe Santos
6d7357b151 Update README.md 2023-09-09 15:16:58 -03:00
Alex X
452d7577f8 Merge pull request #614 from skrashevich/230905-fix-runtime-crash
Refactor LocalIP method to correctly handle non-TCP connections
2023-09-09 17:37:52 +03:00
Alexey Khit
124398115e Restore fix for Chinese buggy cameras 2023-09-06 13:15:04 +03:00
Sergey Krashevich
541a7b28a7 Refactor LocalIP method to correctly handle non-TCP connections 2023-09-05 10:27:12 +03:00
Felipe Santos
947b0970ad Update README.md 2023-09-04 13:23:16 -03:00
Felipe Santos
447fd5b3eb Clarify import from go2rtc to hass generic camera
The protocol is not set by default. According to my tests, only TCP works.
2023-09-04 13:22:11 -03:00
Alexey Khit
064ffef462 Add check config changes during WebUI 2023-09-04 12:05:17 +03:00
Alexey Khit
05360ac284 Fix patch YAML without new line on end of file 2023-09-04 11:52:13 +03:00
Alexey Khit
08dabc7331 Add support HomeKit doorbells 2023-09-02 20:34:39 +03:00
Alexey Khit
d724df7db2 Fix HomeKit PIN in docs 2023-09-02 19:25:48 +03:00
Alexey Khit
fc1b6af436 Update version to 1.7.0 2023-09-02 16:05:09 +03:00
Alexey Khit
88fb589d2e Update readme about new features 2023-09-02 16:04:53 +03:00
Alexey Khit
5c5357cd79 Fix default video codec for HomeKit source 2023-09-02 15:22:03 +03:00
Alexey Khit
5ffd60c429 Update HomeKit default PIN 2023-09-02 14:44:13 +03:00
Alexey Khit
5645c73613 Update FFmpeg device discovery for Linux 2023-09-02 14:40:27 +03:00
Alexey Khit
13a7957cf3 Update HomeKit pairing status 2023-09-02 11:05:02 +03:00
Alexey Khit
c0d5a7c01a Rename PCM codecs print name 2023-09-02 09:17:38 +03:00
Alexey Khit
d87cc9ddb6 Fix homekit pairing table 2023-09-02 09:14:09 +03:00
Alexey Khit
b1c4bcc508 Fix patching YAML in some cases 2023-09-02 08:43:46 +03:00
Alexey Khit
6288c2a57f Add MJPEG support to HomeKit client 2023-09-02 07:39:16 +03:00
Alexey Khit
1c569e690d Fix HomeKit client stat info 2023-09-02 07:38:49 +03:00
Alexey Khit
7fdc6b9472 Update SRTP server constructor 2023-09-02 07:37:49 +03:00
Alexey Khit
60d7d525f2 Update add consumer error message 2023-09-02 07:37:15 +03:00
Alexey Khit
f6f2998e85 Fix JPEG screen length 2023-09-02 07:36:26 +03:00
Alexey Khit
82d1f2cf0b Add new goroutine to debug stack 2023-09-02 07:35:58 +03:00
Alexey Khit
f00e646612 Add support HomeKit server 2023-09-02 06:35:04 +03:00
Alexey Khit
a101387b26 Add debug logger 2023-09-02 06:31:34 +03:00
Alexey Khit
af31ab604d Update FFmpeg preset for OPUS 2023-09-02 06:31:10 +03:00
Alexey Khit
ccdd6ed490 Update SPS parser 2023-09-02 06:30:30 +03:00
Alexey Khit
9f404d965f Rewrite HomeKit pairing API 2023-09-01 22:48:06 +03:00
Alexey Khit
0621b82aff Change response sources API 2023-09-01 22:32:49 +03:00
Alexey Khit
22787b979d Rewrite HomeKit client 2023-09-01 10:38:38 +03:00
Alexey Khit
7d65c60711 Add stream redirect handler 2023-09-01 10:18:50 +03:00
Alexey Khit
69da64a49c Rename streams to sources in the discovery API 2023-09-01 10:17:58 +03:00
Alexey Khit
66c858e00e Rewrite JPEG snapshot consumer 2023-08-30 05:57:00 +03:00
Alexey Khit
ef63cec7a8 Rewrite once buffer for keyframes 2023-08-29 18:04:02 +03:00
Alexey Khit
0ac505ba09 Simplify MJPEG consumer 2023-08-29 17:16:51 +03:00
Alexey Khit
d4444c6257 Code refactoring 2023-08-28 22:43:07 +03:00
Alexey Khit
c6d5bb4eeb Add kasa client and simplify multipart client 2023-08-28 22:31:52 +03:00
Alexey Khit
7f232c5cf2 Add insecure HTTPS requests to IP addresses 2023-08-28 22:29:12 +03:00
Alexey Khit
dc2ab5fcc0 Add support TP-Link Kasa Spot KC401 #545 2023-08-28 19:30:34 +03:00
Alex X
137b23da10 Fix config file validating 2023-08-26 07:13:59 +03:00
Alexey Khit
54e361e3b8 Update go.mod 2023-08-26 07:03:03 +03:00
Alexey Khit
c78da1a7a9 Add about Wyze cameras project to readme 2023-08-25 11:05:19 +03:00
Alex X
27673cb0c1 Merge pull request #592 from skrashevich/go1.21
Update Go version to 1.21 in workflows and Dockerfiles
2023-08-23 18:19:40 +03:00
Alex X
c040a02fa8 Merge pull request #593 from skrashevich/ace-1.24.1
Update ace version to 1.24.1
2023-08-23 18:17:11 +03:00
Alexey Khit
a664e3b838 Code refactoring 2023-08-23 18:14:49 +03:00
Alexey Khit
317b3b5eeb Add support OpenIPC WebRTC format 2023-08-23 18:11:01 +03:00
Sergey Krashevich
9f14b30aae Refactor CSS in editor.html for better readability and remove duplicate body rule 2023-08-23 17:24:17 +03:00
Sergey Krashevich
065a6f4f46 Update Debian and Go versions to bookworm-slim and 1.21-bookworm respectively in hardware.Dockerfile 2023-08-23 17:06:21 +03:00
Alexey Khit
9f9dc7e844 Add support custom timeout for RTSP source 2023-08-23 14:08:15 +03:00
Alexey Khit
b1c0a28366 Update readme about artifacts 2023-08-23 13:27:49 +03:00
Alexey Khit
fc963dfe5c Fix H264 profile parsing for OpenIPC project 2023-08-23 13:26:57 +03:00
Sergey Krashevich
6f5ba2ade6 Update ace library version to 1.24.1 and fix code syntax in editor.html 2023-08-23 12:59:05 +03:00
Alexey Khit
ea708bb606 Add responses on RTSP OPTIONS pings 2023-08-23 10:14:58 +03:00
Sergey Krashevich
0822326900 Update Go version to 1.21 in build and test workflows and Dockerfiles 2023-08-23 10:09:53 +03:00
Alexey Khit
79fc0cd395 Update headers handling for http source 2023-08-23 07:46:34 +03:00
Alex X
357e7c1b18 Merge pull request #557 from h0nIg/patch-1
fix known problem of wrong profile declaration capabilities
2023-08-23 07:01:24 +03:00
Alexey Khit
71f1e445e1 Fix 400 response on PLAY for Reolink Doorbell #562 2023-08-23 06:52:33 +03:00
Alexey Khit
20efe22e60 Update readme about wyze-bridge #588 2023-08-23 06:08:11 +03:00
Alexey Khit
75a3dad745 Fix redirect for rtspx source #565 2023-08-23 06:07:23 +03:00
Alexey Khit
f5cca50830 Add check for empty H265 packet #589 2023-08-22 16:08:02 +03:00
Alexey Khit
8cd977f7ad Add support B-frames for MP4 consumer 2023-08-22 15:55:20 +03:00
Alexey Khit
90f2a9e106 Fix some audio in RTSP server 2023-08-21 20:54:45 +03:00
Alexey Khit
e0ad358aa9 Update timestamp processing for MPEG-TS 2023-08-21 20:34:18 +03:00
Alexey Khit
3db4002420 Support hass source without hass config #541 2023-08-21 16:56:58 +03:00
Alexey Khit
bf248c49c3 Add support two channel PCM family audio #580 2023-08-21 15:35:23 +03:00
Alexey Khit
69a3a30a0e Add media filter for RTSP source #198 2023-08-21 14:07:07 +03:00
Alexey Khit
f80f179e4c Fix MP4 consumer with only audio 2023-08-21 07:31:21 +03:00
Alexey Khit
c1c1d84cef Add AAC consumer 2023-08-21 07:12:30 +03:00
Alexey Khit
c431d888f0 Add AAC raw codec to MPEG-TS consumer 2023-08-21 07:03:40 +03:00
Alexey Khit
2ebb791eb7 Remove old code 2023-08-21 07:00:46 +03:00
Alex X
00b818b4d7 Add support custom headers for HTTP source 2023-08-21 06:30:05 +03:00
Alex X
ce1b0d442c Remove old unnecessary file 2023-08-21 06:29:19 +03:00
Alexey Khit
5283c9781c Update readme about dev version 2023-08-21 00:04:08 +03:00
Alexey Khit
279d8bf799 Rewrite GitHub actions 2023-08-20 23:41:39 +03:00
Alexey Khit
7114d63ba6 Update readme about Reolink cameras 2023-08-20 21:46:12 +03:00
Alexey Khit
120ae89578 Update dependencies 2023-08-20 21:45:08 +03:00
Alexey Khit
d1eb623fd6 Add buffer for RTSP output 2023-08-20 21:25:45 +03:00
Alexey Khit
873cf65317 Increase buffer for RTSP input 2023-08-20 21:24:55 +03:00
Alexey Khit
2091dead3f Refactoring for MP4 file handler 2023-08-20 18:43:42 +03:00
Alexey Khit
2ffd859f0e Update MPEG-TS consumer compatibility 2023-08-20 18:43:12 +03:00
Alexey Khit
da02a97a00 Fix close for HLS source 2023-08-20 18:40:19 +03:00
Alexey Khit
fb51dc781d Improve HLS reader 2023-08-20 16:35:09 +03:00
Alexey Khit
32bf64028d Fix ADTStoRTP parser 2023-08-20 16:33:57 +03:00
Alexey Khit
2e4e75e386 Rewrite MP4, HLS, MPEG-TS consumers 2023-08-20 09:57:46 +03:00
Alexey Khit
f67f6e5b9f Rewrite mpegts producer and consumer 2023-08-19 16:37:52 +03:00
Alexey Khit
24039218a1 Add multipart source to magic source 2023-08-19 16:14:46 +03:00
Alexey Khit
1f447ef73c Rewrite multipart source 2023-08-19 16:14:36 +03:00
Alexey Khit
4509198eef Rewrite FLV source 2023-08-19 16:13:43 +03:00
Alexey Khit
bc60cbefb8 Rewrite magic source 2023-08-19 15:19:09 +03:00
Alexey Khit
a9118562a9 Rewrite MJPEG consumer 2023-08-19 06:09:05 +03:00
Alexey Khit
24637be7c2 Fix panic for multipart client 2023-08-19 06:05:34 +03:00
Alexey Khit
d74be47696 Improve bits reader and writer 2023-08-19 06:04:31 +03:00
Alexey Khit
76a00031cd Code refactoring 2023-08-18 15:53:46 +03:00
Alexey Khit
063a192699 Add support RTMPS source 2023-08-17 08:00:02 +03:00
Alexey Khit
b016b7dc2a Refactoring for RTMP source 2023-08-17 07:59:21 +03:00
Alexey Khit
42f6441512 Fix mpegts reader for tapo client 2023-08-17 07:13:41 +03:00
Alexey Khit
dd066ba040 Add HLS client 2023-08-17 06:55:59 +03:00
Alexey Khit
b3def6cfa2 Rewrite support MPEG-TS client 2023-08-17 05:45:45 +03:00
Alexey Khit
4a82eb3503 Rewrite magic client 2023-08-16 20:13:42 +03:00
Alexey Khit
c3ba8db660 Rewrite FLV/RTMP clients 2023-08-16 19:35:13 +03:00
Alexey Khit
4e1a0e1ab9 Rewrite magic client 2023-08-16 17:15:27 +03:00
Alexey Khit
1dd3dbbcd8 Rewrite AnnexB/AVCC parsers 2023-08-16 16:50:55 +03:00
Alexey Khit
e1be2d9e48 Add buffer to pipe reader 2023-08-16 16:32:09 +03:00
Alexey Khit
8fbfccd024 Add MP4 atoms reader 2023-08-14 14:49:16 +03:00
Alexey Khit
de6bb33f01 Add SPS parser and AVC/HVC conf encoders 2023-08-14 11:55:08 +03:00
Alexey Khit
3a40515a90 Remove old source files 2023-08-14 11:35:34 +03:00
Alexey Khit
5d533338d0 Fix incoming FLV source 2023-08-14 06:49:37 +03:00
Alexey Khit
f412852d50 Remove old HTTP-FLV client 2023-08-14 06:49:37 +03:00
Alexey Khit
5fbec487e2 Add ffplay to links page 2023-08-14 06:49:37 +03:00
Alexey Khit
19c61e20c0 Total rework RTMP client 2023-08-14 06:49:37 +03:00
Alexey Khit
0b6fda2af5 Total rework FLV client 2023-08-14 06:49:37 +03:00
Alexey Khit
e9795e7521 Add goreportcard to readme 2023-08-13 15:44:29 +03:00
Alex X
3b8413a9dd Merge pull request #567 from awatuna/awatuna-patch-1
Update helpers.go
2023-08-07 06:49:32 +04:00
awatuna
b2f9ad7efb Update helpers.go
more tplink ipcams
2023-08-07 06:25:16 +08:00
Alexey Khit
4baa3f5588 Fix rare error with ws.close() 2023-08-04 16:31:13 +04:00
Alex X
9c5ae3260c Merge pull request #561 from dbuezas/fix/another-h265-mediaCode
Add 83 (0x53) to h265 mediaCode
2023-08-04 13:21:00 +04:00
David Buezas
b7baef0a48 Add 83 (0x53) to h265 mediaCode 2023-08-04 11:07:20 +02:00
Alexey Khit
8778d7c9ab Add support http/mixed video/audio #545 2023-08-02 17:57:33 +04:00
Hans-Joachim Kliemeck
d275997e54 fix known problem of wrong profile declaration capabilities 2023-08-01 22:18:45 +02:00
Alexey Khit
2faea1bb69 Fix bug with esp32-cam-webserver #545 2023-07-31 20:55:46 +03:00
Alexey Khit
ba6c96412b Add YAML pkg with Patch function 2023-07-25 18:05:50 +03:00
Alexey Khit
ed38122752 Code refactoring 2023-07-25 18:05:29 +03:00
Alexey Khit
922587ed2e Fix WebUI background color for dark mode browser 2023-07-24 21:57:10 +03:00
Alexey Khit
8e7c9d19e4 Fix H265 codec for bubble source 2023-07-24 14:11:28 +03:00
Alexey Khit
0f33ef0fc5 Add support MJPEG codec for HomeKit cameras 2023-07-23 22:35:53 +03:00
Alexey Khit
a14c87ad60 Code refactoring for MJPEG source 2023-07-23 22:35:31 +03:00
Alexey Khit
6d82b1ce89 Total rework HAP pkg and HomeKit source 2023-07-23 22:22:36 +03:00
Alexey Khit
d73e9f6bcf Fix custom OPUS params inside MP4 2023-07-23 22:19:35 +03:00
Alexey Khit
e6a87fbd69 Add RTSP SDP to stream info JSON 2023-07-23 22:18:54 +03:00
Alexey Khit
3defbd60db Add deadline handler for SRTP server 2023-07-23 17:20:58 +03:00
Alexey Khit
6e9574a1bd Fix receive SRTP with empty sessions 2023-07-23 17:08:14 +03:00
Alexey Khit
7005cd08f2 Improve mDNS handler 2023-07-23 17:07:12 +03:00
Alexey Khit
e94f338b77 Add error msg for producer empty medias 2023-07-23 17:04:19 +03:00
Alexey Khit
d6172587b3 Fix readme about first project in the World 2023-07-21 09:45:19 +03:00
Alexey Khit
f196d83a14 Update version to 1.6.2 2023-07-20 23:40:43 +03:00
Alexey Khit
9d4f4e1509 Fix syscalls on different archs 2023-07-20 23:29:27 +03:00
Alexey Khit
7308652f6e Fix PATCH stream with same name and src 2023-07-20 22:29:59 +03:00
Alexey Khit
870e9c3688 Fix creating stream on the fly #534 2023-07-20 22:09:11 +03:00
Alexey Khit
189f142fae Restore IPv6 support for API and RTSP #532 2023-07-20 22:09:11 +03:00
Alexey Khit
6c0918662e Improve HomeKit source start time 2023-07-20 21:46:06 +03:00
Alexey Khit
2bc01c143a Adds mDNS examples file 2023-07-20 21:34:38 +03:00
Alexey Khit
f310b85ee6 Improve mDNS package 2023-07-20 21:34:16 +03:00
Alexey Khit
97fef36f2f Move cmd to examples 2023-07-20 21:33:29 +03:00
Alexey Khit
a8526ae4eb Update version to 1.6.1 2023-07-20 08:12:10 +03:00
Alexey Khit
966fbe7d61 Update readme about webrtc wyze and kinesis sources 2023-07-20 08:11:26 +03:00
Alexey Khit
a77c2ef71f Update readme about new bubble source 2023-07-20 08:06:04 +03:00
Alexey Khit
61a194e396 Update readme about JPEG snapshot query params 2023-07-20 08:05:42 +03:00
Alexey Khit
ae25784d72 Update readme about MP4 stream query params 2023-07-20 08:04:58 +03:00
Alexey Khit
3343c78699 Add WebRTC sources for Amazon Kinesis and Wyze 2023-07-19 23:36:31 +03:00
Alexey Khit
7928f54a95 Fix handling bubble source 2023-07-17 18:28:21 +03:00
Alexey Khit
e4b68518e5 Remove all listeners from IPv6 interface 2023-07-17 18:28:15 +03:00
Alexey Khit
14ed1cdee8 Add restriction on symbols in dynamic source 2023-07-17 18:28:06 +03:00
Alexey Khit
72f159be88 Update Windows USB audio default settings 2023-07-16 18:40:54 +03:00
Alexey Khit
144954b979 Add default params to Linux ALSA 2023-07-16 14:09:13 +03:00
Alexey Khit
9e15391471 Code refactoring after #517 2023-07-16 13:43:27 +03:00
Alexey Khit
d62b1e445a Merge pull request #517 from skrashevich/230711-jpg-resize 2023-07-16 07:01:40 +03:00
Alexey Khit
ade4c035b7 Fix resample to G711 for WebRTC 2023-07-16 06:36:26 +03:00
Alexey Khit
13ca991c37 Add support pcm_s16le audio 2023-07-15 15:06:49 +03:00
Alexey Khit
e48459f49d Add channels and sample rate params to ALSA 2023-07-15 14:43:47 +03:00
Alexey Khit
facf18e0df Code refactoring for source bubble 2023-07-15 14:43:47 +03:00
Alexey Khit
5c93dc62bd Add support source Bubble (Eseenet/dvr163) 2023-07-15 11:46:10 +03:00
Alexey Khit
d272d4b6c3 Fix FLAC mime type for Chrome 2023-07-15 11:42:50 +03:00
Alexey Khit
1b41edfc7e Fix empty SPS/PPS for HLS/TS 2023-07-15 11:42:12 +03:00
Alexey Khit
d55270bd64 Fix tests 2023-07-13 23:49:17 +03:00
Alexey Khit
85225917f5 Rewritten streams creation 2023-07-13 23:32:01 +03:00
Alexey Khit
eaef62a775 Update RTSPtoWebRTC errors output 2023-07-13 22:52:03 +03:00
Alexey Khit
f6c8d63658 Another fix for OPUS audio quality 2023-07-13 20:31:59 +03:00
Alexey Khit
ea82d7ec2b Add support rotate and scale to MP4 stream 2023-07-13 19:32:55 +03:00
Alexey Khit
e8a7ba056c Add Wyze project to readme 2023-07-13 18:38:08 +03:00
Alexey Khit
9fd40467f2 Update codecs detection for Safari browsers 2023-07-13 16:16:37 +03:00
Alexey Khit
c81e29fe54 Fix FLAC mime type for HLS 2023-07-13 16:14:50 +03:00
Alexey Khit
b9b7bb5489 Adds README for API 2023-07-13 16:10:23 +03:00
Alexey Khit
8036278e29 Fix complex Content-Type for image/jpeg #278 2023-07-11 15:57:21 +03:00
Alexey Khit
39c25215ba Update readme 2023-07-11 15:03:27 +03:00
Sergey Krashevich
490a48cd50 Refactored code to resize JPEG snapshot if "h" parameter exists in the URL query 2023-07-11 10:35:53 +03:00
Alexey Khit
b5d40caffc Update version to 1.6.0 2023-07-11 07:48:51 +03:00
Alexey Khit
1e0952be86 Fix duplicates in mDNS from Hass docker 2023-07-11 07:37:06 +03:00
Alexey Khit
d5fa933772 Update external libraries 2023-07-11 00:49:49 +03:00
Alexey Khit
73bf96e123 Rewrite mDNS processing 2023-07-11 00:44:27 +03:00
Alexey Khit
4ea5a22eda Code refactoring after #510 2023-07-10 11:37:52 +03:00
Alexey Khit
a79fe6041d Merge pull request #510 from horttorrell32/master 2023-07-10 10:58:11 +03:00
Alexey Khit
07440f359e Update MP4 codecs detection 2023-07-08 09:34:00 +03:00
Alexey Khit
01ef67153e Update HLS stream processing 2023-07-08 09:33:31 +03:00
Alexey Khit
fded87aa33 Update stream info for MP4/MSE/HLS 2023-07-08 09:32:54 +03:00
Alexey Khit
52a4fc329c Clear html video resources on disconnect 2023-07-08 09:31:05 +03:00
Alexey Khit
ce61d5759c Fix html video autoplay in some cases 2023-07-08 09:30:41 +03:00
Alexey Khit
39cc4610e3 Add ESLinter and fix JS lint problems 2023-07-08 09:30:02 +03:00
agalindo
67b25015df Update de ffmpeg test after sync 2023-07-07 17:18:01 +02:00
Galindo, Alex
f0d627fa55 Revert "Add framerate parameter option to HTTPs"
This reverts commit f94cd16cb7.
2023-07-07 14:03:53 +02:00
agalindo
9809f41117 Merge branch 'master' of https://github.com/horttorrell32/go2rtc 2023-07-07 07:39:51 +02:00
Galindo, Alex
2ce72dbcca Add more ffmpeg Test 2023-07-06 16:27:23 +02:00
Alexey Khit
ddfeb6fae6 Fix default bin for ffmpeg transcoding to jpeg 2023-07-06 15:22:07 +03:00
Alex X
19130a4858 Merge pull request #414 from skrashevich/230504-patch-dockerfile.hardware
Update hardware.Dockerfile for multi-platform support
2023-07-06 15:18:46 +03:00
Alexey Khit
51b494b193 Add support webrtc/tcp mode to video player 2023-07-06 15:02:39 +03:00
Alexey Khit
fd3b3c9bf1 Replace MP4 stream mode to HLS mode 2023-07-06 15:02:39 +03:00
Alexey Khit
fa763399c2 Improve HLS processing 2023-07-06 15:02:39 +03:00
Alexey Khit
af2398c072 Move mp4 parse codecs func to pkg 2023-07-06 15:02:39 +03:00
Alexey Khit
19b0bc5f44 Update scripts readme 2023-07-06 15:02:39 +03:00
Galindo, Alex
f94cd16cb7 Add framerate parameter option to HTTPs
Some HTTP (a.g. JPEG or MJPEG) needs set the input framerate explicitly.
2023-07-06 12:08:49 +02:00
Alexey Khit
3246e7284c Update API description about WebRTC 2023-07-06 11:36:42 +03:00
Alexey Khit
9339957c13 Add Ezviz to cameras experience 2023-07-06 11:29:46 +03:00
Alexey Khit
4ca397da3d Update OpenAPI link 2023-07-06 11:28:02 +03:00
Galindo, Alex
f6936f7cee Allow add Input param without codecs changes.
Correct the Input Template to delete the 'input' parameter to allow set the copy codecs option, otherwise the '-vn -an' codecs option is selected.
2023-07-06 10:25:09 +02:00
Alexey Khit
bdafaef7dc Add api.html webpage 2023-07-06 11:21:52 +03:00
Alexey Khit
209d7b47d9 Rewrite FFmpeg devices and add support ALSA for Linux 2023-07-04 16:47:07 +03:00
Alexey Khit
4283ae1022 Add RepackG711 func for WebRTC (fix PCMA/PCMU audio) 2023-07-04 16:47:00 +03:00
Alexey Khit
c2a398211c Rewrite repack G711 func (for RTSP backchannel) 2023-07-04 16:46:54 +03:00
Alexey Khit
6c2f883f9e Fix OPUS transcoding quality 2023-07-04 10:37:40 +03:00
Alexey Khit
c34f9ae2b7 Support FFmpeg drawtext param #487 2023-07-03 00:46:35 +03:00
Alexey Khit
c29dd8c4e3 Support templates for FFmpeg raw param #487 2023-07-03 00:44:25 +03:00
Alexey Khit
9e65f18e08 Add interactive OpenAPI to readme 2023-07-02 20:51:40 +03:00
Alexey Khit
db3fb72ac8 Add OpenAPI 2023-07-02 20:47:31 +03:00
Alexey Khit
90cdfafcf5 Add Content-Type to WebRTC API 2023-07-02 20:46:56 +03:00
Alexey Khit
fa8d4e4807 Remove on the fly stream creation for security reason 2023-06-29 22:52:59 +03:00
Alexey Khit
37abe2ce0d Code refactoring after #274 2023-06-29 22:08:17 +03:00
Alexey Khit
1c3835f2a8 Merge pull request #274 from skrashevich/fix-exit-code-on-config-save 2023-06-29 22:04:08 +03:00
Alexey Khit
bc6e4f40bf Code refactoring after #352 2023-06-29 21:39:31 +03:00
Alexey Khit
ac5bcda492 Merge pull request #352 from skrashevich/patch-listen-tls 2023-06-29 21:35:19 +03:00
Alexey Khit
7bd42eb55f Fix onvif discovery close port 2023-06-29 20:29:35 +03:00
Alexey Khit
e4c7ffd1b4 Code refactoring after #462 2023-06-29 17:17:45 +03:00
Alexey Khit
d31cf5521b Merge pull request #462 from dbuezas/dvrip/discovery 2023-06-29 17:14:09 +03:00
Alexey Khit
9de980a63c Fix config tab showing byte string instead of text #479 2023-06-29 16:16:08 +03:00
Alexey Khit
74cef13479 Fix panic after RTSP reconnect feature #433 2023-06-29 16:09:17 +03:00
Alexey Khit
887a491077 Fix panic on processing RTCP from HomeKit cameras #287 2023-06-29 11:13:27 +03:00
Alexey Khit
253fc4c915 Code refactoring for SRTP 2023-06-29 11:13:00 +03:00
Alexey Khit
3a51fa2397 Fix panic with only audio for MP4/MSE #404 2023-06-29 10:55:43 +03:00
Alexey Khit
306451f94f Fix race on pcm pack backchannel #432 2023-06-28 20:25:40 +03:00
Alexey Khit
39811d121b Fix panic on empty path in RTSP link #474 2023-06-28 20:03:09 +03:00
Alexey Khit
99b962e7bb Fix panic on empty RTSP medias #481 2023-06-28 19:59:36 +03:00
Alex X
3dd14a826c Merge pull request #461 from dbuezas/master
For dvrip video codec, only compare least significant 4 bits
2023-06-16 22:48:04 +03:00
David Buezas
a99d7097b9 Revert ignoring high 4 bits and add 0x43 as an h265 code 2023-06-16 21:45:24 +02:00
Alexey Khit
4f97e119ac Update selectall checkbox on index page 2023-06-16 15:18:22 +03:00
Alex X
44ee0066a5 Merge pull request #466 from Vipas-ana/patch-1
Update index.html
2023-06-16 15:14:28 +03:00
Alexey Khit
e5e899450f Fix ws service not load origin from config #469 2023-06-16 15:07:38 +03:00
Alexey Khit
05a2f53b67 Update projects using section in readme 2023-06-16 15:01:42 +03:00
Alexey Khit
63bcaa836a Merge remote-tracking branch 'origin/master' 2023-06-16 15:00:10 +03:00
Alex X
ba68bcb89e Merge pull request #413 from skrashevich/230504-patch-hwdetect-darwin
Refactor video toolbox probe commands to use SVGA resolution in hardw…
2023-06-15 16:52:00 +03:00
Alex X
4a162c9a55 Merge pull request #471 from makuser/patch-1
Fix camera brand typo in README.md
2023-06-12 13:10:41 +03:00
Marc Kolly
c2f5f37f40 Fix camera brand typo in README.md 2023-06-12 10:56:53 +02:00
Vipas-ana
11201790d2 Update index.html
Don't add blank "src" because of the checked "selectall" box.
2023-06-07 10:04:00 -04:00
David Buezas
64804cbc87 Simplify code and improve error handling 2023-06-04 22:37:29 +02:00
David Buezas
75818d6967 Add dvrip discovery 2023-06-04 02:25:22 +02:00
David Buezas
14bb4b40f7 For dvrip video codec, only compare less significant byte 2023-06-03 12:00:35 +02:00
Alex X
0fdb0b128b Merge pull request #460 from dbuezas/master
Add mediaCode 0x12 to CodecH264 identifiers ini DVIRIP stream
2023-06-03 08:07:04 +03:00
David Buezas
fe28c32400 Add mediaCode 0x12 to CodecH264 identifiers ini DVIRIP stream 2023-06-01 21:22:37 +02:00
Alexey Khit
888159d2b6 Update API response mime type 2023-05-31 14:41:57 +03:00
Alexey Khit
397eb0b6ee Fix tests 2023-05-31 14:41:17 +03:00
Alexey Khit
ffeb473918 Remove broken tests 2023-05-31 14:36:00 +03:00
Alexey Khit
966bedd38c Fix MP4 with PCM on Android Telegram 2023-05-30 22:03:20 +03:00
Alexey Khit
0e270081fe Add content-type to API responses 2023-05-30 22:02:16 +03:00
Alexey Khit
1612f9c81e Fix AAC inside MP4 2023-05-25 06:49:04 +03:00
Alexey Khit
bff9b06d5d Add support filename query param for mp4 files 2023-05-25 06:47:51 +03:00
Alexey Khit
59555cfe1d Move WS API to separate module 2023-05-23 14:21:39 +03:00
Sergey Krashevich
c94d1e237d Merge remote-tracking branch 'remotes/upstream/master' into patch-listen-tls 2023-05-21 23:29:08 +03:00
Alexey Khit
82a8e07b66 Rewrite shell signal handling 2023-05-20 06:29:29 +03:00
Alexey Khit
e29307125c Add Nest source for WebRTC cameras 2023-05-20 06:28:33 +03:00
Alexey Khit
1eaacdb217 Add Hass API source for WebRTC cameras 2023-05-20 06:26:05 +03:00
Alexey Khit
c09438d3d0 Set prefer_tcp flag for ffmpeg 2023-05-16 18:39:39 +03:00
Alexey Khit
8b126c0d37 Add support RTSP over WebSocket 2023-05-06 14:31:46 +03:00
Alexey Khit
3139189975 Move ParseQuery from ffmpeg to streams module 2023-05-06 14:29:35 +03:00
Alexey Khit
4fe078c7c0 ONVIF code refactoring 2023-05-05 10:07:14 +03:00
Alexey Khit
083ec127fd Fix video timestamp accuracy 2023-05-05 09:45:55 +03:00
Alexey Khit
bcb9756aca Update readme about ONVIF 2023-05-05 09:08:13 +03:00
Sergey Krashevich
981974eac9 Update hardware.Dockerfile 2023-05-04 22:45:39 +03:00
Sergey Krashevich
5b29306d4f Refactor video toolbox probe commands to use SVGA resolution in hardware_darwin.go 2023-05-04 22:22:00 +03:00
Alexey Khit
e89c5cb429 Add bages to readme 2023-05-04 17:45:17 +03:00
Alexey Khit
04f263aa15 Add binary for old Raspberry 1 and Zero 2023-05-04 17:40:54 +03:00
Alexey Khit
da92256910 Update version to 1.5.0 2023-05-04 15:04:06 +03:00
Alexey Khit
035b824645 Update readme 2023-05-04 15:03:03 +03:00
Alexey Khit
2a91c4625a Add ALSA support inside docker 2023-05-04 13:47:12 +03:00
Alexey Khit
23dd5b450c Add support H265 codec for MPEG-TS 2023-05-04 12:26:56 +03:00
Alexey Khit
f617c148cd Fix FFmpeg template for H265 2023-05-04 11:57:32 +03:00
Alexey Khit
b5f4c7f75b Rewrite exec pipe, TCP and HTTP sources 2023-05-04 11:56:56 +03:00
Alexey Khit
d44efb84a0 Fix buffer size for mpegts 2023-05-04 11:49:38 +03:00
Alexey Khit
03968d2f2e Restore hadrware transcoding for MJPEG 2023-05-04 07:39:15 +03:00
Alexey Khit
3c371e7046 Change FFmpeg output for MJPEG to pipe 2023-05-04 07:38:49 +03:00
Alexey Khit
4656086985 Add auto transcoding to JPEG snapshot 2023-05-04 06:49:54 +03:00
Alexey Khit
e78f9fa69d Add support pipe to exec source 2023-05-04 06:49:54 +03:00
Alexey Khit
2e8be342ef Rework FFmpeg hardware support 2023-05-04 01:24:37 +03:00
Alexey Khit
5387e88fe3 Rework FFmpeg devices support 2023-05-04 00:03:01 +03:00
Alexey Khit
1746f55eda Add pix_fmt to ffmpeg h264 transcoding 2023-05-03 23:57:39 +03:00
Alexey Khit
4d53889519 Improve support ONVIF client 2023-05-03 08:02:56 +03:00
Alexey Khit
6d9d89bbe3 Fix support 2 way audio for Reolink Doorbell #331 2023-05-03 08:01:33 +03:00
Alexey Khit
c1923627c0 Fix panic on Producer GetMedias 2023-05-03 07:57:00 +03:00
Alexey Khit
95ca5f5fe1 Remove unnecessary run.sh file 2023-05-02 14:20:55 +03:00
Alexey Khit
4bbd3a1cd2 Fix ONVIF discovery for buggy camera 2023-05-02 11:25:00 +03:00
Alexey Khit
9c8a1d8b19 Add path to ONVIF requests 2023-05-02 11:07:12 +03:00
Alexey Khit
53967fc72a Update ONVIF discovery request #397 2023-05-02 09:36:38 +03:00
Alexey Khit
31f870e950 Update internal readme 2023-05-01 15:09:58 +03:00
Alexey Khit
c7d228daff Remove mp4 pkg dependency from rtsp pkg 2023-05-01 14:33:03 +03:00
Alexey Khit
378f071e2c Add go2rtc_rtsp app 2023-05-01 12:55:32 +03:00
Alexey Khit
75f61b38ac Move cmd module to internal 2023-05-01 12:55:14 +03:00
Alexey Khit
bc770f1a85 Remove FFmpeg buffer because have problems with MJPEG 2023-04-29 17:04:56 +03:00
Alexey Khit
d276311fcf Add support insecure HTTPS client 2023-04-29 17:00:52 +03:00
Alexey Khit
1e14dc9ab2 Add ONVIF client and server support 2023-04-29 15:12:59 +03:00
Alexey Khit
8dbaa4ba93 Fix RTSP client Session processing 2023-04-29 13:48:17 +03:00
Alexey Khit
f0893bd78b Fix bug in SDP from Annke CZ400 #384 2023-04-27 14:02:55 +03:00
Alexey Khit
6247746177 Change localhost to 127.0.0.1 2023-04-25 06:21:50 +03:00
Alexey Khit
a20de73ab2 Add pkt_size option fort RTSP server 2023-04-24 06:40:11 +03:00
Alexey Khit
813c8b3b3d Make core atoi func public 2023-04-24 06:39:07 +03:00
Alexey Khit
63d9c6c2b7 Fix Chinese cameras with wrong Session header after v1.4.0 #382 2023-04-23 20:42:49 +03:00
Alexey Khit
2610f15eb6 Update readme about codecs 2023-04-23 08:08:52 +03:00
Alexey Khit
9268acf1ca Update version to 1.4.0 2023-04-23 08:08:16 +03:00
Alexey Khit
55fdf1a647 Fix RTSP server handler for some Cloud clients #347 2023-04-22 21:04:44 +03:00
Alexey Khit
5fe07aeea0 Fix FLV to RTSP transport after v1.3 #362 2023-04-22 21:04:26 +03:00
Alexey Khit
e8b22bca99 Fix RTSP server close (panic) without client request #364 2023-04-22 21:04:13 +03:00
Alexey Khit
5926c1deb9 Fix default sample rate for MP3 codec 2023-04-22 18:15:20 +03:00
Alexey Khit
dd98edc48e Add support resampling for PCM for WebRTC 2023-04-22 08:54:31 +03:00
Alexey Khit
fb1cc7dfc2 Update FFmpeg OPUS params 2023-04-22 08:53:35 +03:00
Alexey Khit
7626a09c1c Fix unsupported FLAC encoder params 2023-04-22 08:53:08 +03:00
Alexey Khit
db85533e74 Add more sample rates to FLAC encoder 2023-04-22 08:52:32 +03:00
Alexey Khit
5939c8acba Update MP4 links query 2023-04-20 21:47:28 +03:00
Alexey Khit
e985ad23a2 Fix HLS handler 2023-04-20 21:47:21 +03:00
Alexey Khit
7452eb5e05 Add support FLAC codec to MP4/MSE 2023-04-20 21:32:46 +03:00
Alexey Khit
5f9788209d Move MP4 mutex from HTTP to Muxer 2023-04-20 13:20:52 +03:00
Alexey Khit
c07ddb8309 Add HTTP 500 error response for MP4 API 2023-04-20 13:16:15 +03:00
Alexey Khit
79f1dcfea3 Update version to 1.3.2 2023-04-17 17:03:12 +03:00
Alexey Khit
3feaf852af Fix panic for wrong ffmpeg device in linux 2023-04-17 17:02:24 +03:00
Alexey Khit
76ec70d2a0 Adds RTSP client custom keepalive timeout 2023-04-17 16:54:02 +03:00
Alexey Khit
6cef5faf27 Add timeout value to RTSP SETUP response #289 2023-04-17 15:12:03 +03:00
Alexey Khit
edb4e6eaad Update error msg for stream start 2023-04-17 15:04:45 +03:00
Alexey Khit
116319f876 Restore mutex for MP4 2023-04-17 14:17:45 +03:00
Alexey Khit
a0e6005598 Remove Range header check for MP4 for Chrome 2023-04-17 14:17:21 +03:00
Alexey Khit
fd580b6f2c Fix RTSP passive producer 2023-04-17 10:09:38 +03:00
Alexey Khit
1837e7c86c Fix cons number in trace logs 2023-04-17 10:08:42 +03:00
Alexey Khit
235f2fde0d Add control attr to RTSP server SDP 2023-04-16 14:52:02 +03:00
Alexey Khit
35087e0812 Remove mutex from MP4 2023-04-16 14:48:26 +03:00
Alexey Khit
da08d8e973 Fix RTSP backchannel processing 2023-04-16 14:47:59 +03:00
Alexey Khit
757091e43d Rewrite RTSP keepalive 2023-04-16 14:47:07 +03:00
Alexey Khit
a5c4854aeb Add reconnect logic to RTSP client 2023-04-16 13:57:27 +03:00
Alexey Khit
4b4deaaaf2 Fix missed control in SDP 2023-04-15 12:52:52 +03:00
Alexey Khit
553f5ff0d8 Add timeout to RTSP client requests 2023-04-15 12:51:15 +03:00
Alexey Khit
25dc3664fd Set random session for RTSP server 2023-04-15 12:50:50 +03:00
Alexey Khit
8dd9991268 Fix mutex lock after #339 2023-04-15 07:53:26 +03:00
Alexey Khit
d633d331bb Fix new stream from camera entity from Hass 2023-04-15 07:34:38 +03:00
Alexey Khit
7d3fbf2ee0 Add trace logs for media matching 2023-04-15 07:33:22 +03:00
Sergey Krashevich
af717b2172 add tls support 2023-04-14 18:28:03 +03:00
Alex X
c44aaebd65 Merge pull request #186 from skrashevich/fix-cors-auth
Fix CORS for Basic auth
2023-04-14 06:29:13 +03:00
Alexey Khit
d6259fc0e9 Merge remote-tracking branch 'skrashevich/patch-230328' 2023-04-14 06:23:17 +03:00
Alex X
5c657d557a Merge pull request #332 from acardace/fix-isapi-2way
Fix ISAPI 2 Way Audio
2023-04-14 06:18:26 +03:00
Alex X
93be5cd92f Merge pull request #339 from yousong/mp4-conc
Fixes for working with rtsp stream of TL-IPC44GW
2023-04-14 06:13:30 +03:00
Yousong Zhou
cf6a35d0c7 rtsp: fixup sdp media type returned by TP-LINK camera
Related: https://github.com/aler9/mediamtx/issues/990
2023-04-10 09:06:50 +08:00
Yousong Zhou
af79e6054b rtsp: conn.Close() before retry dialing with backchannel=false 2023-04-07 16:46:20 +08:00
Yousong Zhou
9f3d5e7460 stream.mp4: write response in a serial way 2023-04-07 15:58:40 +08:00
Alexey Khit
abbf180b1b Fix Reolink Doorbell bug with SDP 2023-04-06 17:31:32 +03:00
Alex X
696588e52e Merge pull request #327 from skrashevich/patch-dockerfiles-go120
Update Go version to 1.20 in Dockerfiles and test.yml
2023-04-04 19:32:25 +03:00
Antonio Cardace
3e97ce8b2a Fix ISAPI 2 Way Audio
Signed-off-by: Antonio Cardace <acardace@redhat.com>
2023-04-04 17:41:06 +02:00
Sergey Krashevich
722b2827a1 Update Go version to 1.20 in test.yml and Dockerfiles 2023-04-03 20:30:25 +03:00
Alex X
69598b508c Merge pull request #321 from skrashevich/github-actions-update
Update Docker build-push-action to version 4 in workflows/docker.yml …
2023-04-02 10:08:44 +03:00
Alex X
f49fcc4f68 Merge pull request #322 from skrashevich/actions-docker-cache
Add cache for faster Docker builds from the GitHub Actions environment
2023-04-02 07:12:38 +03:00
Sergey Krashevich
59347a409e Add cache for faster Docker builds from the GitHub Actions environment in ".github/workflows/docker.yml". 2023-04-02 07:05:09 +03:00
Alex X
45b25d29b7 Merge pull request #320 from skrashevich/hardware-dockerfile-patch
Update hardware.Dockerfile
2023-04-02 06:39:48 +03:00
Sergey Krashevich
49e861d1b0 Update Docker build-push-action to version 4 in workflows/docker.yml and workflows/test.yml and deploy-pages to version 2 in workflows/gh-pages.yml 2023-04-02 04:02:28 +03:00
Sergey Krashevich
b1701e856a Update hardware.Dockerfile 2023-04-02 02:28:44 +03:00
Sergey Krashevich
a6260d0f56 Refactor Receiver and Sender methods to use RLock instead of Lock 2023-03-28 16:44:52 +03:00
Sergey Krashevich
693d41be87 Add buffer size of 8M to RTSP output in ffmpeg and handle EOF error when accepting RTSP connection in rtsp.go 2023-03-28 15:47:25 +03:00
Sergey Krashevich
222dc6a5c2 Refactor code to include buffer channels to prevent blocking in handler functions and add RWMutex to Receiver and Sender structs for concurrency-safe data access 2023-03-28 05:56:35 +03:00
Alexey Khit
8fde2b6fe5 Add support TCP MPEG-TS source 2023-03-26 17:29:59 +03:00
Alexey Khit
15e205cc01 Change HLS ID format 2023-03-26 15:45:07 +03:00
Alexey Khit
1db9ed4946 Disable MP4 redirect for Safari with duration query 2023-03-26 15:26:49 +03:00
Alexey Khit
fd83d151d2 Fix HLS after big refactoring 2023-03-26 15:26:18 +03:00
Sergey Krashevich
91a7b5be27 Update editor.html 2023-02-27 05:37:17 +03:00
Sergey Krashevich
a36359f3dd Update api.go 2023-01-20 18:38:46 +03:00
323 changed files with 23163 additions and 10827 deletions

189
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,189 @@
name: Build and Push
on:
workflow_dispatch:
push:
branches:
- 'master'
tags:
- 'v*'
jobs:
build-binaries:
name: Build binaries
runs-on: ubuntu-latest
env: { CGO_ENABLED: 0 }
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v4
with: { go-version: '1.21' }
- name: Build go2rtc_win64
env: { GOOS: windows, GOARCH: amd64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_win64
uses: actions/upload-artifact@v3
with: { name: go2rtc_win64, path: go2rtc.exe }
- name: Build go2rtc_win32
env: { GOOS: windows, GOARCH: 386 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_win32
uses: actions/upload-artifact@v3
with: { name: go2rtc_win32, path: go2rtc.exe }
- name: Build go2rtc_win_arm64
env: { GOOS: windows, GOARCH: arm64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_win_arm64
uses: actions/upload-artifact@v3
with: { name: go2rtc_win_arm64, path: go2rtc.exe }
- name: Build go2rtc_linux_amd64
env: { GOOS: linux, GOARCH: amd64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_amd64
uses: actions/upload-artifact@v3
with: { name: go2rtc_linux_amd64, path: go2rtc }
- name: Build go2rtc_linux_i386
env: { GOOS: linux, GOARCH: 386 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_i386
uses: actions/upload-artifact@v3
with: { name: go2rtc_linux_i386, path: go2rtc }
- name: Build go2rtc_linux_arm64
env: { GOOS: linux, GOARCH: arm64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_arm64
uses: actions/upload-artifact@v3
with: { name: go2rtc_linux_arm64, path: go2rtc }
- name: Build go2rtc_linux_arm
env: { GOOS: linux, GOARCH: arm, GOARM: 7 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_arm
uses: actions/upload-artifact@v3
with: { name: go2rtc_linux_arm, path: go2rtc }
- name: Build go2rtc_linux_armv6
env: { GOOS: linux, GOARCH: arm, GOARM: 6 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_armv6
uses: actions/upload-artifact@v3
with: { name: go2rtc_linux_armv6, path: go2rtc }
- name: Build go2rtc_linux_mipsel
env: { GOOS: linux, GOARCH: mipsle }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_mipsel
uses: actions/upload-artifact@v3
with: { name: go2rtc_linux_mipsel, path: go2rtc }
- name: Build go2rtc_mac_amd64
env: { GOOS: darwin, GOARCH: amd64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_mac_amd64
uses: actions/upload-artifact@v3
with: { name: go2rtc_mac_amd64, path: go2rtc }
- name: Build go2rtc_mac_arm64
env: { GOOS: darwin, GOARCH: arm64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_mac_arm64
uses: actions/upload-artifact@v3
with: { name: go2rtc_mac_arm64, path: go2rtc }
docker-master:
name: Build docker master
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: 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@v4
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 }}
cache-from: type=gha
cache-to: type=gha,mode=max
docker-hardware:
name: Build docker hardware
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
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@v4
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 }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,75 +0,0 @@
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 }}

View File

@@ -34,4 +34,4 @@ jobs:
path: './website'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
uses: actions/deploy-pages@v2

View File

@@ -1,99 +0,0 @@
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: install lipo
run: |
curl -L -o /tmp/lipo https://github.com/konoui/lipo/releases/latest/download/lipo_Linux_amd64
chmod +x /tmp/lipo
mv /tmp/lipo /usr/local/bin
- name: Build Go binaries
run: |
#!/bin/bash
export CGO_ENABLED=0
mkdir -p 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
go build -ldflags "-s -w" -trimpath -o go2rtc.amd64
export GOOS=darwin
export GOARCH=arm64
go build -ldflags "-s -w" -trimpath -o go2rtc.arm64
export FILENAME=artifacts/go2rtc_mac_universal.zip
lipo -output go2rtc -create go2rtc.arm64 go2rtc.amd64 && 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

View File

@@ -1,11 +1,11 @@
name: Test Build and Run
on:
push:
branches:
- '*'
pull_request:
merge_group:
# push:
# branches:
# - '*'
# pull_request:
# merge_group:
workflow_dispatch:
jobs:
@@ -26,7 +26,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: '1.19'
go-version: '1.21'
- name: Build Go binary
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
@@ -76,7 +76,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/${{ matrix.platform }}
@@ -89,7 +89,7 @@ jobs:
- name: Build and push Hardware
if: matrix.platform == 'amd64'
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
file: hardware.Dockerfile

5
.gitignore vendored
View File

@@ -1,8 +1,7 @@
.idea/
.tmp/
go2rtc.yaml
go2rtc.json
0_test.go

View File

@@ -2,7 +2,7 @@
# 0. Prepare images
ARG PYTHON_VERSION="3.11"
ARG GO_VERSION="1.19"
ARG GO_VERSION="1.21"
ARG NGROK_VERSION="3"
FROM python:${PYTHON_VERSION}-alpine AS base
@@ -40,7 +40,9 @@ 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
# alsa-plugins-pulse for ALSA support (+0MB)
# font-droid for FFmpeg drawtext filter (+2MB)
RUN apk add --no-cache tini ffmpeg bash curl jq alsa-plugins-pulse font-droid
# Hardware Acceleration for Intel CPU (+50MB)
ARG TARGETARCH

465
README.md
View File

@@ -1,5 +1,10 @@
# go2rtc
[![](https://img.shields.io/github/stars/AlexxIT/go2rtc?style=flat-square&logo=github)](https://github.com/AlexxIT/go2rtc/stargazers)
[![](https://img.shields.io/docker/pulls/alexxit/go2rtc?style=flat-square&logo=docker&logoColor=white&label=pulls)](https://hub.docker.com/r/alexxit/go2rtc)
[![](https://img.shields.io/github/downloads/AlexxIT/go2rtc/total?color=blue&style=flat-square&logo=github)](https://github.com/AlexxIT/go2rtc/releases)
[![](https://goreportcard.com/badge/github.com/AlexxIT/go2rtc)](https://goreportcard.com/report/github.com/AlexxIT/go2rtc)
Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg, RTMP, etc.
![](assets/go2rtc.png)
@@ -8,9 +13,10 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- zero-delay for many supported protocols (lowest possible streaming latency)
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [DVRIP](#source-dvrip), [HTTP](#source-http) (FLV/MJPEG/JPEG/TS), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
- streaming from any sources, supported by [FFmpeg](#source-ffmpeg)
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4), [HLS](#module-hls) or [MJPEG](#module-mjpeg)
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4), [HomeKit](#module-homekit) [HLS](#module-hls) or [MJPEG](#module-mjpeg)
- [publish](#publish-stream) any source to popular streaming services (YouTube, Telegram, etc.)
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
- first project in the World with support H265 for WebRTC in browser (Safari only, [read more](https://github.com/AlexxIT/Blog/issues/5))
- 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)
- play audio files and live streams on some cameras with [speaker](#stream-to-camera)
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
@@ -36,30 +42,38 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
* [go2rtc: Docker](#go2rtc-docker)
* [go2rtc: Home Assistant Add-on](#go2rtc-home-assistant-add-on)
* [go2rtc: Home Assistant Integration](#go2rtc-home-assistant-integration)
* [go2rtc: Dev version](#go2rtc-dev-version)
* [Configuration](#configuration)
* [Module: Streams](#module-streams)
* [Two way audio](#two-way-audio)
* [Source: RTSP](#source-rtsp)
* [Source: RTMP](#source-rtmp)
* [Source: HTTP](#source-http)
* [Source: ONVIF](#source-onvif)
* [Source: FFmpeg](#source-ffmpeg)
* [Source: FFmpeg Device](#source-ffmpeg-device)
* [Source: Exec](#source-exec)
* [Source: Echo](#source-echo)
* [Source: HomeKit](#source-homekit)
* [Source: Bubble](#source-bubble)
* [Source: DVRIP](#source-dvrip)
* [Source: Tapo](#source-tapo)
* [Source: Kasa](#source-kasa)
* [Source: Ivideon](#source-ivideon)
* [Source: Hass](#source-hass)
* [Source: ISAPI](#source-isapi)
* [Source: Nest](#source-nest)
* [Source: Roborock](#source-roborock)
* [Source: WebRTC](#source-webrtc)
* [Source: WebTorrent](#source-webtorrent)
* [Incoming sources](#incoming-sources)
* [Stream to camera](#stream-to-camera)
* [Publish stream](#publish-stream)
* [Module: API](#module-api)
* [Module: RTSP](#module-rtsp)
* [Module: RTMP](#module-rtmp)
* [Module: WebRTC](#module-webrtc)
* [Module: HomeKit](#module-homekit)
* [Module: WebTorrent](#module-webtorrent)
* [Module: Ngrok](#module-ngrok)
* [Module: Hass](#module-hass)
@@ -97,11 +111,13 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
- `go2rtc_win64.zip` - Windows 64-bit
- `go2rtc_win32.zip` - Windows 32-bit
- `go2rtc_win_arm64.zip` - Windows ARM 64-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 MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3))
- `go2rtc_linux_armv6` - Linux ARMv6 (for old Raspberry 1 and Zero)
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks))
- `go2rtc_mac_amd64.zip` - Mac Intel 64-bit
- `go2rtc_mac_arm64.zip` - Mac ARM 64-bit
@@ -124,6 +140,14 @@ Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support
[WebRTC Camera](https://github.com/AlexxIT/WebRTC) custom component can be used on any [Home Assistant installation](https://www.home-assistant.io/installation/), including [HassWP](https://github.com/AlexxIT/HassWP) on Windows. It can automatically download and use the latest version of go2rtc. Or it can connect to an existing version of go2rtc. Addon installation in this case is optional.
### go2rtc: Dev version
Latest, but maybe unstable version:
- Binary: GitHub > [Actions](https://github.com/AlexxIT/go2rtc/actions) > [Build and Push](https://github.com/AlexxIT/go2rtc/actions/workflows/build.yml) > latest run > Artifacts section (you should be logged in to GitHub)
- Docker: `alexxit/go2rtc:master` or `alexxit/go2rtc:master-hardware` versions
- Hass Add-on: `go2rtc master` or `go2rtc master hardware` versions
## Configuration
- by default go2rtc will search `go2rtc.yaml` in the current work dirrectory
@@ -156,14 +180,17 @@ Available source types:
- [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras with [two way audio](#two-way-audio) support
- [rtmp](#source-rtmp) - `RTMP` streams
- [http](#source-http) - `HTTP-FLV`, `MPEG-TS`, `JPEG` (snapshots), `MJPEG` streams
- [http](#source-http) - `HTTP-FLV`, `MPEG-TS`, `JPEG` (snapshots), `MJPEG` streams
- [onvif](#source-onvif) - get camera `RTSP` link and snapshot link using `ONVIF` protocol
- [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
- [exec](#source-exec) - get media from external app output
- [echo](#source-echo) - get stream link from bash or python
- [homekit](#source-homekit) - streaming from HomeKit Camera
- [bubble](#source-bubble) - streaming from ESeeCloud/dvr163 NVR
- [dvrip](#source-dvrip) - streaming from DVR-IP NVR
- [tapo](#source-tapo) - TP-Link Tapo cameras with [two way audio](#two-way-audio) support
- [kasa](#source-tapo) - TP-Link Kasa cameras
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
- [hass](#source-hass) - Home Assistant integration
- [isapi](#source-isapi) - two way audio for Hikvision (ISAPI) cameras
@@ -178,6 +205,7 @@ Read more about [incoming sources](#incoming-sources)
Supported for sources:
- [RTSP cameras](#source-rtsp) with [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) (back channel connection)
- [DVRIP](#source-dvrip) cameras
- [TP-Link Tapo](#source-tapo) cameras
- [Hikvision ISAPI](#source-isapi) cameras
- [Roborock vacuums](#source-roborock) models with cameras
@@ -197,7 +225,7 @@ streams:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1
amcrest_doorbell:
- rtsp://username:password@192.168.1.123:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
unify_camera: rtspx://192.168.1.123:7441/fD6ouM72bWoFijxK
unifi_camera: rtspx://192.168.1.123:7441/fD6ouM72bWoFijxK
glichy_camera: ffmpeg:rstp://username:password@192.168.1.123/live/ch00_1
```
@@ -205,12 +233,32 @@ streams:
- **Amcrest Doorbell** users may want to disable two way audio, because with an active stream you won't have a call button working. You need to add `#backchannel=0` to the end of your RTSP link in YAML config file
- **Dahua Doorbell** users may want to change backchannel [audio codec](https://github.com/AlexxIT/go2rtc/issues/52)
- **Unify** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81)
- **Reolink** users may want NOT to use RTSP protocol at all, some camera models have a very awful unusable stream implementation
- **Ubiquiti UniFi** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81)
- **TP-Link Tapo** users may skip login and password, because go2rtc support login [without them](https://drmnsamoliu.github.io/video.html)
- If your camera has two RTSP links - you can add both of them as sources. This is useful when streams has different codecs, as example AAC audio with main stream and PCMU/PCMA audio with second stream
- If the stream from your camera is glitchy, try using [ffmpeg source](#source-ffmpeg). It will not add CPU load if you won't use transcoding
- If the stream from your camera is very glitchy, try to use transcoding with [ffmpeg source](#source-ffmpeg)
**Other options**
Format: `rtsp...#{param1}#{param2}#{param3}`
- Add custom timeout `#timeout=30` (in seconds)
- Ignore audio - `#media=video` or ignore video - `#media=audio`
- Ignore two way audio API `#backchannel=0` - important for some glitchy cameras
- Use WebSocket transport `#transport=ws...`
**RTSP over WebSocket**
```yaml
streams:
# WebSocket with authorization, RTSP - without
axis-rtsp-ws: rtsp://192.168.1.123:4567/axis-media/media.amp?overview=0&camera=1&resolution=1280x720&videoframeskipmode=empty&Axis-Orig-Sw=true#transport=ws://user:pass@192.168.1.123:4567/rtsp-over-websocket
# WebSocket without authorization, RTSP - with
dahua-rtsp-ws: rtsp://user:pass@192.168.1.123/cam/realmonitor?channel=1&subtype=1&proto=Private3#transport=ws://192.168.1.123/rtspoverwebsocket
```
#### Source: RTMP
You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp).
@@ -229,6 +277,8 @@ Support Content-Type:
- **HTTP-MJPEG** (`multipart/x`) - simple MJPEG stream over HTTP
- **MPEG-TS** (`video/mpeg`) - legacy [streaming format](https://en.wikipedia.org/wiki/MPEG_transport_stream)
Source also support HTTP and TCP streams with autodetection for different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**.
```yaml
streams:
# [HTTP-FLV] stream in video/x-flv format
@@ -239,10 +289,29 @@ streams:
# [MJPEG] stream will be proxied without modification
http_mjpeg: https://mjpeg.sanford.io/count.mjpeg
# [MJPEG or H.264/H.265 bitstream or MPEG-TS]
tcp_magic: tcp://192.168.1.123:12345
# Add custom header
custom_header: "https://mjpeg.sanford.io/count.mjpeg#header=Authorization: Bearer XXX"
```
**PS.** Dahua camera has bug: if you select MJPEG codec for RTSP second stream - snapshot won't work.
#### Source: ONVIF
The source is not very useful if you already know RTSP and snapshot links for your camera. But it can be useful if you don't.
**WebUI > Add** webpage support ONVIF autodiscovery. Your server must be on the same subnet as the camera. If you use docker, you must use "network host".
```yaml
streams:
dahua1: onvif://admin:password@192.168.1.123
reolink1: onvif://admin:password@192.168.1.123:8000
tapo1: onvif://admin:password@192.168.1.123:2020
```
#### 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.
@@ -273,7 +342,7 @@ streams:
rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
```
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/cmd/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/internal/ffmpeg/ffmpeg.go): `h264`, `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.
@@ -281,45 +350,67 @@ But you can override them via YAML config. You can also add your own formats to
ffmpeg:
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..."
mycodec: "-any args that supported by ffmpeg..."
myinput: "-fflags nobuffer -flags low_delay -timeout 5000000 -i {input}"
myraw: "-ss 00:00:20"
```
- 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 `video` and `audio` params multiple times (ex. `#video=copy#audio=copy#audio=pcmu`)
- You can use `rotate` param 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 `drawtext` to add a timestamp (ex. `drawtext=x=2:y=2:fontsize=12:fontcolor=white:box=1:boxcolor=black`)
- This will greatly increase the CPU of the server, even with hardware acceleration
- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`)
- You can use `input` param to override default input template (ex. `#input=rtsp/udp` will change RTSP transport from TCP to UDP+TCP)
- You can use raw input value (ex. `#input=-timeout 5000000 -i {input}`)
- You can add your own input templates
Read more about encoding [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration).
Read more about [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration).
**PS.** It is recommended to check the available hardware in the WebUI add page.
#### 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!
- `video_size` 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
Format: `ffmpeg:device?{input-params}#{param1}#{param2}#{param3}`
```yaml
streams:
linux_usbcam: ffmpeg:device?video=0&resolution=1280x720#video=h264
linux_usbcam: ffmpeg:device?video=0&video_size=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
macos_facetime: ffmpeg:device?video=0&audio=1&video_size=1280x720&framerate=30#video=h264#audio=pcma
```
**PS.** It is recommended to check the available devices in the WebUI add page.
#### Source: Exec
FFmpeg source just a shortcut to exec source. You can get any stream or file or device via FFmpeg or GStreamer and push it to go2rtc via RTSP protocol:
Exec source can run any external application and expect data from it. Two transports are supported - **pipe** and **RTSP**.
If you want to use **RTSP** transport - the command must contain the `{output}` argument in any place. On launch, it will be replaced by the local address of the RTSP server.
**pipe** reads data from app stdout in different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**.
The source can be used with:
- [FFmpeg](https://ffmpeg.org/) - go2rtc ffmpeg source just a shortcut to exec source
- [GStreamer](https://gstreamer.freedesktop.org/)
- [Raspberry Pi Cameras](https://www.raspberrypi.com/documentation/computers/camera_software.html)
- any your own software
```yaml
streams:
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
stream: exec:ffmpeg -re -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
picam_h264: exec:libcamera-vid -t 0 --inline -o -
picam_mjpeg: exec:libcamera-vid -t 0 --codec mjpeg -o -
```
#### Source: Echo
@@ -352,9 +443,8 @@ If you see a device but it does not have a pair button - it is paired to some ec
**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
- Audio should be transcoded for using with MSE, WebRTC, etc.
Recommended settings for using HomeKit Camera with WebRTC, MSE, MP4, RTSP:
@@ -362,13 +452,25 @@ 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
- ffmpeg:aqara_g3#audio=aac#audio=opus
```
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: Bubble
Other names: [ESeeCloud](http://www.eseecloud.com/), [dvr163](http://help.dvr163.com/).
- you can skip `username`, `password`, `port`, `ch` and `stream` if they are default
- setup separate streams for different channels and streams
```yaml
streams:
camera1: bubble://username:password@192.168.1.123:34567/bubble/live?ch=0&stream=0
```
#### Source: DVRIP
Other names: DVR-IP, NetSurveillance, Sofia protocol (NETsurveillance ActiveX plugin XMeye SDK).
@@ -380,7 +482,11 @@ Other names: DVR-IP, NetSurveillance, Sofia protocol (NETsurveillance ActiveX pl
```yaml
streams:
camera1: dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
only_stream: dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
only_tts: dvrip://username:password@192.168.1.123:34567?backchannel=1
two_way_audio:
- dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
- dvrip://username:password@192.168.1.123:34567?backchannel=1
```
#### Source: Tapo
@@ -399,6 +505,15 @@ streams:
camera2: tapo://admin:MD5-PASSWORD-HASH@192.168.1.123
```
#### Source: Kasa
[TP-Link Kasa](https://www.kasasmart.com/) non-standard protocol [more info](https://medium.com/@hu3vjeen/reverse-engineering-tp-link-kc100-bac4641bf1cd).
```yaml
streams:
kasa: kasa://user:pass@192.168.1.123:19443/https/stream/mixed
```
#### Source: Ivideon
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
@@ -412,8 +527,10 @@ streams:
Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files:
- support [Generic Camera](https://www.home-assistant.io/integrations/generic/), setup via GUI
- support [HomeKit Camera](https://www.home-assistant.io/integrations/homekit_controller/)
- [Generic Camera](https://www.home-assistant.io/integrations/generic/), setup via GUI
- [HomeKit Camera](https://www.home-assistant.io/integrations/homekit_controller/)
- [ONVIF](https://www.home-assistant.io/integrations/onvif/)
- [Roborock](https://github.com/humbertogontijo/homeassistant-roborock) vacuums with camera
```yaml
hass:
@@ -424,7 +541,23 @@ streams:
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).
**WebRTC Cameras**
Any cameras in WebRTC format are supported. But at the moment Home Assistant only supports some [Nest](https://www.home-assistant.io/integrations/nest/) cameras in this fomat.
The Nest API only allows you to get a link to a stream for 5 minutes. So every 5 minutes the stream will be reconnected.
```yaml
streams:
# link to Home Assistant Supervised
hass-webrtc1: hass://supervisor?entity_id=camera.nest_doorbell
# link to external Hass with Long-Lived Access Tokens
hass-webrtc2: hass://192.168.1.123:8123?entity_id=camera.nest_doorbell&token=eyXYZ...
```
**RTSP Cameras**
By default, the Home Assistant API does not allow you to get dynamic RTSP link to a camera stream. So more cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), 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).
#### Source: ISAPI
@@ -437,6 +570,17 @@ streams:
- isapi://admin:password@192.168.1.123:80/
```
#### Source: Nest
Currently only WebRTC cameras are supported. Stream reconnects every 5 minutes.
For simplicity, it is recommended to connect the Nest/WebRTC camera to the [Home Assistant](#source-hass). But if you can somehow get the below parameters - Nest/WebRTC source will work without Hass.
```yaml
streams:
nest-doorbell: nest:?client_id=***&client_secret=***&refresh_token=***&project_id=***&device_id=***
```
#### Source: Roborock
This source type support Roborock vacuums with cameras. Known working models:
@@ -450,17 +594,39 @@ If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456
#### Source: WebRTC
This source type support two connection formats:
This source type support four connection formats.
- [WebRTC/WHEP](https://www.ietf.org/id/draft-murillo-whep-01.html) - is an unapproved standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
- `go2rtc/WebSocket` - This format is only supported in go2rtc. Unlike WHEP it supports asynchronous WebRTC connection and two way audio.
**whep**
[WebRTC/WHEP](https://www.ietf.org/id/draft-murillo-whep-02.html) - is an unapproved standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
**go2rtc**
This format is only supported in go2rtc. Unlike WHEP it supports asynchronous WebRTC connection and two way audio.
**openipc**
Support connection to [OpenIPC](https://openipc.org/) cameras.
**wyze**
Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC protocol. You can use [docker-wyze-bridge](https://github.com/mrlt8/docker-wyze-bridge) project to get connection credentials.
**kinesis**
Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-streams/), using WebRTC protocol. You need to specify signalling WebSocket URL with all credentials in query params, `client_id` and `ice_servers` list in [JSON format](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer).
```yaml
streams:
webrtc1: webrtc:http://192.168.1.123:1984/api/webrtc?src=dahua1
webrtc2: webrtc:ws://192.168.1.123:1984/api/ws?src=dahua1
webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1
webrtc-go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1
webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}]
webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze
webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}]
```
**PS.** For `kinesis` sources you can use [echo](#source-echo) to get connection params using `bash`/`python` or any other script language.
#### Source: WebTorrent
This source can get a stream from another go2rtc via [WebTorrent](#module-webtorrent) protocol.
@@ -474,7 +640,7 @@ streams:
By default, go2rtc establishes a connection to the source when any client requests it. Go2rtc drops the connection to the source when it has no clients left.
- Go2rtc also can accepts incoming sources in [RTSP](#source-rtsp), [HTTP](#source-http) and **WebRTC/WHIP** formats
- Go2rtc also can accepts incoming sources in [RTSP](#module-rtsp), [RTMP](#module-rtmp), [HTTP](#source-http) and **WebRTC/WHIP** formats
- Go2rtc won't stop such a source if it has no clients
- You can push data only to existing stream (create stream with empty source in config)
- You can push multiple incoming sources to same stream
@@ -535,35 +701,44 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
- you can stop active playback by calling the API with the empty `src` parameter
- you will see one active producer and one active consumer in go2rtc WebUI info page during streaming
### Publish stream
You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:
- Supported codecs: H264 for video and AAC for audio
- Pixel format should be `yuv420p`, for cameras with `yuvj420p` format you SHOULD use [transcoding](#source-ffmpeg)
- You don't need to enable [RTMP module](#module-rtmp) listening for this task
You can use API:
```
POST http://localhost:1984/api/streams?src=camera1&dst=rtmps://...
```
Or config file:
```yaml
publish:
# publish stream "tplink_tapo" to Telegram
tplink_tapo: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
# publish stream "other_camera" to Telegram and YouTube
other_camera:
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
- rtmps://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
streams:
# for TP-Link cameras it's important to use transcoding because of wrong pixel format
tplink_tapo: ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware=vaapi#audio=aac
```
- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.
- **YouTube** > Create > Go live > Stream latency: Ultra low-latency > Copy: Stream URL + Stream key.
### Module: API
The HTTP API is the main part for interacting with the application. Default address: `http://127.0.0.1:1984/`.
The HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.
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%`
[API description](https://github.com/AlexxIT/go2rtc/tree/master/api).
**Module config**
@@ -571,6 +746,7 @@ go2rtc has simple HTML page (`stream.html`) with support params in URL:
- you can enable HTTP API only on localhost with `listen: "127.0.0.1:1984"` setting
- you can change API `base_path` and host go2rtc on your main app webserver suburl
- all files from `static_dir` hosted on root path: `/`
- you can use raw TLS cert/key content or path to files
```yaml
api:
@@ -580,11 +756,19 @@ api:
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)
tls_listen: ":443" # default "", enable HTTPS server
tls_cert: | # default "", PEM-encoded fullchain certificate for HTTPS
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
tls_key: | # default "", PEM-encoded private key for HTTPS
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
```
**PS:**
- go2rtc doesn't provide HTTPS. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks
- 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
@@ -611,6 +795,17 @@ By default go2rtc provide RTSP-stream with only one first video and only one fir
Read more about [codecs filters](#codecs-filters).
### Module: RTMP
You can get any stream as RTMP-stream: `rtmp://192.168.1.123/{stream_name}`. Only H264/AAC codecs supported right now.
[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has differnt problems with this format.
```yaml
rtmp:
listen: ":1935" # by default - disabled!
```
### Module: WebRTC
In most cases [WebRTC](https://en.wikipedia.org/wiki/WebRTC) uses direct peer-to-peer connection from your browser to go2rtc and sends media data via UDP.
@@ -678,6 +873,58 @@ webrtc:
credential: your_pass
```
### Module: HomeKit
HomeKit module can work in two modes:
- export any H264 camera to Apple HomeKit
- transparent proxy any Apple HomeKit camera (Aqara, Eve, Eufy, etc.) back to Apple HomeKit, so you will have all camera features in Apple Home and also will have RTSP/WebRTC/MP4/etc. from your HomeKit camera
**Important**
- HomeKit cameras supports only H264 video and OPUS audio
**Minimal config**
```yaml
streams:
dahua1: rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0
homekit:
dahua1: # same stream ID from streams list, default PIN - 19550224
```
**Full config**
```yaml
streams:
dahua1:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0
- ffmpeg:dahua1#video=h264#hardware # if your camera doesn't support H264, important for HomeKit
- ffmpeg:dahua1#audio=opus # only OPUS audio supported by HomeKit
homekit:
dahua1: # same stream ID from streams list
pin: 12345678 # custom PIN, default: 19550224
name: Dahua camera # custom camera name, default: generated from stream ID
device_id: dahua1 # custom ID, default: generated from stream ID
device_private: dahua1 # custom key, default: generated from stream ID
```
**Proxy HomeKit camera**
- Video stream from HomeKit camera to Apple device (iPhone, AppleTV) will be transmitted directly
- Video stream from HomeKit camera to RTSP/WebRTC/MP4/etc. will be transmitted via go2rtc
```yaml
streams:
aqara1:
- homekit://...
- ffmpeg:aqara1#audio=aac#audio=opus # optional audio transcoding
homekit:
aqara1: # same stream ID from streams list
```
### Module: WebTorrent
This module support:
@@ -770,7 +1017,8 @@ You have several options on how to add a camera to Home Assistant:
2. Camera [any source](#module-streams) => [go2rtc config](#configuration) => [Generic Camera](https://www.home-assistant.io/integrations/generic/)
- Install any [go2rtc](#fast-start)
- Add your stream to [go2rtc config](#configuration)
- Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://127.0.0.1:8554/camera1` (change to your stream name)
- Hass > Settings > Integrations > Add Integration > [ONVIF](https://my.home-assistant.io/redirect/config_flow_start/?domain=onvif) > Host: `127.0.0.1`, Port: `1984`
- Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > Stream Source URL: `rtsp://127.0.0.1:8554/camera1` (change to your stream name, leave everything else as is)
You have several options on how to watch the stream from the cameras in Home Assistant:
@@ -791,7 +1039,9 @@ 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.
**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.
**PS.** There is also another nice card with go2rtc support - [Frigate Lovelace Card](https://github.com/dermotduffy/frigate-hass-card).
### Module: MP4
@@ -803,11 +1053,19 @@ Provides several features:
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`
- MP4 snapshot: `http://192.168.1.123:1984/api/frame.mp4?src=camera1` (H264, H265)
- MP4 stream: `http://192.168.1.123:1984/api/stream.mp4?src=camera1` (H264, H265, AAC)
- MP4 file: `http://192.168.1.123:1984/api/stream.mp4?src=camera1` (H264, H265*, AAC, OPUS, MP3, PCMA, PCMU, PCM)
- You can use `mp4`, `mp4=flac` and `mp4=all` param for codec filters
- You can use `duration` param in seconds (ex. `duration=15`)
- You can use `filename` param (ex. `filename=record.mp4`)
- You can use `rotate` param with `90`, `180` or `270` values
- You can use `scale` param with positive integer values (ex. `scale=4:3`)
Read more about [codecs filters](#codecs-filters).
**PS.** Rotate and scale params don't use transcoding and change video using metadata.
### Module: HLS
[HLS](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) is the worst technology for real-time streaming. It can only be useful on devices that do not support more modern technology, like [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4).
@@ -845,6 +1103,9 @@ 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`
- You can use `width`/`w` and/or `height`/`h` params
- You can use `rotate` param with `90`, `180`, `270` or `-90` values
- You can use `hardware`/`hw` param [read more](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)
### Module: Log
@@ -895,7 +1156,7 @@ But it cannot be done for [RTSP](#module-rtsp), [HTTP progressive streaming](#mo
Without filters:
- RTSP will provide only the first video and only the first audio
- RTSP will provide only the first video and only the first audio (any codec)
- MP4 will include only compatible codecs (H264, H265, AAC)
- HLS will output in the legacy TS format (H264 without audio)
@@ -906,23 +1167,29 @@ Some examples:
- `rtsp://192.168.1.123:8554/camera1?video=h264&audio=aac&audio=opus` - H264 video codec and two separate audio tracks
- `rtsp://192.168.1.123:8554/camera1?video&audio=all` - any video codec and all audio codecs as separate tracks
- `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4` - HLS stream with MP4 compatible codecs (HLS/fMP4)
- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&video=h264,h265&audio=aac,opus,mp3,pcma,pcmu` - MP4 file with non standard audio codecs, does not work in some players
- `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4=flac` - HLS stream with PCMA/PCMU/PCM audio support (HLS/fMP4), won't work on old devices
- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=flac` - MP4 file with PCMA/PCMU/PCM audio support, won't work on old devices (ex. iOS 12)
- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=all` - MP4 file with non standard audio codecs, won't work on some players
## Codecs madness
`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
| Device | WebRTC | MSE | HTTP Progressive Streaming |
|---------------------|-------------------------------|------------------------|-----------------------------------------|
| *latency* | best | medium | bad |
| Desktop Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, OPUS | H264, H265*, AAC, OPUS, PCMU, PCMA, MP3 |
| Desktop Edge | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, OPUS | H264, H265*, AAC, OPUS, PCMU, PCMA, MP3 |
| Desktop Safari | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC | **no!** |
| Desktop Firefox | H264, OPUS, PCMU, PCMA | H264, AAC, OPUS | H264, AAC, OPUS |
| Android Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, OPUS | H264, ?, AAC, OPUS, PCMU, PCMA, MP3 |
| iPad Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC | **no!** |
| iPhone Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | **no!** | **no!** |
| masOS Hass App | no | no | no |
| Device | WebRTC | MSE | HTTP | HLS |
|---------------------|-------------------------------|-------------------------------|------------------------------------|------------------------|
| *latency* | best | medium | bad | bad |
| Desktop Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
| Desktop Edge | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
| Android Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
| Desktop Firefox | H264, OPUS, PCMU, PCMA | H264, AAC, FLAC*, OPUS | H264, AAC, FLAC*, OPUS | no |
| Desktop Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** | H264, H265, AAC, FLAC* |
| iPad Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** | H264, H265, AAC, FLAC* |
| iPhone Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | **no!** | **no!** | H264, H265, AAC, FLAC* |
| macOS [Hass App][1] | no | no | no | H264, H265, AAC, FLAC* |
[1]: https://apps.apple.com/app/home-assistant/id1099568401
`HTTP*` - HTTP Progressive Streaming, not related with [Progressive download](https://en.wikipedia.org/wiki/Progressive_download), because the file has no size and no end
- 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/)
@@ -931,9 +1198,9 @@ Some examples:
**Audio**
- Go2rtc support [automatic repack](#built-in-transcoding) `PCMA/PCMU/PCM` codecs to `FLAC` for MSE/MP4/HLS so they will work almost anywhere
- **WebRTC** audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2`
- `OPUS` and `MP3` inside **MP4** is part of the standard, but some players do not support them anyway (especially Apple)
- `PCMU` and `PCMA` inside **MP4** isn't a standard, but some players support them, for example Chromium browsers
**Apple devices**
@@ -941,6 +1208,45 @@ Some examples:
- iPhones don't support MSE technology because it competes with the HTTP Live Streaming (HLS) technology, invented by Apple
- HLS is the worst technology for **live** streaming, it still exists only because of iPhones
**Codec names**
- H264 = H.264 = AVC (Advanced Video Coding)
- H265 = H.265 = HEVC (High Efficiency Video Coding)
- PCMU = G.711 PCM (A-law) = PCM A-law (`alaw`)
- PCMA = G.711 PCM (µ-law) = PCM mu-law (`mulaw`)
- PCM = L16 = PCM signed 16-bit big-endian (`s16be`)
- AAC = MPEG4-GENERIC
- MP3 = MPEG-1 Audio Layer III or MPEG-2 Audio Layer III
## Built-in transcoding
There are no plans to embed complex transcoding algorithms inside go2rtc. [FFmpeg source](#source-ffmpeg) does a great job with this. Including [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration) support.
But go2rtc has some simple algorithms. They are turned on automatically, you do not need to set them up additionally.
**PCM for MSE/MP4/HLS**
Go2rtc can pack `PCMA`, `PCMU` and `PCM` codecs into an MP4 container so that they work in all browsers and all built-in players on modern devices. Including Apple QuickTime:
```
PCMA/PCMU => PCM => FLAC => MSE/MP4/HLS
```
**Resample PCMA/PCMU for WebRTC**
By default WebRTC support only `PCMA/8000` and `PCMU/8000`. But go2rtc can automatically resample PCMA and PCMU codec with with a different sample rate. Also go2rtc can transcode `PCM` codec to `PCMA/8000`, so WebRTC can play it:
```
PCM/xxx => PCMA/8000 => WebRTC
PCMA/xxx => PCMA/8000 => WebRTC
PCMU/xxx => PCMU/8000 => WebRTC
```
**Important**
- FLAC codec not supported in a RTSP stream. If you using Frigate or Hass for recording MP4 files with PCMA/PCMU/PCM audio - you should setup transcoding to AAC codec.
- PCMA and PCMU are VERY low quality codecs. Them support only 256! different sounds. Use them only when you have no other options.
## 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.
@@ -973,16 +1279,23 @@ streams:
## Projects using go2rtc
- [Frigate 12+](https://frigate.video/) - open source NVR built around real-time AI object detection
- [Frigate Lovelace Card](https://github.com/dermotduffy/frigate-hass-card) - custom card for Home Assistant
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge
- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - A small project that provides a Video/Audio Stream from Eufy cameras that don't directly support RTSP
- [ioBroker.euSec](https://github.com/bropat/ioBroker.eusec) - [ioBroker](https://www.iobroker.net/) adapter for control Eufy security devices
- [wz_mini_hacks](https://github.com/gtxaspec/wz_mini_hacks) - Custom firmware for Wyze cameras
- [MMM-go2rtc](https://github.com/Anonym-tsk/MMM-go2rtc) - MagicMirror² Module
- [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=go2rtc)
- [NixOS](https://search.nixos.org/packages?query=go2rtc)
- [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/)
- [Unraid](https://unraid.net/community/apps?q=go2rtc)
## Cameras experience
- [Dahua](https://www.dahuasecurity.com/) - reference implementation streaming protocols, a lot of settings, high stream quality, multiple streaming clients
- [EZVIZ](https://www.ezviz.com/) - awful RTSP protocol realisation, many bugs in SDP
- [Hikvision](https://www.hikvision.com/) - a lot of proprietary streaming technologies
- [Reolink](https://reolink.com/) - some models has awful unusable RTSP realisation and not best HTTP-FLV alternative (I recommend that you contact Reolink support for new firmware), few settings
- [Reolink](https://reolink.com/) - some models has awful unusable RTSP realisation and not best RTMP alternative (I recommend that you contact Reolink support for new firmware), few settings
- [Sonoff](https://sonoff.tech/) - very low stream quality, no settings, not best protocol implementation
- [TP-Link](https://www.tp-link.com/) - few streaming clients, packet loss?
- Chinese cheap noname cameras, Wyze Cams, Xiaomi cameras with hacks (usual has `/live/ch00_1` in RTSP URL) - awful but usable RTSP protocol realisation, low stream quality, few settings, packet loss?

117
api/README.md Normal file
View File

@@ -0,0 +1,117 @@
# API
Fill free to make any API design proposals.
## HTTP API
Interactive [OpenAPI](https://alexxit.github.io/go2rtc/api/).
`www/stream.html` - universal viewer with support params in URL:
- multiple streams on page `src=camera1&src=camera2...`
- stream technology autoselection `mode=webrtc,webrtc/tcp,mse,hls,mp4,mjpeg`
- stream technology comparison `src=camera1&mode=webrtc&mode=mse&mode=mp4`
- player width setting in pixels `width=320px` or percents `width=50%`
`www/webrtc.html` - WebRTC viewer with support two way audio and params in URL:
- `media=video+audio` - simple viewer
- `media=video+audio+microphone` - two way audio from camera
- `media=camera+microphone` - stream from browser
- `media=display+speaker` - stream from desktop
## JavaScript API
- You can write your viewer from the scratch
- You can extend the built-in viewer - `www/video-rtc.js`
- Check example - `www/video-stream.js`
- Check example - https://github.com/AlexxIT/WebRTC
`video-rtc.js` features:
- support technologies:
- WebRTC over UDP or TCP
- MSE or HLS 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 HLS, than MJPEG
## WebSocket API
Endpoint: `/api/ws`
Query parameters:
- `src` (required) - Stream name
### WebRTC
Request SDP:
```json
{"type":"webrtc/offer","value":"v=0\r\n..."}
```
Response SDP:
```json
{"type":"webrtc/answer","value":"v=0\r\n..."}
```
Request/response candidate:
- empty value also allowed and optional
```json
{"type":"webrtc/candidate","value":"candidate:3277516026 1 udp 2130706431 192.168.1.123 54321 typ host"}
```
### MSE
Request:
- codecs list optional
```json
{"type":"mse","value":"avc1.640029,avc1.64002A,avc1.640033,hvc1.1.6.L153.B0,mp4a.40.2,mp4a.40.5,flac,opus"}
```
Response:
```json
{"type":"mse","value":"video/mp4; codecs=\"avc1.64001F,mp4a.40.2\""}
```
### HLS
Request:
```json
{"type":"hls","value":"avc1.640029,avc1.64002A,avc1.640033,hvc1.1.6.L153.B0,mp4a.40.2,mp4a.40.5,flac"}
```
Response:
- you MUST rewrite full HTTP path to `http://192.168.1.123:1984/api/hls/playlist.m3u8`
```json
{"type":"hls","value":"#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS=\"avc1.64001F,mp4a.40.2\"\nhls/playlist.m3u8?id=DvmHdd9w"}
```
### MJPEG
Request/response:
```json
{"type":"mjpeg"}
```

530
api/openapi.yaml Normal file
View File

@@ -0,0 +1,530 @@
openapi: 3.1.0
info:
title: go2rtc
license: { name: MIT,url: https://opensource.org/licenses/MIT }
version: 1.0.0
contact: { url: https://github.com/AlexxIT/go2rtc }
description: |
Ultimate camera streaming application with support RTSP, RTMP, HTTP-FLV, WebRTC, MSE, HLS, MP4, MJPEG, HomeKit, FFmpeg, etc.
servers:
- url: http://localhost:1984
components:
parameters:
stream_src_path:
name: src
in: path
description: Source stream name
required: true
schema: { type: string }
example: camera1
stream_dst_path:
name: dst
in: path
description: Destination stream name
required: true
schema: { type: string }
example: camera1
stream_src_query:
name: src
in: query
description: Source stream name
required: true
schema: { type: string }
example: camera1
mp4_filter:
name: mp4
in: query
description: MP4 codecs filter
required: false
schema:
type: string
enum: [ "", flac, all ]
example: flac
video_filter:
name: video
in: query
description: Video codecs filter
schema:
type: string
enum: [ "", all, h264, h265, mjpeg ]
example: h264,h265
audio_filter:
name: audio
in: query
description: Audio codecs filter
schema:
type: string
enum: [ "", all, aac, opus, pcm, pcmu, pcma ]
example: aac
responses:
discovery:
description: ""
content:
application/json:
example: { streams: [ { "name": "Camera 1","url": "..." } ] }
webtorrent:
description: ""
content:
application/json:
example: { share: AKDypPy4zz, pwd: H0Km1HLTTP }
tags:
- name: Application
description: "[Module: API](https://github.com/AlexxIT/go2rtc#module-api)"
- name: Config
description: "[Configuration](https://github.com/AlexxIT/go2rtc#configuration)"
- name: Streams list
description: "[Module: Streams](https://github.com/AlexxIT/go2rtc#module-streams)"
- name: Consume stream
- name: Snapshot
- name: Produce stream
- name: Discovery
- name: ONVIF
- name: RTSPtoWebRTC
- name: WebTorrent
description: "[Module: WebTorrent](https://github.com/AlexxIT/go2rtc#module-webtorrent)"
- name: Debug
paths:
/api:
get:
summary: Get application info
tags: [ Application ]
responses:
"200":
description: ""
content:
application/json:
example: { config_path: "/config/go2rtc.yaml",host: "192.168.1.123:1984",rtsp: { listen: ":8554",default_query: "video&audio" },version: "1.5.0" }
/api/exit:
post:
summary: Close application
tags: [ Application ]
parameters:
- name: code
in: query
description: Application exit code
required: false
schema: { type: integer }
example: 100
responses:
default:
description: Default response
/api/config:
get:
summary: Get main config file content
tags: [ Config ]
responses:
"200":
description: ""
content:
application/yaml: { example: "streams:..." }
post:
summary: Rewrite main config file
tags: [ Config ]
requestBody:
content:
"*/*": { example: "streams:..." }
responses:
default:
description: Default response
patch:
summary: Merge changes to main config file
tags: [ Config ]
requestBody:
content:
"*/*": { example: "streams:..." }
responses:
default:
description: Default response
/api/streams:
get:
summary: Get all streams info
tags: [ Streams list ]
responses:
"200":
description: ""
content:
application/json: { example: { camera1: { producers: [ ],consumers: [ ] } } }
put:
summary: Create new stream
tags: [ Streams list ]
parameters:
- name: src
in: query
description: Stream source (URI)
required: true
schema: { type: string }
example: "rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0"
- name: name
in: query
description: Stream name
required: false
schema: { type: string }
example: camera1
responses:
default:
description: Default response
patch:
summary: Update stream source
tags: [ Streams list ]
parameters:
- name: src
in: query
description: Stream source (URI)
required: true
schema: { type: string }
example: "rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0"
- name: name
in: query
description: Stream name
required: true
schema: { type: string }
example: camera1
responses:
default:
description: Default response
delete:
summary: Delete stream
tags: [ Streams list ]
parameters:
- name: src
in: query
description: Stream name
required: true
schema: { type: string }
example: camera1
responses:
default:
description: Default response
post:
summary: Send stream from source to destination
description: "[Stream to camera](https://github.com/AlexxIT/go2rtc#stream-to-camera)"
tags: [ Streams list ]
parameters:
- name: src
in: query
description: Stream source (URI)
required: true
schema: { type: string }
example: "ffmpeg:http://example.com/song.mp3#audio=pcma#input=file"
- name: dst
in: query
description: Destination stream name
required: true
schema: { type: string }
example: camera1
responses:
default:
description: Default response
/api/streams?src={src}:
get:
summary: Get stream info in JSON format
tags: [ Consume stream ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
"200":
description: ""
content:
application/json:
example: { producers: [ { url: "rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0" } ], consumers: [ ] }
/api/webrtc?src={src}:
post:
summary: Get stream in WebRTC format (WHEP)
description: "[Module: WebRTC](https://github.com/AlexxIT/go2rtc#module-webrtc)"
tags: [ Consume stream ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
requestBody:
description: |
Support:
- JSON format (`Content-Type: application/json`)
- WHEP standard (`Content-Type: application/sdp`)
- raw SDP (`Content-Type: anything`)
required: true
content:
application/json: { example: { type: offer, sdp: "v=0..." } }
"application/sdp": { example: "v=0..." }
"*/*": { example: "v=0..." }
responses:
"200":
description: "Response on JSON or raw SDP"
content:
application/json: { example: { type: answer, sdp: "v=0..." } }
application/sdp: { example: "v=0..." }
"201":
description: "Response on `Content-Type: application/sdp`"
content:
application/sdp: { example: "v=0..." }
/api/stream.mp4?src={src}:
get:
summary: Get stream in MP4 format (HTTP progressive)
description: "[Module: MP4](https://github.com/AlexxIT/go2rtc#module-mp4)"
tags: [ Consume stream ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
- name: duration
in: query
description: Limit the length of the stream in seconds
required: false
schema: { type: string }
example: 15
- name: filename
in: query
description: Download as a file with this name
required: false
schema: { type: string }
example: camera1.mp4
- $ref: "#/components/parameters/mp4_filter"
- $ref: "#/components/parameters/video_filter"
- $ref: "#/components/parameters/audio_filter"
responses:
200:
description: ""
content: { video/mp4: { example: "" } }
/api/stream.m3u8?src={src}:
get:
summary: Get stream in HLS format
description: "[Module: HLS](https://github.com/AlexxIT/go2rtc#module-hls)"
tags: [ Consume stream ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
- $ref: "#/components/parameters/mp4_filter"
- $ref: "#/components/parameters/video_filter"
- $ref: "#/components/parameters/audio_filter"
responses:
200:
description: ""
content: { application/vnd.apple.mpegurl: { example: "" } }
/api/stream.mjpeg?src={src}:
get:
summary: Get stream in MJPEG format
description: "[Module: MJPEG](https://github.com/AlexxIT/go2rtc#module-mjpeg)"
tags: [ Consume stream ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
200:
description: ""
content: { multipart/x-mixed-replace: { example: "" } }
/api/frame.jpeg?src={src}:
get:
summary: Get snapshot in JPEG format
description: "[Module: MJPEG](https://github.com/AlexxIT/go2rtc#module-mjpeg)"
tags: [ Snapshot ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
200:
description: ""
content: { image/jpeg: { example: "" } }
/api/frame.mp4?src={src}:
get:
summary: Get snapshot in MP4 format
description: "[Module: MP4](https://github.com/AlexxIT/go2rtc#module-mp4)"
tags: [ Snapshot ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
200:
description: ""
content: { video/mp4: { example: "" } }
/api/webrtc?dst={dst}:
post:
summary: Post stream in WebRTC format
description: "[Incoming: WebRTC/WHIP](https://github.com/AlexxIT/go2rtc#incoming-webrtcwhip)"
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses:
default:
description: Default response
/api/stream.flv?dst={dst}:
post:
summary: Post stream in FLV format
description: "[Incoming sources](https://github.com/AlexxIT/go2rtc#incoming-sources)"
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses:
default:
description: Default response
/api/stream.ts?dst={dst}:
post:
summary: Post stream in MPEG-TS format
description: "[Incoming sources](https://github.com/AlexxIT/go2rtc#incoming-sources)"
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses:
default:
description: Default response
/api/stream.mjpeg?dst={dst}:
post:
summary: Post stream in MJPEG format
description: "[Incoming sources](https://github.com/AlexxIT/go2rtc#incoming-sources)"
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses:
default:
description: Default response
/api/dvrip:
get:
summary: DVRIP cameras discovery
description: "[Source: DVRIP](https://github.com/AlexxIT/go2rtc#source-dvrip)"
tags: [ Discovery ]
responses:
default:
description: Default response
/api/ffmpeg/devices:
get:
summary: FFmpeg USB devices discovery
description: "[Source: FFmpeg Device](https://github.com/AlexxIT/go2rtc#source-ffmpeg-device)"
tags: [ Discovery ]
responses:
default:
description: Default response
/api/ffmpeg/hardware:
get:
summary: FFmpeg hardware transcoding discovery
description: "[Hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)"
tags: [ Discovery ]
responses:
default:
description: Default response
/api/hass:
get:
summary: Home Assistant cameras discovery
description: "[Source: Hass](https://github.com/AlexxIT/go2rtc#source-hass)"
tags: [ Discovery ]
responses:
default:
description: Default response
/api/homekit:
get:
summary: HomeKit cameras discovery
description: "[Source: HomeKit](https://github.com/AlexxIT/go2rtc#source-homekit)"
tags: [ Discovery ]
responses:
default:
description: Default response
/api/nest:
get:
summary: Nest cameras discovery
tags: [ Discovery ]
responses:
default:
description: Default response
/api/onvif:
get:
summary: ONVIF cameras discovery
description: "[Source: ONVIF](https://github.com/AlexxIT/go2rtc#source-onvif)"
tags: [ Discovery ]
responses:
default:
description: Default response
/api/roborock:
get:
summary: Roborock vacuums discovery
description: "[Source: Roborock](https://github.com/AlexxIT/go2rtc#source-roborock)"
tags: [ Discovery ]
responses:
default:
description: Default response
/onvif/:
get:
summary: ONVIF server implementation
description: Simple realisation of the ONVIF protocol. Accepts any suburl requests
tags: [ ONVIF ]
responses:
default:
description: Default response
/stream/:
get:
summary: RTSPtoWebRTC server implementation
description: Simple API for support [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration
tags: [ RTSPtoWebRTC ]
responses:
default:
description: Default response
/api/webtorrent?src={src}:
get:
summary: Get WebTorrent share info
tags: [ WebTorrent ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
200: { $ref: "#/components/responses/webtorrent" }
post:
summary: Add WebTorrent share
tags: [ WebTorrent ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
200: { $ref: "#/components/responses/webtorrent" }
delete:
summary: Delete WebTorrent share
tags: [ WebTorrent ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
default:
description: Default response
/api/webtorrent:
get:
summary: Get all WebTorrent shares info
tags: [ WebTorrent ]
responses:
200: { $ref: "#/components/responses/discovery" }
/api/stack:
get:
summary: Show list unknown goroutines
tags: [ Debug ]
responses:
200:
description: ""
content: { text/plain: { example: "" } }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 202 KiB

View File

@@ -1,19 +0,0 @@
#!/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,4 +0,0 @@
**Project layout**
- https://github.com/golang-standards/project-layout
- https://github.com/micro/micro

View File

@@ -1,61 +0,0 @@
package store
import (
"encoding/json"
"github.com/rs/zerolog/log"
"os"
)
const name = "go2rtc.json"
var store map[string]any
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]any)
}
}
func save() error {
data, err := json.Marshal(store)
if err != nil {
return err
}
return os.WriteFile(name, data, 0644)
}
func GetRaw(key string) any {
if store == nil {
load()
}
return store[key]
}
func GetDict(key string) map[string]any {
raw := GetRaw(key)
if raw != nil {
return raw.(map[string]any)
}
return make(map[string]any)
}
func Set(key string, v any) error {
if store == nil {
load()
}
store[key] = v
return save()
}

View File

@@ -1,25 +0,0 @@
package dvrip
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/dvrip"
)
func Init() {
streams.HandleFunc("dvrip", handle)
}
func handle(url string) (core.Producer, error) {
conn := dvrip.NewClient(url)
if err := conn.Dial(); err != nil {
return nil, err
}
if err := conn.Play(); err != nil {
return nil, err
}
if err := conn.Handle(); err != nil {
return nil, err
}
return conn, nil
}

View File

@@ -1,61 +0,0 @@
package device
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/core"
"os/exec"
"strings"
)
// https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f avfoundation"
func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(core.KindVideo, videoIdx)
audio := findMedia(core.KindAudio, audioIdx)
switch {
case video != nil && audio != nil:
return `"` + video.ID + `:` + audio.ID + `"`
case video != nil:
return `"` + video.ID + `"`
case audio != nil:
return `"` + audio.ID + `"`
}
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 = core.KindVideo
continue
case strings.HasSuffix(line, "audio devices:"):
kind = core.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) *core.Media {
return &core.Media{Kind: kind, ID: name}
}

View File

@@ -1,48 +0,0 @@
package device
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/core"
"io/ioutil"
"os/exec"
"strings"
)
// https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f v4l2"
func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(core.KindVideo, videoIdx)
return video.ID
}
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(), core.KindVideo) {
media := loadMedia(core.KindVideo, "/dev/"+file.Name())
if media != nil {
medias = append(medias, media)
}
}
}
}
func loadMedia(kind, name string) *core.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 &core.Media{Kind: kind, ID: name}
}

View File

@@ -1,57 +0,0 @@
package device
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/core"
"os/exec"
"strings"
)
// https://trac.ffmpeg.org/wiki/DirectShow
const deviceInputPrefix = "-f dshow"
func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(core.KindVideo, videoIdx)
audio := findMedia(core.KindAudio, audioIdx)
switch {
case video != nil && audio != nil:
return `video="` + video.ID + `":audio=` + audio.ID + `"`
case video != nil:
return `video="` + video.ID + `"`
case audio != nil:
return `audio="` + audio.ID + `"`
}
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 = core.KindVideo
} else if strings.HasSuffix(line, "(audio)") {
kind = core.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) *core.Media {
return &core.Media{Kind: kind, ID: name}
}

View File

@@ -1,91 +0,0 @@
package device
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/pkg/core"
"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 []*core.Media
func findMedia(kind string, index int) *core.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()
}
var items []api.Stream
var iv, ia int
for _, media := range medias {
var source string
switch media.Kind {
case core.KindVideo:
source = "ffmpeg:device?video=" + strconv.Itoa(iv)
iv++
case core.KindAudio:
source = "ffmpeg:device?audio=" + strconv.Itoa(ia)
ia++
}
items = append(items, api.Stream{Name: media.ID, URL: source})
}
api.ResponseStreams(w, items)
}

View File

@@ -1,14 +0,0 @@
package ffmpeg
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestParseArgs(t *testing.T) {
args := parseArgs("rtsp://example.com#video=h264#rotate=180")
assert.Equal(t, "ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -an -vf transpose=1,transpose=1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=vaapi")
assert.Equal(t, "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload,transpose_vaapi=4 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
}

View File

@@ -1,120 +0,0 @@
package ffmpeg
import (
"os/exec"
"strings"
"github.com/rs/zerolog/log"
)
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:]
}
if strings.HasPrefix(filter, "transpose=") {
if filter == "transpose=1,transpose=1" { // 180 degrees half-turn
args.filters[i] = "transpose_vaapi=4" // reversal
} else {
args.filters[i] = "transpose_vaapi=" + filter[10:]
}
}
}
// 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

@@ -1,21 +0,0 @@
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

@@ -1,67 +0,0 @@
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

@@ -1,40 +0,0 @@
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
}

View File

@@ -1,173 +0,0 @@
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"
"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), "WebRTC/Hass sync", 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), "WebRTC/Hass sync", 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 HassioAddr() string {
ints, _ := net.Interfaces()
for _, i := range ints {
if i.Name != "hassio" {
continue
}
addrs, _ := i.Addrs()
for _, addr := range addrs {
if addr, ok := addr.(*net.IPNet); ok {
return addr.IP.String()
}
}
}
return ""
}
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,145 +0,0 @@
package hass
import (
"bytes"
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/roborock"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/rs/zerolog"
"net/http"
"os"
"path"
)
func Init() {
var conf struct {
API struct {
Listen string `json:"listen"`
} `yaml:"api"`
Mod struct {
Config string `yaml:"config"`
} `yaml:"hass"`
}
app.LoadConfig(&conf)
log = app.GetLogger("hass")
initAPI()
entries := importEntries(conf.Mod.Config)
if entries == nil {
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "no hass config", http.StatusNotFound)
})
return
}
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
var items []api.Stream
for name, url := range entries {
items = append(items, api.Stream{Name: name, URL: url})
}
api.ResponseStreams(w, items)
})
streams.HandleFunc("hass", func(url string) (core.Producer, error) {
if hurl := entries[url[5:]]; hurl != "" {
return streams.GetProducer(hurl)
}
return nil, fmt.Errorf("can't get url: %s", url)
})
// for Addon listen on hassio interface, so WebUI feature will work
if conf.API.Listen == "127.0.0.1:1984" {
if addr := HassioAddr(); addr != "" {
addr += ":1984"
go func() {
log.Info().Str("addr", addr).Msg("[hass] listen")
if err := http.ListenAndServe(addr, api.Handler); err != nil {
log.Error().Err(err).Caller().Send()
}
}()
}
}
}
func importEntries(config string) map[string]string {
// support load cameras from Hass config file
filename := path.Join(config, ".storage/core.config_entries")
b, err := os.ReadFile(filename)
if err != nil {
return nil
}
var storage struct {
Data struct {
Entries []struct {
Title string `json:"title"`
Domain string `json:"domain"`
Data json.RawMessage `json:"data"`
Options json.RawMessage `json:"options"`
} `json:"entries"`
} `json:"data"`
}
if err = json.Unmarshal(b, &storage); err != nil {
return nil
}
urls := map[string]string{}
for _, entrie := range storage.Data.Entries {
switch entrie.Domain {
case "generic":
var options struct {
StreamSource string `json:"stream_source"`
}
if err = json.Unmarshal(entrie.Options, &options); err != nil {
continue
}
urls[entrie.Title] = options.StreamSource
case "homekit_controller":
if !bytes.Contains(entrie.Data, []byte("iOSPairingId")) {
continue
}
var data struct {
ClientID string `json:"iOSPairingId"`
ClientPrivate string `json:"iOSDeviceLTSK"`
ClientPublic string `json:"iOSDeviceLTPK"`
DeviceID string `json:"AccessoryPairingID"`
DevicePublic string `json:"AccessoryLTPK"`
DeviceHost string `json:"AccessoryIP"`
DevicePort uint16 `json:"AccessoryPort"`
}
if err = json.Unmarshal(entrie.Data, &data); err != nil {
continue
}
urls[entrie.Title] = fmt.Sprintf(
"homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s",
data.DeviceHost, data.DevicePort,
data.ClientID, data.ClientPrivate, data.ClientPublic,
data.DeviceID, data.DevicePublic,
)
case "roborock":
_ = json.Unmarshal(entrie.Data, &roborock.Auth)
default:
continue
}
log.Info().Str("url", "hass:"+entrie.Title).Msg("[hass] load stream")
//streams.Get("hass:" + entrie.Title)
}
return urls
}
var log zerolog.Logger

View File

@@ -1,140 +0,0 @@
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([]any, 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"`
}

View File

@@ -1,32 +0,0 @@
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/core"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"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) (core.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
}

View File

@@ -1,65 +0,0 @@
package http
import (
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"net/http"
"strings"
)
func Init() {
streams.HandleFunc("http", handle)
streams.HandleFunc("https", handle)
}
func handle(url string) (core.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
case "video/mpeg":
client := mpegts.NewClient(res)
if err = client.Handle(); err != nil {
return nil, err
}
return client, nil
}
return nil, fmt.Errorf("unsupported Content-Type: %s", ct)
}

View File

@@ -1,162 +0,0 @@
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/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"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) {
// Chrome 105 does two requests: without Range and with `Range: bytes=0-`
ua := r.UserAgent()
if strings.Contains(ua, " Chrome/") {
if r.Header.Values("Range") == nil {
w.Header().Set("Content-Type", "video/mp4")
w.WriteHeader(http.StatusOK)
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 any) {
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)
// Chrome has Safari in UA, so check first Chrome and later Safari
ua := r.UserAgent()
if strings.Contains(ua, " Chrome/") {
if r.Header.Values("Range") == nil {
w.Header().Set("Content-Type", "video/mp4")
w.WriteHeader(http.StatusOK)
return
}
} else if strings.Contains(ua, " Safari/") {
// auto redirect to HLS/fMP4 format, because Safari not support MP4 stream
url := "stream.m3u8?" + r.URL.RawQuery
if !r.URL.Query().Has("mp4") {
url += "&mp4"
}
http.Redirect(w, r, url, http.StatusMovedPermanently)
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: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
Medias: core.ParseQuery(r.URL.Query()),
}
cons.Listen(func(msg any) {
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()
}
}

View File

@@ -1,138 +0,0 @@
package mp4
import (
"errors"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"strings"
)
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: tcp.RemoteAddr(tr.Request),
UserAgent: tr.Request.UserAgent(),
}
if codecs := msg.String(); codecs != "" {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
cons.Medias = parseMedias(codecs, true)
}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
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: tcp.RemoteAddr(tr.Request),
UserAgent: tr.Request.UserAgent(),
OnlyKeyframe: true,
}
if codecs := msg.String(); codecs != "" {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
cons.Medias = parseMedias(codecs, false)
}
cons.Listen(func(msg any) {
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 []*core.Media) {
var videos []*core.Codec
var audios []*core.Codec
for _, name := range strings.Split(codecs, ",") {
switch name {
case mp4.MimeH264:
codec := &core.Codec{Name: core.CodecH264}
videos = append(videos, codec)
case mp4.MimeH265:
codec := &core.Codec{Name: core.CodecH265}
videos = append(videos, codec)
case mp4.MimeAAC:
codec := &core.Codec{Name: core.CodecAAC}
audios = append(audios, codec)
case mp4.MimeOpus:
codec := &core.Codec{Name: core.CodecOpus}
audios = append(audios, codec)
}
}
if videos != nil {
media := &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: videos,
}
medias = append(medias, media)
}
if audios != nil && parseAudio {
media := &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: audios,
}
medias = append(medias, media)
}
return
}

View File

@@ -1,43 +0,0 @@
package mpegts
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"net/http"
)
func Init() {
api.HandleFunc("api/stream.ts", apiHandle)
}
func apiHandle(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "", http.StatusMethodNotAllowed)
return
}
dst := r.URL.Query().Get("dst")
stream := streams.Get(dst)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
res := &http.Response{Body: r.Body, Request: r}
client := mpegts.NewClient(res)
if err := client.Handle(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stream.AddProducer(client)
if err := client.Handle(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stream.RemoveProducer(client)
}

View File

@@ -1,62 +0,0 @@
package rtmp
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/rs/zerolog/log"
"io"
"net/http"
)
func Init() {
streams.HandleFunc("rtmp", streamsHandle)
api.HandleFunc("api/stream.flv", apiHandle)
}
func streamsHandle(url string) (core.Producer, error) {
conn := rtmp.NewClient(url)
if err := conn.Dial(); err != nil {
return nil, err
}
if err := conn.Describe(); err != nil {
return nil, err
}
return conn, nil
}
func apiHandle(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "", http.StatusMethodNotAllowed)
return
}
dst := r.URL.Query().Get("dst")
stream := streams.Get(dst)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
res := &http.Response{Body: r.Body, Request: r}
client, err := rtmp.Accept(res)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = client.Describe(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stream.AddProducer(client)
if err = client.Start(); err != nil && err != io.EOF {
log.Warn().Err(err).Caller().Send()
}
stream.RemoveProducer(client)
}

View File

@@ -1,45 +0,0 @@
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

View File

@@ -1,41 +0,0 @@
package streams
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/core"
"strings"
"sync"
)
type Handler func(url string) (core.Producer, error)
var handlers = map[string]Handler{}
var handlersMu sync.Mutex
func HandleFunc(scheme string, handler 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 {
return getHandler(url) != nil
}
func GetProducer(url string) (core.Producer, error) {
handler := getHandler(url)
if handler == nil {
return nil, fmt.Errorf("unsupported scheme: %s", url)
}
return handler(url)
}

View File

@@ -1,116 +0,0 @@
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() {
var cfg struct {
Mod map[string]any `yaml:"streams"`
}
app.LoadConfig(&cfg)
log = app.GetLogger("streams")
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 {
return streams[name]
}
func New(name string, source any) *Stream {
stream := NewStream(source)
streams[name] = stream
return stream
}
func GetOrNew(src string) *Stream {
if stream, ok := streams[src]; ok {
return stream
}
if !HasProducer(src) {
return nil
}
log.Info().Str("url", src).Msg("[streams] create new stream")
return New(src, src)
}
func streamsHandler(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
src := query.Get("src")
// without source - return all streams list
if src == "" && r.Method != "POST" {
_ = json.NewEncoder(w).Encode(streams)
return
}
// Not sure about all this API. Should be rewrited...
switch r.Method {
case "GET":
e := json.NewEncoder(w)
e.SetIndent("", " ")
_ = e.Encode(streams[src])
case "PUT":
name := query.Get("name")
if name == "" {
name = src
}
New(name, src)
case "PATCH":
name := query.Get("name")
if name == "" {
http.Error(w, "", http.StatusBadRequest)
return
}
if stream := Get(name); stream != nil {
stream.SetSource(src)
} else {
New(name, src)
}
case "POST":
// with dst - redirect source to dst
if dst := query.Get("dst"); dst != "" {
if stream := Get(dst); stream != nil {
if err := stream.Play(src); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
_ = json.NewEncoder(w).Encode(stream)
}
} else {
http.Error(w, "", http.StatusNotFound)
}
} else {
http.Error(w, "", http.StatusBadRequest)
}
case "DELETE":
delete(streams, src)
}
}
var log zerolog.Logger
var streams = map[string]*Stream{}

View File

@@ -1,229 +0,0 @@
package streams
import (
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/core"
"strings"
"sync"
"sync/atomic"
)
type Stream struct {
producers []*Producer
consumers []core.Consumer
mu sync.Mutex
requests int32
}
func NewStream(source any) *Stream {
switch source := source.(type) {
case string:
s := new(Stream)
prod := &Producer{url: source}
s.producers = append(s.producers, prod)
return s
case []any:
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]any:
return NewStream(source["url"])
case nil:
return new(Stream)
default:
panic(core.Caller())
}
}
func (s *Stream) SetSource(source string) {
for _, prod := range s.producers {
prod.SetSource(source)
}
}
func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
// support for multiple simultaneous requests from different consumers
atomic.AddInt32(&s.requests, 1)
var producers []*Producer // matched producers for consumer
var codecs string
// Step 1. Get consumer medias
for _, consMedia := range cons.GetMedias() {
producers:
for _, prod := range s.producers {
if err = prod.Dial(); err != nil {
continue
}
// Step 2. Get producer medias (not tracks yet)
for _, prodMedia := range prod.GetMedias() {
collectCodecs(prodMedia, &codecs)
// Step 3. Match consumer/producer codecs list
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
if prodCodec == nil {
continue
}
var track *core.Receiver
switch prodMedia.Direction {
case core.DirectionRecvonly:
// Step 4. Get recvonly track from producer
if track, err = prod.GetTrack(prodMedia, prodCodec); err != nil {
log.Info().Err(err).Msg("[streams] can't get track")
continue
}
// Step 5. Add track to consumer
if err = cons.AddTrack(consMedia, consCodec, track); err != nil {
log.Info().Err(err).Msg("[streams] can't add track")
continue
}
case core.DirectionSendonly:
// Step 4. Get recvonly track from consumer (backchannel)
if track, err = cons.(core.Producer).GetTrack(consMedia, consCodec); err != nil {
log.Info().Err(err).Msg("[streams] can't get track")
continue
}
// Step 5. Add track to producer
if err = prod.AddTrack(prodMedia, prodCodec, track); err != nil {
log.Info().Err(err).Msg("[streams] can't add track")
continue
}
}
producers = append(producers, prod)
if !consMedia.MatchAll() {
break producers
}
}
}
}
// stop producers if they don't have readers
if atomic.AddInt32(&s.requests, -1) == 0 {
s.stopProducers()
}
if len(producers) == 0 {
if len(codecs) > 0 {
return errors.New("codecs not match: " + codecs)
}
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, cons)
s.mu.Unlock()
// there may be duplicates, but that's not a problem
for _, prod := range producers {
prod.start()
}
return nil
}
func (s *Stream) RemoveConsumer(cons core.Consumer) {
_ = cons.Stop()
s.mu.Lock()
for i, consumer := range s.consumers {
if consumer == cons {
s.consumers = append(s.consumers[:i], s.consumers[i+1:]...)
break
}
}
s.mu.Unlock()
s.stopProducers()
}
func (s *Stream) AddProducer(prod core.Producer) {
producer := &Producer{conn: prod, state: stateExternal}
s.mu.Lock()
s.producers = append(s.producers, producer)
s.mu.Unlock()
}
func (s *Stream) RemoveProducer(prod core.Producer) {
s.mu.Lock()
for i, producer := range s.producers {
if producer.conn == prod {
s.producers = append(s.producers[:i], s.producers[i+1:]...)
break
}
}
s.mu.Unlock()
}
func (s *Stream) stopProducers() {
s.mu.Lock()
producers:
for _, producer := range s.producers {
for _, track := range producer.receivers {
if len(track.Senders()) > 0 {
continue producers
}
}
producer.stop()
}
s.mu.Unlock()
}
func (s *Stream) MarshalJSON() ([]byte, error) {
if !s.mu.TryLock() {
log.Warn().Msgf("[streams] json locked")
return json.Marshal(nil)
}
var info struct {
Producers []*Producer `json:"producers"`
Consumers []core.Consumer `json:"consumers"`
}
info.Producers = s.producers
info.Consumers = s.consumers
s.mu.Unlock()
return json.Marshal(info)
}
func collectCodecs(media *core.Media, codecs *string) {
if media.Direction == core.DirectionRecvonly {
return
}
for _, codec := range media.Codecs {
name := codec.Name
if name == core.CodecAAC {
name = "AAC"
}
if strings.Contains(*codecs, name) {
continue
}
if len(*codecs) > 0 {
*codecs += ","
}
*codecs += name
}
}

View File

@@ -1,178 +0,0 @@
package webrtc
import (
"errors"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/gorilla/websocket"
pion "github.com/pion/webrtc/v3"
"io"
"net/http"
"strings"
"time"
)
func streamsHandler(url string) (core.Producer, error) {
url = url[7:]
if i := strings.Index(url, "://"); i > 0 {
switch url[:i] {
case "ws", "wss":
return asyncClient(url)
case "http", "https":
return syncClient(url)
}
}
return nil, errors.New("unsupported url: " + url)
}
// asyncClient can connect only to go2rtc server
// ex: ws://localhost:1984/api/ws?src=camera1
func asyncClient(url string) (core.Producer, error) {
// 1. Connect to signalign server
ws, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
_ = ws.Close()
}
}()
// 2. Create PeerConnection
pc, err := PeerConnection(true)
if err != nil {
log.Error().Err(err).Caller().Send()
return nil, err
}
var sendOffer core.Waiter
prod := webrtc.NewConn(pc)
prod.Desc = "WebRTC/WebSocket async"
prod.Mode = core.ModeActiveProducer
prod.Listen(func(msg any) {
switch msg := msg.(type) {
case pion.PeerConnectionState:
_ = ws.Close()
case *pion.ICECandidate:
sendOffer.Wait()
s := msg.ToJSON().Candidate
log.Trace().Str("candidate", s).Msg("[webrtc] local")
_ = ws.WriteJSON(&api.Message{Type: "webrtc/candidate", Value: s})
}
})
medias := []*core.Media{
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
{Kind: core.KindAudio, Direction: core.DirectionSendonly},
}
// 3. Create offer
offer, err := prod.CreateOffer(medias)
if err != nil {
return nil, err
}
// 4. Send offer
msg := &api.Message{Type: "webrtc/offer", Value: offer}
if err = ws.WriteJSON(msg); err != nil {
return nil, err
}
sendOffer.Done()
// 5. Get answer
if err = ws.ReadJSON(msg); err != nil {
return nil, err
}
if msg.Type != "webrtc/answer" {
return nil, errors.New("wrong answer: " + msg.Type)
}
answer := msg.String()
if err = prod.SetAnswer(answer); err != nil {
return nil, err
}
// 6. Continue to receiving candidates
go func() {
for {
// receive data from remote
msg := new(api.Message)
if err = ws.ReadJSON(msg); err != nil {
if cerr, ok := err.(*websocket.CloseError); ok {
log.Trace().Err(err).Caller().Msgf("[webrtc] ws code=%d", cerr)
}
break
}
switch msg.Type {
case "webrtc/candidate":
if msg.Value != nil {
_ = prod.AddCandidate(msg.String())
}
}
}
_ = ws.Close()
}()
return prod, nil
}
// syncClient - support WebRTC-HTTP Egress Protocol (WHEP)
// ex: http://localhost:1984/api/webrtc?src=camera1
func syncClient(url string) (core.Producer, error) {
// 2. Create PeerConnection
pc, err := PeerConnection(true)
if err != nil {
log.Error().Err(err).Caller().Send()
return nil, err
}
prod := webrtc.NewConn(pc)
prod.Desc = "WebRTC/WHEP sync"
prod.Mode = core.ModeActiveProducer
medias := []*core.Media{
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
}
// 3. Create offer
offer, err := prod.CreateCompleteOffer(medias)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", url, strings.NewReader(offer))
req.Header.Set("Content-Type", MimeSDP)
if err != nil {
return nil, err
}
client := http.Client{Timeout: time.Second * 5000}
defer client.CloseIdleConnections()
res, err := client.Do(req)
if err != nil {
return nil, err
}
answer, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
if err = prod.SetAnswer(string(answer)); err != nil {
return nil, err
}
return prod, nil
}

View File

@@ -0,0 +1,20 @@
package main
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/hass"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/shell"
)
func main() {
app.Init()
streams.Init()
api.Init()
hass.Init()
shell.RunUntilSignal()
}

View File

@@ -0,0 +1,17 @@
package main
import (
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/shell"
)
func main() {
app.Init()
streams.Init()
rtsp.Init()
shell.RunUntilSignal()
}

View File

@@ -0,0 +1,123 @@
package main
import (
"encoding/json"
"os"
"github.com/AlexxIT/go2rtc/pkg/hap"
)
var servs = map[string]string{
"3E": "Accessory Information",
"7E": "Security System",
"85": "Motion Sensor",
"96": "Battery",
"A2": "Protocol Information",
"110": "Camera RTP Stream Management",
"112": "Microphone",
"113": "Speaker",
"121": "Doorbell",
"129": "Data Stream Transport Management",
"204": "Camera Recording Management",
"21A": "Camera Operating Mode",
"22A": "Wi-Fi Transport",
"239": "Accessory Runtime Information",
}
var chars = map[string]string{
"14": "Identify",
"20": "Manufacturer",
"21": "Model",
"23": "Name",
"30": "Serial Number",
"52": "Firmware Revision",
"53": "Hardware Revision",
"220": "Product Data",
"A6": "Accessory Flags",
"22": "Motion Detected",
"75": "Status Active",
"11A": "Mute",
"119": "Volume",
"B0": "Active",
"209": "Selected Camera Recording Configuration",
"207": "Supported Audio Recording Configuration",
"205": "Supported Camera Recording Configuration",
"206": "Supported Video Recording Configuration",
"226": "Recording Audio Active",
"223": "Event Snapshots Active",
"225": "Periodic Snapshots Active",
"21B": "HomeKit Camera Active",
"21C": "Third Party Camera Active",
"21D": "Camera Operating Mode Indicator",
"11B": "Night Vision",
"129": "Supported Data Stream Transport Configuration",
"37": "Version",
"131": "Setup Data Stream Transport",
"130": "Supported Data Stream Transport Configuration",
"120": "Streaming Status",
"115": "Supported Audio Stream Configuration",
"116": "Supported RTP Configuration",
"114": "Supported Video Stream Configuration",
"117": "Selected RTP Stream Configuration",
"118": "Setup Endpoints",
"22B": "Current Transport",
"22C": "Wi-Fi Capabilities",
"22D": "Wi-Fi Configuration Control",
"23C": "Ping",
"68": "Battery Level",
"79": "Status Low Battery",
"8F": "Charging State",
"73": "Programmable Switch Event",
"232": "Operating State Response",
"66": "Security System Current State",
"67": "Security System Target State",
}
func main() {
src := os.Args[1]
dst := os.Args[2]
f, err := os.Open(src)
if err != nil {
panic(err)
}
var v hap.JSONAccessories
if err = json.NewDecoder(f).Decode(&v); err != nil {
panic(err)
}
for _, acc := range v.Value {
for _, srv := range acc.Services {
if srv.Desc == "" {
srv.Desc = servs[srv.Type]
}
for _, chr := range srv.Characters {
if chr.Desc == "" {
chr.Desc = chars[chr.Type]
}
}
}
}
f, err = os.Create(dst)
if err != nil {
panic(err)
}
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
if err = enc.Encode(v); err != nil {
panic(err)
}
}

39
examples/mdns/main.go Normal file
View File

@@ -0,0 +1,39 @@
package main
import (
"log"
"os"
"github.com/AlexxIT/go2rtc/pkg/mdns"
)
func main() {
var service = mdns.ServiceHAP
if len(os.Args) >= 2 {
service = os.Args[1]
}
onentry := func(entry *mdns.ServiceEntry) bool {
log.Printf("name=%s, addr=%s, info=%s\n", entry.Name, entry.Addr(), entry.Info)
return false
}
var err error
if len(os.Args) >= 3 {
host := os.Args[2]
log.Printf("run discovery service=%s host=%s\n", service, host)
err = mdns.QueryOrDiscovery(host, service, onentry)
} else {
log.Printf("run discovery service=%s\n", service)
err = mdns.Discovery(service, onentry)
}
if err != nil {
log.Println(err)
}
}

63
go.mod
View File

@@ -1,59 +1,44 @@
module github.com/AlexxIT/go2rtc
go 1.20
go 1.21
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.3.1
github.com/pion/interceptor v0.1.12
github.com/miekg/dns v1.1.55
github.com/pion/ice/v2 v2.3.11
github.com/pion/interceptor v0.1.19
github.com/pion/rtcp v1.2.10
github.com/pion/rtp v1.7.13
github.com/pion/rtp v1.8.1
github.com/pion/sdp/v3 v3.0.6
github.com/pion/srtp/v2 v2.0.12
github.com/pion/stun v0.4.0
github.com/pion/webrtc/v3 v3.1.58
github.com/rs/zerolog v1.29.0
github.com/stretchr/testify v1.8.2
github.com/pion/srtp/v2 v2.0.17
github.com/pion/stun v0.6.1
github.com/pion/webrtc/v3 v3.2.19
github.com/rs/zerolog v1.30.0
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
github.com/stretchr/testify v1.8.4
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
golang.org/x/crypto v0.13.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/brutella/dnssd v1.2.5 // 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/google/uuid v1.3.1 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/miekg/dns v1.1.52 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/pion/datachannel v1.5.5 // indirect
github.com/pion/dtls/v2 v2.2.6 // indirect
github.com/pion/dtls/v2 v2.2.7 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.7 // indirect
github.com/pion/mdns v0.0.9 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.6 // indirect
github.com/pion/transport/v2 v2.0.2 // indirect
github.com/pion/turn/v2 v2.1.0 // indirect
github.com/pion/udp/v2 v2.0.1 // indirect
github.com/pion/sctp v1.8.9 // indirect
github.com/pion/transport/v2 v2.2.4 // indirect
github.com/pion/turn/v2 v2.1.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/tools v0.7.0 // indirect
)
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
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/tools v0.13.0 // indirect
)

172
go.sum
View File

@@ -1,18 +1,9 @@
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/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
github.com/brutella/dnssd v1.2.5 h1:b8syhho41/5ikw3X2X4baR9NWEBSlpZnfQgujsv7bk4=
github.com/brutella/dnssd v1.2.5/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/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/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -28,12 +19,10 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
@@ -46,12 +35,10 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.52 h1:Bmlc/qsNNULOe6bpXcUTsuOajd0DzRHwup6D9k1An0c=
github.com/miekg/dns v1.1.52/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/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=
@@ -63,104 +50,111 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
github.com/pion/dtls/v2 v2.2.6 h1:yXMxKr0Skd+Ub6A8UqXTRLSywskx93ooMRHsQUtd+Z4=
github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY=
github.com/pion/ice/v2 v2.3.1 h1:FQCmUfZe2Jpe7LYStVBOP6z1DiSzbIateih3TztgTjc=
github.com/pion/ice/v2 v2.3.1/go.mod h1:aq2kc6MtYNcn4XmMhobAv6hTNJiHzvD0yXRz80+bnP8=
github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8=
github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA=
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/ice/v2 v2.3.11 h1:rZjVmUwyT55cmN8ySMpL7rsS8KYsJERsrxJLLxpKhdw=
github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
github.com/pion/interceptor v0.1.18/go.mod h1:tpvvF4cPM6NGxFA1DUMbhabzQBxdWMATDGEUYOR9x6I=
github.com/pion/interceptor v0.1.19 h1:tq0TGBzuZQqipyBhaC1mVUCfCh8XjDKUuibq9rIl5t4=
github.com/pion/interceptor v0.1.19/go.mod h1:VANhFxdJezB8mwToMMmrmyHyP9gym6xLqIUch31xryg=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U=
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/rtp v1.8.1 h1:26OxTc6lKg/qLSGir5agLyj0QKaOv8OP5wps2SFnVNQ=
github.com/pion/rtp v1.8.1/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sctp v1.8.6 h1:CUex11Vkt9YS++VhLf8b55O3VqKrWL6W3SDwX4jAqsI=
github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/srtp/v2 v2.0.12 h1:WrmiVCubGMOAObBU1vwWjG0H3VSyQHawKeer2PVA5rY=
github.com/pion/srtp/v2 v2.0.12/go.mod h1:C3Ep44hlOo2qEYaq4ddsmK5dL63eLehXFbHaZ9F5V9Y=
github.com/pion/stun v0.4.0 h1:vgRrbBE2htWHy7l3Zsxckk7rkjnjOsSM7PHZnBwo8rk=
github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw=
github.com/pion/srtp/v2 v2.0.17 h1:ECuOk+7uIpY6HUlTb0nXhfvu4REG2hjtC4ronYFCZE4=
github.com/pion/srtp/v2 v2.0.17/go.mod h1:y5WSHcJY4YfNB/5r7ca5YjHeIr1H3LM1rKArGGs8jMc=
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
github.com/pion/transport/v2 v2.0.2 h1:St+8o+1PEzPT51O9bv+tH/KYYLMNR5Vwm5Z3Qkjsywg=
github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0=
github.com/pion/turn/v2 v2.1.0 h1:5wGHSgGhJhP/RpabkUb/T9PdsAjkGLS6toYz5HNzoSI=
github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs=
github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54=
github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8=
github.com/pion/webrtc/v3 v3.1.58 h1:husXqiKQuk6gbOqJlPHs185OskAyxUW6iAEgHghgCrc=
github.com/pion/webrtc/v3 v3.1.58/go.mod h1:jJdqoqGBlZiE3y8Z1tg1fjSkyEDCZLL+foypUBn0Lhk=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA=
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
github.com/pion/webrtc/v3 v3.2.19 h1:XNu5e62mkzafw1qYuKtQ+Dviw4JpbzC/SLx3zZt49JY=
github.com/pion/webrtc/v3 v3.2.19/go.mod h1:vVURQTBOG5BpWKOJz3nlr23NfTDeyKVmubRNqzQp+Tg=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/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/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -171,10 +165,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
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=
@@ -183,31 +174,43 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/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=
@@ -227,7 +230,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
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=

View File

@@ -1,7 +1,9 @@
# syntax=docker/dockerfile:labs
# 0. Prepare images
# only debian 12 (bookworm) has latest ffmpeg
ARG DEBIAN_VERSION="bookworm-slim"
ARG GO_VERSION="1.19-buster"
ARG GO_VERSION="1.21-bookworm"
ARG NGROK_VERSION="3"
FROM debian:${DEBIAN_VERSION} AS base
@@ -10,43 +12,55 @@ FROM ngrok/ngrok:${NGROK_VERSION} AS ngrok
# 1. Build go2rtc binary
FROM go AS build
FROM --platform=$BUILDPLATFORM go AS build
ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH
ENV GOOS=${TARGETOS}
ENV GOARCH=${TARGETARCH}
WORKDIR /build
# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
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/
COPY ./build/docker/run.sh /
COPY --link --from=build /build/go2rtc /usr/local/bin/
COPY --link --from=ngrok /bin/ngrok /usr/local/bin/
# 3. Final image
FROM base
# Prepare apt for buildkit cache
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache
# 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
# libasound2-plugins for ALSA support
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \
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 \
libasound2-plugins
COPY --link --from=rootfs / /
COPY --from=rootfs / /
RUN chmod a+x /run.sh && mkdir -p /config
ENTRYPOINT ["/usr/bin/tini", "--"]
VOLUME /config
WORKDIR /config
# 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"]
CMD ["go2rtc", "-config", "/config/go2rtc.yaml"]

4
internal/api/README.md Normal file
View File

@@ -0,0 +1,4 @@
## Exit codes
- https://tldp.org/LDP/abs/html/exitcodes.html
- https://komodor.com/learn/exit-codes-in-containers-and-kubernetes-the-complete-guide/

View File

@@ -1,15 +1,19 @@
package api
import (
"crypto/tls"
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/rs/zerolog"
"fmt"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/rs/zerolog"
)
func Init() {
@@ -21,11 +25,14 @@ func Init() {
BasePath string `yaml:"base_path"`
StaticDir string `yaml:"static_dir"`
Origin string `yaml:"origin"`
TLSListen string `yaml:"tls_listen"`
TLSCert string `yaml:"tls_cert"`
TLSKey string `yaml:"tls_key"`
} `yaml:"api"`
}
// default config
cfg.Mod.Listen = ":1984"
cfg.Mod.Listen = "0.0.0.0:1984"
// load config from YAML
app.LoadConfig(&cfg)
@@ -38,15 +45,15 @@ func Init() {
log = app.GetLogger("api")
initStatic(cfg.Mod.StaticDir)
initWS(cfg.Mod.Origin)
HandleFunc("api", apiHandler)
HandleFunc("api/config", configHandler)
HandleFunc("api/exit", exitHandler)
HandleFunc("api/ws", apiWS)
HandleFunc("api/restart", restartHandler)
// ensure we can listen without errors
listener, err := net.Listen("tcp", cfg.Mod.Listen)
var err error
ln, err = net.Listen("tcp", cfg.Mod.Listen)
if err != nil {
log.Fatal().Err(err).Msg("[api] listen")
return
@@ -71,12 +78,61 @@ func Init() {
go func() {
s := http.Server{}
s.Handler = Handler
if err = s.Serve(listener); err != nil {
if err = s.Serve(ln); err != nil {
log.Fatal().Err(err).Msg("[api] serve")
}
}()
// Initialize the HTTPS server
if cfg.Mod.TLSListen != "" && cfg.Mod.TLSCert != "" && cfg.Mod.TLSKey != "" {
var cert tls.Certificate
if strings.IndexByte(cfg.Mod.TLSCert, '\n') < 0 && strings.IndexByte(cfg.Mod.TLSKey, '\n') < 0 {
// check if file path
cert, err = tls.LoadX509KeyPair(cfg.Mod.TLSCert, cfg.Mod.TLSKey)
} else {
// if text file content
cert, err = tls.X509KeyPair([]byte(cfg.Mod.TLSCert), []byte(cfg.Mod.TLSKey))
}
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
tlsListener, err := net.Listen("tcp", cfg.Mod.TLSListen)
if err != nil {
log.Fatal().Err(err).Caller().Send()
return
}
log.Info().Str("addr", cfg.Mod.TLSListen).Msg("[api] tls listen")
tlsServer := &http.Server{
Handler: Handler,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
},
}
go func() {
if err := tlsServer.ServeTLS(tlsListener, "", ""); err != nil {
log.Fatal().Err(err).Msg("[api] tls serve")
}
}()
}
}
func Port() int {
if ln == nil {
return 0
}
return ln.Addr().(*net.TCPAddr).Port
}
const (
MimeJSON = "application/json"
MimeText = "text/plain"
)
var Handler http.Handler
// HandleFunc handle pattern with relative path:
@@ -90,6 +146,33 @@ func HandleFunc(pattern string, handler http.HandlerFunc) {
http.HandleFunc(pattern, handler)
}
// ResponseJSON important always add Content-Type
// so go won't need to call http.DetectContentType
func ResponseJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", MimeJSON)
_ = json.NewEncoder(w).Encode(v)
}
func ResponsePrettyJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", MimeJSON)
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
_ = enc.Encode(v)
}
func Response(w http.ResponseWriter, body any, contentType string) {
w.Header().Set("Content-Type", contentType)
switch v := body.(type) {
case []byte:
_, _ = w.Write(v)
case string:
_, _ = w.Write([]byte(v))
default:
_, _ = fmt.Fprint(w, body)
}
}
const StreamNotFound = "stream not found"
var basePath string
@@ -121,10 +204,12 @@ func middlewareCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization")
next.ServeHTTP(w, r)
})
}
var ln net.Listener
var mu sync.Mutex
func apiHandler(w http.ResponseWriter, r *http.Request) {
@@ -132,9 +217,7 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
app.Info["host"] = r.Host
mu.Unlock()
if err := json.NewEncoder(w).Encode(app.Info); err != nil {
log.Warn().Err(err).Caller().Send()
}
ResponseJSON(w, app.Info)
}
func exitHandler(w http.ResponseWriter, r *http.Request) {
@@ -148,22 +231,39 @@ func exitHandler(w http.ResponseWriter, r *http.Request) {
os.Exit(code)
}
type Stream struct {
Name string `json:"name"`
URL string `json:"url"`
}
func ResponseStreams(w http.ResponseWriter, streams []Stream) {
if len(streams) == 0 {
http.Error(w, "no streams", http.StatusNotFound)
func restartHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "", http.StatusBadRequest)
return
}
var response struct {
Streams []Stream `json:"streams"`
}
response.Streams = streams
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
go shell.Restart()
}
type Source struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Info string `json:"info,omitempty"`
URL string `json:"url,omitempty"`
Location string `json:"location,omitempty"`
}
func ResponseSources(w http.ResponseWriter, sources []*Source) {
if len(sources) == 0 {
http.Error(w, "no sources", http.StatusNotFound)
return
}
var response = struct {
Sources []*Source `json:"sources"`
}{
Sources: sources,
}
ResponseJSON(w, response)
}
func Error(w http.ResponseWriter, err error) {
log.Error().Err(err).Caller(1).Send()
http.Error(w, err.Error(), http.StatusInsufficientStorage)
}

View File

@@ -1,11 +1,12 @@
package api
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"gopkg.in/yaml.v3"
"io"
"net/http"
"os"
"github.com/AlexxIT/go2rtc/internal/app"
"gopkg.in/yaml.v3"
)
func configHandler(w http.ResponseWriter, r *http.Request) {
@@ -21,9 +22,8 @@ func configHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "", http.StatusNotFound)
return
}
if _, err = w.Write(data); err != nil {
log.Warn().Err(err).Caller().Send()
}
// https://www.ietf.org/archive/id/draft-ietf-httpapi-yaml-mediatypes-00.html
Response(w, data, "application/yaml")
case "POST", "PATCH":
data, err := io.ReadAll(r.Body)
@@ -41,8 +41,7 @@ func configHandler(w http.ResponseWriter, r *http.Request) {
}
} else {
// validate config
var tmp struct{}
if err = yaml.Unmarshal(data, &tmp); err != nil {
if err = yaml.Unmarshal(data, map[string]any{}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

View File

@@ -1,14 +1,33 @@
package api
package ws
import (
"github.com/gorilla/websocket"
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/gorilla/websocket"
"github.com/rs/zerolog/log"
)
func Init() {
var cfg struct {
Mod struct {
Origin string `yaml:"origin"`
} `yaml:"api"`
}
app.LoadConfig(&cfg)
initWS(cfg.Mod.Origin)
api.HandleFunc("api/ws", apiWS)
}
// Message - struct for data exchange in Web API
type Message struct {
Type string `json:"type"`
@@ -33,7 +52,7 @@ func (m *Message) GetString(key string) string {
type WSHandler func(tr *Transport, msg *Message) error
func HandleWS(msgType string, handler WSHandler) {
func HandleFunc(msgType string, handler WSHandler) {
wsHandlers[msgType] = handler
}
@@ -84,13 +103,13 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
}
tr := &Transport{Request: r}
tr.OnWrite(func(msg any) {
tr.OnWrite(func(msg any) error {
_ = ws.SetWriteDeadline(time.Now().Add(time.Second * 5))
if data, ok := msg.([]byte); ok {
_ = ws.WriteMessage(websocket.BinaryMessage, data)
return ws.WriteMessage(websocket.BinaryMessage, data)
} else {
_ = ws.WriteJSON(msg)
return ws.WriteJSON(msg)
}
})
@@ -130,11 +149,11 @@ type Transport struct {
wrmx sync.Mutex
onChange func()
onWrite func(msg any)
onWrite func(msg any) error
onClose []func()
}
func (t *Transport) OnWrite(f func(msg any)) {
func (t *Transport) OnWrite(f func(msg any) error) {
t.mx.Lock()
if t.onChange != nil {
t.onChange()
@@ -145,7 +164,7 @@ func (t *Transport) OnWrite(f func(msg any)) {
func (t *Transport) Write(msg any) {
t.wrmx.Lock()
t.onWrite(msg)
_ = t.onWrite(msg)
t.wrmx.Unlock()
}
@@ -183,3 +202,20 @@ func (t *Transport) WithContext(f func(ctx map[any]any)) {
f(t.ctx)
t.mx.Unlock()
}
func (t *Transport) Writer() io.Writer {
return &writer{t: t}
}
type writer struct {
t *Transport
}
func (w *writer) Write(p []byte) (n int, err error) {
w.t.wrmx.Lock()
if err = w.t.onWrite(p); err == nil {
n = len(p)
}
w.t.wrmx.Unlock()
return
}

View File

@@ -1,6 +1,7 @@
package app
import (
"errors"
"flag"
"fmt"
"io"
@@ -11,12 +12,12 @@ import (
"time"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/yaml"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
)
var Version = "1.3.1"
var Version = ""
var UserAgent = "go2rtc/" + Version
var ConfigPath string
@@ -81,6 +82,8 @@ func Init() {
modules = cfg.Mod
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
migrateStore()
}
func NewLogger(format string, level string) zerolog.Logger {
@@ -123,6 +126,22 @@ func GetLogger(module string) zerolog.Logger {
return log.Logger
}
func PatchConfig(key string, value any, path ...string) error {
if ConfigPath == "" {
return errors.New("config file disabled")
}
// empty config is OK
b, _ := os.ReadFile(ConfigPath)
b, err := yaml.Patch(b, key, value, path...)
if err != nil {
return err
}
return os.WriteFile(ConfigPath, b, 0644)
}
// internal
type Config []string

35
internal/app/migrate.go Normal file
View File

@@ -0,0 +1,35 @@
package app
import (
"encoding/json"
"os"
"github.com/rs/zerolog/log"
)
func migrateStore() {
const name = "go2rtc.json"
data, _ := os.ReadFile(name)
if data == nil {
return
}
var store struct {
Streams map[string]string `json:"streams"`
}
if err := json.Unmarshal(data, &store); err != nil {
log.Warn().Err(err).Caller().Send()
return
}
for id, url := range store.Streams {
if err := PatchConfig(id, url, "streams"); err != nil {
log.Warn().Err(err).Caller().Send()
return
}
}
_ = os.Remove(name)
}

View File

@@ -1,17 +1,17 @@
package tapo
package bubble
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/bubble"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/tapo"
)
func Init() {
streams.HandleFunc("tapo", handle)
streams.HandleFunc("bubble", handle)
}
func handle(url string) (core.Producer, error) {
conn := tapo.NewClient(url)
conn := bubble.NewClient(url)
if err := conn.Dial(); err != nil {
return nil, err
}

View File

@@ -1,8 +1,8 @@
package debug
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
)

View File

@@ -5,6 +5,8 @@ import (
"fmt"
"net/http"
"runtime"
"github.com/AlexxIT/go2rtc/internal/api"
)
var stackSkip = [][]byte{
@@ -13,15 +15,18 @@ var stackSkip = [][]byte{
[]byte("created by os/signal.Notify"),
// api/stack.go
[]byte("github.com/AlexxIT/go2rtc/cmd/api.stackHandler"),
[]byte("github.com/AlexxIT/go2rtc/internal/api.stackHandler"),
// api/api.go
[]byte("created by github.com/AlexxIT/go2rtc/cmd/api.Init"),
[]byte("created by github.com/AlexxIT/go2rtc/internal/api.Init"),
[]byte("created by net/http.(*connReader).startBackgroundRead"),
[]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"),
[]byte("created by github.com/AlexxIT/go2rtc/internal/rtsp.Init"),
[]byte("created by github.com/AlexxIT/go2rtc/internal/srtp.Init"),
// homekit
[]byte("created by github.com/AlexxIT/go2rtc/internal/homekit.Init"),
// webrtc/api.go
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),
@@ -51,7 +56,5 @@ func stackHandler(w http.ResponseWriter, r *http.Request) {
"Total: %d, Skipped: %d", runtime.NumGoroutine(), skipped),
)
if _, err := w.Write(buf[:i]); err != nil {
panic(err)
}
api.Response(w, buf[:i], api.MimeText)
}

174
internal/dvrip/dvrip.go Normal file
View File

@@ -0,0 +1,174 @@
package dvrip
import (
"encoding/hex"
"encoding/json"
"fmt"
"net"
"net/http"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/dvrip"
"github.com/rs/zerolog/log"
)
func Init() {
streams.HandleFunc("dvrip", handle)
// DVRIP client autodiscovery
api.HandleFunc("api/dvrip", apiDvrip)
}
func handle(url string) (core.Producer, error) {
client, err := dvrip.Dial(url)
if err != nil {
return nil, err
}
return client, nil
}
const Port = 34569 // UDP port number for dvrip discovery
func apiDvrip(w http.ResponseWriter, r *http.Request) {
items, err := discover()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
api.ResponseSources(w, items)
}
func discover() ([]*api.Source, error) {
addr := &net.UDPAddr{
Port: Port,
IP: net.IP{239, 255, 255, 250},
}
conn, err := net.ListenUDP("udp4", addr)
if err != nil {
return nil, err
}
defer conn.Close()
go sendBroadcasts(conn)
var items []*api.Source
for _, info := range getResponses(conn) {
if info.HostIP == "" || info.HostName == "" {
continue
}
host, err := hexToDecimalBytes(info.HostIP)
if err != nil {
continue
}
items = append(items, &api.Source{
Name: info.HostName,
URL: "dvrip://user:pass@" + host + "?channel=0&subtype=0",
})
}
return items, nil
}
func sendBroadcasts(conn *net.UDPConn) {
// broadcasting the same multiple times because the devies some times don't answer
data, err := hex.DecodeString("ff00000000000000000000000000fa0500000000")
if err != nil {
return
}
addr := &net.UDPAddr{
Port: Port,
IP: net.IP{255, 255, 255, 255},
}
for i := 0; i < 3; i++ {
time.Sleep(100 * time.Millisecond)
if _, err = conn.WriteToUDP(data, addr); err != nil {
log.Err(err).Caller().Send()
}
}
}
type Message struct {
NetCommon NetCommon `json:"NetWork.NetCommon"`
Ret int `json:"Ret"`
SessionID string `json:"SessionID"`
}
type NetCommon struct {
BuildDate string `json:"BuildDate"`
ChannelNum int `json:"ChannelNum"`
DeviceType int `json:"DeviceType"`
GateWay string `json:"GateWay"`
HostIP string `json:"HostIP"`
HostName string `json:"HostName"`
HttpPort int `json:"HttpPort"`
MAC string `json:"MAC"`
MonMode string `json:"MonMode"`
NetConnectState int `json:"NetConnectState"`
OtherFunction string `json:"OtherFunction"`
SN string `json:"SN"`
SSLPort int `json:"SSLPort"`
Submask string `json:"Submask"`
TCPMaxConn int `json:"TCPMaxConn"`
TCPPort int `json:"TCPPort"`
UDPPort int `json:"UDPPort"`
UseHSDownLoad bool `json:"UseHSDownLoad"`
Version string `json:"Version"`
}
func getResponses(conn *net.UDPConn) (infos []*NetCommon) {
if err := conn.SetReadDeadline(time.Now().Add(time.Second * 2)); err != nil {
return
}
var ips []net.IP // processed IPs
b := make([]byte, 4096)
loop:
for {
n, addr, err := conn.ReadFromUDP(b)
if err != nil {
break
}
for _, ip := range ips {
if ip.Equal(addr.IP) {
continue loop
}
}
if n <= 20+1 {
continue
}
var msg Message
if err = json.Unmarshal(b[20:n-1], &msg); err != nil {
continue
}
infos = append(infos, &msg.NetCommon)
ips = append(ips, addr.IP)
}
return
}
func hexToDecimalBytes(hexIP string) (string, error) {
b, err := hex.DecodeString(hexIP[2:]) // remove the '0x' prefix
if err != nil {
return "", err
}
return fmt.Sprintf("%d.%d.%d.%d", b[3], b[2], b[1], b[0]), nil
}

View File

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

View File

@@ -5,26 +5,22 @@ import (
"encoding/hex"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"os"
"os/exec"
"sync"
"time"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/magic"
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/rs/zerolog"
"os"
"os/exec"
"strings"
"sync"
"time"
)
func Init() {
// depends on RTSP server
if rtsp.Port == "" {
return
}
rtsp.HandleFunc(func(conn *pkg.Conn) bool {
waitersMu.Lock()
waiter := waiters[conn.URL.Path]
@@ -43,30 +39,58 @@ func Init() {
}
})
streams.HandleFunc("exec", Handle)
streams.HandleFunc("exec", execHandle)
log = app.GetLogger("exec")
}
func Handle(url string) (core.Producer, error) {
sum := md5.Sum([]byte(url))
path := "/" + hex.EncodeToString(sum[:])
func execHandle(url string) (core.Producer, error) {
var path string
url = strings.Replace(
url, "{output}", "rtsp://localhost:"+rtsp.Port+path, 1,
)
args := shell.QuoteSplit(url[5:]) // remove `exec:`
for i, arg := range args {
if arg == "{output}" {
if rtsp.Port == "" {
return nil, errors.New("rtsp module disabled")
}
// remove `exec:`
args := shell.QuoteSplit(url[5:])
cmd := exec.Command(args[0], args[1:]...)
if log.Trace().Enabled() {
cmd.Stdout = os.Stdout
sum := md5.Sum([]byte(url))
path = "/" + hex.EncodeToString(sum[:])
args[i] = "rtsp://127.0.0.1:" + rtsp.Port + path
break
}
}
cmd := exec.Command(args[0], args[1:]...)
if log.Debug().Enabled() {
cmd.Stderr = os.Stderr
}
if path == "" {
return handlePipe(url, cmd)
}
return handleRTSP(url, path, cmd)
}
func handlePipe(url string, cmd *exec.Cmd) (core.Producer, error) {
r, err := PipeCloser(cmd)
if err != nil {
return nil, err
}
if err = cmd.Start(); err != nil {
return nil, err
}
return magic.Open(r)
}
func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
if log.Trace().Enabled() {
cmd.Stdout = os.Stdout
}
ch := make(chan core.Producer)
waitersMu.Lock()

30
internal/exec/pipe.go Normal file
View File

@@ -0,0 +1,30 @@
package exec
import (
"bufio"
"io"
"os/exec"
"github.com/AlexxIT/go2rtc/pkg/core"
)
// PipeCloser - return StdoutPipe that Kill cmd on Close call
func PipeCloser(cmd *exec.Cmd) (io.ReadCloser, error) {
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
// add buffer for pipe reader to reduce syscall
return pipeCloser{bufio.NewReaderSize(stdout, core.BufferSize), stdout, cmd}, nil
}
type pipeCloser struct {
io.Reader
io.Closer
cmd *exec.Cmd
}
func (p pipeCloser) Close() error {
return core.Any(p.Closer.Close(), p.cmd.Process.Kill(), p.cmd.Wait())
}

View File

@@ -0,0 +1,86 @@
package device
import (
"net/url"
"os/exec"
"regexp"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func queryToInput(query url.Values) string {
video := query.Get("video")
audio := query.Get("audio")
if video == "" && audio == "" {
return ""
}
// https://ffmpeg.org/ffmpeg-devices.html#avfoundation
input := "-f avfoundation"
if video != "" {
video = indexToItem(videos, video)
for key, value := range query {
switch key {
case "resolution":
input += " -video_size " + value[0]
case "pixel_format", "framerate", "video_size", "capture_cursor", "capture_mouse_clicks", "capture_raw_data":
input += " -" + key + " " + value[0]
}
}
}
if audio != "" {
audio = indexToItem(audios, audio)
}
return input + ` -i "` + video + `:` + audio + `"`
}
func initDevices() {
// [AVFoundation indev @ 0x147f04510] AVFoundation video devices:
// [AVFoundation indev @ 0x147f04510] [0] FaceTime HD Camera
// [AVFoundation indev @ 0x147f04510] [1] Capture screen 0
// [AVFoundation indev @ 0x147f04510] AVFoundation audio devices:
// [AVFoundation indev @ 0x147f04510] [0] MacBook Pro Microphone
cmd := exec.Command(
Bin, "-hide_banner", "-list_devices", "true", "-f", "avfoundation", "-i", "",
)
b, _ := cmd.CombinedOutput()
re := regexp.MustCompile(`\[\d+] (.+)`)
var kind string
for _, line := range strings.Split(string(b), "\n") {
switch {
case strings.HasSuffix(line, "video devices:"):
kind = core.KindVideo
continue
case strings.HasSuffix(line, "audio devices:"):
kind = core.KindAudio
continue
}
m := re.FindStringSubmatch(line)
if m == nil {
continue
}
name := m[1]
switch kind {
case core.KindVideo:
videos = append(videos, name)
case core.KindAudio:
audios = append(audios, name)
}
streams = append(streams, &api.Source{
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
})
}
}

View File

@@ -0,0 +1,99 @@
package device
import (
"net/url"
"os"
"os/exec"
"regexp"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func queryToInput(query url.Values) string {
if video := query.Get("video"); video != "" {
// https://ffmpeg.org/ffmpeg-devices.html#video4linux2_002c-v4l2
input := "-f v4l2"
for key, value := range query {
switch key {
case "resolution":
input += " -video_size " + value[0]
case "video_size", "pixel_format", "input_format", "framerate", "use_libv4l2":
input += " -" + key + " " + value[0]
}
}
return input + " -i " + indexToItem(videos, video)
}
if audio := query.Get("audio"); audio != "" {
// https://trac.ffmpeg.org/wiki/Capture/ALSA
input := "-f alsa"
for key, value := range query {
switch key {
case "channels", "sample_rate":
input += " -" + key + " " + value[0]
}
}
return input + " -i " + indexToItem(audios, audio)
}
return ""
}
func initDevices() {
files, err := os.ReadDir("/dev")
if err != nil {
return
}
for _, file := range files {
if !strings.HasPrefix(file.Name(), core.KindVideo) {
continue
}
name := "/dev/" + file.Name()
cmd := exec.Command(
Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name,
)
b, _ := cmd.CombinedOutput()
// [video4linux2,v4l2 @ 0x204e1c0] Compressed: mjpeg : Motion-JPEG : 640x360 1280x720 1920x1080
// [video4linux2,v4l2 @ 0x204e1c0] Raw : yuyv422 : YUYV 4:2:2 : 640x360 1280x720 1920x1080
// [video4linux2,v4l2 @ 0x204e1c0] Compressed: h264 : H.264 : 640x360 1280x720 1920x1080
re := regexp.MustCompile("(Raw *|Compressed): +(.+?) : +(.+?) : (.+)")
m := re.FindAllStringSubmatch(string(b), -1)
for _, i := range m {
size, _, _ := strings.Cut(i[4], " ")
stream := &api.Source{
Name: i[3],
Info: i[4],
URL: "ffmpeg:device?video=" + name + "&input_format=" + i[2] + "&video_size=" + size,
}
if i[1] != "Compressed" {
stream.URL += "#video=h264#hardware"
}
videos = append(videos, name)
streams = append(streams, stream)
}
}
err = exec.Command(Bin, "-f", "alsa", "-i", "default", "-t", "1", "-f", "null", "-").Run()
if err == nil {
stream := &api.Source{
Name: "ALSA default",
Info: " ",
URL: "ffmpeg:device?audio=default&channels=1&sample_rate=16000&#audio=opus",
}
audios = append(audios, "default")
streams = append(streams, stream)
}
}

View File

@@ -0,0 +1,98 @@
package device
import (
"net/url"
"os/exec"
"regexp"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func queryToInput(query url.Values) string {
video := query.Get("video")
audio := query.Get("audio")
if video == "" && audio == "" {
return ""
}
// https://ffmpeg.org/ffmpeg-devices.html#dshow
input := "-f dshow"
if video != "" {
video = indexToItem(videos, video)
for key, value := range query {
switch key {
case "resolution":
input += " -video_size " + value[0]
case "video_size", "framerate", "pixel_format":
input += " -" + key + " " + value[0]
}
}
}
if audio != "" {
audio = indexToItem(audios, audio)
for key, value := range query {
switch key {
case "sample_rate", "sample_size", "channels", "audio_buffer_size":
input += " -" + key + " " + value[0]
}
}
}
if video != "" {
input += ` -i video="` + video + `"`
if audio != "" {
input += `:audio="` + audio + `"`
}
} else {
input += ` -i audio="` + audio + `"`
}
return input
}
func deviceInputSuffix(video, audio string) string {
switch {
case video != "" && audio != "":
return `video="` + video + `":audio=` + audio + `"`
case video != "":
return `video="` + video + `"`
case audio != "":
return `audio="` + audio + `"`
}
return ""
}
func initDevices() {
cmd := exec.Command(
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
)
b, _ := cmd.CombinedOutput()
re := regexp.MustCompile(`"([^"]+)" \((video|audio)\)`)
for _, m := range re.FindAllStringSubmatch(string(b), -1) {
name := m[1]
kind := m[2]
stream := &api.Source{
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
}
switch kind {
case core.KindVideo:
videos = append(videos, name)
stream.URL += "#video=h264#hardware"
case core.KindAudio:
audios = append(audios, name)
stream.URL += "&channels=1&sample_rate=16000&audio_buffer_size=10"
}
streams = append(streams, stream)
}
}

View File

@@ -0,0 +1,57 @@
package device
import (
"errors"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"github.com/AlexxIT/go2rtc/internal/api"
)
func Init(bin string) {
Bin = bin
api.HandleFunc("api/ffmpeg/devices", apiDevices)
}
func GetInput(src string) (string, error) {
i := strings.IndexByte(src, '?')
if i < 0 {
return "", errors.New("empty query: " + src)
}
query, err := url.ParseQuery(src[i+1:])
if err != nil {
return "", err
}
runonce.Do(initDevices)
if input := queryToInput(query); input != "" {
return input, nil
}
return "", errors.New("wrong query: " + src)
}
var Bin string
var videos, audios []string
var streams []*api.Source
var runonce sync.Once
func apiDevices(w http.ResponseWriter, r *http.Request) {
runonce.Do(initDevices)
api.ResponseSources(w, streams)
}
func indexToItem(items []string, index string) string {
if i, err := strconv.Atoi(index); err == nil && i < len(items) {
return items[i]
}
return index
}

View File

@@ -1,17 +1,15 @@
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/core"
"net/url"
"strconv"
"strings"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
)
func Init() {
@@ -27,16 +25,13 @@ func Init() {
defaults["global"] += " -v error"
}
streams.HandleFunc("ffmpeg", func(url string) (core.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())
streams.RedirectFunc("ffmpeg", func(url string) (string, error) {
args := parseArgs(url[7:])
return "exec:" + args.String(), nil
})
device.Bin = defaults["bin"]
device.Init()
device.Init(defaults["bin"])
hardware.Init(defaults["bin"])
}
var defaults = map[string]string{
@@ -46,21 +41,28 @@ var defaults = map[string]string{
// inputs
"file": "-re -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}",
"rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}",
"rtsp/udp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}",
// output
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
"output/mjpeg": "-f mjpeg -",
// `-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",
// `-pix_fmt:v yuv420p` - important for Telegram
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency",
"mjpeg": "-c:v mjpeg",
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
"opus": "-c:a libopus -ar:a 48000 -ac:a 2",
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
// https://github.com/pion/webrtc/issues/1514
// https://ffmpeg.org/ffmpeg-resampler.html
// `-async 1` or `-min_comp 0` - force frame_size=960, important for WebRTC audio quality
"opus": "-c:a libopus -application:a lowdelay -frame_duration 20 -min_comp 0",
"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",
@@ -70,10 +72,11 @@ var defaults = map[string]string{
"aac": "-c:a aac", // keep sample rate and channels
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
"mp3": "-c:a libmp3lame -q:a 8",
"pcm": "-c:a pcm_s16be",
"pcm/8000": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
"pcm": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
"pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
"pcml": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
"pcml/44100": "-c:a pcm_s16le -ar:a 44100 -ac:a 1",
// hardware Intel and AMD on Linux
// better not to set `-async_depth:v 1` like for QSV, because framedrops
@@ -88,8 +91,8 @@ var defaults = map[string]string{
// 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",
"h264/cuda": "-c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
"h265/cuda": "-c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto",
// 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",
@@ -101,34 +104,40 @@ var defaults = map[string]string{
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1",
}
// configTemplate - return template from config (defaults) if exist or return raw template
func configTemplate(template string) string {
if s := defaults[template]; s != "" {
return s
}
return template
}
// inputTemplate - select input template from YAML config by template name
// if query has input param - select another tempalte by this name
// if query has input param - select another template by this name
// if there is no another template - use input param as template
func inputTemplate(name, s string, query url.Values) string {
var template string
if input := query.Get("input"); input != "" {
if template = defaults[input]; template == "" {
template = input
}
template = configTemplate(input)
} else {
template = defaults[name]
}
return strings.Replace(template, "{input}", s, 1)
}
func parseArgs(s string) *Args {
func parseArgs(s string) *ffmpeg.Args {
// init FFmpeg arguments
args := &Args{
bin: defaults["bin"],
global: defaults["global"],
output: defaults["output"],
args := &ffmpeg.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"])
query = streams.ParseQuery(s[i+1:])
args.Video = len(query["video"])
args.Audio = len(query["audio"])
s = s[:i]
}
@@ -139,46 +148,46 @@ func parseArgs(s string) *Args {
if i := strings.Index(s, "://"); i > 0 {
switch s[:i] {
case "http", "https", "rtmp":
args.input = inputTemplate("http", s, query)
args.Input = inputTemplate("http", s, query)
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 "
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 += inputTemplate("rtsp", s, query)
args.Input += inputTemplate("rtsp", s, query)
default:
args.input = "-i " + s
args.Input = "-i " + s
}
} else if streams.Get(s) != nil {
s = "rtsp://localhost:" + rtsp.Port + "/" + s
s = "rtsp://127.0.0.1:" + rtsp.Port + "/" + s
switch {
case args.video > 0 && args.audio == 0:
case args.Video > 0 && args.Audio == 0:
s += "?video"
case args.audio > 0 && args.video == 0:
case args.Audio > 0 && args.Video == 0:
s += "?audio"
default:
s += "?video&audio"
}
args.input = inputTemplate("rtsp", s, query)
args.Input = inputTemplate("rtsp", s, query)
} else if strings.HasPrefix(s, "device?") {
var err error
args.input, err = device.GetInput(s)
args.Input, err = device.GetInput(s)
if err != nil {
return nil
}
} else {
args.input = inputTemplate("file", s, query)
args.Input = inputTemplate("file", s, query)
}
if query["async"] != nil {
args.input = "-use_wallclock_as_timestamps 1 -async 1 " + args.input
args.Input = "-use_wallclock_as_timestamps 1 -async 1 " + args.Input
}
// Parse query params:
@@ -190,6 +199,8 @@ func parseArgs(s string) *Args {
if query != nil {
// 1. Process raw params for FFmpeg
for _, raw := range query["raw"] {
// support templates https://github.com/AlexxIT/go2rtc/issues/487
raw = configTemplate(raw)
args.AddCodec(raw)
}
@@ -225,8 +236,20 @@ func parseArgs(s string) *Args {
}
}
for _, drawtext := range query["drawtext"] {
// support templates https://github.com/AlexxIT/go2rtc/issues/487
drawtext = configTemplate(drawtext)
// support default timestamp format
if !strings.Contains(drawtext, "text=") {
drawtext += `:text='%{localtime\:%Y-%m-%d %X}'`
}
args.AddFilter("drawtext=" + drawtext)
}
// 3. Process video codecs
if args.video > 0 {
if args.Video > 0 {
for _, video := range query["video"] {
if video != "copy" {
if codec := defaults[video]; codec != "" {
@@ -238,12 +261,10 @@ func parseArgs(s string) *Args {
args.AddCodec("-c:v copy")
}
}
} else {
args.AddCodec("-vn")
}
// 4. Process audio codecs
if args.audio > 0 {
if args.Audio > 0 {
for _, audio := range query["audio"] {
if audio != "copy" {
if codec := defaults[audio]; codec != "" {
@@ -255,104 +276,28 @@ func parseArgs(s string) *Args {
args.AddCodec("-c:a copy")
}
}
} else {
args.AddCodec("-an")
}
if query["hardware"] != nil {
MakeHardware(args, query["hardware"][0])
hardware.MakeHardware(args, query["hardware"][0], defaults)
}
}
if args.codecs == nil {
switch {
case args.Video == 0 && args.Audio == 0:
args.AddCodec("-c copy")
case args.Video == 0:
args.AddCodec("-vn")
case args.Audio == 0:
args.AddCodec("-an")
}
// transcoding to only mjpeg
if (args.Video == 1 && args.Audio == 0 && query.Get("video") == "mjpeg") ||
// no transcoding from mjpeg input
(args.Video == 0 && args.Audio == 0 && strings.Contains(args.Input, " mjpeg ")) {
args.Output = defaults["output/mjpeg"]
}
return args
}
func parseQuery(s string) map[string][]string {
query := map[string][]string{}
for _, key := range strings.Split(s, "#") {
var value string
i := strings.IndexByte(key, '=')
if i > 0 {
key, value = key[:i], key[i+1:]
}
query[key] = append(query[key], value)
}
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()
}

View File

@@ -0,0 +1,215 @@
package ffmpeg
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestParseArgsFile(t *testing.T) {
// [FILE] all tracks will be copied without transcoding codecs
args := parseArgs("/media/bbb.mp4")
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be transcoded to H264, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=h264")
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be copied, audio will be transcoded to pcmu
args = parseArgs("/media/bbb.mp4#video=copy#audio=pcmu")
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v copy -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=h265#rotate=-90")
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=mjpeg")
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -`, args.String())
// https://github.com/AlexxIT/go2rtc/issues/509
args = parseArgs("ffmpeg:test.mp4#raw=-ss 00:00:20")
require.Equal(t, `ffmpeg -hide_banner -re -i ffmpeg:test.mp4 -ss 00:00:20 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsDevice(t *testing.T) {
// [DEVICE] video will be output for MJPEG to pipe, with size 1920x1080
args := parseArgs("device?video=0&video_size=1920x1080")
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped
//args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma")
args = parseArgs("device?video=0&framerate=20#video=h265#audio=pcma")
require.Equal(t, `ffmpeg -hide_banner -f dshow -framerate 20 -i video="0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -c:a pcm_alaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsIpCam(t *testing.T) {
// [HTTP] video will be copied
args := parseArgs("http://example.com")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [HTTP-MJPEG] video will be transcoded to H264
args = parseArgs("http://example.com#video=h264")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [HLS] video will be copied, audio will be skipped
args = parseArgs("https://example.com#video=copy")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i https://example.com -c:v copy -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video will be copied without transcoding codecs
args = parseArgs("rtsp://example.com")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP
args = parseArgs("rtsp://example.com#input=rtsp/udp")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTMP] video will be copied, changing RTSP transport from TCP to UDP+TCP
args = parseArgs("rtmp://example.com#input=rtsp/udp")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsAudio(t *testing.T) {
// [AUDIO] audio will be transcoded to AAC, video will be skipped
args := parseArgs("rtsp:///example.com#audio=aac")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a aac -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to AAC/16000, video will be skipped
args = parseArgs("rtsp:///example.com#audio=aac/16000")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a aac -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to OPUS, video will be skipped
args = parseArgs("rtsp:///example.com#audio=opus")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -application:a lowdelay -frame_duration 20 -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMU, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcmu")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMU/16000, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcmu/16000")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMU/48000, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcmu/48000")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 48000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMA, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcma")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 8000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMA/16000, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcma/16000")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMA/48000, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcma/48000")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 48000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsHwVaapi(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,transpose_vaapi=4,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=1280:720:out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -f mjpeg -`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsHwV4l2m2m(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=v4l2m2m")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=v4l2m2m")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_v4l2m2m -g 50 -bf 0 -an -vf "transpose=1,transpose=1" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=v4l2m2m")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_v4l2m2m -g 50 -bf 0 -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=v4l2m2m")
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsHwCuda(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=cuda")
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=cuda")
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format nv12 -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll -an -vf "transpose=1,transpose=1,hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=cuda")
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto -an -vf "scale_cuda=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=cuda")
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsHwDxva2(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv,transpose=1,transpose=1" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv,scale_qsv=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -re -i /media/bbb.mp4 -c:v mjpeg_qsv -profile:v high -level:v 5.1 -an -vf "hwmap=derive_device=qsv,format=qsv" -f mjpeg -`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsHwVideotoolbox(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=videotoolbox")
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=videotoolbox")
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf "transpose=1,transpose=1" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=videotoolbox")
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1 -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=videotoolbox")
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestDeckLink(t *testing.T) {
args := parseArgs(`DeckLink SDI (2)#video=h264#hardware=vaapi#input=-format_code Hp29 -f decklink -i "{input}"`)
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -format_code Hp29 -f decklink -i "DeckLink SDI (2)" -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}

View File

@@ -0,0 +1,176 @@
package hardware
import (
"net/http"
"os/exec"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"github.com/rs/zerolog/log"
)
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
)
func Init(bin string) {
api.HandleFunc("api/ffmpeg/hardware", func(w http.ResponseWriter, r *http.Request) {
api.ResponseSources(w, ProbeAll(bin))
})
}
// MakeHardware converts software FFmpeg args to hardware args
// empty engine for autoselect
func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string) {
for i, codec := range args.Codecs {
if len(codec) < 10 {
continue // skip short line (-c:v mjpeg...)
}
// 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
if engine == "" && name != "h265" {
if engine = cache[name]; engine == "" {
engine = ProbeHardware(args.Bin, name)
cache[name] = engine
}
}
switch engine {
case EngineVAAPI:
args.Codecs[i] = defaults[name+"/"+engine]
fixYCbCrRange(args)
if !args.HasFilters("drawtext=") {
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch " + args.Input
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_vaapi=" + filter[6:]
}
if strings.HasPrefix(filter, "transpose=") {
if filter == "transpose=1,transpose=1" { // 180 degrees half-turn
args.Filters[i] = "transpose_vaapi=4" // reversal
} else {
args.Filters[i] = "transpose_vaapi=" + filter[10:]
}
}
}
// fix if input doesn't support hwaccel, do nothing when support
// insert as first filter before hardware scale and transpose
args.InsertFilter("format=vaapi|nv12,hwupload")
} else {
// enable software pixel for drawtext, scale and transpose
args.Input = "-hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch " + args.Input
args.AddFilter("hwupload")
}
case EngineCUDA:
args.Codecs[i] = defaults[name+"/"+engine]
// CUDA doesn't support hardware transpose
// https://github.com/AlexxIT/go2rtc/issues/389
if !args.HasFilters("drawtext=", "transpose=") {
args.Input = "-hwaccel cuda -hwaccel_output_format cuda " + args.Input
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_cuda=" + filter[6:]
}
}
} else {
args.Input = "-hwaccel cuda -hwaccel_output_format nv12 " + args.Input
args.AddFilter("hwupload")
}
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]
}
}
}
var cache = map[string]string{}
func run(bin string, args string) bool {
err := exec.Command(bin, strings.Split(args, " ")...).Run()
log.Printf("%v %v", args, err)
return err == nil
}
func runToString(bin string, args string) string {
if run(bin, args) {
return "OK"
} else {
return "ERROR"
}
}
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
}
// fixYCbCrRange convert jpeg/pc range to mpeg/tv range
// vaapi(pc, bt709, progressive) == yuvj420p (jpeg/full/pc)
// vaapi(tv, bt709, progressive) == yuv420p (mpeg/limited/tv)
// https://ffmpeg.org/ffmpeg-all.html#scale-1
func fixYCbCrRange(args *ffmpeg.Args) {
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
if !strings.Contains(filter, "out_range=") {
args.Filters[i] = filter + ":out_range=tv"
}
return
}
}
// scale=out_color_matrix=bt709:out_range=tv
args.Filters = append(args.Filters, "scale=out_range=tv")
}

View File

@@ -0,0 +1,37 @@
package hardware
import (
"github.com/AlexxIT/go2rtc/internal/api"
)
const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2=size=svga -t 1 -c h264_videotoolbox -f null -"
const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2=size=svga -t 1 -c hevc_videotoolbox -f null -"
func ProbeAll(bin string) []*api.Source {
return []*api.Source{
{
Name: runToString(bin, ProbeVideoToolboxH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineVideoToolbox,
},
{
Name: runToString(bin, ProbeVideoToolboxH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineVideoToolbox,
},
}
}
func ProbeHardware(bin, name string) string {
switch name {
case "h264":
if run(bin, ProbeVideoToolboxH264) {
return EngineVideoToolbox
}
case "h265":
if run(bin, ProbeVideoToolboxH265) {
return EngineVideoToolbox
}
}
return EngineSoftware
}

View File

@@ -0,0 +1,95 @@
package hardware
import (
"runtime"
"github.com/AlexxIT/go2rtc/internal/api"
)
const ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
const ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
const ProbeVAAPIH264 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -f null -"
const ProbeVAAPIH265 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c hevc_vaapi -f null -"
const ProbeVAAPIJPEG = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c mjpeg_vaapi -f null -"
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
func ProbeAll(bin string) []*api.Source {
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
return []*api.Source{
{
Name: runToString(bin, ProbeV4L2M2MH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M,
},
{
Name: runToString(bin, ProbeV4L2M2MH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M,
},
}
}
return []*api.Source{
{
Name: runToString(bin, ProbeVAAPIH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineVAAPI,
},
{
Name: runToString(bin, ProbeVAAPIH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineVAAPI,
},
{
Name: runToString(bin, ProbeVAAPIJPEG),
URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineVAAPI,
},
{
Name: runToString(bin, ProbeCUDAH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineCUDA,
},
{
Name: runToString(bin, ProbeCUDAH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineCUDA,
},
}
}
func ProbeHardware(bin, name string) string {
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
switch name {
case "h264":
if run(bin, ProbeV4L2M2MH264) {
return EngineV4L2M2M
}
case "h265":
if run(bin, ProbeV4L2M2MH265) {
return EngineV4L2M2M
}
}
return EngineSoftware
}
switch name {
case "h264":
if run(bin, ProbeCUDAH264) {
return EngineCUDA
}
if run(bin, ProbeVAAPIH264) {
return EngineVAAPI
}
case "h265":
if run(bin, ProbeCUDAH265) {
return EngineCUDA
}
if run(bin, ProbeVAAPIH265) {
return EngineVAAPI
}
case "mjpeg":
if run(bin, ProbeVAAPIJPEG) {
return EngineVAAPI
}
}
return EngineSoftware
}

View File

@@ -0,0 +1,61 @@
package hardware
import "github.com/AlexxIT/go2rtc/internal/api"
const ProbeDXVA2H264 = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c h264_qsv -f null -"
const ProbeDXVA2H265 = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c hevc_qsv -f null -"
const ProbeDXVA2JPEG = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c mjpeg_qsv -f null -"
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
func ProbeAll(bin string) []*api.Source {
return []*api.Source{
{
Name: runToString(bin, ProbeDXVA2H264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineDXVA2,
},
{
Name: runToString(bin, ProbeDXVA2H265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineDXVA2,
},
{
Name: runToString(bin, ProbeDXVA2JPEG),
URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineDXVA2,
},
{
Name: runToString(bin, ProbeCUDAH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineCUDA,
},
{
Name: runToString(bin, ProbeCUDAH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineCUDA,
},
}
}
func ProbeHardware(bin, name string) string {
switch name {
case "h264":
if run(bin, ProbeCUDAH264) {
return EngineCUDA
}
if run(bin, ProbeDXVA2H264) {
return EngineDXVA2
}
case "h265":
if run(bin, ProbeCUDAH265) {
return EngineCUDA
}
if run(bin, ProbeDXVA2H265) {
return EngineDXVA2
}
case "mjpeg":
if run(bin, ProbeDXVA2JPEG) {
return EngineDXVA2
}
}
return EngineSoftware
}

83
internal/ffmpeg/jpeg.go Normal file
View File

@@ -0,0 +1,83 @@
package ffmpeg
import (
"bytes"
"fmt"
"net/url"
"os/exec"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"github.com/AlexxIT/go2rtc/pkg/shell"
)
func JPEGWithQuery(b []byte, query url.Values) ([]byte, error) {
args := parseQuery(query)
return transcode(b, args.String())
}
func JPEGWithScale(b []byte, width, height int) ([]byte, error) {
args := defaultArgs()
args.AddFilter(fmt.Sprintf("scale=%d:%d", width, height))
return transcode(b, args.String())
}
func transcode(b []byte, args string) ([]byte, error) {
cmdArgs := shell.QuoteSplit(args)
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
cmd.Stdin = bytes.NewBuffer(b)
return cmd.Output()
}
func defaultArgs() *ffmpeg.Args {
return &ffmpeg.Args{
Bin: defaults["bin"],
Global: defaults["global"],
Input: "-i -",
Codecs: []string{defaults["mjpeg"]},
Output: defaults["output/mjpeg"],
}
}
func parseQuery(query url.Values) *ffmpeg.Args {
args := defaultArgs()
var width = -1
var height = -1
var r, hw string
for k, v := range query {
switch k {
case "width", "w":
width = core.Atoi(v[0])
case "height", "h":
height = core.Atoi(v[0])
case "rotate":
r = v[0]
case "hardware", "hw":
hw = v[0]
}
}
if width > 0 || height > 0 {
args.AddFilter(fmt.Sprintf("scale=%d:%d", width, height))
}
if r != "" {
switch r {
case "90":
args.AddFilter("transpose=1") // 90 degrees clockwise
case "180":
args.AddFilter("transpose=1,transpose=1")
case "-90", "270":
args.AddFilter("transpose=2") // 90 degrees counterclockwise
}
}
if hw != "" {
hardware.MakeHardware(args, hw, defaults)
}
return args
}

View File

@@ -0,0 +1,23 @@
package ffmpeg
import (
"net/url"
"testing"
"github.com/stretchr/testify/require"
)
func TestParseQuery(t *testing.T) {
args := parseQuery(nil)
require.Equal(t, `ffmpeg -hide_banner -i - -c:v mjpeg -f mjpeg -`, args.String())
query, err := url.ParseQuery("h=480")
require.Nil(t, err)
args = parseQuery(query)
require.Equal(t, `ffmpeg -hide_banner -i - -c:v mjpeg -vf "scale=-1:480" -f mjpeg -`, args.String())
query, err = url.ParseQuery("hw=vaapi")
require.Nil(t, err)
args = parseQuery(query)
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -i - -c:v mjpeg_vaapi -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, args.String())
}

104
internal/hass/api.go Normal file
View File

@@ -0,0 +1,104 @@
package hass
import (
"encoding/base64"
"encoding/json"
"net"
"net/http"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/webrtc"
)
func apiOK(w http.ResponseWriter, r *http.Request) {
api.Response(w, `{"status":1,"payload":{}}`, api.MimeJSON)
}
func apiStream(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 {
http.Error(w, err.Error(), http.StatusBadRequest)
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
if streams.Patch(v.Name, v.Channels.First.Url) != nil {
apiOK(w, r)
} else {
http.Error(w, "", http.StatusBadRequest)
}
// /stream/{id}/channel/0/webrtc
default:
i := strings.IndexByte(r.RequestURI[8:], '/')
if i <= 0 {
http.Error(w, "", http.StatusBadRequest)
return
}
name := r.RequestURI[8 : 8+i]
stream := streams.Get(name)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s := r.FormValue("data")
offer, err := base64.StdEncoding.DecodeString(s)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
s = base64.StdEncoding.EncodeToString([]byte(s))
_, _ = w.Write([]byte(s))
}
}
func HassioAddr() string {
ints, _ := net.Interfaces()
for _, i := range ints {
if i.Name != "hassio" {
continue
}
addrs, _ := i.Addrs()
for _, addr := range addrs {
if addr, ok := addr.(*net.IPNet); ok {
return addr.IP.String()
}
}
}
return ""
}
type addJSON struct {
Name string `json:"name"`
Channels struct {
First struct {
//Name string `json:"name"`
Url string `json:"url"`
} `json:"0"`
} `json:"channels"`
}

218
internal/hass/hass.go Normal file
View File

@@ -0,0 +1,218 @@
package hass
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"path"
"sync"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/roborock"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hass"
"github.com/rs/zerolog"
)
func Init() {
var conf struct {
API struct {
Listen string `json:"listen"`
} `yaml:"api"`
Mod struct {
Config string `yaml:"config"`
} `yaml:"hass"`
}
app.LoadConfig(&conf)
log = app.GetLogger("hass")
// support API for https://www.home-assistant.io/integrations/rtsp_to_webrtc/
api.HandleFunc("/static", apiOK)
api.HandleFunc("/streams", apiOK)
api.HandleFunc("/stream/", apiStream)
streams.RedirectFunc("hass", func(url string) (string, error) {
if location := entities[url[5:]]; location != "" {
return location, nil
}
return "", nil
})
streams.HandleFunc("hass", func(url string) (core.Producer, error) {
// support hass://supervisor?entity_id=camera.driveway_doorbell
client, err := hass.NewClient(url)
if err != nil {
return nil, err
}
return client, nil
})
// load static entries from Hass config
if err := importConfig(conf.Mod.Config); err != nil {
log.Debug().Msgf("[hass] can't import config: %s", err)
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "no hass config", http.StatusNotFound)
})
return
}
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
once.Do(func() {
// load WebRTC entities from Hass API, works only for add-on version
if token := hass.SupervisorToken(); token != "" {
if err := importWebRTC(token); err != nil {
log.Warn().Err(err).Caller().Send()
}
}
})
var items []*api.Source
for name, url := range entities {
items = append(items, &api.Source{
Name: name, URL: "hass:" + name, Location: url,
})
}
api.ResponseSources(w, items)
})
// for Addon listen on hassio interface, so WebUI feature will work
if conf.API.Listen == "127.0.0.1:1984" {
if addr := HassioAddr(); addr != "" {
addr += ":1984"
go func() {
log.Info().Str("addr", addr).Msg("[hass] listen")
if err := http.ListenAndServe(addr, api.Handler); err != nil {
log.Error().Err(err).Caller().Send()
}
}()
}
}
}
func importConfig(config string) error {
// support load cameras from Hass config file
filename := path.Join(config, ".storage/core.config_entries")
b, err := os.ReadFile(filename)
if err != nil {
return err
}
var storage struct {
Data struct {
Entries []struct {
Title string `json:"title"`
Domain string `json:"domain"`
Data json.RawMessage `json:"data"`
Options json.RawMessage `json:"options"`
} `json:"entries"`
} `json:"data"`
}
if err = json.Unmarshal(b, &storage); err != nil {
return err
}
for _, entrie := range storage.Data.Entries {
switch entrie.Domain {
case "generic":
var options struct {
StreamSource string `json:"stream_source"`
}
if err = json.Unmarshal(entrie.Options, &options); err != nil {
continue
}
entities[entrie.Title] = options.StreamSource
case "homekit_controller":
if !bytes.Contains(entrie.Data, []byte("iOSPairingId")) {
continue
}
var data struct {
ClientID string `json:"iOSPairingId"`
ClientPrivate string `json:"iOSDeviceLTSK"`
ClientPublic string `json:"iOSDeviceLTPK"`
DeviceID string `json:"AccessoryPairingID"`
DevicePublic string `json:"AccessoryLTPK"`
DeviceHost string `json:"AccessoryIP"`
DevicePort uint16 `json:"AccessoryPort"`
}
if err = json.Unmarshal(entrie.Data, &data); err != nil {
continue
}
entities[entrie.Title] = fmt.Sprintf(
"homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s",
data.DeviceHost, data.DevicePort,
data.ClientID, data.ClientPrivate, data.ClientPublic,
data.DeviceID, data.DevicePublic,
)
case "roborock":
_ = json.Unmarshal(entrie.Data, &roborock.Auth)
case "onvif":
var data struct {
Host string `json:"host" json:"host"`
Port uint16 `json:"port" json:"port"`
Username string `json:"username" json:"username"`
Password string `json:"password" json:"password"`
}
if err = json.Unmarshal(entrie.Data, &data); err != nil {
continue
}
if data.Username != "" && data.Password != "" {
entities[entrie.Title] = fmt.Sprintf(
"onvif://%s:%s@%s:%d", data.Username, data.Password, data.Host, data.Port,
)
} else {
entities[entrie.Title] = fmt.Sprintf("onvif://%s:%d", data.Host, data.Port)
}
default:
continue
}
log.Debug().Str("url", "hass:"+entrie.Title).Msg("[hass] load config")
//streams.Get("hass:" + entrie.Title)
}
return nil
}
func importWebRTC(token string) error {
hassAPI, err := hass.NewAPI("ws://supervisor/core/websocket", token)
if err != nil {
return err
}
webrtcEntities, err := hassAPI.GetWebRTCEntities()
if err != nil {
return err
}
if len(webrtcEntities) == 0 {
log.Debug().Msg("[hass] webrtc cameras not found")
}
for name, entityID := range webrtcEntities {
entities[name] = "hass://supervisor?entity_id=" + entityID
log.Debug().Msgf("[hass] load webrtc name=%s entity_id=%s", name, entityID)
}
return nil
}
var entities = map[string]string{}
var log zerolog.Logger
var once sync.Once

3
internal/hls/README.md Normal file
View File

@@ -0,0 +1,3 @@
## Useful links
- https://walterebert.com/playground/video/hls/

View File

@@ -1,21 +1,24 @@
package hls
import (
"fmt"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"net/http"
"sync"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog/log"
"net/http"
"strconv"
"sync"
"time"
"github.com/rs/zerolog"
)
func Init() {
log = app.GetLogger("hls")
api.HandleFunc("api/stream.m3u8", handlerStream)
api.HandleFunc("api/hls/playlist.m3u8", handlerPlaylist)
@@ -25,28 +28,17 @@ func Init() {
// HLS (fMP4)
api.HandleFunc("api/hls/init.mp4", handlerInit)
api.HandleFunc("api/hls/segment.m4s", handlerSegmentMP4)
ws.HandleFunc("hls", handlerWSHLS)
}
type Consumer interface {
core.Consumer
Init() ([]byte, error)
MimeCodecs() string
Start()
}
type Session struct {
cons Consumer
playlist string
init []byte
segment []byte
seq int
alive *time.Timer
mu sync.Mutex
}
var log zerolog.Logger
const keepalive = 5 * time.Second
// once I saw 404 on MP4 segment, so better to use mutex
var sessions = map[string]*Session{}
var sessionsMu sync.RWMutex
func handlerStream(w http.ResponseWriter, r *http.Request) {
// CORS important for Chromecast
@@ -59,83 +51,51 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
}
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
var cons Consumer
var cons core.Consumer
// use fMP4 with codecs filter and TS without
medias := mp4.ParseQuery(r.URL.Query())
if medias != nil {
cons = &mp4.Consumer{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
Medias: medias,
}
c := mp4.NewConsumer(medias)
c.Type = "HLS/fMP4 consumer"
c.RemoteAddr = tcp.RemoteAddr(r)
c.UserAgent = r.UserAgent()
cons = c
} else {
cons = &mpegts.Consumer{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
}
c := mpegts.NewConsumer()
c.Type = "HLS/TS consumer"
c.RemoteAddr = tcp.RemoteAddr(r)
c.UserAgent = r.UserAgent()
cons = c
}
session := &Session{cons: cons}
cons.(any).(*core.Listener).Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
session.mu.Lock()
session.segment = append(session.segment, data...)
session.mu.Unlock()
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
session := NewSession(cons)
session.alive = time.AfterFunc(keepalive, func() {
sessionsMu.Lock()
delete(sessions, session.id)
sessionsMu.Unlock()
stream.RemoveConsumer(cons)
})
session.init, _ = cons.Init()
cons.Start()
sessionsMu.Lock()
sessions[session.id] = session
sessionsMu.Unlock()
sid := strconv.FormatInt(time.Now().UnixNano(), 10)
go session.Run()
// two segments important for Chromecast
if medias != nil {
session.playlist = `#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
#EXT-X-MAP:URI="init.mp4?id=` + sid + `"
#EXTINF:0.500,
segment.m4s?id=` + sid + `&n=%d
#EXTINF:0.500,
segment.m4s?id=` + sid + `&n=%d`
} else {
session.playlist = `#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
#EXTINF:0.500,
segment.ts?id=` + sid + `&n=%d
#EXTINF:0.500,
segment.ts?id=` + sid + `&n=%d`
}
sessions[sid] = session
// bandwidth important for Safari, codecs useful for smooth playback
data := []byte(`#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + cons.MimeCodecs() + `"
hls/playlist.m3u8?id=` + sid)
if _, err := w.Write(data); err != nil {
if _, err := w.Write(session.Main()); err != nil {
log.Error().Err(err).Caller().Send()
}
}
@@ -150,15 +110,15 @@ func handlerPlaylist(w http.ResponseWriter, r *http.Request) {
}
sid := r.URL.Query().Get("id")
sessionsMu.RLock()
session := sessions[sid]
sessionsMu.RUnlock()
if session == nil {
http.NotFound(w, r)
return
}
s := fmt.Sprintf(session.playlist, session.seq, session.seq, session.seq+1)
if _, err := w.Write([]byte(s)); err != nil {
if _, err := w.Write(session.Playlist()); err != nil {
log.Error().Err(err).Caller().Send()
}
}
@@ -173,7 +133,9 @@ func handlerSegmentTS(w http.ResponseWriter, r *http.Request) {
}
sid := r.URL.Query().Get("id")
sessionsMu.RLock()
session := sessions[sid]
sessionsMu.RUnlock()
if session == nil {
http.NotFound(w, r)
return
@@ -181,22 +143,13 @@ func handlerSegmentTS(w http.ResponseWriter, r *http.Request) {
session.alive.Reset(keepalive)
var i byte
for len(session.segment) == 0 {
if i++; i > 10 {
http.NotFound(w, r)
return
}
time.Sleep(time.Millisecond * 100)
data := session.Segment()
if data == nil {
log.Warn().Msgf("[hls] can't get segment %s", r.URL.RawQuery)
http.NotFound(w, r)
return
}
session.mu.Lock()
data := session.segment
// important to start new segment with init
session.segment = session.init
session.seq++
session.mu.Unlock()
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}
@@ -212,13 +165,22 @@ func handlerInit(w http.ResponseWriter, r *http.Request) {
}
sid := r.URL.Query().Get("id")
sessionsMu.RLock()
session := sessions[sid]
sessionsMu.RUnlock()
if session == nil {
http.NotFound(w, r)
return
}
if _, err := w.Write(session.init); err != nil {
data := session.Init()
if data == nil {
log.Warn().Msgf("[hls] can't get init %s", r.URL.RawQuery)
http.NotFound(w, r)
return
}
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}
}
@@ -228,12 +190,16 @@ func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "video/iso.segment")
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Methods", "GET")
return
}
sid := r.URL.Query().Get("id")
query := r.URL.Query()
sid := query.Get("id")
sessionsMu.RLock()
session := sessions[sid]
sessionsMu.RUnlock()
if session == nil {
http.NotFound(w, r)
return
@@ -241,21 +207,13 @@ func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) {
session.alive.Reset(keepalive)
var i byte
for len(session.segment) == 0 {
if i++; i > 10 {
http.NotFound(w, r)
return
}
time.Sleep(time.Millisecond * 100)
data := session.Segment()
if data == nil {
log.Warn().Msgf("[hls] can't get segment %s", r.URL.RawQuery)
http.NotFound(w, r)
return
}
session.mu.Lock()
data := session.segment
session.segment = nil
session.seq++
session.mu.Unlock()
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}

127
internal/hls/session.go Normal file
View File

@@ -0,0 +1,127 @@
package hls
import (
"fmt"
"io"
"strings"
"sync"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
)
type Session struct {
cons core.Consumer
id string
template string
init []byte
buffer []byte
seq int
alive *time.Timer
mu sync.Mutex
}
func NewSession(cons core.Consumer) *Session {
s := &Session{
id: core.RandString(8, 62),
cons: cons,
}
// two segments important for Chromecast
if _, ok := cons.(*mp4.Consumer); ok {
s.template = `#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
#EXT-X-MAP:URI="init.mp4?id=` + s.id + `"
#EXTINF:0.500,
segment.m4s?id=` + s.id + `&n=%d
#EXTINF:0.500,
segment.m4s?id=` + s.id + `&n=%d`
} else {
s.template = `#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
#EXTINF:0.500,
segment.ts?id=` + s.id + `&n=%d
#EXTINF:0.500,
segment.ts?id=` + s.id + `&n=%d`
}
return s
}
func (s *Session) Write(p []byte) (n int, err error) {
s.mu.Lock()
if s.init == nil {
s.init = p
} else {
s.buffer = append(s.buffer, p...)
}
s.mu.Unlock()
return len(p), nil
}
func (s *Session) Run() {
_, _ = s.cons.(io.WriterTo).WriteTo(s)
}
func (s *Session) Main() []byte {
type withCodecs interface {
Codecs() []*core.Codec
}
codecs := mp4.MimeCodecs(s.cons.(withCodecs).Codecs())
codecs = strings.Replace(codecs, mp4.MimeFlac, "fLaC", 1)
// bandwidth important for Safari, codecs useful for smooth playback
return []byte(`#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=192000,CODECS="` + codecs + `"
hls/playlist.m3u8?id=` + s.id)
}
func (s *Session) Playlist() []byte {
return []byte(fmt.Sprintf(s.template, s.seq, s.seq, s.seq+1))
}
func (s *Session) Init() (init []byte) {
for i := 0; i < 60 && init == nil; i++ {
if i > 0 {
time.Sleep(50 * time.Millisecond)
}
s.mu.Lock()
// return init only when have some buffer
if len(s.buffer) > 0 {
init = s.init
}
s.mu.Unlock()
}
return
}
func (s *Session) Segment() (segment []byte) {
for i := 0; i < 60 && segment == nil; i++ {
if i > 0 {
time.Sleep(50 * time.Millisecond)
}
s.mu.Lock()
if len(s.buffer) > 0 {
segment = s.buffer
if _, ok := s.cons.(*mp4.Consumer); ok {
s.buffer = nil
} else {
// for TS important to start new segment with init
s.buffer = s.init
}
s.seq++
}
s.mu.Unlock()
}
return
}

54
internal/hls/ws.go Normal file
View File

@@ -0,0 +1,54 @@
package hls
import (
"errors"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
)
func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
stream := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
codecs := msg.String()
medias := mp4.ParseCodecs(codecs, true)
cons := mp4.NewConsumer(medias)
cons.Type = "HLS/fMP4 consumer"
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
cons.UserAgent = tr.Request.UserAgent()
log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return err
}
session := NewSession(cons)
session.alive = time.AfterFunc(keepalive, func() {
sessionsMu.Lock()
delete(sessions, session.id)
sessionsMu.Unlock()
stream.RemoveConsumer(cons)
})
sessionsMu.Lock()
sessions[session.id] = session
sessionsMu.Unlock()
go session.Run()
main := session.Main()
tr.Write(&ws.Message{Type: "hls", Value: string(main)})
return nil
}

139
internal/homekit/api.go Normal file
View File

@@ -0,0 +1,139 @@
package homekit
import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/mdns"
)
func apiHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
sources, err := discovery()
if err != nil {
api.Error(w, err)
return
}
urls := findHomeKitURLs()
for id, u := range urls {
deviceID := u.Query().Get("device_id")
for _, source := range sources {
if strings.Contains(source.URL, deviceID) {
source.Location = id
break
}
}
}
for _, source := range sources {
if source.Location == "" {
source.Location = " "
}
}
api.ResponseSources(w, sources)
case "POST":
if err := r.ParseMultipartForm(1024); err != nil {
api.Error(w, err)
return
}
if err := apiPair(r.Form.Get("id"), r.Form.Get("url")); err != nil {
api.Error(w, err)
}
case "DELETE":
if err := r.ParseMultipartForm(1024); err != nil {
api.Error(w, err)
return
}
if err := apiUnpair(r.Form.Get("id")); err != nil {
api.Error(w, err)
}
}
}
func discovery() ([]*api.Source, error) {
var sources []*api.Source
// 1. Get streams from Discovery
err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
log.Trace().Msgf("[homekit] mdns=%s", entry)
category := entry.Info[hap.TXTCategory]
if entry.Complete() && (category == hap.CategoryCamera || category == hap.CategoryDoorbell) {
source := &api.Source{
Name: entry.Name,
Info: entry.Info[hap.TXTModel],
URL: fmt.Sprintf(
"homekit://%s:%d?device_id=%s&feature=%s&status=%s",
entry.IP, entry.Port, entry.Info[hap.TXTDeviceID],
entry.Info[hap.TXTFeatureFlags], entry.Info[hap.TXTStatusFlags],
),
}
sources = append(sources, source)
}
return false
})
if err != nil {
return nil, err
}
return sources, nil
}
func apiPair(id, url string) error {
conn, err := hap.Pair(url)
if err != nil {
return err
}
streams.New(id, conn.URL())
return app.PatchConfig(id, conn.URL(), "streams")
}
func apiUnpair(id string) error {
stream := streams.Get(id)
if stream == nil {
return errors.New(api.StreamNotFound)
}
rawURL := findHomeKitURL(stream)
if rawURL == "" {
return errors.New("not homekit source")
}
if err := hap.Unpair(rawURL); err != nil {
return err
}
streams.Delete(id)
return app.PatchConfig(id, nil, "streams")
}
func findHomeKitURLs() map[string]*url.URL {
urls := map[string]*url.URL{}
for id, stream := range streams.Streams() {
if rawURL := findHomeKitURL(stream); rawURL != "" {
if u, err := url.Parse(rawURL); err == nil {
urls[id] = u
}
}
}
return urls
}

197
internal/homekit/homekit.go Normal file
View File

@@ -0,0 +1,197 @@
package homekit
import (
"io"
"net"
"net/http"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/srtp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/mdns"
"github.com/rs/zerolog"
)
func Init() {
var cfg struct {
Mod map[string]struct {
Pin string `json:"pin"`
Name string `json:"name"`
DeviceID string `json:"device_id"`
DevicePrivate string `json:"device_private"`
Pairings []string `json:"pairings"`
//Listen string `json:"listen"`
} `yaml:"homekit"`
}
app.LoadConfig(&cfg)
log = app.GetLogger("homekit")
streams.HandleFunc("homekit", streamHandler)
api.HandleFunc("api/homekit", apiHandler)
if cfg.Mod == nil {
return
}
servers = map[string]*server{}
var entries []*mdns.ServiceEntry
for id, conf := range cfg.Mod {
stream := streams.Get(id)
if stream == nil {
log.Warn().Msgf("[homekit] missing stream: %s", id)
continue
}
if conf.Pin == "" {
conf.Pin = "19550224" // default PIN
}
pin, err := hap.SanitizePin(conf.Pin)
if err != nil {
log.Error().Err(err).Caller().Send()
continue
}
deviceID := calcDeviceID(conf.DeviceID, id) // random MAC-address
name := calcName(conf.Name, deviceID)
srv := &server{
stream: id,
srtp: srtp.Server,
pairings: conf.Pairings,
}
srv.hap = &hap.Server{
Pin: pin,
DeviceID: deviceID,
DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id),
GetPair: srv.GetPair,
AddPair: srv.AddPair,
Handler: homekit.ServerHandler(srv),
}
if url := findHomeKitURL(stream); url != "" {
// 1. Act as transparent proxy for HomeKit camera
dial := func() (net.Conn, error) {
client, err := homekit.Dial(url, srtp.Server)
if err != nil {
return nil, err
}
return client.Conn(), nil
}
srv.hap.Handler = homekit.ProxyHandler(srv, dial)
} else {
// 2. Act as basic HomeKit camera
srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", app.Version)
srv.hap.Handler = homekit.ServerHandler(srv)
}
srv.mdns = &mdns.ServiceEntry{
Name: name,
Port: uint16(api.Port()),
Info: map[string]string{
hap.TXTConfigNumber: "1",
hap.TXTFeatureFlags: "0",
hap.TXTDeviceID: deviceID,
hap.TXTModel: app.UserAgent,
hap.TXTProtoVersion: "1.1",
hap.TXTStateNumber: "1",
hap.TXTStatusFlags: hap.StatusNotPaired,
hap.TXTCategory: hap.CategoryCamera,
hap.TXTSetupHash: srv.hap.SetupHash(),
},
}
entries = append(entries, srv.mdns)
srv.UpdateStatus()
host := srv.mdns.Host(mdns.ServiceHAP)
servers[host] = srv
}
api.HandleFunc(hap.PathPairSetup, hapPairSetup)
api.HandleFunc(hap.PathPairVerify, hapPairVerify)
log.Trace().Msgf("[homekit] mnds: %s", entries)
go func() {
if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil {
log.Error().Err(err).Caller().Send()
}
}()
}
var log zerolog.Logger
var servers map[string]*server
func streamHandler(url string) (core.Producer, error) {
return homekit.Dial(url, srtp.Server)
}
func hapPairSetup(w http.ResponseWriter, r *http.Request) {
srv, ok := servers[r.Host]
if !ok {
log.Error().Msg("[homekit] unknown host: " + r.Host)
return
}
conn, rw, err := w.(http.Hijacker).Hijack()
if err != nil {
return
}
defer conn.Close()
if err = srv.hap.PairSetup(r, rw, conn); err != nil {
log.Error().Err(err).Caller().Send()
}
}
func hapPairVerify(w http.ResponseWriter, r *http.Request) {
srv, ok := servers[r.Host]
if !ok {
log.Error().Msg("[homekit] unknown host: " + r.Host)
return
}
conn, rw, err := w.(http.Hijacker).Hijack()
if err != nil {
return
}
defer conn.Close()
if err = srv.hap.PairVerify(r, rw, conn); err != nil && err != io.EOF {
log.Error().Err(err).Caller().Send()
}
}
func findHomeKitURL(stream *streams.Stream) string {
sources := stream.Sources()
if len(sources) == 0 {
return ""
}
url := sources[0]
if strings.HasPrefix(url, "homekit") {
return url
}
if strings.HasPrefix(url, "hass") {
location, _ := streams.Location(url)
if strings.HasPrefix(location, "homekit") {
return url
}
}
return ""
}

265
internal/homekit/server.go Normal file
View File

@@ -0,0 +1,265 @@
package homekit
import (
"crypto/ed25519"
"crypto/sha512"
"encoding/hex"
"fmt"
"net"
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
srtp2 "github.com/AlexxIT/go2rtc/internal/srtp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/mdns"
"github.com/AlexxIT/go2rtc/pkg/srtp"
)
type server struct {
stream string // stream name from YAML
hap *hap.Server // server for HAP connection and encryption
mdns *mdns.ServiceEntry
srtp *srtp.Server
accessory *hap.Accessory // HAP accessory
pairings []string // pairings list
streams map[string]*homekit.Consumer
consumer *homekit.Consumer
}
func (s *server) UpdateStatus() {
// true status is important, or device may be offline in Apple Home
if len(s.pairings) == 0 {
s.mdns.Info[hap.TXTStatusFlags] = hap.StatusNotPaired
} else {
s.mdns.Info[hap.TXTStatusFlags] = hap.StatusPaired
}
}
func (s *server) GetAccessories(_ net.Conn) []*hap.Accessory {
return []*hap.Accessory{s.accessory}
}
func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any {
log.Trace().Msgf("[homekit] %s: get char aid=%d iid=0x%x", conn.RemoteAddr(), aid, iid)
char := s.accessory.GetCharacterByID(iid)
if char == nil {
log.Warn().Msgf("[homekit] get unknown characteristic: %d", iid)
return nil
}
switch char.Type {
case camera.TypeSetupEndpoints:
if s.consumer == nil {
return nil
}
answer := s.consumer.GetAnswer()
v, err := tlv8.MarshalBase64(answer)
if err != nil {
return nil
}
return v
}
return char.Value
}
func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value any) {
log.Trace().Msgf("[homekit] %s: set char aid=%d iid=0x%x value=%v", conn.RemoteAddr(), aid, iid, value)
char := s.accessory.GetCharacterByID(iid)
if char == nil {
log.Warn().Msgf("[homekit] set unknown characteristic: %d", iid)
return
}
switch char.Type {
case camera.TypeSetupEndpoints:
var offer camera.SetupEndpoints
if err := tlv8.UnmarshalBase64(value.(string), &offer); err != nil {
return
}
s.consumer = homekit.NewConsumer(conn, srtp2.Server)
s.consumer.SetOffer(&offer)
case camera.TypeSelectedStreamConfiguration:
var conf camera.SelectedStreamConfig
if err := tlv8.UnmarshalBase64(value.(string), &conf); err != nil {
return
}
log.Trace().Msgf("[homekit] %s stream id=%x cmd=%d", conn.RemoteAddr(), conf.Control.SessionID, conf.Control.Command)
switch conf.Control.Command {
case camera.SessionCommandEnd:
if consumer := s.streams[conf.Control.SessionID]; consumer != nil {
_ = consumer.Stop()
}
case camera.SessionCommandStart:
if s.consumer == nil {
return
}
if !s.consumer.SetConfig(&conf) {
log.Warn().Msgf("[homekit] wrong config")
return
}
if s.streams == nil {
s.streams = map[string]*homekit.Consumer{}
}
s.streams[conf.Control.SessionID] = s.consumer
stream := streams.Get(s.stream)
if err := stream.AddConsumer(s.consumer); err != nil {
return
}
go func() {
_, _ = s.consumer.WriteTo(nil)
stream.RemoveConsumer(s.consumer)
delete(s.streams, conf.Control.SessionID)
}()
}
}
}
func (s *server) GetImage(conn net.Conn, width, height int) []byte {
log.Trace().Msgf("[homekit] %s: get image width=%d height=%d", conn.RemoteAddr(), width, height)
stream := streams.Get(s.stream)
cons := magic.NewKeyframe()
if err := stream.AddConsumer(cons); err != nil {
return nil
}
once := &core.OnceBuffer{} // init and first frame
_, _ = cons.WriteTo(once)
b := once.Buffer()
stream.RemoveConsumer(cons)
switch cons.CodecName() {
case core.CodecH264, core.CodecH265:
var err error
if b, err = ffmpeg.JPEGWithScale(b, width, height); err != nil {
return nil
}
}
return b
}
func (s *server) GetPair(conn net.Conn, id string) []byte {
log.Trace().Msgf("[homekit] %s: get pair id=%s", conn.RemoteAddr(), id)
for _, pairing := range s.pairings {
if !strings.Contains(pairing, id) {
continue
}
query, err := url.ParseQuery(pairing)
if err != nil {
continue
}
if query.Get("client_id") != id {
continue
}
s := query.Get("client_public")
b, _ := hex.DecodeString(s)
return b
}
return nil
}
func (s *server) AddPair(conn net.Conn, id string, public []byte, permissions byte) {
log.Trace().Msgf("[homekit] %s: add pair id=%s public=%x perm=%d", conn.RemoteAddr(), id, public, permissions)
query := url.Values{
"client_id": []string{id},
"client_public": []string{hex.EncodeToString(public)},
"permissions": []string{string('0' + permissions)},
}
if s.GetPair(conn, id) == nil {
s.pairings = append(s.pairings, query.Encode())
s.UpdateStatus()
s.PatchConfig()
}
}
func (s *server) DelPair(conn net.Conn, id string) {
log.Trace().Msgf("[homekit] %s: del pair id=%s", conn.RemoteAddr(), id)
id = "client_id=" + id
for i, pairing := range s.pairings {
if !strings.Contains(pairing, id) {
continue
}
s.pairings = append(s.pairings[:i], s.pairings[i+1:]...)
s.UpdateStatus()
s.PatchConfig()
break
}
}
func (s *server) PatchConfig() {
if err := app.PatchConfig("pairings", s.pairings, "homekit", s.stream); err != nil {
log.Error().Err(err).Msgf(
"[homekit] can't save %s pairings=%v", s.stream, s.pairings,
)
}
}
func calcName(name, seed string) string {
if name != "" {
return name
}
b := sha512.Sum512([]byte(seed))
return fmt.Sprintf("go2rtc-%02X%02X", b[0], b[2])
}
func calcDeviceID(deviceID, seed string) string {
if deviceID != "" {
if len(deviceID) >= 17 {
// 1. Returd device_id as is (ex. AA:BB:CC:DD:EE:FF)
return deviceID
}
// 2. Use device_id as seed if not zero
seed = deviceID
}
b := sha512.Sum512([]byte(seed))
return fmt.Sprintf("%02X:%02X:%02X:%02X:%02X:%02X", b[32], b[34], b[36], b[38], b[40], b[42])
}
func calcDevicePrivate(private, seed string) []byte {
if private != "" {
// 1. Decode private from HEX string
if b, _ := hex.DecodeString(private); len(b) == ed25519.PrivateKeySize {
// 2. Return if OK
return b
}
// 3. Use private as seed if not zero
seed = private
}
b := sha512.Sum512([]byte(seed))
return ed25519.NewKeyFromSeed(b[:ed25519.SeedSize])
}

91
internal/http/http.go Normal file
View File

@@ -0,0 +1,91 @@
package http
import (
"errors"
"net"
"net/http"
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hls"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/multipart"
"github.com/AlexxIT/go2rtc/pkg/tcp"
)
func Init() {
streams.HandleFunc("http", handleHTTP)
streams.HandleFunc("https", handleHTTP)
streams.HandleFunc("httpx", handleHTTP)
streams.HandleFunc("tcp", handleTCP)
}
func handleHTTP(rawURL string) (core.Producer, error) {
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
// first we get the Content-Type to define supported producer
req, err := http.NewRequest("GET", rawURL, nil)
if err != nil {
return nil, err
}
if rawQuery != "" {
query := streams.ParseQuery(rawQuery)
for _, header := range query["header"] {
key, value, _ := strings.Cut(header, ":")
req.Header.Add(key, strings.TrimSpace(value))
}
}
res, err := tcp.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, errors.New(res.Status)
}
// 1. Guess format from content type
ct := res.Header.Get("Content-Type")
if i := strings.IndexByte(ct, ';'); i > 0 {
ct = ct[:i]
}
var ext string
if i := strings.LastIndexByte(req.URL.Path, '.'); i > 0 {
ext = req.URL.Path[i+1:]
}
switch {
case ct == "image/jpeg":
return mjpeg.NewClient(res), nil
case ct == "multipart/x-mixed-replace":
return multipart.Open(res.Body)
case ct == "application/vnd.apple.mpegurl" || ext == "m3u8":
return hls.OpenURL(req.URL, res.Body)
}
return magic.Open(res.Body)
}
func handleTCP(rawURL string) (core.Producer, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
conn, err := net.DialTimeout("tcp", u.Host, core.ConnDialTimeout)
if err != nil {
return nil, err
}
return magic.Open(conn)
}

View File

@@ -1,7 +1,7 @@
package isapi
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/isapi"
)

View File

@@ -1,7 +1,7 @@
package ivideon
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ivideon"
"strings"

View File

@@ -2,67 +2,75 @@ package mjpeg
import (
"errors"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog/log"
"io"
"net/http"
"strconv"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog/log"
)
func Init() {
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
api.HandleFunc("api/stream.mjpeg", handlerStream)
api.HandleWS("mjpeg", handlerWS)
ws.HandleFunc("mjpeg", handlerWS)
}
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
exit := make(chan []byte)
cons := &mjpeg.Consumer{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg any) {
switch msg := msg.(type) {
case []byte:
exit <- msg
}
})
cons := magic.NewKeyframe()
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
data := <-exit
once := &core.OnceBuffer{} // init and first frame
_, _ = cons.WriteTo(once)
b := once.Buffer()
stream.RemoveConsumer(cons)
switch cons.CodecName() {
case core.CodecH264, core.CodecH265:
ts := time.Now()
var err error
if b, err = ffmpeg.JPEGWithQuery(b, r.URL.Query()); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Debug().Msgf("[mjpeg] transcoding time=%s", time.Since(ts))
}
h := w.Header()
h.Set("Content-Type", "image/jpeg")
h.Set("Content-Length", strconv.Itoa(len(data)))
h.Set("Content-Length", strconv.Itoa(len(b)))
h.Set("Cache-Control", "no-cache")
h.Set("Connection", "close")
h.Set("Pragma", "no-cache")
if _, err := w.Write(data); err != nil {
if _, err := w.Write(b); 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) {
if r.Method != "POST" {
outputMjpeg(w, r)
@@ -73,32 +81,15 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
func outputMjpeg(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
flusher := w.(http.Flusher)
cons := &mjpeg.Consumer{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg any) {
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()
}
})
cons := mjpeg.NewConsumer()
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
@@ -111,11 +102,33 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
h.Set("Connection", "close")
h.Set("Pragma", "no-cache")
<-r.Context().Done()
wr := &writer{wr: w, buf: []byte(header)}
_, _ = cons.WriteTo(wr)
stream.RemoveConsumer(cons)
}
//log.Trace().Msg("[api.mjpeg] close")
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
type writer struct {
wr io.Writer
buf []byte
}
func (w *writer) Write(p []byte) (n int, err error) {
w.buf = w.buf[:len(header)]
w.buf = append(w.buf, strconv.Itoa(len(p))...)
w.buf = append(w.buf, "\r\n\r\n"...)
w.buf = append(w.buf, p...)
w.buf = append(w.buf, "\r\n"...)
// Chrome bug: mjpeg image always shows the second to last image
// https://bugs.chromium.org/p/chromium/issues/detail?id=527446
if n, err = w.wr.Write(w.buf); err == nil {
w.wr.(http.Flusher).Flush()
}
return
}
func inputMjpeg(w http.ResponseWriter, r *http.Request) {
@@ -139,29 +152,24 @@ func inputMjpeg(w http.ResponseWriter, r *http.Request) {
stream.RemoveProducer(client)
}
func handlerWS(tr *api.Transport, _ *api.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
func handlerWS(tr *ws.Transport, _ *ws.Message) error {
stream := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
cons := &mjpeg.Consumer{
RemoteAddr: tcp.RemoteAddr(tr.Request),
UserAgent: tr.Request.UserAgent(),
}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
tr.Write(data)
}
})
cons := mjpeg.NewConsumer()
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
cons.UserAgent = tr.Request.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return err
}
tr.Write(&api.Message{Type: "mjpeg"})
tr.Write(&ws.Message{Type: "mjpeg"})
go cons.WriteTo(tr.Writer())
tr.OnClose(func() {
stream.RemoveConsumer(cons)

147
internal/mp4/mp4.go Normal file
View File

@@ -0,0 +1,147 @@
package mp4
import (
"net/http"
"strconv"
"strings"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
)
func Init() {
log = app.GetLogger("mp4")
ws.HandleFunc("mse", handlerWSMSE)
ws.HandleFunc("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) {
// Chrome 105 does two requests: without Range and with `Range: bytes=0-`
ua := r.UserAgent()
if strings.Contains(ua, " Chrome/") {
if r.Header.Values("Range") == nil {
w.Header().Set("Content-Type", "video/mp4")
w.WriteHeader(http.StatusOK)
return
}
}
query := r.URL.Query()
src := query.Get("src")
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
cons := mp4.NewKeyframe(nil)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
once := &core.OnceBuffer{} // init and first frame
_, _ = cons.WriteTo(once)
stream.RemoveConsumer(cons)
// Apple Safari won't show frame without length
header := w.Header()
header.Set("Content-Length", strconv.Itoa(once.Len()))
header.Set("Content-Type", mp4.ContentType(cons.Codecs()))
if filename := query.Get("filename"); filename != "" {
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
}
if _, err := once.WriteTo(w); 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)
query := r.URL.Query()
ua := r.UserAgent()
if strings.Contains(ua, " Safari/") && !strings.Contains(ua, " Chrome/") && !query.Has("duration") {
// auto redirect to HLS/fMP4 format, because Safari not support MP4 stream
url := "stream.m3u8?" + r.URL.RawQuery
if !query.Has("mp4") {
url += "&mp4"
}
http.Redirect(w, r, url, http.StatusMovedPermanently)
return
}
src := query.Get("src")
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
medias := mp4.ParseQuery(r.URL.Query())
cons := mp4.NewConsumer(medias)
cons.Type = "MP4/HTTP active consumer"
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if rotate := query.Get("rotate"); rotate != "" {
cons.Rotate = core.Atoi(rotate)
}
if scale := query.Get("scale"); scale != "" {
if sx, sy, ok := strings.Cut(scale, ":"); ok {
cons.ScaleX = core.Atoi(sx)
cons.ScaleY = core.Atoi(sy)
}
}
header := w.Header()
header.Set("Content-Type", mp4.ContentType(cons.Codecs()))
if filename := query.Get("filename"); filename != "" {
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
}
var duration *time.Timer
if s := query.Get("duration"); s != "" {
if i, _ := strconv.Atoi(s); i > 0 {
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
_ = cons.Stop()
})
}
}
_, _ = cons.WriteTo(w)
stream.RemoveConsumer(cons)
if duration != nil {
duration.Stop()
}
}

78
internal/mp4/ws.go Normal file
View File

@@ -0,0 +1,78 @@
package mp4
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
)
func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
stream := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
var medias []*core.Media
if codecs := msg.String(); codecs != "" {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
medias = mp4.ParseCodecs(codecs, true)
}
cons := mp4.NewConsumer(medias)
cons.Type = "MSE/WebSocket active consumer"
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
cons.UserAgent = tr.Request.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Debug().Err(err).Msg("[mp4] add consumer")
return err
}
tr.Write(&ws.Message{Type: "mse", Value: mp4.ContentType(cons.Codecs())})
go cons.WriteTo(tr.Writer())
tr.OnClose(func() {
stream.RemoveConsumer(cons)
})
return nil
}
func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error {
stream := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
var medias []*core.Media
if codecs := msg.String(); codecs != "" {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
medias = mp4.ParseCodecs(codecs, false)
}
cons := mp4.NewKeyframe(medias)
cons.Type = "MP4/WebSocket active consumer"
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
cons.UserAgent = tr.Request.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return err
}
tr.Write(&ws.Message{Type: "mse", Value: mp4.ContentType(cons.Codecs())})
go cons.WriteTo(tr.Writer())
tr.OnClose(func() {
stream.RemoveConsumer(cons)
})
return nil
}

36
internal/mpegts/aac.go Normal file
View File

@@ -0,0 +1,36 @@
package mpegts
import (
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog/log"
)
func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
cons := aac.NewConsumer()
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "audio/aac")
_, _ = cons.WriteTo(w)
stream.RemoveConsumer(cons)
}

74
internal/mpegts/mpegts.go Normal file
View File

@@ -0,0 +1,74 @@
package mpegts
import (
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog/log"
)
func Init() {
api.HandleFunc("api/stream.ts", apiHandle)
api.HandleFunc("api/stream.aac", apiStreamAAC)
}
func apiHandle(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
outputMpegTS(w, r)
} else {
inputMpegTS(w, r)
}
}
func outputMpegTS(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
cons := mpegts.NewConsumer()
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "video/mp2t")
_, _ = cons.WriteTo(w)
stream.RemoveConsumer(cons)
}
func inputMpegTS(w http.ResponseWriter, r *http.Request) {
dst := r.URL.Query().Get("dst")
stream := streams.Get(dst)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
res := &http.Response{Body: r.Body, Request: r}
client, err := mpegts.Open(res.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stream.AddProducer(client)
if err = client.Start(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stream.RemoveProducer(client)
}

56
internal/nest/init.go Normal file
View File

@@ -0,0 +1,56 @@
package nest
import (
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/nest"
)
func Init() {
streams.HandleFunc("nest", streamNest)
api.HandleFunc("api/nest", apiNest)
}
func streamNest(url string) (core.Producer, error) {
client, err := nest.NewClient(url)
if err != nil {
return nil, err
}
return client, nil
}
func apiNest(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
cliendID := query.Get("client_id")
cliendSecret := query.Get("client_secret")
refreshToken := query.Get("refresh_token")
projectID := query.Get("project_id")
nestAPI, err := nest.NewAPI(cliendID, cliendSecret, refreshToken)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
devices, err := nestAPI.GetDevices(projectID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var items []*api.Source
for name, deviceID := range devices {
query.Set("device_id", deviceID)
items = append(items, &api.Source{
Name: name, URL: "nest:?" + query.Encode(),
})
}
api.ResponseSources(w, items)
}

View File

@@ -2,8 +2,8 @@ package ngrok
import (
"fmt"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"github.com/AlexxIT/go2rtc/pkg/ngrok"
"github.com/rs/zerolog"
"net"

195
internal/onvif/init.go Normal file
View File

@@ -0,0 +1,195 @@
package onvif
import (
"io"
"net"
"net/http"
"net/url"
"os"
"strconv"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/onvif"
"github.com/rs/zerolog"
)
func Init() {
log = app.GetLogger("onvif")
streams.HandleFunc("onvif", streamOnvif)
// ONVIF server on all suburls
api.HandleFunc("/onvif/", onvifDeviceService)
// ONVIF client autodiscovery
api.HandleFunc("api/onvif", apiOnvif)
}
var log zerolog.Logger
func streamOnvif(rawURL string) (core.Producer, error) {
client, err := onvif.NewClient(rawURL)
if err != nil {
return nil, err
}
uri, err := client.GetURI()
if err != nil {
return nil, err
}
log.Debug().Msgf("[onvif] new uri=%s", uri)
return streams.GetProducer(uri)
}
func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
b, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
action := onvif.GetRequestAction(b)
if action == "" {
http.Error(w, "malformed request body", http.StatusBadRequest)
return
}
log.Trace().Msgf("[onvif] %s", action)
var res string
switch action {
case onvif.ActionGetCapabilities:
// important for Hass: Media section
res = onvif.GetCapabilitiesResponse(r.Host)
case onvif.ActionGetSystemDateAndTime:
// important for Hass
res = onvif.GetSystemDateAndTimeResponse()
case onvif.ActionGetNetworkInterfaces:
// important for Hass: none
res = onvif.GetNetworkInterfacesResponse()
case onvif.ActionGetDeviceInformation:
// important for Hass: SerialNumber (unique server ID)
res = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
case onvif.ActionGetServiceCapabilities:
// important for Hass
res = onvif.GetServiceCapabilitiesResponse()
case onvif.ActionSystemReboot:
res = onvif.SystemRebootResponse()
time.AfterFunc(time.Second, func() {
os.Exit(0)
})
case onvif.ActionGetProfiles:
// important for Hass: H264 codec, width, height
res = onvif.GetProfilesResponse(streams.GetAll())
case onvif.ActionGetStreamUri:
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken")
res = onvif.GetStreamUriResponse(uri)
default:
http.Error(w, "unsupported action", http.StatusBadRequest)
log.Debug().Msgf("[onvif] unsupported request:\n%s", b)
return
}
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
if _, err = w.Write([]byte(res)); err != nil {
log.Error().Err(err).Caller().Send()
}
}
func apiOnvif(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
var items []*api.Source
if src == "" {
urls, err := onvif.DiscoveryStreamingURLs()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, rawURL := range urls {
u, err := url.Parse(rawURL)
if err != nil {
log.Warn().Str("url", rawURL).Msg("[onvif] broken")
continue
}
if u.Scheme != "http" {
log.Warn().Str("url", rawURL).Msg("[onvif] unsupported")
continue
}
u.Scheme = "onvif"
u.User = url.UserPassword("user", "pass")
if u.Path == onvif.PathDevice {
u.Path = ""
}
items = append(items, &api.Source{Name: u.Host, URL: u.String()})
}
} else {
client, err := onvif.NewClient(src)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if l := log.Trace(); l.Enabled() {
b, _ := client.GetProfiles()
l.Msgf("[onvif] src=%s profiles:\n%s", src, b)
}
name, err := client.GetName()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tokens, err := client.GetProfilesTokens()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for i, token := range tokens {
items = append(items, &api.Source{
Name: name + " stream" + strconv.Itoa(i),
URL: src + "?subtype=" + token,
})
}
if len(tokens) > 0 && client.HasSnapshots() {
items = append(items, &api.Source{
Name: name + " snapshot",
URL: src + "?subtype=" + tokens[0] + "&snapshot",
})
}
}
api.ResponseSources(w, items)
}

View File

@@ -2,11 +2,12 @@ package roborock
import (
"fmt"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/roborock"
"net/http"
)
func Init() {
@@ -84,7 +85,7 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
return
}
var items []api.Stream
var items []*api.Source
for _, device := range devices {
source := fmt.Sprintf(
@@ -93,8 +94,8 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
Auth.UserData.IoT.User, Auth.UserData.IoT.Pass, Auth.UserData.IoT.Domain,
device.DID, device.Key,
)
items = append(items, api.Stream{Name: device.Name, URL: source})
items = append(items, &api.Source{Name: device.Name, URL: source})
}
api.ResponseStreams(w, items)
api.ResponseSources(w, items)
}

206
internal/rtmp/rtmp.go Normal file
View File

@@ -0,0 +1,206 @@
package rtmp
import (
"errors"
"io"
"net"
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/flv"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
)
func Init() {
var conf struct {
Mod struct {
Listen string `yaml:"listen" json:"listen"`
} `yaml:"rtmp"`
}
app.LoadConfig(&conf)
log = app.GetLogger("rtmp")
streams.HandleFunc("rtmp", streamsHandle)
streams.HandleFunc("rtmps", streamsHandle)
streams.HandleFunc("rtmpx", streamsHandle)
api.HandleFunc("api/stream.flv", apiHandle)
streams.HandleConsumerFunc("rtmp", streamsConsumerHandle)
streams.HandleConsumerFunc("rtmps", streamsConsumerHandle)
streams.HandleConsumerFunc("rtmpx", streamsConsumerHandle)
address := conf.Mod.Listen
if address == "" {
return
}
ln, err := net.Listen("tcp", address)
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
log.Info().Str("addr", address).Msg("[rtmp] listen")
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go func() {
if err = tcpHandle(conn); err != nil {
log.Error().Err(err).Caller().Send()
}
}()
}
}()
}
func tcpHandle(netConn net.Conn) error {
rtmpConn, err := rtmp.NewServer(netConn)
if err != nil {
return err
}
if err = rtmpConn.ReadCommands(); err != nil {
return err
}
switch rtmpConn.Intent {
case rtmp.CommandPlay:
stream := streams.Get(rtmpConn.App)
if stream == nil {
return errors.New("stream not found: " + rtmpConn.App)
}
cons := flv.NewConsumer()
if err = stream.AddConsumer(cons); err != nil {
return err
}
defer stream.RemoveConsumer(cons)
if err = rtmpConn.WriteStart(); err != nil {
return err
}
_, _ = cons.WriteTo(rtmpConn)
return nil
case rtmp.CommandPublish:
stream := streams.Get(rtmpConn.App)
if stream == nil {
return errors.New("stream not found: " + rtmpConn.App)
}
if err = rtmpConn.WriteStart(); err != nil {
return err
}
prod, err := rtmpConn.Producer()
if err != nil {
return err
}
stream.AddProducer(prod)
defer stream.RemoveProducer(prod)
_ = prod.Start()
return nil
}
return errors.New("rtmp: unknown command: " + rtmpConn.Intent)
}
var log zerolog.Logger
func streamsHandle(url string) (core.Producer, error) {
client, err := rtmp.DialPlay(url)
if err != nil {
return nil, err
}
return client, nil
}
func streamsConsumerHandle(url string) (core.Consumer, func(), error) {
cons := flv.NewConsumer()
run := func() {
wr, err := rtmp.DialPublish(url)
if err != nil {
return
}
_, err = cons.WriteTo(wr)
}
return cons, run, nil
}
func apiHandle(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
outputFLV(w, r)
} else {
inputFLV(w, r)
}
}
func outputFLV(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
cons := flv.NewConsumer()
cons.Type = "HTTP-FLV consumer"
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
h := w.Header()
h.Set("Content-Type", "video/x-flv")
_, _ = cons.WriteTo(w)
stream.RemoveConsumer(cons)
}
func inputFLV(w http.ResponseWriter, r *http.Request) {
dst := r.URL.Query().Get("dst")
stream := streams.Get(dst)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
client, err := flv.Open(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stream.AddProducer(client)
if err = client.Start(); err != nil && err != io.EOF {
log.Warn().Err(err).Caller().Send()
}
stream.RemoveProducer(client)
}

View File

@@ -1,16 +1,17 @@
package rtsp
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
"io"
"net"
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
)
func Init() {
@@ -20,11 +21,12 @@ func Init() {
Username string `yaml:"username" json:"-"`
Password string `yaml:"password" json:"-"`
DefaultQuery string `yaml:"default_query" json:"default_query"`
PacketSize uint16 `yaml:"pkt_size"`
} `yaml:"rtsp"`
}
// default config
conf.Mod.Listen = ":8554"
conf.Mod.Listen = "0.0.0.0:8554"
conf.Mod.DefaultQuery = "video&audio"
app.LoadConfig(&conf)
@@ -54,7 +56,7 @@ func Init() {
log.Info().Str("addr", address).Msg("[rtsp] listen")
if query, err := url.ParseQuery(conf.Mod.DefaultQuery); err == nil {
defaultMedias = mp4.ParseQuery(query)
defaultMedias = ParseQuery(query)
}
go func() {
@@ -65,6 +67,7 @@ func Init() {
}
c := rtsp.NewServer(conn)
c.PacketSize = conf.Mod.PacketSize
// skip check auth for localhost
if conf.Mod.Username != "" && !conn.RemoteAddr().(*net.TCPAddr).IP.IsLoopback() {
c.Auth(conf.Mod.Username, conf.Mod.Password)
@@ -88,19 +91,21 @@ var log zerolog.Logger
var handlers []Handler
var defaultMedias []*core.Media
func rtspHandler(url string) (core.Producer, error) {
backchannel := true
func rtspHandler(rawURL string) (core.Producer, error) {
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
if i := strings.IndexByte(url, '#'); i > 0 {
if url[i+1:] == "backchannel=0" {
backchannel = false
}
url = url[:i]
}
conn := rtsp.NewClient(url)
conn := rtsp.NewClient(rawURL)
conn.Backchannel = true
conn.UserAgent = app.UserAgent
if rawQuery != "" {
query := streams.ParseQuery(rawQuery)
conn.Backchannel = query.Get("backchannel") == "1"
conn.Media = query.Get("media")
conn.Timeout = core.Atoi(query.Get("timeout"))
conn.Transport = query.Get("transport")
}
if log.Trace().Enabled() {
conn.Listen(func(msg any) {
switch msg := msg.(type) {
@@ -118,11 +123,11 @@ func rtspHandler(url string) (core.Producer, error) {
return nil, err
}
conn.Backchannel = backchannel
if err := conn.Describe(); err != nil {
if !backchannel {
if !conn.Backchannel {
return nil, err
}
log.Trace().Msgf("[rtsp] describe (backchannel=%t) err: %v", conn.Backchannel, err)
// second try without backchannel, we need to reconnect
conn.Backchannel = false
@@ -171,13 +176,18 @@ func tcpHandler(conn *rtsp.Conn) {
conn.SessionName = app.UserAgent
conn.Medias = mp4.ParseQuery(conn.URL.Query())
query := conn.URL.Query()
conn.Medias = ParseQuery(query)
if conn.Medias == nil {
for _, media := range defaultMedias {
conn.Medias = append(conn.Medias, media.Clone())
}
}
if s := query.Get("pkt_size"); s != "" {
conn.PacketSize = uint16(core.Atoi(s))
}
if err := stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
return
@@ -211,7 +221,9 @@ func tcpHandler(conn *rtsp.Conn) {
})
if err := conn.Accept(); err != nil {
log.Warn().Err(err).Caller().Send()
if err != io.EOF {
log.Warn().Err(err).Caller().Send()
}
if closer != nil {
closer()
}
@@ -237,3 +249,27 @@ func tcpHandler(conn *rtsp.Conn) {
_ = conn.Close()
}
func ParseQuery(query map[string][]string) []*core.Media {
if v := query["mp4"]; v != nil {
return []*core.Media{
{
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecH264},
{Name: core.CodecH265},
},
},
{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecAAC},
},
},
}
}
return core.ParseQuery(query)
}

29
internal/srtp/srtp.go Normal file
View File

@@ -0,0 +1,29 @@
package srtp
import (
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/pkg/srtp"
)
func Init() {
var cfg struct {
Mod struct {
Listen string `yaml:"listen"`
} `yaml:"srtp"`
}
// default config
cfg.Mod.Listen = "0.0.0.0:8443"
// load config from YAML
app.LoadConfig(&cfg)
if cfg.Mod.Listen == "" {
return
}
// create SRTP server (endpoint) for receiving video from HomeKit cameras
Server = srtp.NewServer(cfg.Mod.Listen)
}
var Server *srtp.Server

View File

@@ -0,0 +1,149 @@
package streams
import (
"errors"
"strings"
"sync/atomic"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
// support for multiple simultaneous requests from different consumers
consN := atomic.AddInt32(&s.requests, 1) - 1
var prodErrors []error
var prodMedias []*core.Media
var prods []*Producer // matched producers for consumer
// Step 1. Get consumer medias
consMedias := cons.GetMedias()
for _, consMedia := range consMedias {
log.Trace().Msgf("[streams] check cons=%d media=%s", consN, consMedia)
producers:
for prodN, prod := range s.producers {
if err = prod.Dial(); err != nil {
log.Trace().Err(err).Msgf("[streams] skip prod=%s", prod.url)
prodErrors = append(prodErrors, err)
continue
}
// Step 2. Get producer medias (not tracks yet)
for _, prodMedia := range prod.GetMedias() {
log.Trace().Msgf("[streams] check prod=%d media=%s", prodN, prodMedia)
prodMedias = append(prodMedias, prodMedia)
// Step 3. Match consumer/producer codecs list
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
if prodCodec == nil {
continue
}
var track *core.Receiver
switch prodMedia.Direction {
case core.DirectionRecvonly:
log.Trace().Msgf("[streams] match prod=%d => cons=%d", prodN, consN)
// Step 4. Get recvonly track from producer
if track, err = prod.GetTrack(prodMedia, prodCodec); err != nil {
log.Info().Err(err).Msg("[streams] can't get track")
continue
}
// Step 5. Add track to consumer
if err = cons.AddTrack(consMedia, consCodec, track); err != nil {
log.Info().Err(err).Msg("[streams] can't add track")
continue
}
case core.DirectionSendonly:
log.Trace().Msgf("[streams] match cons=%d => prod=%d", consN, prodN)
// Step 4. Get recvonly track from consumer (backchannel)
if track, err = cons.(core.Producer).GetTrack(consMedia, consCodec); err != nil {
log.Info().Err(err).Msg("[streams] can't get track")
continue
}
// Step 5. Add track to producer
if err = prod.AddTrack(prodMedia, prodCodec, track); err != nil {
log.Info().Err(err).Msg("[streams] can't add track")
continue
}
}
prods = append(prods, prod)
if !consMedia.MatchAll() {
break producers
}
}
}
}
// stop producers if they don't have readers
if atomic.AddInt32(&s.requests, -1) == 0 {
s.stopProducers()
}
if len(prods) == 0 {
return formatError(consMedias, prodMedias, prodErrors)
}
s.mu.Lock()
s.consumers = append(s.consumers, cons)
s.mu.Unlock()
// there may be duplicates, but that's not a problem
for _, prod := range prods {
prod.start()
}
return nil
}
func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error {
if prodMedias != nil {
var prod, cons string
for _, media := range prodMedias {
if media.Direction == core.DirectionRecvonly {
for _, codec := range media.Codecs {
prod = appendString(prod, codec.PrintName())
}
}
}
for _, media := range consMedias {
if media.Direction == core.DirectionSendonly {
for _, codec := range media.Codecs {
cons = appendString(cons, codec.PrintName())
}
}
}
return errors.New("streams: codecs not matched: " + prod + " => " + cons)
}
if prodErrors != nil {
var text string
for _, err := range prodErrors {
text = appendString(text, err.Error())
}
return errors.New("streams: " + text)
}
return errors.New("streams: unknown error")
}
func appendString(s, elem string) string {
if strings.Contains(s, elem) {
return s
}
if len(s) == 0 {
return elem
}
return s + ", " + elem
}

View File

@@ -0,0 +1,97 @@
package streams
import (
"errors"
"strings"
"github.com/AlexxIT/go2rtc/pkg/core"
)
type Handler func(url string) (core.Producer, error)
var handlers = map[string]Handler{}
func HandleFunc(scheme string, handler Handler) {
handlers[scheme] = handler
}
func HasProducer(url string) bool {
if i := strings.IndexByte(url, ':'); i > 0 {
scheme := url[:i]
if _, ok := handlers[scheme]; ok {
return true
}
if _, ok := redirects[scheme]; ok {
return true
}
}
return false
}
func GetProducer(url string) (core.Producer, error) {
if i := strings.IndexByte(url, ':'); i > 0 {
scheme := url[:i]
if redirect, ok := redirects[scheme]; ok {
location, err := redirect(url)
if err != nil {
return nil, err
}
if location != "" {
return GetProducer(location)
}
}
if handler, ok := handlers[scheme]; ok {
return handler(url)
}
}
return nil, errors.New("streams: unsupported scheme: " + url)
}
// Redirect can return: location URL or error or empty URL and error
type Redirect func(url string) (string, error)
var redirects = map[string]Redirect{}
func RedirectFunc(scheme string, redirect Redirect) {
redirects[scheme] = redirect
}
func Location(url string) (string, error) {
if i := strings.IndexByte(url, ':'); i > 0 {
scheme := url[:i]
if redirect, ok := redirects[scheme]; ok {
return redirect(url)
}
}
return "", nil
}
// TODO: rework
type ConsumerHandler func(url string) (core.Consumer, func(), error)
var consumerHandlers = map[string]ConsumerHandler{}
func HandleConsumerFunc(scheme string, handler ConsumerHandler) {
consumerHandlers[scheme] = handler
}
func GetConsumer(url string) (core.Consumer, func(), error) {
if i := strings.IndexByte(url, ':'); i > 0 {
scheme := url[:i]
if handler, ok := consumerHandlers[scheme]; ok {
return handler(url)
}
}
return nil, nil, errors.New("streams: unsupported scheme: " + url)
}

View File

@@ -0,0 +1,19 @@
package streams
import (
"net/url"
"strings"
)
func ParseQuery(s string) url.Values {
params := url.Values{}
for _, key := range strings.Split(s, "#") {
var value string
i := strings.IndexByte(key, '=')
if i > 0 {
key, value = key[:i], key[i+1:]
}
params[key] = append(params[key], value)
}
return params
}

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