Compare commits

...

578 Commits

Author SHA1 Message Date
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
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
Alexey Khit
71051e7dcf Update version to 1.3.1 2023-03-26 15:00:47 +03:00
Alex X
cdb3ee45cf Merge pull request #271 from skrashevich/testing-ci
Testing action CI
2023-03-26 11:23:15 +03:00
Alex X
ae99c1da03 Merge pull request #273 from skrashevich/fix-urlencoding-in-delete-request
Fix double url-encoding in streams DELETE request
2023-03-26 11:15:51 +03:00
Alexey Khit
863cc0c1d7 Add tests for FFmpeg parse args 2023-03-26 11:13:36 +03:00
Alexey Khit
40494ab87c Code refactoring 2023-03-26 11:13:23 +03:00
Alex X
bffe5f0aa2 Merge pull request #280 from horttorrell32/master
Add HW ROTATION support to vaapi engine
2023-03-26 11:02:25 +03:00
Alexey Khit
8241af8b9d Fix GetMedias on stream reconnection issue 2023-03-26 08:09:54 +03:00
Alexey Khit
5c164de393 Fix listening on hassio interface 2023-03-26 07:27:29 +03:00
Alexey Khit
8bf5c85b79 Add support X-Forwarded-For 2023-03-25 11:59:55 +03:00
Alexey Khit
a42c3e21c9 Fix input browser via WebTorrent 2023-03-25 11:41:15 +03:00
Alexey Khit
7016289f14 Adds dynamic timeouts on reconnect 2023-03-25 11:39:29 +03:00
Alexey Khit
54302d3bda Fix json locked 2023-03-25 07:36:21 +03:00
Alexey Khit
af6b8a400d Adds about pin for Roborock source 2023-03-23 15:06:45 +03:00
Alexey Khit
a1b5eae653 Update readme 2023-03-23 14:07:29 +03:00
Alexey Khit
bb3c64598c Update version to 1.3.0 2023-03-23 14:02:44 +03:00
Alexey Khit
3002d5f4f1 Fix Roborock support 2023-03-21 14:05:10 +03:00
Alexey Khit
cca4f0500e Bump go version to 1.20 and update dependencies 2023-03-20 07:33:41 +03:00
Alexey Khit
b087be9c56 Fix zero packets from webrtc 2023-03-20 07:25:11 +03:00
Alexey Khit
2d5a0e4822 Update webrtc section in the links.html page 2023-03-20 06:16:57 +03:00
Alexey Khit
acf5ec5256 Fix webtorrent not found share 2023-03-20 06:15:55 +03:00
Alexey Khit
e1e8abc334 Add PCM codec 2023-03-19 17:20:49 +03:00
Alexey Khit
d84efd1238 Add WebTorrent shares to add.html page 2023-03-19 17:17:05 +03:00
Alexey Khit
7c79c1ff26 Fix import cameras from Hass config 2023-03-19 17:17:05 +03:00
Alexey Khit
43840576ea Add selectall checkbox 2023-03-19 17:17:05 +03:00
Alexey Khit
bd79b24db3 Add "add" html page 2023-03-19 17:17:05 +03:00
Alexey Khit
e728643aad Add support Roborock source 2023-03-19 17:17:05 +03:00
Alexey Khit
12a7b96289 BIG core logic rewrite 2023-03-19 17:17:05 +03:00
Alexey Khit
2146ea470b Code refactoring (change interface to any) 2023-03-19 17:17:05 +03:00
Alexey Khit
d4d91e4920 Update support sendrecv medias for WebRTC 2023-03-19 17:17:05 +03:00
Alexey Khit
a6393da956 Fix support sendrecv media for WebRTC passive consumer 2023-03-19 17:17:05 +03:00
Alexey Khit
d686d4f691 Fix WebRTC active producer with backchannel 2023-03-19 17:17:05 +03:00
Alexey Khit
58849fd1e5 Adds error output for WebTorrent 2023-03-19 17:17:05 +03:00
Alexey Khit
31c86272bb Fix webtorrent support on i386 2023-03-19 17:17:05 +03:00
Alexey Khit
0382fbf8a9 Support multiple codecs for WebRTC producer 2023-03-19 17:17:05 +03:00
Alexey Khit
0b714a59e5 Adds stream play logic to active producer 2023-03-19 17:17:05 +03:00
Alexey Khit
13c426e2a9 Update WebRTC passive producer handling 2023-03-19 17:17:05 +03:00
Alexey Khit
d6d21286c1 Increase WebRTC receive MTU size 2023-03-19 17:17:05 +03:00
Alexey Khit
ce2898ac3a Fix remote track processing for WebRTC passive producer 2023-03-19 17:17:05 +03:00
Alexey Khit
e0320b8ead Adds media selection to links and webrtc html pages 2023-03-19 17:17:05 +03:00
Alexey Khit
a960b9b9ee Disable UDPMux for WebRTC by default 2023-03-19 17:17:05 +03:00
Alexey Khit
0b4ebb4e21 Add support WebRTC async passive producer 2023-03-19 17:17:05 +03:00
Alexey Khit
a1dd941814 Add support multiple PPS in a row for H264 payloader 2023-03-19 17:17:05 +03:00
Alexey Khit
146fb62b8e Adds force keyframe for WebRTC passive producer 2023-03-19 17:17:05 +03:00
Alexey Khit
53e8fed0b0 Update medias for WebRTC passive producer 2023-03-19 17:17:05 +03:00
Alexey Khit
3d34854387 Rewrite WebRTC producer/consumer tracks handlers 2023-03-19 17:17:05 +03:00
Alexey Khit
77842643c8 Rewrite Tapo producer 2023-03-19 17:17:05 +03:00
Alexey Khit
f9fe22569c Rewrite WebRTC HTML pages 2023-03-19 17:17:05 +03:00
Alexey Khit
e17645ac02 Add mic support to WebTorrent share page 2023-03-19 17:17:05 +03:00
Alexey Khit
1fc2cf3175 Update WebRTC type in info JSON 2023-03-19 17:14:59 +03:00
Alexey Khit
775b1818d1 Add WebTorrent module 2023-03-19 17:14:59 +03:00
Alexey Khit
1e83dc85f7 Rewrite WebRTC peer connection constructor 2023-03-19 17:14:59 +03:00
Alexey Khit
03a4393ce3 Update WebSocket default buffers 2023-03-19 17:14:59 +03:00
Alexey Khit
5f2368b0f9 Update main readme about webrtc 2023-03-19 17:14:59 +03:00
Alexey Khit
59b35f2501 Add useful links to readme 2023-03-19 17:14:59 +03:00
Alexey Khit
9ab9412c95 Update go2rtc candidates processing 2023-03-19 17:14:59 +03:00
Alexey Khit
d805d560b9 Remove dummy fix for Ezviz C6N 2023-03-19 17:14:59 +03:00
Alexey Khit
5aa20f0845 Update streamer NewTrack function 2023-03-19 17:14:59 +03:00
Alexey Khit
c2cdf60ffc Code refactoring
Code refactoring
2023-03-19 17:14:59 +03:00
Alexey Khit
c70c3a58f1 Add media list option to webrtc create function 2023-03-19 17:14:59 +03:00
Alexey Khit
df0ab77791 Update receiving remote candidate info for webrtc 2023-03-19 17:14:59 +03:00
Alexey Khit
402df50b65 Remove 90000 from stream info json 2023-03-19 17:14:59 +03:00
Alexey Khit
5c084c9989 Fix WebRTC send candidates before send answer 2023-03-19 17:14:59 +03:00
Alexey Khit
1703e0dce8 Add more compatibility go webrtc api 2023-03-19 17:14:59 +03:00
Alexey Khit
7301f55e4a Update go mod dependencies 2023-03-19 17:14:59 +03:00
Alexey Khit
06fc9717df Add new Waiter class 2023-03-19 17:14:59 +03:00
Alexey Khit
4e19c54467 Set custom pion API for WebRTC client 2023-03-19 17:14:59 +03:00
Alexey Khit
a0e04fb70e Fix WebRTC client 2023-03-19 17:14:59 +03:00
Alexey Khit
218eea6806 Big rewrite for WebRTC processing 2023-03-19 17:14:59 +03:00
Alexey Khit
ad3c5440fe Code refactoring 2023-03-19 17:14:59 +03:00
Alexey Khit
f5892e4cfc Fix WebRTC async candidates processing 2023-03-19 17:14:59 +03:00
Alexey Khit
4328d2a573 Refactor WebRTC candidates processing 2023-03-19 17:14:59 +03:00
Alexey Khit
3fb917f00f WebRTC module refactoring 2023-03-19 17:14:59 +03:00
Alexey Khit
eca79f1c0b Add FmptLine to Track info 2023-03-19 17:14:59 +03:00
Alexey Khit
bd9b69d0d5 Remove chunks from MSE 2023-03-19 17:14:59 +03:00
Alexey Khit
676ec25a7f Update readme 2023-03-17 11:10:14 +03:00
Alexey Khit
12d10ae14e Update gh-pages 2023-03-12 15:27:28 +03:00
Galindo, Alex
1912a43679 Add HW ROTATION support to vaapi engine 2023-03-09 12:22:47 +01:00
Alexey Khit
eb1f423da3 Add gh-pages 2023-03-06 14:07:15 +03:00
Alexey Khit
5846cbd989 Add support Hikvision ISAPI 2023-02-28 22:55:19 +03:00
Alexey Khit
ab1b3932ac Fix stream audio to second source 2023-02-28 22:54:09 +03:00
Sergey Krashevich
eca311717a Update index.html 2023-02-27 02:02:32 +03:00
Sergey Krashevich
d3b2b8fdae add docker testing 2023-02-21 18:24:41 +03:00
Sergey Krashevich
3b9a0059df Create test.yml 2023-02-21 17:54:55 +03:00
Alexey Khit
1fe21bb300 Improve MPEG TS H264 processing 2023-02-18 19:48:09 +03:00
Alexey Khit
41cdcb69c6 Refactoring webrtc sync handler 2023-02-18 11:24:44 +03:00
Alex X
6b00134575 Merge pull request #265 from jkolo/feature/json_offer_webrtc
Adds support for json offer and answer then also in json
2023-02-18 11:03:42 +03:00
Jerzy Kołosowski
5519f3e061 Adds support for json offer and answer then also in json
Signed-off-by: Jerzy Kołosowski <jerzy@kolosowscy.pl>
2023-02-17 19:01:10 +01:00
Alexey Khit
e312d0b46b Add about Tapo RTSP to readme 2023-02-17 15:10:26 +03:00
Alexey Khit
eff7b27293 Add about new features to readme 2023-02-17 14:19:12 +03:00
Alexey Khit
e3f6c459c7 Update version to 1.2.0 2023-02-17 14:16:21 +03:00
Alexey Khit
91399d3194 Fix SDP parsing for Ezviz C6N 2023-02-17 13:07:07 +03:00
Alexey Khit
338da2a747 Fix timeout for RTSP with only Recv track 2023-02-17 13:07:07 +03:00
Alexey Khit
bb5df24ecf Add support consumer feature for Tapo source 2023-02-17 13:07:07 +03:00
Alexey Khit
adb424033f Fix consumer interface check panic 2023-02-17 13:07:07 +03:00
Alexey Khit
70c415a1d8 Add support backchannel for Tapo source 2023-02-17 13:07:07 +03:00
Alexey Khit
9fd783793e Add basic support MPEG TS source 2023-02-17 13:07:07 +03:00
Alexey Khit
665545903c Add support tapo source 2023-02-17 13:07:07 +03:00
Alexey Khit
830baafffe Add mpegts Reader 2023-02-17 13:07:07 +03:00
Alexey Khit
a22c33fd4e Add support stream mode for HTTP Request 2023-02-17 13:07:07 +03:00
Alexey Khit
1ad09f48cc Create GetFmtpLine func for H264 2023-02-17 13:07:07 +03:00
Alexey Khit
5b1ec08341 Move ts to mpegts package 2023-02-17 13:07:07 +03:00
Alexey Khit
3f22c010ce Add play audio feature to links page 2023-02-17 13:06:58 +03:00
Alexey Khit
df5f585064 Add support to play src on cameras with speaker 2023-02-17 09:23:46 +03:00
Alexey Khit
c1b810a5fe Improve RTSP consumer logic 2023-02-17 09:21:43 +03:00
Alexey Khit
e43b1e4ab6 Restore Announce method for RTSP 2023-02-17 09:20:46 +03:00
Alexey Khit
a8612fca43 Remove loop from FFmpeg file template 2023-02-17 09:17:55 +03:00
Alexey Khit
0d18c23cc2 Add support raw video or audio params for FFmpeg 2023-02-17 09:17:33 +03:00
Alexey Khit
c22ede2396 Add support input param to FFmpeg source 2023-02-16 17:57:25 +03:00
Alexey Khit
6b3a2652b2 WebRTC consumer refactoring 2023-02-16 14:29:37 +03:00
Alexey Khit
4bf5034ce7 MJPEG module refactoring 2023-02-16 14:29:37 +03:00
Alexey Khit
b57027441c Ivideon source refactoring 2023-02-16 14:28:50 +03:00
Alexey Khit
d3b62d82cf RTSP source refactoring 2023-02-16 14:17:57 +03:00
Alexey Khit
836701cb68 Add stream PATCH API 2023-02-15 17:35:08 +03:00
Alexey Khit
3aee438e37 Add support HTTP MJPEG to RTSP MJPEG 2023-02-14 14:45:40 +03:00
Alexey Khit
116e2f739b Add support incoming HTTP-FLV stream 2023-02-14 14:26:37 +03:00
Alexey Khit
47371fbdcf Add support incoming MJPEG stream 2023-02-14 14:25:13 +03:00
Alexey Khit
6e62c442f8 Add another DVRIP H265 media code ID 2023-02-11 12:48:52 +03:00
Alexey Khit
57b49d735e Add FmtpLine for DVRIP H264 codec 2023-02-11 12:47:36 +03:00
Alexey Khit
a72fa7fb23 Code refactoring 2023-02-11 12:46:29 +03:00
Alexey Khit
b2029d1004 Support channels for DVRIP 2023-02-11 12:45:46 +03:00
Alexey Khit
3f338c83b7 Add support DVRIP source 2023-02-11 09:56:32 +03:00
Alexey Khit
00b445a170 Add H265 payloader to RTSP Server 2023-02-11 09:55:39 +03:00
Alexey Khit
4cbbb5407c Create AnnexB2AVC func 2023-02-11 09:54:34 +03:00
Alexey Khit
80f77d28c8 Add bypass login for TP-Link cameras 2023-02-10 11:16:03 +03:00
Alexey Khit
f60b55b6fa Update version to 1.1.2 2023-02-09 07:48:28 +03:00
Alexey Khit
c42413866d Remove RTSP wrong channel ID from logs 2023-02-09 07:48:11 +03:00
Alexey Khit
b137eb66d0 Fix more sizes for RTSP MJPEG #83 2023-02-09 07:18:58 +03:00
Alexey Khit
6a40039645 Fix MJPEG processing for wallpanel project #248 2023-02-09 07:18:58 +03:00
Alexey Khit
2e4b28d871 Fix RTSP auth for RtspServer project #244 2023-02-09 07:18:58 +03:00
Alexey Khit
58146b7e7e Fix H265 processing for RtspServer project #244 2023-02-09 07:18:58 +03:00
Alexey Khit
23db40220b Fix H264 processing for RtspServer project #244 2023-02-09 07:18:58 +03:00
Alex X
557aac185d Merge pull request #220 from skrashevich/macos-lipo
Generate universal macOS binary on release
2023-02-09 07:18:06 +03:00
Alexey Khit
9ed4d4cedb Fix parsing SDP from Reolink Doorbell 2023-02-07 20:02:23 +03:00
Alexey Khit
b05cbdf3d3 Make GetProfileLevelID func more smarter 2023-02-06 20:53:55 +03:00
Alexey Khit
497594f53f Fix buggy SDP parsing 2023-02-06 11:46:00 +03:00
Alexey Khit
73cdb39335 Add camera experience section to readme 2023-02-06 09:32:01 +03:00
Sergey Krashevich
a388002b12 Merge branch 'master' into macos-lipo 2023-02-04 20:40:46 +03:00
Alexey Khit
6d1c0a2459 Fix SDP parsing from cheap Chinese cameras 2023-02-04 10:01:35 +03:00
Alexey Khit
da3137b6f0 Add User-Agent to RTSP Describe #235 2023-02-03 14:11:30 +03:00
Alexey Khit
d21ce3d27d Jump over wrong packets from RTSP 2023-02-03 12:54:49 +03:00
Alexey Khit
8cee4179f2 Fix another buggy Chinese cameras 2023-02-03 12:54:49 +03:00
Alexey Khit
1153ee3652 Fix support WebRTC for Chromecast 1 2023-02-03 11:41:51 +03:00
Alexey Khit
3240301f27 Fix autofullscreen with MP4 for iPhones 2023-02-03 11:41:43 +03:00
Alexey Khit
2a20251dbd Fix autoplay after background 2023-02-03 11:41:37 +03:00
Alexey Khit
5a2d7de56b Add projects using go2rtc section to readme 2023-02-03 11:38:26 +03:00
Alexey Khit
38ea8b56b8 Update version to 1.1.1 2023-02-01 17:57:00 +03:00
Alexey Khit
08c2174e94 Fix default_query bug #227 2023-02-01 10:40:29 +03:00
Alexey Khit
b48f1c1a0b Update default_query param name in API response 2023-02-01 10:39:54 +03:00
Alexey Khit
cf58a6f952 Update readme about Hass integration 2023-01-31 19:47:54 +03:00
Alexey Khit
350e677838 Update readme 2023-01-31 11:30:36 +03:00
Alexey Khit
7b3505f4f4 Update version to 1.1.0 2023-01-31 10:32:28 +03:00
Alexey Khit
98af8c3dbf Update links page 2023-01-31 08:56:49 +03:00
Alexey Khit
762edf157a Add default_query setting for RTSP server 2023-01-31 07:35:50 +03:00
Alexey Khit
4a633cd9b5 Move stream useful links to separate page 2023-01-30 23:02:06 +03:00
Alexey Khit
f4d2c801f0 Add redirect for Safari from MP4 to HLS 2023-01-30 22:00:07 +03:00
Alexey Khit
fb4b609914 Add support output as HLS (TS+fMP4) 2023-01-30 21:22:12 +03:00
Alexey Khit
56633229ed Fix AAC support for old MP4 consumer 2023-01-30 21:21:17 +03:00
Alexey Khit
2d49cfd4b6 Code refactoring 2023-01-30 19:15:32 +03:00
Alexey Khit
0f934be9b6 Add MimeCodecs to mp4 Muxer 2023-01-30 19:15:12 +03:00
Alexey Khit
c1d6adc189 Move ParseQuery from rtsp to mp4 module 2023-01-30 19:13:35 +03:00
Alexey Khit
500b8720d5 Fix bug with no stream from some Dahua cameras 2023-01-29 18:55:37 +03:00
Sergey Krashevich
b7391f58a5 Update release.yml 2023-01-28 03:21:03 +03:00
Alexey Khit
bef8e6454d Update RTSP Server response with all tracks by default 2023-01-27 20:43:56 +03:00
Alexey Khit
5243aca8e9 Remove Title field from Media object 2023-01-27 19:30:48 +03:00
Alexey Khit
69dd4d26ec Add support OPUS, MP3, PCMU, PCMA for MP4 2023-01-27 17:11:44 +03:00
Alexey Khit
e93d89ec96 Add mp3 preset for ffmpeg 2023-01-27 17:10:41 +03:00
Alexey Khit
ec56227900 Add codecs filter to stream.mp4 2023-01-27 17:05:45 +03:00
Alexey Khit
decd3af941 Add OR to RTSP Server codecs filter 2023-01-27 17:05:01 +03:00
Alexey Khit
e8e43f9d68 Fix MSE in Safari 2023-01-27 12:39:51 +03:00
Alexey Khit
a1fec1c6f6 Add support OPUS audio for MSE/MP4 2023-01-27 12:37:02 +03:00
Alexey Khit
073acdfec9 Code refactoring 2023-01-27 12:27:19 +03:00
Alexey Khit
d05ab79f88 Total rewrite mov/mp4 encoder 2023-01-26 22:29:12 +03:00
Alexey Khit
e295bc4eaf Fix RTSP AAC sound from some Reolink cameras 2023-01-26 22:02:02 +03:00
Alexey Khit
2f436bba4e Fix RTSP URL parse bug #208 2023-01-26 09:09:48 +03:00
Alexey Khit
0e28b0c797 Fix API base_path support #205 2023-01-25 16:40:06 +03:00
Alexey Khit
3acea1ed5a Update version to 1.0.1 2023-01-24 22:29:15 +03:00
Alexey Khit
3fb8d9af66 Disable release autobuild 2023-01-24 22:29:04 +03:00
Alexey Khit
9bbaf41d54 Second fix for Chinese buggy cameras 2023-01-24 21:38:58 +03:00
Alexey Khit
c43530fbd3 Fix mp4f consumer 2023-01-24 21:05:51 +03:00
Alexey Khit
15777a3d94 Fix Chinese buggy cameras 2023-01-24 21:05:35 +03:00
Alexey Khit
6e61ac6d2f Fix HTTP-FLV for Reolink cameras 2023-01-24 17:48:31 +03:00
Alexey Khit
6d7d5f53d8 Update websocket disconnect log message 2023-01-24 17:48:08 +03:00
Alexey Khit
d2bca8d461 Update processing HTTP-FLV without video or audio 2023-01-24 17:47:26 +03:00
Alexey Khit
94b089d1e3 Fix bug in URL for D-Link cameras 2023-01-23 21:14:52 +03:00
Alexey Khit
b3d16c9fcc Update TOC in readme 2023-01-23 15:37:06 +03:00
Sergey Krashevich
a36359f3dd Update api.go 2023-01-20 18:38:46 +03:00
Alexey Khit
f0def68482 Update readme 2023-01-20 17:45:35 +03:00
Alexey Khit
9ddbb326b4 Update version to 1.0.0 2023-01-20 17:07:43 +03:00
Alexey Khit
a2e58d928e Fix timezone in logs 2023-01-20 13:45:01 +03:00
Alexey Khit
3c48fb8bea Simplify Dockerfile 2023-01-20 11:23:28 +03:00
Alexey Khit
4b0cbb5a73 Add support basic auth for API 2023-01-20 10:54:26 +03:00
Alexey Khit
e28b49ea86 Ignore errors for RTCP packets 2023-01-20 10:26:57 +03:00
Alexey Khit
5c17d8fcb6 Add support AAC audio for HTTP-FLV 2023-01-19 21:44:15 +03:00
Alexey Khit
e040fb591f Disable CGO for git releases 2023-01-18 15:07:42 +03:00
Alexey Khit
140014f2a6 Fix info for WS/MP4 2023-01-18 15:04:06 +03:00
Alexey Khit
23f72d111e Add Teardown handler for RTSP server (untested) 2023-01-18 12:21:54 +03:00
Alexey Khit
f9d5ab9d0a Fix RTSP server SDP for some clients 2023-01-18 11:45:39 +03:00
Alexey Khit
8628c48db8 Add no-cache for all GET API requests 2023-01-18 10:01:00 +03:00
Alexey Khit
6e49d51c33 Update GET config API when config file not set 2023-01-18 10:00:20 +03:00
Alexey Khit
6a61b5234e Fix HTTP-FLV support for Reolink cameras 2023-01-18 09:36:32 +03:00
Alexey Khit
7a0091777d Fix relative config path #171 2023-01-16 11:00:04 +03:00
Alexey Khit
d23d2a7eff Fix release binaries for mac 2023-01-16 00:40:02 +03:00
Alexey Khit
cecbe4166c Update version to 0.1-rc.9 2023-01-16 00:06:55 +03:00
Alexey Khit
dcb457235c Rewrite stream info API 2023-01-15 23:51:20 +03:00
Alexey Khit
bc4e032830 Update readme 2023-01-15 11:13:38 +03:00
Alexey Khit
8218cda149 Add version, config_path to web UI and fix RTSP link 2023-01-15 09:57:15 +03:00
Alexey Khit
d1e56feeb6 Update full path to config file 2023-01-15 09:55:32 +03:00
Alexey Khit
463d05dfd3 Update readme 2023-01-15 00:28:48 +03:00
Alexey Khit
a1a73f7b45 Rewrite WS+MP4 format to keyframes stream 2023-01-15 00:12:26 +03:00
Alexey Khit
39662e10af Fix errors in JS player 2023-01-15 00:11:31 +03:00
Alexey Khit
1c830d6e60 Code refactoring 2023-01-14 22:49:12 +03:00
Alex X
2039aa60b3 Merge pull request #170 from skrashevich/config-api-patch-method
PATH api/config method for merge configuration
2023-01-14 21:57:34 +03:00
Sergey Krashevich
b7016e798f Update config.go 2023-01-14 21:27:23 +03:00
Alexey Khit
0b291f5185 Support multiple configs and config in raw yaml form 2023-01-14 21:12:17 +03:00
Alexey Khit
395304654a Code refactoring 2023-01-14 19:15:13 +03:00
Alexey Khit
e472397705 Add general info API 2023-01-14 18:00:43 +03:00
Alexey Khit
7c1f48e0ad Support empty default environment value 2023-01-14 17:25:05 +03:00
Alexey Khit
f4346a104f Add support env variables in config file #143 2023-01-14 17:19:51 +03:00
Alexey Khit
030972b436 Auto build binaries on release #158 2023-01-14 14:14:23 +03:00
Alexey Khit
efddefa123 Add web config editor #153 2023-01-14 13:47:34 +03:00
Alexey Khit
3c1bdd0dab Fix WebRTC candidate type 2023-01-14 09:45:03 +03:00
Alexey Khit
7e7e15d7c8 Update readme 2023-01-14 09:22:22 +03:00
Alex X
a1a9f77535 Merge pull request #167 from felipecrs/master
Match docs with new webrtc udp fixed port
2023-01-14 09:10:46 +03:00
Alexey Khit
a06462729d Code refactoring 2023-01-14 09:04:54 +03:00
Alex X
331c5bbcad Merge pull request #166 from tsightler/udp-candidate-fix
Fix invalid tcpType for UDP candidate
2023-01-14 08:59:25 +03:00
Felipe Santos
58a76efc8a Match docs with new webrtc udp fixed port 2023-01-13 23:15:04 -03:00
tsightler
5e0f010885 Update helper.go 2023-01-13 18:18:39 -05:00
Alexey Khit
4ae733aa11 Update version to 0.1-rc.8 2023-01-13 22:39:24 +03:00
Alexey Khit
27d8b33b62 Fix concurrency in ivideon 2023-01-13 21:52:29 +03:00
Alexey Khit
ff8b0fbb9c Set default 8555 port for WebRTC (UDP+TCP) 2023-01-13 21:51:48 +03:00
Alexey Khit
c6ad7ac39f Add single UDP port for WebRTC Server 2023-01-13 21:51:48 +03:00
Alexey Khit
7a3adf17be Fix mp4f consumer (unused) 2023-01-13 21:51:24 +03:00
Alexey Khit
94f6c07b28 Fix mjpeg client network connection 2023-01-13 18:03:54 +03:00
Alexey Khit
7b326d4753 Fix simultaneous stream reconnect and start 2023-01-13 18:03:17 +03:00
Alexey Khit
5407a3bc4b Fix multiple requests from different consumers 2023-01-13 18:02:03 +03:00
Alexey Khit
6b24421722 Fix unblocking exec error 2023-01-13 18:01:01 +03:00
Alexey Khit
d12775a2d7 Fix unblocking exec waiter 2023-01-13 18:00:48 +03:00
Alexey Khit
6151593c08 Fix ws lock on write and close 2023-01-13 17:28:01 +03:00
Alexey Khit
dba0989c54 Fix empty streams json on stream lock 2023-01-13 13:37:36 +03:00
Alexey Khit
ba0c7d911d Fix ffmpeg link to same stream 2023-01-13 13:36:43 +03:00
Alexey Khit
09fefca712 Remove backchannel codec from add consumer error 2023-01-13 13:35:58 +03:00
Alexey Khit
b3f177e2ec Handle closed state for ws connection 2023-01-13 13:34:41 +03:00
Alexey Khit
228abb8fbe Change logs msg from WRN to DBG for fail on add consumer 2023-01-13 13:33:55 +03:00
Alexey Khit
eee70c07b7 Fix closer for ivideon source 2023-01-13 13:32:48 +03:00
Alexey Khit
d92b0f29af Fix states handle for RTSP 2023-01-13 13:32:09 +03:00
Alexey Khit
fca6c87b2c Fix RTSP tracks list in info json 2023-01-13 13:31:22 +03:00
Alexey Khit
0601091772 Fix closer for RTSP server #163 2023-01-13 13:30:41 +03:00
Alexey Khit
89eb653d67 Update version to 0.1-rc.7 2023-01-08 23:18:52 +03:00
Alexey Khit
0e49ffdfff Fix GetMedias for producer in reconnect state 2023-01-08 21:42:13 +03:00
Alexey Khit
bd2fc1252d Update last error for reconnect stream 2023-01-08 21:36:28 +03:00
Alexey Khit
78ac88448c Fix close problem ivideon client 2023-01-08 21:35:45 +03:00
Alexey Khit
4cd9757e53 Fix status info in JS player 2023-01-08 21:05:50 +03:00
Alexey Khit
f9cb6fd670 Fix wrong RTSP H264 profile for some cameras 2023-01-08 21:05:17 +03:00
Alexey Khit
57fa6a5530 Add support for simultaneous requests from different consumers 2023-01-08 20:31:00 +03:00
Alexey Khit
6906b56524 Fix double start for RTSP source 2023-01-08 20:01:38 +03:00
Alexey Khit
c9b0806c84 Add producer url to logs 2023-01-08 20:00:48 +03:00
Alexey Khit
a9d1e64f88 Fix STUN candidate in IPv6 format 2023-01-08 15:45:11 +03:00
Alex X
9e9f07f3f7 Merge pull request #150 from skrashevich/dockerfile-crossbuild
Speedup container building using Golang cross-building
2023-01-06 14:06:50 +03:00
Sergey Krashevich
b51aabd3d9 Update Dockerfile 2023-01-06 11:52:09 +03:00
Alexey Khit
368562c540 Update version to 0.1-rc.6 2023-01-02 20:53:04 +03:00
Alexey Khit
6d6e7010b4 Rewrite JS player for better integration 2023-01-02 16:33:00 +03:00
Alexey Khit
4157a53dd8 Response with error on codec negotiation 2023-01-02 16:32:08 +03:00
Alexey Khit
bdf5654c01 Change WS default buffer 2023-01-02 16:31:11 +03:00
Alexey Khit
66f729aa0e Send WS response on MJPEG or MP4 stream starts 2023-01-02 16:30:54 +03:00
Alexey Khit
96d1ef2d2c Adds about RTSPtoWebRTC STUN server to readme 2022-12-27 15:59:08 +03:00
Alexey Khit
9739f7f416 Add auto create new stream for async webrtc 2022-12-25 11:17:14 +03:00
Alexey Khit
654fa32b3a Fix packet size for MSE 2022-12-25 11:09:19 +03:00
Alexey Khit
db2263c7fe Fix stream page for raw urls in src 2022-12-25 08:55:58 +03:00
Alexey Khit
e6c36f1cf7 Rename Hardware Dockerfile 2022-12-19 12:16:38 +03:00
Alexey Khit
110f90cb34 Disable JS stream background by default 2022-12-18 22:39:52 +03:00
Alexey Khit
aca3bab238 Fix Firefox WebRTC support 2022-12-18 22:38:44 +03:00
Alexey Khit
4df44645d7 Fix lags for Intel HW transcoding 2022-12-18 21:33:38 +03:00
Alexey Khit
097fdfbbb8 Rename HW engine for Raspberry 2022-12-18 10:25:04 +03:00
Alexey Khit
dc21a04da7 Fix freezing with VAAPI HW 2022-12-18 10:24:30 +03:00
Alexey Khit
db255b476a Add hardware acceleration support to FFmpeg 2022-12-18 01:02:12 +03:00
Alexey Khit
464ea417ef Add docker image with Hardware drivers 2022-12-18 01:00:39 +03:00
Alexey Khit
c1fac66329 Refactoring CI 2022-12-17 23:40:56 +03:00
Alex X
a6057a2eca Merge pull request #72 from felipecrs/refactor-docker
Refactor docker image and ci
2022-12-17 23:07:55 +03:00
Alexey Khit
7c69ba13b0 Fix RTP H264 with two SEI in packet 2022-12-17 22:59:06 +03:00
Alexey Khit
2b8bfe8bd9 Add support width and height params for FFmpeg 2022-12-09 22:07:57 +03:00
Alexey Khit
0bd54da456 Increase compression level for 7zip 2022-12-09 00:25:38 +03:00
Alexey Khit
9f6af1c9e4 Update connection method in JS player so it can be extended 2022-12-09 00:24:29 +03:00
Alexey Khit
c9dd0e37e4 Fix RTSP JPEG processing 2022-12-09 00:22:25 +03:00
Felipe Santos
562872beb8 Add jq 2022-12-06 10:38:18 -03:00
Felipe Santos
46a278c067 Add curl and exit on error run.sh 2022-12-06 10:38:18 -03:00
Felipe Santos
270fc7c1b6 Allow to run container without mounting /config 2022-12-06 10:38:18 -03:00
Felipe Santos
6feb635522 Delete config.yaml not used anymore 2022-12-06 10:38:18 -03:00
Felipe Santos
6f48131e4d Remove armv6 which was never supported 2022-12-06 10:38:18 -03:00
Felipe Santos
f120db71a3 Add all platforms 2022-12-06 10:38:18 -03:00
Felipe Santos
72823af9d0 Refactor docker workflow 2022-12-06 10:38:18 -03:00
Felipe Santos
15d9d4ebf4 Use official python as base and add tini 2022-12-06 10:38:18 -03:00
Felipe Santos
b09bbd79c4 Fix VERSION 2022-12-06 10:38:18 -03:00
Felipe Santos
1830273f02 Refactor docker image 2022-12-06 10:38:18 -03:00
Alexey Khit
07f3972794 Update readme 2022-12-06 16:17:28 +03:00
Alexey Khit
4c2ebd20bc Update version to 0.1-rc.5 2022-12-06 12:19:29 +03:00
Alexey Khit
440c7bd6e1 Recover link to 2-way audio on Web UI 2022-12-06 12:15:22 +03:00
Alexey Khit
74c3510a10 Trace to log supported MP4 codecs 2022-12-06 10:51:42 +03:00
Alexey Khit
c2748fc77b Add check H264 High@5.1 for JS player 2022-12-06 10:48:42 +03:00
Alexey Khit
d334551591 Update main Web UI page 2022-12-06 01:34:24 +03:00
Alexey Khit
cfe20925ac Big rewrite JS player WebSocket processing 2022-12-05 23:54:40 +03:00
Alexey Khit
5b39f78ace Update error msg for fail codecs negotiation 2022-12-05 23:52:28 +03:00
Alexey Khit
b965c191b7 Adds errors output to API 2022-12-05 20:03:26 +03:00
Alexey Khit
7057b4846f Code refactoring 2022-12-05 00:47:46 +03:00
Alexey Khit
a746b96adc Remove old video.html file 2022-12-04 23:24:44 +03:00
Alexey Khit
b7718b33b8 Rewrite WS transport handler 2022-12-04 23:24:20 +03:00
Alexey Khit
69b17230f3 Collect producers codecs during negotiation 2022-12-04 23:16:34 +03:00
Alexey Khit
e2ecd909ab Update stream HTML page 2022-12-04 22:38:44 +03:00
Alexey Khit
ea79da0d53 Rename modes to mode in JS player 2022-12-04 22:38:21 +03:00
Alexey Khit
e64919838c Update MP4 over WebSocket support 2022-12-04 22:37:15 +03:00
Alexey Khit
162b11213d Allow JS player URL to http 2022-12-04 20:09:46 +03:00
Alexey Khit
d27acbd7e3 Fix MSE buffering 2022-12-04 18:38:15 +03:00
Alexey Khit
a692ecd7c1 Fix H265 for WebRTC in Safari 2022-12-03 11:44:26 +03:00
Alexey Khit
98c5366ba9 Fix supported codecs check in JS player 2022-12-03 09:39:45 +03:00
Alexey Khit
3eaaa3fcfa Add support URL as JS player src 2022-12-03 09:39:23 +03:00
Alexey Khit
7409b32836 Fix support old iOS version 2022-12-02 23:12:37 +03:00
Alexey Khit
e2cfdf8419 Support WebRTC, MSE, MSE2, MP4, MJPEG in JS player 2022-12-02 21:37:17 +03:00
Alexey Khit
4c0929d854 Support MP4 over WebSocket 2022-12-02 21:33:51 +03:00
Alexey Khit
258a0ffb91 Support MJPEG over WebSocket 2022-12-02 21:31:41 +03:00
Alexey Khit
999e81c2dd Code refactoring for webrtc candidates 2022-12-01 23:41:12 +03:00
Alexey Khit
8c6729027b Add feature to SETUP new RTSP tracks after PLAY 2022-12-01 23:37:11 +03:00
Alexey Khit
d3bd5eeab5 Added a blank payloader for MJPEG RTSP 2022-12-01 23:20:31 +03:00
Alexey Khit
dbbf2ea310 Update readme about Dahua bug 2022-12-01 14:43:13 +03:00
Alexey Khit
b8234e0c76 Update codecs table in readme 2022-12-01 13:46:59 +03:00
Alexey Khit
96cd753e27 Adds about RTSP server password to readme 2022-12-01 13:35:46 +03:00
Alexey Khit
c522e5bb08 Update docs about new sources 2022-12-01 13:02:29 +03:00
Alexey Khit
a16d8acc30 Add support HTTP JPEG and MJPEG sources 2022-12-01 13:01:48 +03:00
Alexey Khit
684878b4b1 Skip check RTSP auth for localhost requests 2022-11-29 21:04:38 +03:00
Alexey Khit
140a742cee Fix MSE2 check in JS 2022-11-27 10:22:58 +03:00
Alexey Khit
1b518b94fd Add support auth for RTSP server 2022-11-27 10:08:37 +03:00
Alexey Khit
5678121c50 Fix build for mac arm64 2022-11-27 09:57:27 +03:00
Alexey Khit
4915f12bde Add build for win arm64 2022-11-27 09:57:04 +03:00
Alexey Khit
bb91240b95 Remove unnecessary build scripts 2022-11-27 09:54:46 +03:00
Alexey Khit
b1d5d53832 Update to go 1.19 2022-11-27 09:53:11 +03:00
Alexey Khit
31fbbf91bb Remove websocket error on disconnect 2022-11-25 10:04:51 +03:00
Alexey Khit
a3f72fbab9 Fix websocket origin check with wrong port 2022-11-25 10:04:13 +03:00
Alexey Khit
fae59c7992 Update version to 0.1-rc.4 2022-11-24 02:00:31 +03:00
Alexey Khit
aff34f1d21 Totally rewritten MSE player 2022-11-24 01:59:48 +03:00
Alexey Khit
65e7efa775 Support codecs negotiation for MSE 2022-11-23 21:45:10 +03:00
Alexey Khit
3c3e9d282b Update about H265 support in readme 2022-11-23 20:35:11 +03:00
Alexey Khit
bd51069086 Add support CORS for API 2022-11-23 20:34:06 +03:00
Alexey Khit
1ddf7f1a6c Change trace log for stream.mp4 2022-11-23 12:57:11 +03:00
Alexey Khit
0e281e36d3 Fix race (concurency) for Track 2022-11-22 20:03:36 +03:00
Alexey Khit
3d6472cfb1 Update H265 preset for FFmpeg 2022-11-22 17:22:54 +03:00
Alexey Khit
7c31fa2ffd Fix empty SPS for MSE H265 2022-11-22 17:22:26 +03:00
Alexey Khit
0ed9d2410a Fix H265 support for MSE in Safari 2022-11-22 17:21:58 +03:00
Alexey Khit
1c89e7945e Remove printf for webrtc ontrack 2022-11-18 09:13:24 +03:00
Alexey Khit
48635ae341 Add two locks for Track 2022-11-18 09:12:48 +03:00
Alexey Khit
fdb316910f Fix WebRTC async connection 2022-11-16 11:26:56 +03:00
Alexey Khit
e29f2594fa Fix multiple transcoding when track not exists 2022-11-15 16:16:22 +03:00
Alexey Khit
c3da7584b0 Add check on RTSP server requers with empty url path 2022-11-14 19:06:43 +03:00
Alexey Khit
1e247cba92 Igroner app media for WebRTC from Hass 2022-11-14 17:26:59 +03:00
Alexey Khit
01631d9eb0 Update readme 2022-11-14 14:57:43 +03:00
Alexey Khit
4b27d119f0 Update version to 0.1-rc.3 2022-11-14 09:50:47 +03:00
Alexey Khit
dd55c03dc2 Add support multiple transcoding for ffmpeg 2022-11-14 02:26:14 +03:00
Alexey Khit
a4eab1944a Add ffmpeg async option for HomeKit audio 2022-11-14 01:29:45 +03:00
Alexey Khit
eea413a36c Support stream name as ffmpeg input 2022-11-14 01:22:07 +03:00
Alexey Khit
cdd42a8ed2 Change HomeKit codec from AAC to ELD 2022-11-14 00:58:34 +03:00
Alexey Khit
4815ce1baf Fix stop ffmpeg without matching tracks 2022-11-14 00:58:34 +03:00
Alexey Khit
e6d3939c78 Fix external producers 2022-11-13 21:33:09 +03:00
Alexey Khit
220b9ca318 Remove old example file 2022-11-13 20:54:19 +03:00
Alexey Khit
d625620dfd Fix ffmpeg profile warning 2022-11-13 20:09:18 +03:00
Alexey Khit
dd503f3410 Adds rotate template for ffmpeg 2022-11-13 20:05:54 +03:00
Alexey Khit
3e8e87bfcc Fix RTSP unknown response handler 2022-11-13 19:26:22 +03:00
Alexey Khit
64d218886e Add exec early exit handler 2022-11-13 19:24:26 +03:00
Alexey Khit
e91ccc211e Change ffmpeg verbose level to error 2022-11-13 19:18:53 +03:00
Alexey Khit
9f8a219483 Exec stderr will show with debug log 2022-11-13 19:15:12 +03:00
Alexey Khit
b617796941 Improve RTCP for HomeKit 2022-11-13 14:35:53 +03:00
Alexey Khit
77888fe086 Refactoring for HomeKit client 2022-11-11 22:47:14 +03:00
Alexey Khit
7bc3534bcb Add useful links to readmes 2022-11-11 22:44:54 +03:00
Alexey Khit
77bc0630d6 Add teaps to main readme 2022-11-11 22:44:49 +03:00
Alexey Khit
2f68711405 Fix MP4f test consumer 2022-11-11 22:44:34 +03:00
Alexey Khit
b8cab5db60 Remove aacparser from MP4 muxer 2022-11-11 22:44:05 +03:00
Alexey Khit
eae01be71f Add User-Agent to FFmpeg input and output 2022-11-11 18:04:31 +03:00
Alexey Khit
0127115180 Add User-Agent to go2rtc RTSP requests 2022-11-11 18:02:08 +03:00
Alexey Khit
aef84cef6b Add go2rtc version info 2022-11-11 18:01:38 +03:00
Alexey Khit
d478436758 Set video track for WebRTC always first 2022-11-11 16:33:08 +03:00
Alexey Khit
f77db44529 Refactoring for HomeKit client 2022-11-11 13:24:09 +03:00
Alex X
149d1bf235 Merge pull request #101 from felipecrs/patch-2
Mention alternative method to import hass cameras
2022-11-09 20:34:31 +03:00
Felipe Santos
b650475b10 Mention alternative method to import hass cameras 2022-11-09 13:00:30 -03:00
Alexey Khit
16e5406156 Update readme 2022-11-08 09:57:17 +03:00
Alexey Khit
49f6233bde Update RTSP server filters 2022-11-08 01:50:28 +03:00
Alexey Khit
78c5c70c73 Add duration API for MP4 file 2022-11-08 01:29:58 +03:00
Alexey Khit
32651c74ab Fix frame.mp4 support 2022-11-08 01:13:38 +03:00
Alexey Khit
5c64d1f847 Update MSE procession on JS side 2022-11-08 00:37:32 +03:00
Alexey Khit
717af29630 Refactoring 2022-11-08 00:37:13 +03:00
Alexey Khit
ea18475d31 Split MSE data on packets 2022-11-07 23:35:36 +03:00
Alexey Khit
701a9c69ec Update write websocket func 2022-11-07 23:35:08 +03:00
Alexey Khit
c06253c8b2 Fix producer request new track after start 2022-11-07 17:48:45 +03:00
Alexey Khit
3a07e9fa03 Fix lock on mp4 restarts 2022-11-07 13:32:27 +03:00
Alexey Khit
e1bc30fab3 Add support AAC for RTSP 2022-11-07 11:02:26 +03:00
Alexey Khit
d16ae0972f Code refactoring 2022-11-07 11:01:19 +03:00
Alexey Khit
8b93c97e69 Add support AAC for RTMP to MP4 2022-11-06 22:44:48 +03:00
Alexey Khit
d8158bc1e3 Update stream log message 2022-11-04 22:27:11 +03:00
Alexey Khit
f4f588d2c6 Add mutex to stream 2022-11-04 22:20:52 +03:00
Alexey Khit
e287b52808 Add check for RTSPtoWeb API 2022-11-04 22:12:00 +03:00
Alexey Khit
ff96257252 Add backchannel=0 option to readme 2022-11-04 21:49:36 +03:00
Alexey Khit
909f21b7e4 Update docs about TURN server 2022-11-04 21:44:12 +03:00
Alexey Khit
7d6a5b44f8 Add frame.jpeg api for MJPEG stream 2022-11-04 21:22:33 +03:00
Alexey Khit
278f7696b6 Make sink private for Track 2022-11-04 20:54:35 +03:00
Alexey Khit
3cbf2465ae Fix loopback producer 2022-11-04 17:52:26 +03:00
Alexey Khit
e9ea7a0b1f Add reconnection feature 2022-11-04 17:23:42 +03:00
Alexey Khit
0231fc3a90 Code refactoring 2022-11-04 17:16:42 +03:00
Alexey Khit
9ef2633840 Add 5 sec timeout to ffmpeg rtsp 2022-11-04 17:06:24 +03:00
Alexey Khit
5a8df3e90a Change RTSP dial timeout to 5 sec 2022-11-04 17:05:57 +03:00
Alexey Khit
a31cbec3eb Fix check RTSP transport prefix 2022-11-04 17:05:30 +03:00
Alexey Khit
54f547977e Add mutext to streams handlers 2022-11-04 17:04:47 +03:00
Alexey Khit
65d91e02bd Move NewLogger to function 2022-11-04 17:03:56 +03:00
Alexey Khit
7fc3f0f641 Ignore srtp init in stack list func 2022-11-04 10:07:50 +03:00
Alexey Khit
7725d5ed31 Rewrite get handlers code 2022-11-04 06:24:39 +03:00
Alexey Khit
6c1b9daa8b Update logs for RTSP packets (disabled) 2022-11-03 11:25:47 +03:00
Alexey Khit
6d432574bf Make main logger global 2022-11-03 10:26:26 +03:00
Alexey Khit
616f69c88b Cache public IP for 5 min 2022-11-02 12:48:36 +03:00
Alexey Khit
f72440712b Add timeout to GetPublicIP func 2022-11-02 12:47:26 +03:00
Alexey Khit
ceed146fb8 Add webrtc sync API 2022-11-02 12:46:39 +03:00
Alexey Khit
f17dadbbbf Rewrite keepalive and add timeouts for RTSP 2022-11-02 10:50:11 +03:00
Alexey Khit
3d4514eab9 Fix loopback for stream 2022-11-02 08:51:54 +03:00
Alexey Khit
2629dccb81 Rename HTTP-FLV 2022-10-31 20:05:28 +03:00
Alexey Khit
04f1aa2900 Fix trash in webrtc.html 2022-10-31 08:34:32 +03:00
Alexey Khit
0dacdea1c3 Add support RTMPT (flv over HTTP) 2022-10-30 17:17:42 +03:00
Alexey Khit
24082b1616 Fix backchannel reconnection issue 2022-10-29 11:33:01 +03:00
Alexey Khit
7964b1743b Fix RTSP processing for Amcrest IP4M-1051 2022-10-29 11:29:53 +03:00
Alexey Khit
49773a1ece Add mjpeg link to stream to main page 2022-10-21 12:01:00 +03:00
Alexey Khit
c97a48a73f Fix mjpeg for 2K cameras 2022-10-21 12:00:00 +03:00
Alexey Khit
e03231ebb4 fix ffmpeg transcoding for Reolink RLC-510A 2022-10-21 10:58:56 +03:00
Alexey Khit
649525a842 Merge remote-tracking branch 'origin/master' 2022-10-21 10:54:54 +03:00
Alex X
d411c1a25c Merge pull request #76 from NickM-27/name_api_stream
Add ability for API to set name of stream
2022-10-14 06:59:31 +03:00
Nicolas Mowen
2f0bcf4ae0 Add ability for API to set name of stream 2022-10-13 14:58:26 -06:00
Alexey Khit
831c504cab Fix memory usage for RTSP processing 2022-10-05 21:15:59 +03:00
Alexey Khit
12925a6bc5 Fix TP-Link Tapo TC70 support 2022-10-05 19:43:36 +03:00
Alexey Khit
e50e929150 Fix empty SPS for mp4 format 2022-10-05 15:35:30 +03:00
Alexey Khit
d0c87e0379 Support SEI NAL from ffmpeg transcoding 2022-10-05 15:35:04 +03:00
Alexey Khit
247b61790e Update EncodeAVC for empty NALs 2022-10-05 15:34:34 +03:00
Alexey Khit
2ec618334a Adds NALs types logger 2022-10-05 15:33:51 +03:00
Alexey Khit
6f9976c806 Rework RTSP and RTMP processing 2022-10-05 13:25:29 +03:00
Alexey Khit
17b3a4cf3a Code refactoring 2022-10-05 13:23:31 +03:00
Alexey Khit
ba30f46c02 Fix FmtpLine for RTMP 2022-10-05 10:50:00 +03:00
Alexey Khit
4134f2a89c Fix timestamp for RTMP 2022-10-05 10:48:37 +03:00
Alexey Khit
a81160bea1 Fix support Escam Q6 camera 2022-10-03 21:12:27 +03:00
Alexey Khit
80392acb78 Fix audio copy #46 2022-09-24 08:24:52 +03:00
Alexey Khit
5afac513b4 Adds codecs section to readme 2022-09-22 00:22:44 +03:00
Alexey Khit
2243110e08 Fix H265 support for different sources 2022-09-21 23:43:52 +03:00
Alexey Khit
04a6e64650 Adds support WebRTC + H265 to Safari 2022-09-21 22:28:59 +03:00
Alexey Khit
62c13f016b Remove broken Safari codec from WebRTC API 2022-09-21 22:27:46 +03:00
Alexey Khit
9596c6139f Adds support H265 for MP4 2022-09-18 20:24:21 +03:00
Alexey Khit
34f5b99126 Update codecs.html 2022-09-18 17:37:44 +03:00
Alexey Khit
b562392d45 Remove unnecessary imports 2022-09-18 17:14:59 +03:00
Alexey Khit
eb8a4919a2 Adds fix on RemoveConsumer panic 2022-09-18 17:14:18 +03:00
Alexey Khit
237fbf23a1 FIx backward support for RTSPtoWebRTC API 2022-09-18 02:01:43 +03:00
Alexey Khit
12a73b00cb Fix readme 2022-09-17 20:32:17 +03:00
Alexey Khit
ce0fac959f Adds module MJPEG 2022-09-17 19:13:25 +03:00
Alexey Khit
1b14be7033 Update readme about Hass module 2022-09-17 06:41:56 +03:00
Alexey Khit
bbbade4097 Adds rtsp link to index.html 2022-09-16 17:59:54 +03:00
Alexey Khit
8f43ad2a35 Adds pretty print to info 2022-09-16 17:39:29 +03:00
Alexey Khit
105331d50f Fix track async access 2022-09-16 17:22:48 +03:00
Alexey Khit
a45d0b507b Code refactoring 2022-09-16 17:04:00 +03:00
Alexey Khit
407ccc45bc Adds URL templates to integration with Hass 2022-09-16 17:03:03 +03:00
Alexey Khit
428628fcce Code refactoring 2022-09-16 17:00:56 +03:00
Alexey Khit
fa23bb6899 Handle FFmpeg RTMP as HTTP source 2022-09-16 17:00:24 +03:00
Alexey Khit
71e1c840a7 Fix base_path for integration with Hass 2022-09-16 14:19:23 +03:00
Alexey Khit
63b9639e86 Adds trace logs for API 2022-09-16 12:11:40 +03:00
Alexey Khit
ae3e1372c8 Adds support RTSPtoWeb API (entity_id for zero config from Hass) 2022-09-16 12:10:58 +03:00
Alexey Khit
800ebb39be Adds canditates from domain resolver 2022-09-15 09:07:53 +03:00
Alexey Khit
3a10cb25bb Fix green video from RTSP H264 2022-09-15 06:55:05 +03:00
Alexey Khit
7784b0e64c Adds about ivideon to readme 2022-09-14 17:59:12 +03:00
Alexey Khit
945b486fe0 Update readme about new source Echo 2022-09-14 15:53:46 +03:00
Alexey Khit
d72d7b089c Reduce docker container size and add python inside 2022-09-14 14:12:43 +03:00
Alexey Khit
d339fbe712 Fix backchannel option for Dahua VTO2111D 2022-09-13 21:57:07 +03:00
Alexey Khit
3aeb278c47 Option to disable backchannel for RTSP 2022-09-13 21:54:49 +03:00
Alexey Khit
c92c1fc3e9 Adds echo source 2022-09-13 15:42:39 +03:00
Alexey Khit
def57119f4 Move shell QuoteSplit to separate pkg 2022-09-13 15:42:23 +03:00
Alexey Khit
b20275d2b5 Adds support ivideon source 2022-09-13 14:41:28 +03:00
Alexey Khit
a11ca1da6e Adds error output to producer start function 2022-09-13 14:40:58 +03:00
Alexey Khit
0fb7132947 Move SplitAVC to public function 2022-09-13 14:39:55 +03:00
Alexey Khit
0f9e3c97c5 Update mp4 entry duration 2022-09-13 14:39:19 +03:00
Alexey Khit
e049a17216 Adds error handler for mp4 init 2022-09-13 14:38:54 +03:00
Alexey Khit
217c8c2bf6 Update readme about MP4 module 2022-09-10 06:26:15 +03:00
Alexey Khit
9f0153e2a8 Adds skip SEI frame 2022-09-09 19:32:56 +03:00
Alexey Khit
b2eaf03914 Adds match any clockrate or channels 2022-09-09 19:32:36 +03:00
Alexey Khit
8b54444c89 Adds mp4 module 2022-09-09 19:31:52 +03:00
Alexey Khit
76b352d67f Add exec launch time to debug 2022-09-07 12:21:36 +03:00
Alexey Khit
e8edb65a31 Adds ignoring unnecessary ffmpeg rtsp input tracks 2022-09-07 11:31:15 +03:00
Alexey Khit
88a6208912 Update ffmpeg output param name 2022-09-07 11:29:59 +03:00
Alexey Khit
14b6df68ce Adds support nginx with wrong port 2022-09-06 18:09:44 +03:00
Alexey Khit
77080663ee Add the feature for update to any version 2022-09-06 14:10:08 +03:00
Alexey Khit
d25d27a0ee Fix SDP parsing for noname camera 2022-09-06 12:43:10 +03:00
268 changed files with 23619 additions and 6562 deletions

View File

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

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

@@ -0,0 +1,78 @@
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@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
- name: Build and push Hardware
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

37
.github/workflows/gh-pages.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages
on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow one concurrent deployment
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: './website'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2

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

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

103
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,103 @@
name: Test Build and Run
on:
push:
branches:
- '*'
pull_request:
merge_group:
workflow_dispatch:
jobs:
build-test:
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
arch: [amd64, arm64]
runs-on: ${{ matrix.os }}
continue-on-error: true
env:
GOARCH: ${{ matrix.arch }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: '1.20'
- name: Build Go binary
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
- name: Test Go binary on linux
if: matrix.os == 'ubuntu-latest'
run: |
if [ "${{ matrix.arch }}" = "amd64" ]; then
./go2rtc -version
else
sudo apt-get update && sudo apt-get install -y qemu-user-static
sudo cp /usr/bin/qemu-aarch64-static .
sudo chown $USER:$USER ./qemu-aarch64-static
qemu-aarch64-static ./go2rtc -version
fi
- name: Test Go binary on macos
if: matrix.os == 'macos-latest'
run: |
if [ "${{ matrix.arch }}" = "amd64" ]; then
./go2rtc -version
else
echo "ARM64 architecture is not yet supported on macOS"
fi
- name: Test Go binary on windows
if: matrix.os == 'windows-latest'
run: |
if ("${{ matrix.arch }}" -eq "amd64") {
.\go2rtc* -version
} else {
Write-Host "ARM64 architecture is not yet supported on Windows"
}
docker-test:
strategy:
matrix:
platform:
- amd64
- "386"
- arm/v7
- arm64/v8
continue-on-error: true
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/${{ matrix.platform }}
push: false
load: true
tags: go2rtc-${{ matrix.platform }}
- name: test run
run: |
docker run --platform=linux/${{ matrix.platform }} --rm go2rtc-${{ matrix.platform }} go2rtc -version
- name: Build and push Hardware
if: matrix.platform == 'amd64'
uses: docker/build-push-action@v4
with:
context: .
file: hardware.Dockerfile
platforms: linux/amd64
push: false
load: true
tags: go2rtc-${{ matrix.platform }}-hardware
- name: test run
if: matrix.platform == 'amd64'
run: |
docker run --platform=linux/${{ matrix.platform }} --rm go2rtc-${{ matrix.platform }}-hardware go2rtc -version

62
Dockerfile Normal file
View File

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

850
README.md

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,14 +0,0 @@
#!/usr/bin/with-contenv bashio
set +e
# set cwd for go2rtc (for config file, Hass itegration, etc)
cd /config
# add the feature to override go2rtc binary from Hass config folder
export PATH="/config:$PATH"
while true; do
go2rtc
sleep 5
done

View File

@@ -1,4 +0,0 @@
**Project layout**
- https://github.com/golang-standards/project-layout
- https://github.com/micro/micro

View File

@@ -1,123 +0,0 @@
package api
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/gorilla/websocket"
"github.com/rs/zerolog"
"net"
"net/http"
)
func Init() {
var cfg struct {
Mod struct {
Listen string `yaml:"listen"`
BasePath string `yaml:"base_path"`
StaticDir string `yaml:"static_dir"`
} `yaml:"api"`
}
// default config
cfg.Mod.Listen = ":1984"
// load config from YAML
app.LoadConfig(&cfg)
if cfg.Mod.Listen == "" {
return
}
basePath = cfg.Mod.BasePath
log = app.GetLogger("api")
initStatic(cfg.Mod.StaticDir)
HandleFunc("/api/frame.mp4", frameHandler)
HandleFunc("/api/frame.raw", frameHandler)
HandleFunc("/api/streams", streamsHandler)
HandleFunc("/api/ws", apiWS)
// ensure we can listen without errors
listener, err := net.Listen("tcp", cfg.Mod.Listen)
if err != nil {
log.Fatal().Err(err).Msg("[api] listen")
return
}
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
go func() {
s := http.Server{}
if err = s.Serve(listener); err != nil {
log.Fatal().Err(err).Msg("[api] serve")
}
}()
}
func HandleFunc(pattern string, handler http.HandlerFunc) {
http.HandleFunc(basePath+pattern, handler)
}
func HandleWS(msgType string, handler WSHandler) {
wsHandlers[msgType] = handler
}
var basePath string
var log zerolog.Logger
var wsHandlers = make(map[string]WSHandler)
func streamsHandler(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
switch r.Method {
case "PUT":
streams.Get(src)
return
case "DELETE":
streams.Delete(src)
return
}
var v interface{}
if src != "" {
v = streams.Get(src)
} else {
v = streams.All()
}
data, err := json.Marshal(v)
if err != nil {
log.Error().Err(err).Msg("[api.streams] marshal")
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.streams] write")
}
}
func apiWS(w http.ResponseWriter, r *http.Request) {
ctx := new(Context)
if err := ctx.Upgrade(w, r); err != nil {
log.Error().Err(err).Msg("[api.ws] upgrade")
return
}
defer ctx.Close()
for {
msg := new(streamer.Message)
if err := ctx.Conn.ReadJSON(msg); err != nil {
if websocket.IsUnexpectedCloseError(
err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure,
) {
log.Error().Err(err).Msg("[api.ws] readJSON")
}
return
}
handler := wsHandlers[msg.Type]
if handler != nil {
handler(ctx, msg)
}
}
}

View File

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

View File

@@ -1,67 +0,0 @@
package api
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/gorilla/websocket"
"net/http"
"sync"
)
type WSHandler func(ctx *Context, msg *streamer.Message)
var apiWsUp = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 512000,
}
type Context struct {
Conn *websocket.Conn
Request *http.Request
Consumer interface{} // TODO: rewrite
onClose []func()
mu sync.Mutex
}
func (ctx *Context) Upgrade(w http.ResponseWriter, r *http.Request) (err error) {
ctx.Conn, err = apiWsUp.Upgrade(w, r, nil)
ctx.Request = r
return
}
func (ctx *Context) Close() {
for _, f := range ctx.onClose {
f()
}
_ = ctx.Conn.Close()
}
func (ctx *Context) Write(msg interface{}) {
ctx.mu.Lock()
defer ctx.mu.Unlock()
var err error
switch msg := msg.(type) {
case *streamer.Message:
err = ctx.Conn.WriteJSON(msg)
case []byte:
err = ctx.Conn.WriteMessage(websocket.BinaryMessage, msg)
default:
return
}
if err != nil {
//panic(err) // TODO: fix panic
}
}
func (ctx *Context) Error(err error) {
ctx.Write(&streamer.Message{
Type: "error", Value: err.Error(),
})
}
func (ctx *Context) OnClose(f func()) {
ctx.onClose = append(ctx.onClose, f)
}

View File

@@ -1,91 +0,0 @@
package app
import (
"flag"
"github.com/rs/zerolog"
"gopkg.in/yaml.v3"
"io"
"os"
"runtime"
)
func Init() {
config := flag.String(
"config",
"go2rtc.yaml",
"Path to go2rtc configuration file",
)
flag.Parse()
data, _ = os.ReadFile(*config)
var cfg struct {
Mod map[string]string `yaml:"log"`
}
if data != nil {
if err := yaml.Unmarshal(data, &cfg); err != nil {
println("ERROR: " + err.Error())
}
}
var writer io.Writer = os.Stdout
// styles
format := cfg.Mod["format"]
if format != "json" {
writer = zerolog.ConsoleWriter{
Out: writer, TimeFormat: "15:04:05.000",
NoColor: writer != os.Stdout || format == "text",
}
}
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
lvl, err := zerolog.ParseLevel(cfg.Mod["level"])
if err != nil || lvl == zerolog.NoLevel {
lvl = zerolog.InfoLevel
}
log = zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
modules = cfg.Mod
path, _ := os.Getwd()
log.Debug().Str("os", runtime.GOOS).Str("arch", runtime.GOARCH).
Str("cwd", path).Int("conf_size", len(data)).Msgf("[app]")
}
func LoadConfig(v interface{}) {
if data != nil {
if err := yaml.Unmarshal(data, v); err != nil {
log.Warn().Err(err).Msg("[app] read config")
}
}
}
func GetLogger(module string) zerolog.Logger {
if s, ok := modules[module]; ok {
lvl, err := zerolog.ParseLevel(s)
if err != nil {
log.Warn().Err(err).Msg("[log]")
return log
}
return log.Level(lvl)
}
return log
}
// internal
// data - config content
var data []byte
// log - main logger
var log zerolog.Logger
// modules log levels
var modules map[string]string

View File

@@ -1,27 +0,0 @@
package debug
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"net/http"
"os"
"strconv"
)
func Init() {
api.HandleFunc("/api/stack", stackHandler)
api.HandleFunc("/api/exit", exitHandler)
streams.HandleFunc("null", nullHandler)
}
func exitHandler(_ http.ResponseWriter, r *http.Request) {
s := r.URL.Query().Get("code")
code, _ := strconv.Atoi(s)
os.Exit(code)
}
func nullHandler(string) (streamer.Producer, error) {
return nil, nil
}

View File

@@ -1,121 +0,0 @@
package exec
import (
"crypto/md5"
"encoding/hex"
"errors"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
"os"
"os/exec"
"strings"
"time"
)
func Init() {
// depends on RTSP server
if rtsp.Port == "" {
return
}
rtsp.OnProducer = func(prod streamer.Producer) bool {
if conn := prod.(*pkg.Conn); conn != nil {
if waiter := waiters[conn.URL.Path]; waiter != nil {
waiter <- prod
return true
}
}
return false
}
streams.HandleFunc("exec", Handle)
log = app.GetLogger("exec")
// TODO: add sync.Mutex
waiters = map[string]chan streamer.Producer{}
}
func Handle(url string) (streamer.Producer, error) {
sum := md5.Sum([]byte(url))
path := "/" + hex.EncodeToString(sum[:])
url = strings.Replace(
url, "{output}", "rtsp://localhost:"+rtsp.Port+path, 1,
)
// remove `exec:`
args := QuoteSplit(url[5:])
cmd := exec.Command(args[0], args[1:]...)
if log.Trace().Enabled() {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
ch := make(chan streamer.Producer)
waiters[path] = ch
defer delete(waiters, path)
log.Debug().Str("url", url).Msg("[exec] run")
if err := cmd.Start(); err != nil {
log.Error().Err(err).Str("url", url).Msg("[exec]")
return nil, err
}
select {
case <-time.After(time.Second * 15):
_ = cmd.Process.Kill()
log.Error().Str("url", url).Msg("[exec] timeout")
return nil, errors.New("timeout")
case prod := <-ch:
return prod, nil
}
}
// internal
var log zerolog.Logger
var waiters map[string]chan streamer.Producer
func QuoteSplit(s string) []string {
var a []string
for len(s) > 0 {
is := strings.IndexByte(s, ' ')
if is >= 0 {
// skip prefix and double spaces
if is == 0 {
// goto next symbol
s = s[1:]
continue
}
// check if quote in word
if i := strings.IndexByte(s[:is], '"'); i >= 0 {
// search quote end
if is = strings.Index(s, `" `); is > 0 {
is += 1
} else {
is = -1
}
}
}
if is >= 0 {
a = append(a, strings.ReplaceAll(s[:is], `"`, ""))
s = s[is+1:]
} else {
//add last word
a = append(a, s)
break
}
}
return a
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,141 +0,0 @@
package ffmpeg
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"net/url"
"strings"
)
func Init() {
var cfg struct {
Mod map[string]string `yaml:"ffmpeg"`
}
// defaults
cfg.Mod = map[string]string{
"bin": "ffmpeg",
// inputs
"file": "-re -stream_loop -1 -i {input}",
"http": "-fflags nobuffer -flags low_delay -i {input}",
"rtsp": "-fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input}",
// output
"out": "-rtsp_transport tcp -f rtsp {output}",
// `-g 30` - group of picture, GOP, keyframe interval
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
// `-tune zerolatency` - for minimal latency
// `-profile main -level 4.1` - most used streaming profile
// `-pix_fmt yuv420p` - if input pix format 4:2:2
"h264": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1 -pix_fmt yuv420p",
"h264/ultra": "-codec:v libx264 -g 30 -preset ultrafast -tune zerolatency",
"h264/high": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency",
"h265": "-codec:v libx265 -g 30 -preset ultrafast -tune zerolatency",
"opus": "-codec:a libopus -ar 48000 -ac 2",
"pcmu": "-codec:a pcm_mulaw -ar 8000 -ac 1",
"pcmu/16000": "-codec:a pcm_mulaw -ar 16000 -ac 1",
"pcmu/48000": "-codec:a pcm_mulaw -ar 48000 -ac 1",
"pcma": "-codec:a pcm_alaw -ar 8000 -ac 1",
"pcma/16000": "-codec:a pcm_alaw -ar 16000 -ac 1",
"pcma/48000": "-codec:a pcm_alaw -ar 48000 -ac 1",
"aac/16000": "-codec:a aac -ar 16000 -ac 1",
}
app.LoadConfig(&cfg)
tpl := cfg.Mod
streams.HandleFunc("ffmpeg", func(s string) (streamer.Producer, error) {
s = s[7:] // remove `ffmpeg:`
var query url.Values
if i := strings.IndexByte(s, '#'); i > 0 {
query = parseQuery(s[i+1:])
s = s[:i]
}
var input string
if i := strings.IndexByte(s, ':'); i > 0 {
switch s[:i] {
case "http", "https":
input = strings.Replace(tpl["http"], "{input}", s, 1)
case "rtsp", "rtsps":
input = strings.Replace(tpl["rtsp"], "{input}", s, 1)
}
}
if input == "" {
if strings.HasPrefix(s, "device?") {
var err error
input, err = device.GetInput(s)
if err != nil {
return nil, err
}
} else {
input = strings.Replace(tpl["file"], "{input}", s, 1)
}
}
s = "exec:" + tpl["bin"] + " -hide_banner " + input
if query != nil {
for _, raw := range query["raw"] {
s += " " + raw
}
// TODO: multiple codecs via -map
// s += fmt.Sprintf(" -map 0:v:0 -c:v:%d copy", i)
for _, video := range query["video"] {
if video == "copy" {
s += " -codec:v copy"
} else {
s += " " + tpl[video]
}
}
for _, audio := range query["audio"] {
if audio == "copy" {
s += " -codec:v copy"
} else {
s += " " + tpl[audio]
}
}
if query["video"] == nil {
s += " -vn"
}
if query["audio"] == nil {
s += " -an"
}
} else {
s += " -c copy"
}
s += " " + tpl["out"]
return exec.Handle(s)
})
device.Bin = cfg.Mod["bin"]
device.Init()
}
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
}

23
cmd/go2rtc_rtsp/main.go Normal file
View File

@@ -0,0 +1,23 @@
package main
import (
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"os"
"os/signal"
"syscall"
)
func main() {
app.Init()
streams.Init()
rtsp.Init()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
println("exit OK")
}

View File

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

View File

@@ -1,157 +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/homekit"
"github.com/AlexxIT/go2rtc/pkg/homekit/mdns"
"net/http"
"net/url"
"strings"
)
func apiHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
items := make([]interface{}, 0)
for name, src := range store.GetDict("streams") {
if src := src.(string); strings.HasPrefix(src, "homekit") {
u, err := url.Parse(src)
if err != nil {
continue
}
device := Device{
Name: name,
Addr: u.Host,
Paired: true,
}
items = append(items, device)
}
}
for info := range mdns.GetAll() {
if !strings.HasSuffix(info.Name, mdns.Suffix) {
continue
}
name := info.Name[:len(info.Name)-len(mdns.Suffix)]
device := Device{
Name: strings.ReplaceAll(name, "\\", ""),
Addr: fmt.Sprintf("%s:%d", info.AddrV4, info.Port),
}
for _, field := range info.InfoFields {
switch field[:2] {
case "id":
device.ID = field[3:]
case "md":
device.Model = field[3:]
case "sf":
device.Paired = field[3] == '0'
}
}
items = append(items, device)
}
data, err := json.Marshal(items)
if err != nil {
log.Error().Err(err).Msg("[api.homekit]")
return
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.homekit]")
}
case "POST":
// TODO: post params...
id := r.URL.Query().Get("id")
pin := r.URL.Query().Get("pin")
client, err := homekit.Pair(id, pin)
if err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] pair")
// response error
_, err = w.Write([]byte(err.Error()))
return
}
name := r.URL.Query().Get("name")
dict := store.GetDict("streams")
dict[name] = client.URL()
if err = store.Set("streams", dict); err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] save to store")
// response error
_, err = w.Write([]byte(err.Error()))
}
streams.New(name, client.URL())
case "DELETE":
src := r.URL.Query().Get("src")
dict := store.GetDict("streams")
for name, rawURL := range dict {
if name != src {
continue
}
client, err := homekit.NewClient(rawURL.(string))
if err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] new client")
// response error
_, err = w.Write([]byte(err.Error()))
return
}
if err = client.Dial(); err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] client dial")
// response error
_, err = w.Write([]byte(err.Error()))
return
}
go client.Handle()
if err = client.ListPairings(); err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] unpair")
// response error
_, err = w.Write([]byte(err.Error()))
return
}
if err = client.DeletePairing(client.ClientID); err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] unpair")
// response error
_, err = w.Write([]byte(err.Error()))
}
delete(dict, name)
if err = store.Set("streams", dict); err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] store set")
// response error
_, err = w.Write([]byte(err.Error()))
}
return
}
}
}
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,39 +0,0 @@
package homekit
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
)
func Init() {
log = app.GetLogger("homekit")
streams.HandleFunc("homekit", streamHandler)
api.HandleFunc("/api/homekit", apiHandler)
}
var log zerolog.Logger
func streamHandler(url string) (streamer.Producer, error) {
client, err := homekit.NewClient(url)
if err != nil {
return nil, err
}
if err = client.Dial(); err != nil {
return nil, err
}
// start gorutine for reading responses from camera
go func() {
if err = client.Handle(); err != nil {
log.Warn().Err(err).Msg("[homekit] client")
}
}()
return &Producer{client: client}, nil
}

View File

@@ -1,189 +0,0 @@
package homekit
import (
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/srtp"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/homekit/camera"
pkg "github.com/AlexxIT/go2rtc/pkg/srtp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/brutella/hap/characteristic"
"github.com/brutella/hap/rtp"
"net"
"strconv"
)
type Producer struct {
streamer.Element
client *homekit.Client
medias []*streamer.Media
tracks []*streamer.Track
sessions []*pkg.Session
}
func (c *Producer) GetMedias() []*streamer.Media {
if c.medias == nil {
c.medias = c.getMedias()
}
return c.medias
}
func (c *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
for _, track := range c.tracks {
if track.Codec == codec {
return track
}
}
track := &streamer.Track{Codec: codec, Direction: media.Direction}
c.tracks = append(c.tracks, track)
return track
}
func (c *Producer) Start() error {
if c.tracks == nil {
return errors.New("producer without tracks")
}
// get our server local IP-address
host, _, err := net.SplitHostPort(c.client.LocalAddr())
if err != nil {
return err
}
// get our server SRTP port
port, err := strconv.Atoi(srtp.Port)
if err != nil {
return err
}
// setup HomeKit stream session
hkSession := camera.NewSession()
hkSession.SetLocalEndpoint(host, uint16(port))
// create client for processing camera accessory
cam := camera.NewClient(c.client)
// try to start HomeKit stream
if err = cam.StartStream2(hkSession); err != nil {
panic(err) // TODO: fixme
}
// SRTP Video Session
vs := &pkg.Session{
LocalSSRC: hkSession.Config.Video.RTP.Ssrc,
RemoteSSRC: hkSession.Answer.SsrcVideo,
Track: c.tracks[0],
}
if err = vs.SetKeys(
hkSession.Offer.Video.MasterKey, hkSession.Offer.Video.MasterSalt,
hkSession.Answer.Video.MasterKey, hkSession.Answer.Video.MasterSalt,
); err != nil {
return err
}
// SRTP Audio Session
as := &pkg.Session{
LocalSSRC: hkSession.Config.Audio.RTP.Ssrc,
RemoteSSRC: hkSession.Answer.SsrcAudio,
Track: &streamer.Track{},
}
if err = as.SetKeys(
hkSession.Offer.Audio.MasterKey, hkSession.Offer.Audio.MasterSalt,
hkSession.Answer.Audio.MasterKey, hkSession.Answer.Audio.MasterSalt,
); err != nil {
return err
}
srtp.AddSession(vs)
srtp.AddSession(as)
c.sessions = []*pkg.Session{vs, as}
return nil
}
func (c *Producer) Stop() error {
err := c.client.Close()
for _, session := range c.sessions {
srtp.RemoveSession(session)
}
return err
}
func (c *Producer) getMedias() []*streamer.Media {
var medias []*streamer.Media
accs, err := c.client.GetAccessories()
acc := accs[0]
if err != nil {
panic(err)
}
// get supported video config (not really necessary)
char := acc.GetCharacter(characteristic.TypeSupportedVideoStreamConfiguration)
v1 := &rtp.VideoStreamConfiguration{}
if err = char.ReadTLV8(v1); err != nil {
panic(err)
}
for _, hkCodec := range v1.Codecs {
codec := &streamer.Codec{ClockRate: 90000}
switch hkCodec.Type {
case rtp.VideoCodecType_H264:
codec.Name = streamer.CodecH264
default:
panic(fmt.Sprintf("unknown codec: %d", hkCodec.Type))
}
media := &streamer.Media{
Kind: streamer.KindVideo, Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
}
medias = append(medias, media)
}
char = acc.GetCharacter(characteristic.TypeSupportedAudioStreamConfiguration)
v2 := &rtp.AudioStreamConfiguration{}
if err = char.ReadTLV8(v2); err != nil {
panic(err)
}
for _, hkCodec := range v2.Codecs {
codec := &streamer.Codec{
Channels: uint16(hkCodec.Parameters.Channels),
}
switch hkCodec.Type {
case rtp.AudioCodecType_AAC_ELD:
codec.Name = streamer.CodecAAC
default:
panic(fmt.Sprintf("unknown codec: %d", hkCodec.Type))
}
switch hkCodec.Parameters.Samplerate {
case rtp.AudioCodecSampleRate8Khz:
codec.ClockRate = 8000
case rtp.AudioCodecSampleRate16Khz:
codec.ClockRate = 16000
case rtp.AudioCodecSampleRate24Khz:
codec.ClockRate = 24000
default:
panic(fmt.Sprintf("unknown clockrate: %d", hkCodec.Parameters.Samplerate))
}
media := &streamer.Media{
Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
}
medias = append(medias, media)
}
return medias
}

View File

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

View File

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

View File

@@ -1,211 +0,0 @@
package rtsp
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
"net"
)
func Init() {
var conf struct {
Mod struct {
Listen string `yaml:"listen"`
} `yaml:"rtsp"`
}
// default config
conf.Mod.Listen = ":8554"
app.LoadConfig(&conf)
log = app.GetLogger("rtsp")
// RTSP client support
streams.HandleFunc("rtsp", rtspHandler)
streams.HandleFunc("rtsps", rtspHandler)
streams.HandleFunc("rtspx", rtspHandler)
// RTSP server support
address := conf.Mod.Listen
if address != "" {
_, Port, _ = net.SplitHostPort(address)
go worker(address)
}
}
var Port string
var OnProducer func(conn streamer.Producer) bool // TODO: maybe rewrite...
// internal
var log zerolog.Logger
func rtspHandler(url string) (streamer.Producer, error) {
conn, err := rtsp.NewClient(url)
if err != nil {
return nil, err
}
if log.Trace().Enabled() {
conn.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case *tcp.Request:
log.Trace().Msgf("[rtsp] client request:\n%s", msg)
case *tcp.Response:
log.Trace().Msgf("[rtsp] client response:\n%s", msg)
}
})
}
if err = conn.Dial(); err != nil {
return nil, err
}
conn.Backchannel = true
if err = conn.Describe(); err != nil {
// second try without backchannel, we need to reconnect
if err = conn.Dial(); err != nil {
return nil, err
}
conn.Backchannel = false
if err = conn.Describe(); err != nil {
return nil, err
}
}
return conn, nil
}
func worker(address string) {
srv, err := tcp.NewServer(address)
if err != nil {
log.Error().Err(err).Msg("[rtsp] listen")
return
}
log.Info().Str("addr", address).Msg("[rtsp] listen")
srv.Listen(func(msg interface{}) {
switch msg.(type) {
case net.Conn:
var name string
var onDisconnect func()
trace := log.Trace().Enabled()
conn := rtsp.NewServer(msg.(net.Conn))
conn.Listen(func(msg interface{}) {
if trace {
switch msg := msg.(type) {
case *tcp.Request:
log.Trace().Msgf("[rtsp] server request:\n%s", msg)
case *tcp.Response:
log.Trace().Msgf("[rtsp] server response:\n%s", msg)
}
}
switch msg {
case rtsp.MethodDescribe:
name = conn.URL.Path[1:]
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
stream := streams.Get(name) // TODO: rewrite
if stream == nil {
return
}
initMedias(conn)
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
return
}
onDisconnect = func() {
stream.RemoveConsumer(conn)
}
case rtsp.MethodAnnounce:
if OnProducer != nil {
if OnProducer(conn) {
return
}
}
name = conn.URL.Path[1:]
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
str := streams.Get(conn.URL.Path[1:])
if str == nil {
return
}
str.AddProducer(conn)
onDisconnect = func() {
str.RemoveProducer(conn)
}
case streamer.StatePlaying:
log.Debug().Str("stream", name).Msg("[rtsp] start")
}
})
if err = conn.Accept(); err != nil {
log.Warn().Err(err).Msg("[rtsp] accept")
return
}
if err = conn.Handle(); err != nil {
//log.Warn().Err(err).Msg("[rtsp] handle server")
}
if onDisconnect != nil {
onDisconnect()
}
log.Debug().Str("stream", name).Msg("[rtsp] disconnect")
}
})
srv.Serve()
}
func initMedias(conn *rtsp.Conn) {
// set media candidates from query list
for key, value := range conn.URL.Query() {
switch key {
case streamer.KindVideo, streamer.KindAudio:
for _, value := range value {
media := &streamer.Media{
Kind: key, Direction: streamer.DirectionRecvonly,
}
switch value {
case "", "copy": // pass empty codecs list
default:
codec := streamer.NewCodec(value)
media.Codecs = append(media.Codecs, codec)
}
conn.Medias = append(conn.Medias, media)
}
}
}
// set default media candidates if query is empty
if conn.Medias == nil {
conn.Medias = []*streamer.Media{
{Kind: streamer.KindVideo, Direction: streamer.DirectionRecvonly},
{Kind: streamer.KindAudio, Direction: streamer.DirectionRecvonly},
}
}
}

View File

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

View File

@@ -1,100 +0,0 @@
package streams
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"sync"
)
type state byte
const (
stateNone state = iota
stateMedias
stateTracks
stateStart
)
type Producer struct {
streamer.Element
url string
element streamer.Producer
tracks []*streamer.Track
state state
mx sync.Mutex
}
func (p *Producer) GetMedias() []*streamer.Media {
p.mx.Lock()
defer p.mx.Unlock()
if p.state == stateNone {
log.Debug().Str("url", p.url).Msg("[streams] probe producer")
var err error
p.element, err = GetProducer(p.url)
if err != nil || p.element == nil {
log.Error().Err(err).Str("url", p.url).Msg("[streams] probe producer")
return nil
}
p.state = stateMedias
}
return p.element.GetMedias()
}
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
p.mx.Lock()
defer p.mx.Unlock()
if p.state == stateMedias {
p.state = stateTracks
}
track := p.element.GetTrack(media, codec)
for _, t := range p.tracks {
if track == t {
return track
}
}
p.tracks = append(p.tracks, track)
return track
}
// internals
func (p *Producer) start() {
p.mx.Lock()
defer p.mx.Unlock()
if p.state != stateTracks {
return
}
log.Debug().Str("url", p.url).Msg("[streams] start producer")
p.state = stateStart
go p.element.Start()
}
func (p *Producer) stop() {
p.mx.Lock()
log.Debug().Str("url", p.url).Msg("[streams] stop producer")
if p.element != nil {
_ = p.element.Stop()
p.element = nil
} else {
log.Warn().Str("url", p.url).Msg("[streams] stop empty producer")
}
p.tracks = nil
p.state = stateNone
p.mx.Unlock()
}

View File

@@ -1,190 +0,0 @@
package streams
import (
"encoding/json"
"errors"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
type Consumer struct {
element streamer.Consumer
tracks []*streamer.Track
}
type Stream struct {
producers []*Producer
consumers []*Consumer
}
func NewStream(source interface{}) *Stream {
s := new(Stream)
switch source := source.(type) {
case string:
prod := &Producer{url: source}
s.producers = append(s.producers, prod)
case []interface{}:
for _, source := range source {
prod := &Producer{url: source.(string)}
s.producers = append(s.producers, prod)
}
case map[string]interface{}:
return NewStream(source["url"])
case nil:
default:
panic("wrong source type")
}
return s
}
func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
ic := len(s.consumers)
consumer := &Consumer{element: cons}
// Step 1. Get consumer medias
for icc, consMedia := range cons.GetMedias() {
log.Trace().Stringer("media", consMedia).
Msgf("[streams] consumer:%d:%d candidate", ic, icc)
producers:
for ip, prod := range s.producers {
// Step 2. Get producer medias (not tracks yet)
for ipc, prodMedia := range prod.GetMedias() {
log.Trace().Stringer("media", prodMedia).
Msgf("[streams] producer:%d:%d candidate", ip, ipc)
// Step 3. Match consumer/producer codecs list
prodCodec := prodMedia.MatchMedia(consMedia)
if prodCodec != nil {
log.Trace().Stringer("codec", prodCodec).
Msgf("[streams] match producer:%d:%d => consumer:%d:%d", ip, ipc, ic, icc)
// Step 4. Get producer track
prodTrack := prod.GetTrack(prodMedia, prodCodec)
if prodTrack == nil {
log.Warn().Msg("[stream] can't get track")
continue
}
// Step 5. Add track to consumer and get new track
consTrack := consumer.element.AddTrack(consMedia, prodTrack)
consumer.tracks = append(consumer.tracks, consTrack)
break producers
}
}
}
}
// can't match tracks for consumer
if len(consumer.tracks) == 0 {
return errors.New("couldn't find the matching tracks")
}
s.consumers = append(s.consumers, consumer)
for _, prod := range s.producers {
prod.start()
}
return nil
}
func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
for i, consumer := range s.consumers {
if consumer.element == cons {
// remove consumer pads from all producers
for _, track := range consumer.tracks {
track.Unbind()
}
// remove consumer from slice
s.removeConsumer(i)
break
}
}
for _, producer := range s.producers {
var sink bool
for _, track := range producer.tracks {
if len(track.Sink) > 0 {
sink = true
}
}
if !sink {
producer.stop()
}
}
}
func (s *Stream) AddProducer(prod streamer.Producer) {
producer := &Producer{element: prod, state: stateTracks}
s.producers = append(s.producers, producer)
}
func (s *Stream) RemoveProducer(prod streamer.Producer) {
for i, producer := range s.producers {
if producer.element == prod {
s.removeProducer(i)
break
}
}
}
func (s *Stream) Active() bool {
if len(s.consumers) > 0 {
return true
}
for _, prod := range s.producers {
if prod.element != nil {
return true
}
}
return false
}
func (s *Stream) MarshalJSON() ([]byte, error) {
var v []interface{}
for _, prod := range s.producers {
if prod.element != nil {
v = append(v, prod.element)
}
}
for _, cons := range s.consumers {
// cons.element always not nil
v = append(v, cons.element)
}
if len(v) == 0 {
v = nil
}
return json.Marshal(v)
}
func (s *Stream) removeConsumer(i int) {
switch {
case len(s.consumers) == 1: // only one element
s.consumers = nil
case i == 0: // first element
s.consumers = s.consumers[1:]
case i == len(s.consumers)-1: // last element
s.consumers = s.consumers[:i]
default: // middle element
s.consumers = append(s.consumers[:i], s.consumers[i+1:]...)
}
}
func (s *Stream) removeProducer(i int) {
switch {
case len(s.producers) == 1: // only one element
s.producers = nil
case i == 0: // first element
s.producers = s.producers[1:]
case i == len(s.producers)-1: // last element
s.producers = s.producers[:i]
default: // middle element
s.producers = append(s.producers[:i], s.producers[i+1:]...)
}
}

View File

@@ -1,135 +0,0 @@
package streams
import (
"github.com/AlexxIT/go2rtc/pkg/fake"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
// Google Chrome 104.0.5112.79
const chrome = `v=0
o=- 0 0 IN IP4 0.0.0.0
s=-
t=0 0
m=audio 9 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8 110 112 113 126
a=sendrecv
a=rtpmap:111 opus/48000/2
a=rtpmap:63 red/48000/2
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:110 telephone-event/48000
a=rtpmap:112 telephone-event/32000
a=rtpmap:113 telephone-event/16000
a=rtpmap:126 telephone-event/8000
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 122 127 121 125 107 108 109 124 120 123 119 35 36 37 38 39 40 41 42 114 115 116 117 118 43
a=recvonly
a=rtpmap:96 VP8/90000
a=rtpmap:97 rtx/90000
a=rtpmap:98 VP9/90000
a=rtpmap:99 rtx/90000
a=rtpmap:100 VP9/90000
a=rtpmap:101 rtx/90000
a=rtpmap:102 VP9/90000
a=rtpmap:122 rtx/90000
a=rtpmap:127 H264/90000
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtpmap:121 rtx/90000
a=rtpmap:125 H264/90000
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtpmap:107 rtx/90000
a=rtpmap:108 H264/90000
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:109 rtx/90000
a=rtpmap:124 H264/90000
a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtpmap:120 rtx/90000
a=rtpmap:123 H264/90000
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f
a=rtpmap:119 rtx/90000
a=rtpmap:35 H264/90000
a=fmtp:35 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f
a=rtpmap:36 rtx/90000
a=rtpmap:37 H264/90000
a=fmtp:37 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=f4001f
a=rtpmap:38 rtx/90000
a=rtpmap:39 H264/90000
a=fmtp:39 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=f4001f
a=rtpmap:40 rtx/90000
a=rtpmap:41 AV1/90000
a=rtpmap:42 rtx/90000
a=rtpmap:114 H264/90000
a=fmtp:114 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f
a=rtpmap:115 rtx/90000
a=rtpmap:116 red/90000
a=rtpmap:117 rtx/90000
a=rtpmap:118 ulpfec/90000
a=rtpmap:43 flexfec-03/90000
`
const dahuaSimple = `v=0
o=- 0 0 IN IP4 0.0.0.0
s=-
t=0 0
m=video 0 RTP/AVP 96
a=control:trackID=0
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1;profile-level-id=42401E;sprop-parameter-sets=Z0JAHqaAoD2QAA==,aM48gAA=
a=recvonly
m=audio 0 RTP/AVP 97
a=control:trackID=1
a=rtpmap:97 MPEG4-GENERIC/16000
a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
a=recvonly
m=audio 0 RTP/AVP 8
a=control:trackID=5
a=rtpmap:8 PCMA/8000
a=sendonly
`
const ffmpegPCMU48000 = `v=0
o=- 0 0 IN IP4 127.0.0.1
s=-
t=0 0
m=audio 0 RTP/AVP 96
b=AS:384
a=rtpmap:96 PCMU/48000/1
a=control:streamid=0
`
func TestRouting(t *testing.T) {
prod := &fake.Producer{}
prod.Medias, _ = rtsp.UnmarshalSDP([]byte(dahuaSimple))
assert.Len(t, prod.Medias, 3)
HandleFunc("fake", func(url string) (streamer.Producer, error) {
return prod, nil
})
cons := &fake.Consumer{}
cons.Medias, _ = streamer.UnmarshalSDP([]byte(chrome))
assert.Len(t, cons.Medias, 3)
// setup stream with one producer
stream := NewStream("fake:")
// main check:
err := stream.AddConsumer(cons)
assert.Nil(t, err)
assert.Len(t, prod.Tracks, 2)
assert.Len(t, cons.Tracks, 2)
time.Sleep(time.Second)
assert.Greater(t, prod.SendPackets,0)
assert.Greater(t, cons.RecvPackets,0)
assert.Greater(t, prod.RecvPackets,0)
assert.Greater(t, cons.SendPackets,0)
}

View File

@@ -1,67 +0,0 @@
package streams
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/app/store"
"github.com/rs/zerolog"
)
func Init() {
var cfg struct {
Mod map[string]interface{} `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)
}
}
func Get(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")
stream := NewStream(src)
streams[src] = stream
return stream
}
func Has(src string) bool {
return streams[src] != nil
}
func New(name string, source interface{}) {
streams[name] = NewStream(source)
}
func Delete(name string) {
delete(streams, name)
}
func All() map[string]interface{} {
all := map[string]interface{}{}
for name, stream := range streams {
all[name] = stream
//if stream.Active() {
// all[name] = stream
//}
}
return all
}
var log zerolog.Logger
var streams = map[string]*Stream{}

View File

@@ -1,75 +0,0 @@
package webrtc
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/pion/sdp/v3"
"strings"
)
var candidates []string
func AddCandidate(address string) {
candidates = append(candidates, address)
}
func addCanditates(answer string) (string, error) {
if len(candidates) == 0 {
return answer, nil
}
sd := &sdp.SessionDescription{}
if err := sd.Unmarshal([]byte(answer)); err != nil {
return "", err
}
md := sd.MediaDescriptions[0]
_, end := md.Attribute("end-of-candidates")
if end {
md.Attributes = md.Attributes[:len(md.Attributes)-1]
}
for _, address := range candidates {
if strings.HasPrefix(address, "stun:") {
ip, err := webrtc.GetPublicIP()
if err != nil {
log.Warn().Err(err).Msg("[webrtc] public IP")
continue
}
address = ip.String() + address[4:]
log.Debug().Str("addr", address).Msg("[webrtc] stun public address")
}
cand, err := webrtc.NewCandidate(address)
if err != nil {
log.Warn().Err(err).Msg("[webrtc] candidate")
continue
}
md.WithPropertyAttribute(cand)
}
if end {
md.WithPropertyAttribute("end-of-candidates")
}
data, err := sd.Marshal()
if err != nil {
return "", err
}
return string(data), nil
}
func candidateHandler(ctx *api.Context, msg *streamer.Message) {
if ctx.Consumer == nil {
return
}
if conn := ctx.Consumer.(*webrtc.Conn); conn != nil {
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] remote")
conn.Push(msg)
}
}

View File

@@ -1,191 +0,0 @@
package webrtc
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
"github.com/rs/zerolog"
"net"
)
func Init() {
var cfg struct {
Mod struct {
Listen string `yaml:"listen"`
Candidates []string `yaml:"candidates"`
IceServers []pion.ICEServer `yaml:"ice_servers"`
} `yaml:"webrtc"`
}
cfg.Mod.IceServers = []pion.ICEServer{
{URLs: []string{"stun:stun.l.google.com:19302"}},
}
app.LoadConfig(&cfg)
log = app.GetLogger("webrtc")
address := cfg.Mod.Listen
pionAPI, err := webrtc.NewAPI(address)
if pionAPI == nil {
log.Error().Err(err).Msg("[webrtc] init API")
return
}
if err != nil {
log.Warn().Err(err).Msg("[webrtc] listen")
} else if address != "" {
log.Info().Str("addr", address).Msg("[webrtc] listen")
_, Port, _ = net.SplitHostPort(address)
}
pionConf := pion.Configuration{
ICEServers: cfg.Mod.IceServers,
SDPSemantics: pion.SDPSemanticsUnifiedPlanWithFallback,
}
NewPConn = func() (*pion.PeerConnection, error) {
return pionAPI.NewPeerConnection(pionConf)
}
candidates = cfg.Mod.Candidates
api.HandleWS(webrtc.MsgTypeOffer, offerHandler)
api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler)
}
var Port string
var log zerolog.Logger
var NewPConn func() (*pion.PeerConnection, error)
func offerHandler(ctx *api.Context, msg *streamer.Message) {
src := ctx.Request.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
return
}
log.Debug().Str("url", src).Msg("[webrtc] new consumer")
var err error
// create new webrtc instance
conn := new(webrtc.Conn)
conn.Conn, err = NewPConn()
if err != nil {
log.Error().Err(err).Msg("[webrtc] new conn")
return
}
conn.UserAgent = ctx.Request.UserAgent()
conn.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateClosed {
stream.RemoveConsumer(conn)
}
case *streamer.Message:
// subscribe on webrtc server candidates
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] local")
ctx.Write(msg)
}
})
// 1. SetOffer, so we can get remote client codecs
offer := msg.Value.(string)
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
if err = conn.SetOffer(offer); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] set offer")
ctx.Error(err)
return
}
// 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
_ = conn.Conn.Close()
ctx.Error(err)
return
}
conn.Init()
// exchange sdp without waiting all candidates
//answer, err := conn.ExchangeSDP(offer, false)
//answer, err := conn.GetAnswer()
answer, err := conn.GetCompleteAnswer()
if err == nil {
answer, err = addCanditates(answer)
}
log.Trace().Msgf("[webrtc] answer\n%s", answer)
if err != nil {
log.Error().Err(err).Msg("[webrtc] get answer")
ctx.Error(err)
return
}
ctx.Write(&streamer.Message{
Type: webrtc.MsgTypeAnswer, Value: answer,
})
ctx.Consumer = conn
}
func ExchangeSDP(
stream *streams.Stream, offer string, userAgent string,
) (answer string, err error) {
// create new webrtc instance
conn := new(webrtc.Conn)
conn.Conn, err = NewPConn()
if err != nil {
log.Error().Err(err).Msg("[webrtc] new conn")
return
}
conn.UserAgent = userAgent
conn.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateClosed {
stream.RemoveConsumer(conn)
}
}
})
// 1. SetOffer, so we can get remote client codecs
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
if err = conn.SetOffer(offer); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] set offer")
return
}
// 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
_ = conn.Conn.Close()
return
}
conn.Init()
// exchange sdp without waiting all candidates
//answer, err := conn.ExchangeSDP(offer, false)
answer, err = conn.GetCompleteAnswer()
if err == nil {
answer, err = addCanditates(answer)
}
log.Trace().Msgf("[webrtc] answer\n%s", answer)
if err != nil {
log.Error().Err(err).Msg("[webrtc] get answer")
}
return
}

View File

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

66
go.mod
View File

@@ -1,61 +1,61 @@
module github.com/AlexxIT/go2rtc
go 1.17
go 1.20
require (
github.com/brutella/hap v0.0.17
github.com/deepch/vdk v0.0.19
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/mdns v1.0.5
github.com/pion/ice/v2 v2.2.6
github.com/pion/interceptor v0.1.11
github.com/pion/rtcp v1.2.9
github.com/pion/ice/v2 v2.3.1
github.com/pion/interceptor v0.1.12
github.com/pion/rtcp v1.2.10
github.com/pion/rtp v1.7.13
github.com/pion/sdp/v3 v3.0.5
github.com/pion/srtp/v2 v2.0.10
github.com/pion/stun v0.3.5
github.com/pion/webrtc/v3 v3.1.43
github.com/rs/zerolog v1.27.0
github.com/stretchr/testify v1.7.1
github.com/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/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
github.com/stretchr/testify v1.8.2
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/brutella/dnssd v1.2.3 // indirect
github.com/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/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/miekg/dns v1.1.50 // indirect
github.com/pion/datachannel v1.5.2 // indirect
github.com/pion/dtls/v2 v2.1.5 // indirect
github.com/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/pion/datachannel v1.5.5 // indirect
github.com/pion/dtls/v2 v2.2.6 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.5 // indirect
github.com/pion/mdns v0.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.2 // indirect
github.com/pion/transport v0.13.1 // indirect
github.com/pion/turn/v2 v2.0.8 // indirect
github.com/pion/udp v0.1.1 // indirect
github.com/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/pmezard/go-difflib v1.0.0 // indirect
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // 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.20220823033740-ce7d1564e657
// MSE update
github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e
// AES_256_CM_HMAC_SHA1_80 support
github.com/pion/srtp/v2 v2.0.10 => github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10
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
)

209
go.sum
View File

@@ -1,12 +1,10 @@
github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657 h1:FUzXAJfm6sRLJ8T6vfzvy/Hm3aioX8+fbxgx2VZoI78=
github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657/go.mod h1:c2vEL5pzjRWEx07sa32kTVjzI9bBVlstrwBwKe3DlJ0=
github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10 h1:4aKRthhmkYcStKuk1hcyvkeNJ/BDx5BTIvYmDO9ZJvg=
github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e h1:NAgHHZB+JUN3/J4/yq1q1EAc8xwJ8bb/Qp0AcjkfzAA=
github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e/go.mod h1:KqQ/KU3hOc4a62l/jPRH5Hiz5fhTq5cGCl8IqeCxWQI=
github.com/brutella/dnssd v1.2.3 h1:4fBLjZjPH7SbcHhEcIJhZcC9nOhIDZ0m3rn9bjl1/i0=
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/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
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/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=
@@ -17,7 +15,6 @@ github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@@ -31,7 +28,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
@@ -39,164 +35,143 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.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 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U=
github.com/pion/dtls/v2 v2.0.2/go.mod h1:27PEO3MDdaCfo21heT59/vsdmZc0zMt9wQPcSlLu/1I=
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c=
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
github.com/pion/ice v0.7.18 h1:KbAWlzWRUdX9SmehBh3gYpIFsirjhSQsCw6K2MjYMK0=
github.com/pion/ice v0.7.18/go.mod h1:+Bvnm3nYC6Nnp7VV6glUkuOfToB/AtMRZpOU8ihuf4c=
github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs=
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
github.com/pion/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/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0=
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k=
github.com/pion/randutil v0.0.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/mdns v0.0.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U=
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I=
github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI=
github.com/pion/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/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E=
github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw=
github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA=
github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog=
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
github.com/pion/webrtc/v2 v2.2.26/go.mod h1:XMZbZRNHyPDe1gzTIHFcQu02283YO45CbiwFgKvXnmc=
github.com/pion/webrtc/v3 v3.1.41/go.mod h1:sUcW9SFPEWerDqGOBmdYEMfRvbdd7rgwo4bNzfsXww4=
github.com/pion/webrtc/v3 v3.1.43 h1:YT3ZTO94UT4kSBvZnRAH82+0jJPUruiKr9CEstdlQzk=
github.com/pion/webrtc/v3 v3.1.43/go.mod h1:G/J8k0+grVsjC/rjCZ24AKoCCxcFFODgh7zThNZGs0M=
github.com/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/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/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/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.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
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/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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/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/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=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-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.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 h1:y+mHpWoQJNAHt26Nhh6JP7hvM71IRZureyvZhoVALIs=
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/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/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
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/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220630215102-69896b714898 h1:K7wO6V1IrczY9QOQ2WkVpw4JQSwCd52UsxVEirZUfiw=
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
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/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -207,27 +182,39 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/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/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 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0=
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/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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -242,9 +229,9 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

60
hardware.Dockerfile Normal file
View File

@@ -0,0 +1,60 @@
# syntax=docker/dockerfile:labs
# 0. Prepare images
# only debian 12 (bookworm) has latest ffmpeg
ARG DEBIAN_VERSION="bookworm-slim"
ARG GO_VERSION="1.20-buster"
ARG NGROK_VERSION="3"
FROM debian:${DEBIAN_VERSION} AS base
FROM golang:${GO_VERSION} AS go
FROM ngrok/ngrok:${NGROK_VERSION} AS ngrok
# 1. Build go2rtc binary
FROM go AS build
WORKDIR /build
# Cache dependencies
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
# 2. Collect all files
FROM scratch AS rootfs
COPY --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)
# 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 / /
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 ["go2rtc", "-config", "/config/go2rtc.yaml"]

11
internal/README.md Normal file
View File

@@ -0,0 +1,11 @@
## Go
```
go mod why github.com/pion/rtcp
go list -deps .\cmd\go2rtc_rtsp\
```
## Useful links
- https://github.com/golang-standards/project-layout
- https://github.com/micro/micro

170
internal/api/api.go Normal file
View File

@@ -0,0 +1,170 @@
package api
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/rs/zerolog"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync"
)
func Init() {
var cfg struct {
Mod struct {
Listen string `yaml:"listen"`
Username string `yaml:"username"`
Password string `yaml:"password"`
BasePath string `yaml:"base_path"`
StaticDir string `yaml:"static_dir"`
Origin string `yaml:"origin"`
} `yaml:"api"`
}
// default config
cfg.Mod.Listen = ":1984"
// load config from YAML
app.LoadConfig(&cfg)
if cfg.Mod.Listen == "" {
return
}
basePath = cfg.Mod.BasePath
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)
// ensure we can listen without errors
listener, err := net.Listen("tcp", cfg.Mod.Listen)
if err != nil {
log.Fatal().Err(err).Msg("[api] listen")
return
}
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
Handler = http.DefaultServeMux // 4th
if cfg.Mod.Origin == "*" {
Handler = middlewareCORS(Handler) // 3rd
}
if cfg.Mod.Username != "" {
Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, Handler) // 2nd
}
if log.Trace().Enabled() {
Handler = middlewareLog(Handler) // 1st
}
go func() {
s := http.Server{}
s.Handler = Handler
if err = s.Serve(listener); err != nil {
log.Fatal().Err(err).Msg("[api] serve")
}
}()
}
var Handler http.Handler
// HandleFunc handle pattern with relative path:
// - "api/streams" => "{basepath}/api/streams"
// - "/streams" => "/streams"
func HandleFunc(pattern string, handler http.HandlerFunc) {
if len(pattern) == 0 || pattern[0] != '/' {
pattern = basePath + "/" + pattern
}
log.Trace().Str("path", pattern).Msg("[api] register path")
http.HandleFunc(pattern, handler)
}
const StreamNotFound = "stream not found"
var basePath string
var log zerolog.Logger
func middlewareLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Trace().Msgf("[api] %s %s %s", r.Method, r.URL, r.RemoteAddr)
next.ServeHTTP(w, r)
})
}
func middlewareAuth(username, password string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") {
user, pass, ok := r.BasicAuth()
if !ok || user != username || pass != password {
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
}
next.ServeHTTP(w, r)
})
}
func middlewareCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization")
next.ServeHTTP(w, r)
})
}
var mu sync.Mutex
func apiHandler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
app.Info["host"] = r.Host
mu.Unlock()
if err := json.NewEncoder(w).Encode(app.Info); err != nil {
log.Warn().Err(err).Caller().Send()
}
}
func exitHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "", http.StatusBadRequest)
return
}
s := r.URL.Query().Get("code")
code, _ := strconv.Atoi(s)
os.Exit(code)
}
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)
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)
}
}

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

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

View File

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

185
internal/api/ws.go Normal file
View File

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

142
internal/app/app.go Normal file
View File

@@ -0,0 +1,142 @@
package app
import (
"flag"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
)
var Version = "1.5.0"
var UserAgent = "go2rtc/" + Version
var ConfigPath string
var Info = map[string]any{
"version": Version,
}
func Init() {
var confs Config
var version bool
flag.Var(&confs, "config", "go2rtc config (path to file or raw text), support multiple")
flag.BoolVar(&version, "version", false, "Print the version of the application and exit")
flag.Parse()
if version {
fmt.Println("Current version: ", Version)
os.Exit(0)
}
if confs == nil {
confs = []string{"go2rtc.yaml"}
}
for _, conf := range confs {
if conf[0] != '{' {
// config as file
if ConfigPath == "" {
ConfigPath = conf
}
data, _ := os.ReadFile(conf)
if data == nil {
continue
}
data = []byte(shell.ReplaceEnvVars(string(data)))
configs = append(configs, data)
} else {
// config as raw YAML
configs = append(configs, []byte(conf))
}
}
if ConfigPath != "" {
if !filepath.IsAbs(ConfigPath) {
if cwd, err := os.Getwd(); err == nil {
ConfigPath = filepath.Join(cwd, ConfigPath)
}
}
Info["config_path"] = ConfigPath
}
var cfg struct {
Mod map[string]string `yaml:"log"`
}
LoadConfig(&cfg)
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"])
modules = cfg.Mod
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
}
func NewLogger(format string, level string) zerolog.Logger {
var writer io.Writer = os.Stdout
if format != "json" {
writer = zerolog.ConsoleWriter{
Out: writer, TimeFormat: "15:04:05.000",
NoColor: writer != os.Stdout || format == "text",
}
}
zerolog.TimeFieldFormat = time.RFC3339Nano
lvl, err := zerolog.ParseLevel(level)
if err != nil || lvl == zerolog.NoLevel {
lvl = zerolog.InfoLevel
}
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
}
func LoadConfig(v any) {
for _, data := range configs {
if err := yaml.Unmarshal(data, v); err != nil {
log.Warn().Err(err).Msg("[app] read config")
}
}
}
func GetLogger(module string) zerolog.Logger {
if s, ok := modules[module]; ok {
lvl, err := zerolog.ParseLevel(s)
if err == nil {
return log.Level(lvl)
}
log.Warn().Err(err).Caller().Send()
}
return log.Logger
}
// internal
type Config []string
func (c *Config) String() string {
return strings.Join(*c, " ")
}
func (c *Config) Set(value string) error {
*c = append(*c, value)
return nil
}
var configs [][]byte
// modules log levels
var modules map[string]string

View File

@@ -8,7 +8,7 @@ import (
const name = "go2rtc.json"
var store map[string]interface{}
var store map[string]any
func load() {
data, _ := os.ReadFile(name)
@@ -20,7 +20,7 @@ func load() {
}
if store == nil {
store = make(map[string]interface{})
store = make(map[string]any)
}
}
@@ -33,7 +33,7 @@ func save() error {
return os.WriteFile(name, data, 0644)
}
func GetRaw(key string) interface{} {
func GetRaw(key string) any {
if store == nil {
load()
}
@@ -41,16 +41,16 @@ func GetRaw(key string) interface{} {
return store[key]
}
func GetDict(key string) map[string]interface{} {
func GetDict(key string) map[string]any {
raw := GetRaw(key)
if raw != nil {
return raw.(map[string]interface{})
return raw.(map[string]any)
}
return make(map[string]interface{})
return make(map[string]any)
}
func Set(key string, v interface{}) error {
func Set(key string, v any) error {
if store == nil {
load()
}

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

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

View File

@@ -13,17 +13,19 @@ 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/internal/rtsp.Init"),
[]byte("created by github.com/AlexxIT/go2rtc/internal/srtp.Init"),
// webrtc/api.go
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),
[]byte("created by github.com/pion/ice/v2.NewUDPMuxDefault"),
}
func stackHandler(w http.ResponseWriter, r *http.Request) {

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

@@ -0,0 +1,25 @@
package dvrip
import (
"github.com/AlexxIT/go2rtc/internal/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
}

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

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

151
internal/exec/exec.go Normal file
View File

@@ -0,0 +1,151 @@
package exec
import (
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"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"
"sync"
"time"
)
func Init() {
rtsp.HandleFunc(func(conn *pkg.Conn) bool {
waitersMu.Lock()
waiter := waiters[conn.URL.Path]
waitersMu.Unlock()
if waiter == nil {
return false
}
// unblocking write to channel
select {
case waiter <- conn:
return true
default:
return false
}
})
streams.HandleFunc("exec", execHandle)
log = app.GetLogger("exec")
}
func execHandle(url string) (core.Producer, error) {
var path string
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")
}
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
}
client := magic.NewClient(r)
if err = client.Probe(); err != nil {
return nil, err
}
client.Desc = "exec active producer"
client.URL = url
return client, nil
}
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()
waiters[path] = ch
waitersMu.Unlock()
defer func() {
waitersMu.Lock()
delete(waiters, path)
waitersMu.Unlock()
}()
log.Debug().Str("url", url).Msg("[exec] run")
ts := time.Now()
if err := cmd.Start(); err != nil {
log.Error().Err(err).Str("url", url).Msg("[exec]")
return nil, err
}
chErr := make(chan error)
go func() {
err := cmd.Wait()
// unblocking write to channel
select {
case chErr <- err:
default:
log.Trace().Str("url", url).Msg("[exec] close")
}
}()
select {
case <-time.After(time.Second * 60):
_ = cmd.Process.Kill()
log.Error().Str("url", url).Msg("[exec] timeout")
return nil, errors.New("timeout")
case err := <-chErr:
return nil, fmt.Errorf("exec: %s", err)
case prod := <-ch:
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run")
return prod, nil
}
}
// internal
var log zerolog.Logger
var waiters = map[string]chan core.Producer{}
var waitersMu sync.Mutex

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

@@ -0,0 +1,26 @@
package exec
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"io"
"os/exec"
)
// 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
}
return pipeCloser{stdout, cmd}, nil
}
type pipeCloser struct {
io.ReadCloser
cmd *exec.Cmd
}
func (p pipeCloser) Close() error {
return core.Any(p.ReadCloser.Close(), p.cmd.Process.Kill(), p.cmd.Wait())
}

View File

@@ -1,3 +1,17 @@
## FFplay output
[FFplay](https://stackoverflow.com/questions/27778678/what-are-mv-fd-aq-vq-sq-and-f-in-a-video-stream) `7.11 A-V: 0.003 fd= 1 aq= 21KB vq= 321KB sq= 0B f=0/0`:
- `7.11` - master clock, is the time from start of the stream/video
- `A-V` - av_diff, difference between audio and video timestamps
- `fd` - frames dropped
- `aq` - audio queue (0 - no delay)
- `vq` - video queue (0 - no delay)
- `sq` - subtitle queue
- `f` - timestamp error correction rate (Not 100% sure)
`M-V`, `M-A` means video stream only, audio stream only respectively.
## Devices Windows
```
@@ -39,3 +53,9 @@
- https://html5test.com/
- https://trac.ffmpeg.org/wiki/Capture/Webcam
- https://trac.ffmpeg.org/wiki/DirectShow
- https://stackoverflow.com/questions/53207692/libav-mjpeg-encoding-and-huffman-table
- https://github.com/tuupola/esp_video/blob/master/README.md
- https://github.com/leandromoreira/ffmpeg-libav-tutorial
- https://www.reddit.com/user/VeritablePornocopium/comments/okw130/ffmpeg_with_libfdk_aac_for_windows_x64/
- https://slhck.info/video/2017/02/24/vbr-settings.html
- [HomeKit audio samples problem](https://superuser.com/questions/1290996/non-monotonous-dts-with-igndts-flag)

View File

@@ -0,0 +1,68 @@
package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"os/exec"
"regexp"
"strings"
)
// https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f avfoundation"
func deviceInputSuffix(video, audio string) string {
switch {
case video != "" && audio != "":
return `"` + video + `:` + audio + `"`
case video != "":
return `"` + video + `"`
case audio != "":
return `":` + audio + `"`
}
return ""
}
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.Stream{
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
})
}
}

View File

@@ -0,0 +1,60 @@
package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"os"
"os/exec"
"regexp"
"strings"
)
// https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f v4l2"
func deviceInputSuffix(video, audio string) string {
if video != "" {
return video
}
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.Stream{
Name: i[3] + " | " + 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)
}
}
}

View File

@@ -0,0 +1,50 @@
package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"os/exec"
"regexp"
)
// https://trac.ffmpeg.org/wiki/DirectShow
const deviceInputPrefix = "-f dshow"
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.Stream{
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)
}
streams = append(streams, stream)
}
}

View File

@@ -0,0 +1,70 @@
package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
)
func Init(bin string) {
Bin = bin
api.HandleFunc("api/ffmpeg/devices", apiDevices)
}
func GetInput(src string) (string, error) {
runonce.Do(initDevices)
input := deviceInputPrefix
var video, audio string
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":
video = value[0]
case "audio":
audio = value[0]
case "resolution":
input += " -video_size " + value[0]
default: // "input_format", "framerate", "video_size"
input += " -" + key + " " + value[0]
}
}
}
if video != "" {
if i, err := strconv.Atoi(video); err == nil && i < len(videos) {
video = videos[i]
}
}
if audio != "" {
if i, err := strconv.Atoi(audio); err == nil && i < len(audios) {
audio = audios[i]
}
}
input += " -i " + deviceInputSuffix(video, audio)
return input, nil
}
var Bin string
var videos, audios []string
var streams []api.Stream
var runonce sync.Once
func apiDevices(w http.ResponseWriter, r *http.Request) {
runonce.Do(initDevices)
api.ResponseStreams(w, streams)
}

293
internal/ffmpeg/ffmpeg.go Normal file
View File

@@ -0,0 +1,293 @@
package ffmpeg
import (
"errors"
"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/core"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"net/url"
"strings"
)
func Init() {
var cfg struct {
Mod map[string]string `yaml:"ffmpeg"`
}
cfg.Mod = defaults // will be overriden from yaml
app.LoadConfig(&cfg)
if app.GetLogger("exec").GetLevel() >= 0 {
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 streams.GetProducer("exec:" + args.String())
})
device.Init(defaults["bin"])
hardware.Init(defaults["bin"])
}
var defaults = map[string]string{
"bin": "ffmpeg",
"global": "-hide_banner",
// 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/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/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 -pix_fmt:v yuvj420p",
"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",
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
"opus": "-c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -compression_level:a 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",
"pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
"pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
"aac": "-c:a aac", // keep sample rate and channels
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
"mp3": "-c:a libmp3lame -q:a 8",
"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",
// hardware Intel and AMD on Linux
// better not to set `-async_depth:v 1` like for QSV, because framedrops
// `-bf 0` - disable B-frames is very important
"h264/vaapi": "-c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0",
"h265/vaapi": "-c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0",
"mjpeg/vaapi": "-c:v mjpeg_vaapi",
// hardware Raspberry
"h264/v4l2m2m": "-c:v h264_v4l2m2m -g 50 -bf 0",
"h265/v4l2m2m": "-c:v hevc_v4l2m2m -g 50 -bf 0",
// hardware NVidia on Linux and Windows
// preset=p2 - faster, tune=ll - low latency
"h264/cuda": "-c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
"h265/cuda": "-c:v hevc_nvenc -g 50 -profile:v high -level:v auto",
// hardware Intel on Windows
"h264/dxva2": "-c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1",
"h265/dxva2": "-c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1",
"mjpeg/dxva2": "-c:v mjpeg_qsv -profile:v high -level:v 5.1",
// hardware macOS
"h264/videotoolbox": "-c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1",
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1",
}
// inputTemplate - select input template from YAML config by template name
// if query has input param - select another tempalte 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
}
} else {
template = defaults[name]
}
return strings.Replace(template, "{input}", s, 1)
}
func parseArgs(s string) *ffmpeg.Args {
// init FFmpeg arguments
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"])
s = s[:i]
}
// Parse input:
// 1. Input as xxxx:// link (http or rtsp or any other)
// 2. Input as stream name
// 3. Input as FFmpeg device (local USB camera)
if i := strings.Index(s, "://"); i > 0 {
switch s[:i] {
case "http", "https", "rtmp":
args.Input = 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 "
}
args.Input += inputTemplate("rtsp", s, query)
default:
args.Input = "-i " + s
}
} else if streams.Get(s) != nil {
s = "rtsp://127.0.0.1:" + rtsp.Port + "/" + s
switch {
case args.Video > 0 && args.Audio == 0:
s += "?video"
case args.Audio > 0 && args.Video == 0:
s += "?audio"
default:
s += "?video&audio"
}
args.Input = inputTemplate("rtsp", s, query)
} else if strings.HasPrefix(s, "device?") {
var err error
args.Input, err = device.GetInput(s)
if err != nil {
return nil
}
} else {
args.Input = inputTemplate("file", s, query)
}
if query["async"] != nil {
args.Input = "-use_wallclock_as_timestamps 1 -async 1 " + args.Input
}
// Parse query params:
// 1. `width`/`height` params
// 2. `rotate` param
// 3. `video` params (support multiple)
// 4. `audio` params (support multiple)
// 5. `hardware` param
if query != nil {
// 1. Process raw params for FFmpeg
for _, raw := range query["raw"] {
args.AddCodec(raw)
}
// 2. Process video filters (resize and rotation)
if query["width"] != nil || query["height"] != nil {
filter := "scale="
if query["width"] != nil {
filter += query["width"][0]
} else {
filter += "-1"
}
filter += ":"
if query["height"] != nil {
filter += query["height"][0]
} else {
filter += "-1"
}
args.AddFilter(filter)
}
if query["rotate"] != nil {
var filter string
switch query["rotate"][0] {
case "90":
filter = "transpose=1" // 90 degrees clockwise
case "180":
filter = "transpose=1,transpose=1"
case "-90", "270":
filter = "transpose=2" // 90 degrees counterclockwise
}
if filter != "" {
args.AddFilter(filter)
}
}
// 3. Process video codecs
if args.Video > 0 {
for _, video := range query["video"] {
if video != "copy" {
if codec := defaults[video]; codec != "" {
args.AddCodec(codec)
} else {
args.AddCodec(video)
}
} else {
args.AddCodec("-c:v copy")
}
}
} else {
args.AddCodec("-vn")
}
// 4. Process audio codecs
if args.Audio > 0 {
for _, audio := range query["audio"] {
if audio != "copy" {
if codec := defaults[audio]; codec != "" {
args.AddCodec(codec)
} else {
args.AddCodec(audio)
}
} else {
args.AddCodec("-c:a copy")
}
}
} else {
args.AddCodec("-an")
}
if query["hardware"] != nil {
hardware.MakeHardware(args, query["hardware"][0], defaults)
}
}
if args.Codecs == nil {
args.AddCodec("-c copy")
}
// 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
}

View File

@@ -0,0 +1,23 @@
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 -pix_fmt:v yuvj420p -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())
args = parseArgs("/media/bbb.mp4#video=mjpeg")
assert.Equal(t, "ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -", args.String())
args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=vaapi")
assert.Equal(t, "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf format=vaapi|nv12,hwupload -f mjpeg -", args.String())
args = parseArgs("device?video=0&input_format=mjpeg&video_size=1920x1080")
assert.Equal(t, `ffmpeg -hide_banner -f dshow -input_format mjpeg -video_size 1920x1080 -i video="0" -c copy -f mjpeg -`, args.String())
}

View File

@@ -0,0 +1,137 @@
package hardware
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"net/http"
"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
)
func Init(bin string) {
api.HandleFunc("api/ffmpeg/hardware", func(w http.ResponseWriter, r *http.Request) {
api.ResponseStreams(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.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]
}
}
}
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
}

View File

@@ -0,0 +1,37 @@
package hardware
import (
"github.com/AlexxIT/go2rtc/internal/api"
)
const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2 -t 1 -c h264_videotoolbox -f null -"
const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_videotoolbox -f null -"
func ProbeAll(bin string) []api.Stream {
return []api.Stream{
{
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,94 @@
package hardware
import (
"github.com/AlexxIT/go2rtc/internal/api"
"runtime"
)
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.Stream {
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
return []api.Stream{
{
Name: runToString(bin, ProbeV4L2M2MH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M,
},
{
Name: runToString(bin, ProbeV4L2M2MH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M,
},
}
}
return []api.Stream{
{
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.Stream {
return []api.Stream{
{
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
}

View File

@@ -0,0 +1,12 @@
package ffmpeg
import (
"bytes"
"os/exec"
)
func TranscodeToJPEG(b []byte) ([]byte, error) {
cmd := exec.Command("ffmpeg", "-hide_banner", "-i", "-", "-f", "mjpeg", "-")
cmd.Stdin = bytes.NewBuffer(b)
return cmd.Output()
}

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

@@ -0,0 +1,116 @@
package hass
import (
"encoding/base64"
"encoding/json"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"net"
"net/http"
"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 from RTSPtoWeb
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 {
stream = streams.NewTemplate(v.Name, v.Channels.First.Url)
}
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))
}
})
}
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"`
}

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

@@ -0,0 +1,164 @@
package hass
import (
"bytes"
"encoding/json"
"fmt"
"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/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)
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 != "" {
urls[entrie.Title] = fmt.Sprintf(
"onvif://%s:%s@%s:%d", data.Username, data.Password, data.Host, data.Port,
)
} else {
urls[entrie.Title] = fmt.Sprintf("onvif://%s:%d", data.Host, data.Port)
}
default:
continue
}
log.Info().Str("url", "hass:"+entrie.Title).Msg("[hass] load stream")
//streams.Get("hass:" + entrie.Title)
}
return urls
}
var log zerolog.Logger

279
internal/hls/hls.go Normal file
View File

@@ -0,0 +1,279 @@
package hls
import (
"fmt"
"github.com/AlexxIT/go2rtc/internal/api"
"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"
"strings"
"sync"
"time"
)
func Init() {
api.HandleFunc("api/stream.m3u8", handlerStream)
api.HandleFunc("api/hls/playlist.m3u8", handlerPlaylist)
// HLS (TS)
api.HandleFunc("api/hls/segment.ts", handlerSegmentTS)
// HLS (fMP4)
api.HandleFunc("api/hls/init.mp4", handlerInit)
api.HandleFunc("api/hls/segment.m4s", handlerSegmentMP4)
}
type Consumer interface {
core.Consumer
Listen(f core.EventFunc)
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
}
const keepalive = 5 * time.Second
var sessions = map[string]*Session{}
// once I saw 404 on MP4 segment, so better to use mutex
var sessionsMu sync.RWMutex
func handlerStream(w http.ResponseWriter, r *http.Request) {
// CORS important for Chromecast
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/vnd.apple.mpegurl")
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET")
return
}
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
var cons 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,
}
} else {
cons = &mpegts.Consumer{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
}
}
session := &Session{cons: cons}
cons.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.alive = time.AfterFunc(keepalive, func() {
stream.RemoveConsumer(cons)
})
session.init, _ = cons.Init()
cons.Start()
sid := core.RandString(8, 62)
// 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`
}
sessionsMu.Lock()
sessions[sid] = session
sessionsMu.Unlock()
// Apple Safari can play FLAC codec, but fail it it in m3u8 playlist
codecs := strings.Replace(cons.MimeCodecs(), mp4.MimeFlac, mp4.MimeAAC, 1)
// bandwidth important for Safari, codecs useful for smooth playback
data := []byte(`#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + codecs + `"
hls/playlist.m3u8?id=` + sid)
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}
}
func handlerPlaylist(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/vnd.apple.mpegurl")
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET")
return
}
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 {
log.Error().Err(err).Caller().Send()
}
}
func handlerSegmentTS(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "video/mp2t")
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET")
return
}
sid := r.URL.Query().Get("id")
sessionsMu.RLock()
session := sessions[sid]
sessionsMu.RUnlock()
if session == nil {
http.NotFound(w, r)
return
}
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)
}
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()
}
}
func handlerInit(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Add("Content-Type", "video/mp4")
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET")
return
}
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 {
log.Error().Err(err).Caller().Send()
}
}
func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Add("Content-Type", "video/iso.segment")
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
return
}
sid := r.URL.Query().Get("id")
sessionsMu.RLock()
session := sessions[sid]
sessionsMu.RUnlock()
if session == nil {
http.NotFound(w, r)
return
}
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)
}
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()
}
}

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

@@ -0,0 +1,140 @@
package homekit
import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/internal/app/store"
"github.com/AlexxIT/go2rtc/internal/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

@@ -0,0 +1,32 @@
package homekit
import (
"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/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
}

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

@@ -0,0 +1,95 @@
package http
import (
"errors"
"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/rtmp"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"net"
"net/http"
"net/url"
"strings"
"time"
)
func Init() {
streams.HandleFunc("http", handleHTTP)
streams.HandleFunc("https", handleHTTP)
streams.HandleFunc("httpx", handleHTTP)
streams.HandleFunc("tcp", handleTCP)
}
func handleHTTP(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
default: // "video/mpeg":
}
client := magic.NewClient(res.Body)
if err = client.Probe(); err != nil {
return nil, err
}
client.Desc = "HTTP active producer"
client.URL = url
return client, nil
}
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, time.Second*3)
if err != nil {
return nil, err
}
client := magic.NewClient(conn)
if err = client.Probe(); err != nil {
return nil, err
}
client.Desc = "TCP active producer"
client.URL = rawURL
return client, nil
}

22
internal/isapi/init.go Normal file
View File

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

View File

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

188
internal/mjpeg/init.go Normal file
View File

@@ -0,0 +1,188 @@
package mjpeg
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/api"
"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"
"io"
"net/http"
"strconv"
"time"
)
func Init() {
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
api.HandleFunc("api/stream.mjpeg", handlerStream)
api.HandleWS("mjpeg", handlerWS)
}
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
exit := make(chan []byte)
cons := &magic.Keyframe{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg any) {
if b, ok := msg.([]byte); ok {
select {
case exit <- b:
default:
}
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
data := <-exit
stream.RemoveConsumer(cons)
switch cons.CodecName() {
case core.CodecH264, core.CodecH265:
ts := time.Now()
var err error
if data, err = ffmpeg.TranscodeToJPEG(data); 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("Cache-Control", "no-cache")
h.Set("Connection", "close")
h.Set("Pragma", "no-cache")
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}
}
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
func handlerStream(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
outputMjpeg(w, r)
} else {
inputMjpeg(w, r)
}
}
func outputMjpeg(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
flusher := w.(http.Flusher)
cons := &mjpeg.Consumer{
RemoteAddr: 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()
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
return
}
h := w.Header()
h.Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
h.Set("Cache-Control", "no-cache")
h.Set("Connection", "close")
h.Set("Pragma", "no-cache")
<-r.Context().Done()
stream.RemoveConsumer(cons)
//log.Trace().Msg("[api.mjpeg] close")
}
func inputMjpeg(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, Header: r.Header, Request: r}
res.Header.Set("Content-Type", "multipart/mixed;boundary=")
client := mjpeg.NewClient(res)
stream.AddProducer(client)
if err := client.Start(); err != nil && err != io.EOF {
log.Warn().Err(err).Caller().Send()
}
stream.RemoveProducer(client)
}
func handlerWS(tr *api.Transport, _ *api.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return errors.New(api.StreamNotFound)
}
cons := &mjpeg.Consumer{
RemoteAddr: tcp.RemoteAddr(tr.Request),
UserAgent: tr.Request.UserAgent(),
}
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: "mjpeg"})
tr.OnClose(func() {
stream.RemoveConsumer(cons)
})
return nil
}

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

@@ -0,0 +1,174 @@
package mp4
import (
"net/http"
"strconv"
"strings"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
)
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, 1)
cons := &mp4.Segment{OnlyKeyframe: true}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok && exit != nil {
select {
case exit <- data:
default:
}
exit = nil
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
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)
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.GetOrNew(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
exit := make(chan error, 1) // Add buffer to prevent blocking
cons := &mp4.Consumer{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
Medias: mp4.ParseQuery(r.URL.Query()),
}
cons.Listen(func(msg any) {
if exit == nil {
return
}
if data, ok := msg.([]byte); ok {
if _, err := w.Write(data); err != nil {
select {
case exit <- err:
default:
}
exit = nil
}
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
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()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
cons.Start()
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() {
if exit != nil {
select {
case exit <- nil:
default:
}
exit = nil
}
})
}
}
err = <-exit
exit = nil
log.Trace().Err(err).Caller().Send()
if duration != nil {
duration.Stop()
}
}

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

@@ -0,0 +1,144 @@
package mp4
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/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.MimeFlac:
audios = append(audios,
&core.Codec{Name: core.CodecPCMA},
&core.Codec{Name: core.CodecPCMU},
&core.Codec{Name: core.CodecPCM},
)
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
}

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

@@ -0,0 +1,43 @@
package mpegts
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/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

@@ -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"
@@ -30,7 +30,7 @@ func Init() {
log.Error().Err(err).Msg("[ngrok] start")
}
ngr.Listen(func(msg interface{}) {
ngr.Listen(func(msg any) {
if msg := msg.(*ngrok.Message); msg != nil {
if strings.HasPrefix(msg.Line, "ERROR:") {
log.Warn().Msg("[ngrok] " + msg.Line)

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

@@ -0,0 +1,194 @@
package onvif
import (
"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"
"io"
"net"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
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.Stream
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.Stream{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.Stream{
Name: name + " stream" + strconv.Itoa(i),
URL: src + "?subtype=" + token,
})
}
if len(tokens) > 0 && client.HasSnapshots() {
items = append(items, api.Stream{
Name: name + " snapshot",
URL: src + "?subtype=" + tokens[0] + "&snapshot",
})
}
}
api.ResponseStreams(w, items)
}

View File

@@ -0,0 +1,100 @@
package roborock
import (
"fmt"
"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() {
streams.HandleFunc("roborock", handle)
api.HandleFunc("api/roborock", apiHandle)
}
func handle(url string) (core.Producer, error) {
conn := roborock.NewClient(url)
if err := conn.Dial(); err != nil {
return nil, err
}
if err := conn.Connect(); err != nil {
return nil, err
}
return conn, nil
}
var Auth struct {
UserData *roborock.UserInfo `json:"user_data"`
BaseURL string `json:"base_url"`
}
func apiHandle(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
if Auth.UserData == nil {
http.Error(w, "no auth", http.StatusNotFound)
return
}
case "POST":
if err := r.ParseMultipartForm(1024); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
username := r.Form.Get("username")
password := r.Form.Get("password")
if username == "" || password == "" {
http.Error(w, "empty username or password", http.StatusBadRequest)
return
}
base, err := roborock.GetBaseURL(username)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ui, err := roborock.Login(base, username, password)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
Auth.BaseURL = base
Auth.UserData = ui
default:
http.Error(w, "", http.StatusMethodNotAllowed)
return
}
homeID, err := roborock.GetHomeID(Auth.BaseURL, Auth.UserData.Token)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
devices, err := roborock.GetDevices(Auth.UserData, homeID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var items []api.Stream
for _, device := range devices {
source := fmt.Sprintf(
"roborock://%s?u=%s&s=%s&k=%s&did=%s&key=%s&pin=",
Auth.UserData.IoT.URL.MQTT[6:],
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})
}
api.ResponseStreams(w, items)
}

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

@@ -0,0 +1,62 @@
package rtmp
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/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)
}

274
internal/rtsp/rtsp.go Normal file
View File

@@ -0,0 +1,274 @@
package rtsp
import (
"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() {
var conf struct {
Mod struct {
Listen string `yaml:"listen" json:"listen"`
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.DefaultQuery = "video&audio"
app.LoadConfig(&conf)
app.Info["rtsp"] = conf.Mod
log = app.GetLogger("rtsp")
// RTSP client support
streams.HandleFunc("rtsp", rtspHandler)
streams.HandleFunc("rtsps", rtspHandler)
streams.HandleFunc("rtspx", rtspHandler)
// RTSP server support
address := conf.Mod.Listen
if address == "" {
return
}
ln, err := net.Listen("tcp", address)
if err != nil {
log.Error().Err(err).Msg("[rtsp] listen")
return
}
_, Port, _ = net.SplitHostPort(address)
log.Info().Str("addr", address).Msg("[rtsp] listen")
if query, err := url.ParseQuery(conf.Mod.DefaultQuery); err == nil {
defaultMedias = ParseQuery(query)
}
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
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)
}
go tcpHandler(c)
}
}()
}
type Handler func(conn *rtsp.Conn) bool
func HandleFunc(handler Handler) {
handlers = append(handlers, handler)
}
var Port string
// internal
var log zerolog.Logger
var handlers []Handler
var defaultMedias []*core.Media
func rtspHandler(url string) (core.Producer, error) {
backchannel := true
if i := strings.IndexByte(url, '#'); i > 0 {
if url[i+1:] == "backchannel=0" {
backchannel = false
}
url = url[:i]
}
conn := rtsp.NewClient(url)
conn.UserAgent = app.UserAgent
if log.Trace().Enabled() {
conn.Listen(func(msg any) {
switch msg := msg.(type) {
case *tcp.Request:
log.Trace().Msgf("[rtsp] client request:\n%s", msg)
case *tcp.Response:
log.Trace().Msgf("[rtsp] client response:\n%s", msg)
case string:
log.Trace().Msgf("[rtsp] client msg: %s", msg)
}
})
}
if err := conn.Dial(); err != nil {
return nil, err
}
conn.Backchannel = backchannel
if err := conn.Describe(); err != nil {
if !backchannel {
return nil, err
}
log.Trace().Msgf("[rtsp] describe (backchannel=%t) err: %v", backchannel, err)
// second try without backchannel, we need to reconnect
conn.Backchannel = false
if err = conn.Dial(); err != nil {
return nil, err
}
if err = conn.Describe(); err != nil {
return nil, err
}
}
return conn, nil
}
func tcpHandler(conn *rtsp.Conn) {
var name string
var closer func()
trace := log.Trace().Enabled()
conn.Listen(func(msg any) {
if trace {
switch msg := msg.(type) {
case *tcp.Request:
log.Trace().Msgf("[rtsp] server request:\n%s", msg)
case *tcp.Response:
log.Trace().Msgf("[rtsp] server response:\n%s", msg)
}
}
switch msg {
case rtsp.MethodDescribe:
if len(conn.URL.Path) == 0 {
log.Warn().Msg("[rtsp] server empty URL on DESCRIBE")
return
}
name = conn.URL.Path[1:]
stream := streams.Get(name)
if stream == nil {
return
}
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
conn.SessionName = app.UserAgent
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
}
closer = func() {
stream.RemoveConsumer(conn)
}
case rtsp.MethodAnnounce:
if len(conn.URL.Path) == 0 {
log.Warn().Msg("[rtsp] server empty URL on ANNOUNCE")
return
}
name = conn.URL.Path[1:]
stream := streams.Get(name)
if stream == nil {
return
}
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
stream.AddProducer(conn)
closer = func() {
stream.RemoveProducer(conn)
}
}
})
if err := conn.Accept(); err != nil {
if err != io.EOF {
log.Warn().Err(err).Caller().Send()
}
if closer != nil {
closer()
}
_ = conn.Close()
return
}
for _, handler := range handlers {
if handler(conn) {
return
}
}
if closer != nil {
if err := conn.Handle(); err != nil {
log.Debug().Msgf("[rtsp] handle=%s", err)
}
closer()
log.Debug().Str("stream", name).Msg("[rtsp] disconnect")
}
_ = conn.Close()
}
func 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)
}

View File

@@ -1,9 +1,8 @@
package srtp
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/pkg/srtp"
"github.com/rs/zerolog"
"net"
)
@@ -24,36 +23,23 @@ func Init() {
return
}
log = app.GetLogger("srtp")
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).Msg("[srtp] listen")
log.Warn().Err(err).Caller().Send()
}
log.Info().Str("addr", cfg.Mod.Listen).Msg("[srtp] listen")
_, Port, _ = net.SplitHostPort(cfg.Mod.Listen)
// run server
go func() {
server = &srtp.Server{}
if err = server.Serve(conn); err != nil {
log.Warn().Err(err).Msg("[srtp] serve")
Server = &srtp.Server{}
if err = Server.Serve(conn); err != nil {
log.Warn().Err(err).Caller().Send()
}
}()
}
var log zerolog.Logger
var server *srtp.Server
var Port string
func AddSession(session *srtp.Session) {
server.AddSession(session)
}
func RemoveSession(session *srtp.Session) {
server.RemoveSession(session)
}
var Server *srtp.Server

View File

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

139
internal/streams/init.go Normal file
View File

@@ -0,0 +1,139 @@
package streams
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/app/store"
"github.com/rs/zerolog"
"net/http"
"net/url"
)
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 NewTemplate(name string, source any) *Stream {
// check if source links to some stream name from go2rtc
if rawURL, ok := source.(string); ok {
if u, err := url.Parse(rawURL); err == nil && u.Scheme == "rtsp" {
if stream, ok := streams[u.Path[1:]]; ok {
streams[name] = stream
return stream
}
}
}
return New(name, "{input}")
}
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 GetAll() (names []string) {
for name := range streams {
names = append(names, name)
}
return
}
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
}
// support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass
stream := Get(name)
if stream == nil {
stream = NewTemplate(name, src)
}
stream.SetSource(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{}

151
internal/streams/play.go Normal file
View File

@@ -0,0 +1,151 @@
package streams
import (
"errors"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func (s *Stream) Play(source string) error {
s.mu.Lock()
for _, producer := range s.producers {
if producer.state == stateInternal && producer.conn != nil {
_ = producer.conn.Stop()
}
}
s.mu.Unlock()
if source == "" {
return nil
}
var src core.Producer
for _, producer := range s.producers {
if producer.conn == nil {
continue
}
cons, ok := producer.conn.(core.Consumer)
if !ok {
continue
}
if src == nil {
var err error
if src, err = GetProducer(source); err != nil {
return err
}
}
if !matchMedia(src, cons) {
continue
}
s.AddInternalProducer(src)
go func() {
_ = src.Start()
s.RemoveProducer(src)
}()
return nil
}
for _, producer := range s.producers {
// start new client
dst, err := GetProducer(producer.url)
if err != nil {
continue
}
// check if client support consumer interface
cons, ok := dst.(core.Consumer)
if !ok {
_ = dst.Stop()
continue
}
// start new producer
if src == nil {
if src, err = GetProducer(source); err != nil {
return err
}
}
if !matchMedia(src, cons) {
_ = dst.Stop()
continue
}
s.AddInternalProducer(src)
s.AddInternalConsumer(cons)
go func() {
_ = src.Start()
_ = dst.Stop()
s.RemoveProducer(src)
}()
go func() {
_ = dst.Start()
_ = src.Stop()
s.RemoveInternalConsumer(cons)
}()
return nil
}
return errors.New("can't find consumer")
}
func (s *Stream) AddInternalProducer(conn core.Producer) {
producer := &Producer{conn: conn, state: stateInternal}
s.mu.Lock()
s.producers = append(s.producers, producer)
s.mu.Unlock()
}
func (s *Stream) AddInternalConsumer(conn core.Consumer) {
s.mu.Lock()
s.consumers = append(s.consumers, conn)
s.mu.Unlock()
}
func (s *Stream) RemoveInternalConsumer(conn core.Consumer) {
s.mu.Lock()
for i, consumer := range s.consumers {
if consumer == conn {
s.consumers = append(s.consumers[:i], s.consumers[i+1:]...)
break
}
}
s.mu.Unlock()
}
func matchMedia(prod core.Producer, cons core.Consumer) bool {
for _, consMedia := range cons.GetMedias() {
for _, prodMedia := range prod.GetMedias() {
if prodMedia.Direction != core.DirectionRecvonly {
continue
}
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
if prodCodec == nil {
continue
}
track, err := prod.GetTrack(prodMedia, prodCodec)
if err != nil {
continue
}
if err = cons.AddTrack(consMedia, consCodec, track); err != nil {
continue
}
return true
}
}
return false
}

View File

@@ -0,0 +1,255 @@
package streams
import (
"encoding/json"
"errors"
"github.com/AlexxIT/go2rtc/pkg/core"
"strings"
"sync"
"time"
)
type state byte
const (
stateNone state = iota
stateMedias
stateTracks
stateStart
stateExternal
stateInternal
)
type Producer struct {
core.Listener
url string
template string
conn core.Producer
receivers []*core.Receiver
senders []*core.Receiver
state state
mu sync.Mutex
workerID int
}
func (p *Producer) Dial() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.state == stateNone {
conn, err := GetProducer(p.url)
if err != nil {
return err
}
p.conn = conn
p.state = stateMedias
}
return nil
}
func (p *Producer) GetMedias() []*core.Media {
p.mu.Lock()
defer p.mu.Unlock()
if p.conn == nil {
return nil
}
return p.conn.GetMedias()
}
func (p *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.state == stateNone {
return nil, errors.New("get track from none state")
}
for _, track := range p.receivers {
if track.Codec == codec {
return track, nil
}
}
track, err := p.conn.GetTrack(media, codec)
if err != nil {
return nil, err
}
p.receivers = append(p.receivers, track)
if p.state == stateMedias {
p.state = stateTracks
}
return track, nil
}
func (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
p.mu.Lock()
defer p.mu.Unlock()
if p.state == stateNone {
return errors.New("add track from none state")
}
if err := p.conn.(core.Consumer).AddTrack(media, codec, track); err != nil {
return err
}
p.senders = append(p.senders, track)
if p.state == stateMedias {
p.state = stateTracks
}
return nil
}
func (p *Producer) SetSource(s string) {
if p.template == "" {
p.template = p.url
}
p.url = strings.Replace(p.template, "{input}", s, 1)
}
func (p *Producer) MarshalJSON() ([]byte, error) {
if p.conn != nil {
return json.Marshal(p.conn)
}
info := core.Info{URL: p.url}
return json.Marshal(info)
}
// internals
func (p *Producer) start() {
p.mu.Lock()
defer p.mu.Unlock()
if p.state != stateTracks {
return
}
log.Debug().Msgf("[streams] start producer url=%s", p.url)
p.state = stateStart
p.workerID++
go p.worker(p.conn, p.workerID)
}
func (p *Producer) worker(conn core.Producer, workerID int) {
if err := conn.Start(); err != nil {
p.mu.Lock()
closed := p.workerID != workerID
p.mu.Unlock()
if closed {
return
}
log.Warn().Err(err).Str("url", p.url).Caller().Send()
}
p.reconnect(workerID, 0)
}
func (p *Producer) reconnect(workerID, retry int) {
p.mu.Lock()
defer p.mu.Unlock()
if p.workerID != workerID {
log.Trace().Msgf("[streams] stop reconnect url=%s", p.url)
return
}
log.Debug().Msgf("[streams] retry=%d to url=%s", retry, p.url)
conn, err := GetProducer(p.url)
if err != nil {
log.Debug().Msgf("[streams] producer=%s", err)
timeout := time.Minute
if retry < 5 {
timeout = time.Second
} else if retry < 10 {
timeout = time.Second * 5
} else if retry < 20 {
timeout = time.Second * 10
}
time.AfterFunc(timeout, func() {
p.reconnect(workerID, retry+1)
})
return
}
for _, media := range conn.GetMedias() {
switch media.Direction {
case core.DirectionRecvonly:
for _, receiver := range p.receivers {
codec := media.MatchCodec(receiver.Codec)
if codec == nil {
continue
}
track, err := conn.GetTrack(media, codec)
if err != nil {
continue
}
receiver.Replace(track)
break
}
case core.DirectionSendonly:
for _, sender := range p.senders {
codec := media.MatchCodec(sender.Codec)
if codec == nil {
continue
}
_ = conn.(core.Consumer).AddTrack(media, codec, sender)
}
}
}
p.conn = conn
go p.worker(conn, workerID)
}
func (p *Producer) stop() {
p.mu.Lock()
defer p.mu.Unlock()
switch p.state {
case stateExternal:
log.Debug().Msgf("[streams] can't stop external producer")
return
case stateNone:
log.Debug().Msgf("[streams] can't stop none producer")
return
case stateStart:
p.workerID++
}
log.Debug().Msgf("[streams] stop producer url=%s", p.url)
if p.conn != nil {
_ = p.conn.Stop()
p.conn = nil
}
p.state = stateNone
p.receivers = nil
p.senders = nil
}

254
internal/streams/stream.go Normal file
View File

@@ -0,0 +1,254 @@
package streams
import (
"encoding/json"
"errors"
"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 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
consN := atomic.AddInt32(&s.requests, 1) - 1
var statErrors []error
var statMedias []*core.Media
var statProds []*Producer // matched producers for consumer
// Step 1. Get consumer medias
for _, consMedia := range cons.GetMedias() {
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)
statErrors = append(statErrors, 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)
statMedias = append(statMedias, 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
}
}
statProds = append(statProds, 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(statProds) == 0 {
return formatError(statMedias, statErrors)
}
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 statProds {
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
}
}
for _, track := range producer.senders {
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 formatError(statMedias []*core.Media, statErrors []error) error {
var text string
for _, media := range statMedias {
if media.Direction == core.DirectionRecvonly {
continue
}
for _, codec := range media.Codecs {
name := codec.Name
if name == core.CodecAAC {
name = "AAC"
}
if strings.Contains(text, name) {
continue
}
if len(text) > 0 {
text += ","
}
text += name
}
}
if text != "" {
return errors.New(text)
}
for _, err := range statErrors {
s := err.Error()
if strings.Contains(text, s) {
continue
}
if len(text) > 0 {
text += ","
}
text += s
}
if text != "" {
return errors.New(text)
}
return errors.New("unknown error")
}

View File

@@ -0,0 +1,19 @@
package streams
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestTemplate(t *testing.T) {
source1 := "does not matter"
stream1 := New("from_yaml", source1)
require.Len(t, streams, 1)
stream2 := NewTemplate("camera.from_hass", "rtsp://localhost:8554/from_yaml?video")
require.Equal(t, stream1, stream2)
require.Equal(t, stream2.producers[0].url, source1)
require.Len(t, streams, 2)
}

19
internal/tapo/tapo.go Normal file
View File

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

View File

@@ -0,0 +1,8 @@
## Userful links
- https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html
- https://www.ietf.org/id/draft-murillo-whep-01.html
- https://github.com/Glimesh/broadcast-box/
- https://github.com/obsproject/obs-studio/pull/7926
- https://misi.github.io/webrtc-c0d3l4b/
- https://github.com/webtorrent/webtorrent/blob/master/docs/faq.md

View File

@@ -0,0 +1,122 @@
package webrtc
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/pion/sdp/v3"
"strconv"
"strings"
)
type Address struct {
Host string
Port int
}
var addresses []Address
func AddCandidate(address string) {
var port int
// try to get port from address string
if i := strings.LastIndexByte(address, ':'); i > 0 {
if v, _ := strconv.Atoi(address[i+1:]); v != 0 {
address = address[:i]
port = v
}
}
// use default WebRTC port
if port == 0 {
port, _ = strconv.Atoi(Port)
}
addresses = append(addresses, Address{Host: address, Port: port})
}
func GetCandidates() (candidates []string) {
for _, address := range addresses {
// using stun server for receive public IP-address
if address.Host == "stun" {
ip, err := webrtc.GetCachedPublicIP()
if err != nil {
continue
}
// this is a copy, original host unchanged
address.Host = ip.String()
}
candidates = append(
candidates,
webrtc.CandidateManualHostUDP(address.Host, address.Port),
webrtc.CandidateManualHostTCPPassive(address.Host, address.Port),
)
}
return
}
func asyncCandidates(tr *api.Transport, cons *webrtc.Conn) {
tr.WithContext(func(ctx map[any]any) {
if candidates, ok := ctx["candidate"].([]string); ok {
// process candidates that receive before this moment
for _, candidate := range candidates {
_ = cons.AddCandidate(candidate)
}
// remove already processed candidates
delete(ctx, "candidate")
}
// set variable for process candidates after this moment
ctx["webrtc"] = cons
})
for _, candidate := range GetCandidates() {
log.Trace().Str("candidate", candidate).Msg("[webrtc] config")
tr.Write(&api.Message{Type: "webrtc/candidate", Value: candidate})
}
}
func syncCanditates(answer string) (string, error) {
if len(addresses) == 0 {
return answer, nil
}
sd := &sdp.SessionDescription{}
if err := sd.Unmarshal([]byte(answer)); err != nil {
return "", err
}
md := sd.MediaDescriptions[0]
for _, candidate := range GetCandidates() {
md.WithPropertyAttribute(candidate)
}
data, err := sd.Marshal()
if err != nil {
return "", err
}
return string(data), nil
}
func candidateHandler(tr *api.Transport, msg *api.Message) error {
// process incoming candidate in sync function
tr.WithContext(func(ctx map[any]any) {
candidate := msg.String()
log.Trace().Str("candidate", candidate).Msg("[webrtc] remote")
if cons, ok := ctx["webrtc"].(*webrtc.Conn); ok {
// if webrtc.Server already initialized - process candidate
_ = cons.AddCandidate(candidate)
} else {
// or collect candidate and process it later
list, _ := ctx["candidate"].([]string)
ctx["candidate"] = append(list, candidate)
}
})
return nil
}

178
internal/webrtc/client.go Normal file
View File

@@ -0,0 +1,178 @@
package webrtc
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/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
}

275
internal/webrtc/init.go Normal file
View File

@@ -0,0 +1,275 @@
package webrtc
import (
"errors"
"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/webrtc"
pion "github.com/pion/webrtc/v3"
"github.com/rs/zerolog"
"net"
)
func Init() {
var cfg struct {
Mod struct {
Listen string `yaml:"listen"`
Candidates []string `yaml:"candidates"`
IceServers []pion.ICEServer `yaml:"ice_servers"`
} `yaml:"webrtc"`
}
cfg.Mod.Listen = ":8555/tcp"
cfg.Mod.IceServers = []pion.ICEServer{
{URLs: []string{"stun:stun.l.google.com:19302"}},
}
app.LoadConfig(&cfg)
log = app.GetLogger("webrtc")
address := cfg.Mod.Listen
// create pionAPI with custom codecs list and custom network settings
serverAPI, err := webrtc.NewAPI(address)
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
// use same API for WebRTC server and client if no address
clientAPI := serverAPI
if address != "" {
log.Info().Str("addr", address).Msg("[webrtc] listen")
_, Port, _ = net.SplitHostPort(address)
clientAPI, _ = webrtc.NewAPI("")
}
pionConf := pion.Configuration{
ICEServers: cfg.Mod.IceServers,
SDPSemantics: pion.SDPSemanticsUnifiedPlanWithFallback,
}
PeerConnection = func(active bool) (*pion.PeerConnection, error) {
// active - client, passive - server
if active {
return clientAPI.NewPeerConnection(pionConf)
} else {
return serverAPI.NewPeerConnection(pionConf)
}
}
for _, candidate := range cfg.Mod.Candidates {
AddCandidate(candidate)
}
// async WebRTC server (two API versions)
api.HandleWS("webrtc", asyncHandler)
api.HandleWS("webrtc/offer", asyncHandler)
api.HandleWS("webrtc/candidate", candidateHandler)
// sync WebRTC server (two API versions)
api.HandleFunc("api/webrtc", syncHandler)
// WebRTC client
streams.HandleFunc("webrtc", streamsHandler)
}
var Port string
var log zerolog.Logger
var PeerConnection func(active bool) (*pion.PeerConnection, error)
func asyncHandler(tr *api.Transport, msg *api.Message) error {
var stream *streams.Stream
var mode core.Mode
query := tr.Request.URL.Query()
if name := query.Get("src"); name != "" {
stream = streams.GetOrNew(name)
mode = core.ModePassiveConsumer
log.Debug().Str("src", name).Msg("[webrtc] new consumer")
} else if name = query.Get("dst"); name != "" {
stream = streams.Get(name)
mode = core.ModePassiveProducer
log.Debug().Str("src", name).Msg("[webrtc] new producer")
}
if stream == nil {
return errors.New(api.StreamNotFound)
}
// create new PeerConnection instance
pc, err := PeerConnection(false)
if err != nil {
log.Error().Err(err).Caller().Send()
return err
}
var sendAnswer core.Waiter
conn := webrtc.NewConn(pc)
conn.Desc = "WebRTC/WebSocket async"
conn.Mode = mode
conn.UserAgent = tr.Request.UserAgent()
conn.Listen(func(msg any) {
switch msg := msg.(type) {
case pion.PeerConnectionState:
if msg != pion.PeerConnectionStateClosed {
return
}
switch mode {
case core.ModePassiveConsumer:
stream.RemoveConsumer(conn)
case core.ModePassiveProducer:
stream.RemoveProducer(conn)
}
case *pion.ICECandidate:
sendAnswer.Wait()
s := msg.ToJSON().Candidate
log.Trace().Str("candidate", s).Msg("[webrtc] local")
tr.Write(&api.Message{Type: "webrtc/candidate", Value: s})
}
})
// V2 - json/object exchange, V1 - raw SDP exchange
apiV2 := msg.Type == "webrtc"
// 1. SetOffer, so we can get remote client codecs
var offer string
if apiV2 {
offer = msg.GetString("sdp")
} else {
offer = msg.String()
}
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
if err = conn.SetOffer(offer); err != nil {
log.Warn().Err(err).Caller().Send()
return err
}
switch mode {
case core.ModePassiveConsumer:
// 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil {
log.Debug().Err(err).Msg("[webrtc] add consumer")
_ = conn.Close()
return err
}
case core.ModePassiveProducer:
stream.AddProducer(conn)
}
// 3. Exchange SDP without waiting all candidates
answer, err := conn.GetAnswer()
log.Trace().Msgf("[webrtc] answer\n%s", answer)
if err != nil {
log.Error().Err(err).Caller().Send()
return err
}
if apiV2 {
desc := pion.SessionDescription{Type: pion.SDPTypeAnswer, SDP: answer}
tr.Write(&api.Message{Type: "webrtc", Value: desc})
} else {
tr.Write(&api.Message{Type: "webrtc/answer", Value: answer})
}
sendAnswer.Done()
asyncCandidates(tr, conn)
return nil
}
func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer string, err error) {
pc, err := PeerConnection(false)
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
// create new webrtc instance
conn := webrtc.NewConn(pc)
conn.Desc = desc
conn.UserAgent = userAgent
conn.Listen(func(msg any) {
switch msg := msg.(type) {
case pion.PeerConnectionState:
if msg != pion.PeerConnectionStateClosed {
return
}
if conn.Mode == core.ModePassiveConsumer {
stream.RemoveConsumer(conn)
} else {
stream.RemoveProducer(conn)
}
}
})
// 1. SetOffer, so we can get remote client codecs
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
if err = conn.SetOffer(offer); err != nil {
log.Warn().Err(err).Caller().Send()
return
}
if IsConsumer(conn) {
conn.Mode = core.ModePassiveConsumer
// 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Caller().Send()
_ = conn.Close()
return
}
} else {
conn.Mode = core.ModePassiveProducer
stream.AddProducer(conn)
}
answer, err = conn.GetCompleteAnswer()
if err == nil {
answer, err = syncCanditates(answer)
}
log.Trace().Msgf("[webrtc] answer\n%s", answer)
if err != nil {
log.Error().Err(err).Caller().Send()
}
return
}
func IsConsumer(conn *webrtc.Conn) bool {
// if wants get video - consumer
for _, media := range conn.GetMedias() {
if media.Kind == core.KindVideo && media.Direction == core.DirectionSendonly {
return true
}
}
// if wants send video - producer
for _, media := range conn.GetMedias() {
if media.Kind == core.KindVideo && media.Direction == core.DirectionRecvonly {
return false
}
}
// if wants something - consumer
for _, media := range conn.GetMedias() {
if media.Direction == core.DirectionSendonly {
return true
}
}
return false
}

210
internal/webrtc/server.go Normal file
View File

@@ -0,0 +1,210 @@
package webrtc
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
"io"
"net/http"
"strconv"
"strings"
"time"
)
const MimeSDP = "application/sdp"
var sessions = map[string]*webrtc.Conn{}
func syncHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "POST":
query := r.URL.Query()
if query.Get("src") != "" {
// WHEP or JSON SDP or raw SDP exchange
outputWebRTC(w, r)
} else if query.Get("dst") != "" {
// WHIP SDP exchange
inputWebRTC(w, r)
} else {
http.Error(w, "", http.StatusBadRequest)
}
case "PATCH":
// TODO: WHEP/WHIP
http.Error(w, "", http.StatusMethodNotAllowed)
case "DELETE":
if id := r.URL.Query().Get("id"); id != "" {
if conn, ok := sessions[id]; ok {
delete(sessions, id)
_ = conn.Close()
} else {
http.Error(w, "", http.StatusNotFound)
}
} else {
http.Error(w, "", http.StatusBadRequest)
}
default:
http.Error(w, "", http.StatusMethodNotAllowed)
}
}
// outputWebRTC support API depending on Content-Type:
// 1. application/json - receive {"type":"offer","sdp":"v=0\r\n..."} and response {"type":"answer","sdp":"v=0\r\n..."}
// 2. application/sdp - receive/response SDP via WebRTC-HTTP Egress Protocol (WHEP)
// 3. other - receive/response raw SDP
func outputWebRTC(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("src")
stream := streams.Get(url)
if stream == nil {
return
}
mediaType := r.Header.Get("Content-Type")
if mediaType != "" {
mediaType, _, _ = strings.Cut(mediaType, ";")
mediaType = strings.ToLower(strings.TrimSpace(mediaType))
}
var offer string
switch mediaType {
case "application/json":
var desc pion.SessionDescription
if err := json.NewDecoder(r.Body).Decode(&desc); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
offer = desc.SDP
default:
body, err := io.ReadAll(r.Body)
if err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
offer = string(body)
}
var desc string
switch mediaType {
case "application/json":
desc = "WebRTC/JSON sync"
case MimeSDP:
desc = "WebRTC/WHEP sync"
default:
desc = "WebRTC/HTTP sync"
}
answer, err := ExchangeSDP(stream, offer, desc, r.UserAgent())
if err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
switch mediaType {
case "application/json":
w.Header().Set("Content-Type", mediaType)
v := pion.SessionDescription{
Type: pion.SDPTypeAnswer, SDP: answer,
}
err = json.NewEncoder(w).Encode(v)
case MimeSDP:
w.Header().Set("Content-Type", mediaType)
w.WriteHeader(http.StatusCreated)
_, err = w.Write([]byte(answer))
default:
_, err = w.Write([]byte(answer))
}
if err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func inputWebRTC(w http.ResponseWriter, r *http.Request) {
dst := r.URL.Query().Get("dst")
stream := streams.Get(dst)
if stream == nil {
stream = streams.New(dst, nil)
}
// 1. Get offer
offer, err := io.ReadAll(r.Body)
if err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Trace().Msgf("[webrtc] WHIP offer\n%s", offer)
pc, err := PeerConnection(false)
if err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// create new webrtc instance
prod := webrtc.NewConn(pc)
prod.Desc = "WebRTC/WHIP sync"
prod.Mode = core.ModePassiveProducer
prod.UserAgent = r.UserAgent()
if err = prod.SetOffer(string(offer)); err != nil {
log.Warn().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
answer, err := prod.GetCompleteAnswer()
if err == nil {
answer, err = syncCanditates(answer)
}
if err != nil {
log.Warn().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Trace().Msgf("[webrtc] WHIP answer\n%s", answer)
id := strconv.FormatInt(time.Now().UnixNano(), 36)
sessions[id] = prod
prod.Listen(func(msg any) {
switch msg := msg.(type) {
case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateClosed {
stream.RemoveProducer(prod)
if _, ok := sessions[id]; ok {
delete(sessions, id)
}
}
}
})
stream.AddProducer(prod)
w.Header().Set("Content-Type", MimeSDP)
w.Header().Set("Location", "webrtc?id="+id)
w.WriteHeader(http.StatusCreated)
if _, err = w.Write([]byte(answer)); err != nil {
log.Warn().Err(err).Caller().Send()
return
}
}

175
internal/webtorrent/init.go Normal file
View File

@@ -0,0 +1,175 @@
package webtorrent
import (
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webtorrent"
"github.com/rs/zerolog"
"net/http"
"net/url"
)
func Init() {
var cfg struct {
Mod struct {
Trackers []string `yaml:"trackers"`
Shares map[string]struct {
Pwd string `yaml:"pwd"`
Src string `yaml:"src"`
} `yaml:"shares"`
} `yaml:"webtorrent"`
}
cfg.Mod.Trackers = []string{"wss://tracker.openwebtorrent.com"}
app.LoadConfig(&cfg)
if len(cfg.Mod.Trackers) == 0 {
return
}
log = app.GetLogger("webtorrent")
streams.HandleFunc("webtorrent", streamHandle)
api.HandleFunc("api/webtorrent", apiHandle)
srv = &webtorrent.Server{
URL: cfg.Mod.Trackers[0],
Exchange: func(src, offer string) (answer string, err error) {
stream := streams.Get(src)
if stream == nil {
return "", errors.New(api.StreamNotFound)
}
return webrtc.ExchangeSDP(stream, offer, "WebRTC/WebTorrent sync", "")
},
}
if log.Debug().Enabled() {
srv.Listen(func(msg any) {
switch msg.(type) {
case string, error:
log.Debug().Msgf("[webtorrent] %s", msg)
case *webtorrent.Message:
log.Trace().Any("msg", msg).Msgf("[webtorrent]")
}
})
}
for name, share := range cfg.Mod.Shares {
if len(name) < 8 {
log.Warn().Str("name", name).Msgf("min share name len - 8 symbols")
continue
}
if len(share.Pwd) < 4 {
log.Warn().Str("name", name).Str("pwd", share.Pwd).Msgf("min share pwd len - 4 symbols")
continue
}
if streams.Get(share.Src) == nil {
log.Warn().Str("stream", share.Src).Msgf("stream not exists")
continue
}
srv.AddShare(name, share.Pwd, share.Src)
// adds to GET /api/webtorrent
shares[name] = name
}
}
var log zerolog.Logger
var shares = map[string]string{}
var srv *webtorrent.Server
func apiHandle(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
share, ok := shares[src]
switch r.Method {
case "GET":
// support act as WebTorrent tracker (for testing purposes)
if r.Header.Get("Connection") == "Upgrade" {
tracker(w, r)
return
}
if src != "" {
// response one share
if ok {
pwd := srv.GetSharePwd(share)
data := fmt.Sprintf(`{"share":%q,"pwd":%q}`, share, pwd)
_, _ = w.Write([]byte(data))
} else {
http.Error(w, "", http.StatusNotFound)
}
} else {
// response all shares
var items []api.Stream
for src, share := range shares {
pwd := srv.GetSharePwd(share)
source := fmt.Sprintf("webtorrent:?share=%s&pwd=%s", share, pwd)
items = append(items, api.Stream{Name: src, URL: source})
}
api.ResponseStreams(w, items)
}
case "POST":
// check if share already exist
if ok {
http.Error(w, "", http.StatusBadRequest)
return
}
// check if stream exists
if stream := streams.Get(src); stream == nil {
http.Error(w, "", http.StatusNotFound)
return
}
// create new random share
share = core.RandString(10, 62)
pwd := core.RandString(10, 62)
srv.AddShare(share, pwd, src)
shares[src] = share
w.WriteHeader(http.StatusCreated)
data := fmt.Sprintf(`{"share":%q,"pwd":%q}`, share, pwd)
_, _ = w.Write([]byte(data))
case "DELETE":
if ok {
srv.RemoveShare(share)
delete(shares, src)
} else {
http.Error(w, "", http.StatusNotFound)
}
}
}
func streamHandle(rawURL string) (core.Producer, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
query := u.Query()
share := query.Get("share")
pwd := query.Get("pwd")
if len(share) < 8 || len(pwd) < 4 {
return nil, errors.New("wrong URL: " + rawURL)
}
pc, err := webrtc.PeerConnection(true)
if err != nil {
return nil, err
}
return webtorrent.NewClient(srv.URL, share, pwd, pc)
}

View File

@@ -0,0 +1,107 @@
package webtorrent
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/webtorrent"
"github.com/gorilla/websocket"
"net/http"
)
var upgrader *websocket.Upgrader
var hashes map[string]map[string]*websocket.Conn
func tracker(w http.ResponseWriter, r *http.Request) {
if upgrader == nil {
upgrader = &websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 2028,
}
upgrader.CheckOrigin = func(r *http.Request) bool {
return true
}
}
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Warn().Err(err).Send()
return
}
defer ws.Close()
for {
var msg webtorrent.Message
if err = ws.ReadJSON(&msg); err != nil {
return
}
//log.Trace().Msgf("[webtorrent] message=%v", msg)
if msg.InfoHash == "" || msg.PeerId == "" {
continue
}
if hashes == nil {
hashes = map[string]map[string]*websocket.Conn{}
}
// new or old client with offers
clients := hashes[msg.InfoHash]
if clients == nil {
clients = map[string]*websocket.Conn{
msg.PeerId: ws,
}
hashes[msg.InfoHash] = clients
} else {
clients[msg.PeerId] = ws
}
switch {
case msg.Offers != nil:
// ask for ping
raw := fmt.Sprintf(
`{"action":"announce","interval":120,"info_hash":"%s","complete":0,"incomplete":1}`,
msg.InfoHash,
)
if err = ws.WriteMessage(websocket.TextMessage, []byte(raw)); err != nil {
log.Warn().Err(err).Send()
return
}
// skip if no offers (server)
if len(msg.Offers) == 0 {
continue
}
// get and check only first offer
offer := msg.Offers[0]
if offer.OfferId == "" || offer.Offer.Type != "offer" || offer.Offer.SDP == "" {
continue
}
// send offer to all clients (one of them - server)
raw = fmt.Sprintf(
`{"action":"announce","info_hash":"%s","peer_id":"%s","offer_id":"%s","offer":{"type":"offer","sdp":"%s"}}`,
msg.InfoHash, msg.PeerId, offer.OfferId, offer.Offer.SDP,
)
for _, server := range clients {
if server != ws {
_ = server.WriteMessage(websocket.TextMessage, []byte(raw))
}
}
case msg.OfferId != "" && msg.ToPeerId != "" && msg.Answer != nil:
ws1, ok := clients[msg.ToPeerId]
if !ok {
continue
}
raw := fmt.Sprintf(
`{"action":"announce","info_hash":"%s","peer_id":"%s","offer_id":"%s","answer":{"type":"answer","sdp":"%s"}}`,
msg.InfoHash, msg.PeerId, msg.OfferId, msg.Answer.SDP,
)
_ = ws1.WriteMessage(websocket.TextMessage, []byte(raw))
}
}
}

61
main.go
View File

@@ -1,20 +1,32 @@
package main
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/debug"
"github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
"github.com/AlexxIT/go2rtc/cmd/hass"
"github.com/AlexxIT/go2rtc/cmd/homekit"
"github.com/AlexxIT/go2rtc/cmd/mse"
"github.com/AlexxIT/go2rtc/cmd/ngrok"
"github.com/AlexxIT/go2rtc/cmd/rtmp"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/srtp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/debug"
"github.com/AlexxIT/go2rtc/internal/dvrip"
"github.com/AlexxIT/go2rtc/internal/echo"
"github.com/AlexxIT/go2rtc/internal/exec"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
"github.com/AlexxIT/go2rtc/internal/hass"
"github.com/AlexxIT/go2rtc/internal/hls"
"github.com/AlexxIT/go2rtc/internal/homekit"
"github.com/AlexxIT/go2rtc/internal/http"
"github.com/AlexxIT/go2rtc/internal/isapi"
"github.com/AlexxIT/go2rtc/internal/ivideon"
"github.com/AlexxIT/go2rtc/internal/mjpeg"
"github.com/AlexxIT/go2rtc/internal/mp4"
"github.com/AlexxIT/go2rtc/internal/mpegts"
"github.com/AlexxIT/go2rtc/internal/ngrok"
"github.com/AlexxIT/go2rtc/internal/onvif"
"github.com/AlexxIT/go2rtc/internal/roborock"
"github.com/AlexxIT/go2rtc/internal/rtmp"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/srtp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/tapo"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"github.com/AlexxIT/go2rtc/internal/webtorrent"
"os"
"os/signal"
"syscall"
@@ -22,22 +34,33 @@ import (
func main() {
app.Init() // init config and logs
api.Init() // init HTTP API server
streams.Init() // load streams list
onvif.Init()
rtsp.Init() // add support RTSP client and RTSP server
rtmp.Init() // add support RTMP client
exec.Init() // add support exec scheme (depends on RTSP server)
ffmpeg.Init() // add support ffmpeg scheme (depends on exec scheme)
hass.Init() // add support hass scheme
api.Init() // init HTTP API server
webrtc.Init()
mse.Init()
echo.Init()
ivideon.Init()
http.Init()
dvrip.Init()
tapo.Init()
isapi.Init()
mpegts.Init()
roborock.Init()
srtp.Init()
homekit.Init()
webrtc.Init()
mp4.Init()
hls.Init()
mjpeg.Init()
webtorrent.Init()
ngrok.Init()
debug.Init()

8
pkg/README.md Normal file
View File

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

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