Compare commits

...

269 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
Sergey Krashevich
a36359f3dd Update api.go 2023-01-20 18:38:46 +03:00
226 changed files with 17165 additions and 5747 deletions

View File

@@ -52,7 +52,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
platforms: |
@@ -63,9 +63,10 @@ jobs:
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@v3
uses: docker/build-push-action@v4
with:
context: .
file: hardware.Dockerfile
@@ -73,3 +74,5 @@ jobs:
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

View File

@@ -15,13 +15,19 @@ jobs:
- 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
esport CGO_ENABLED=0
export CGO_ENABLED=0
mkdir artifacts
mkdir -p artifacts
export GOOS=windows
export GOARCH=amd64
export FILENAME=artifacts/go2rtc_win64.zip
@@ -65,13 +71,14 @@ jobs:
export GOOS=darwin
export GOARCH=amd64
export FILENAME=artifacts/go2rtc_mac_amd64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc
go build -ldflags "-s -w" -trimpath -o go2rtc.amd64
export GOOS=darwin
export GOARCH=arm64
export FILENAME=artifacts/go2rtc_mac_arm64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc
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

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

View File

@@ -2,7 +2,7 @@
# 0. Prepare images
ARG PYTHON_VERSION="3.11"
ARG GO_VERSION="1.19"
ARG GO_VERSION="1.20"
ARG NGROK_VERSION="3"
FROM python:${PYTHON_VERSION}-alpine AS base
@@ -40,7 +40,8 @@ FROM base
# Install ffmpeg, tini (for signal handling),
# and other common tools for the echo source.
RUN apk add --no-cache tini ffmpeg bash curl jq
# alsa-plugins-pulse for ALSA support (+0MB)
RUN apk add --no-cache tini ffmpeg bash curl jq alsa-plugins-pulse
# Hardware Acceleration for Intel CPU (+50MB)
ARG TARGETARCH

509
README.md
View File

@@ -6,15 +6,17 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- zero-dependency and zero-config [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, ARM)
- zero-delay for many supported protocols (lowest possible streaming latency)
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [HTTP](#source-http) (FLV/MJPEG/JPEG), [FFmpeg](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4) or [MJPEG](#module-mjpeg)
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [DVRIP](#source-dvrip), [HTTP](#source-http) (FLV/MJPEG/JPEG/TS), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
- streaming from any sources, supported by [FFmpeg](#source-ffmpeg)
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4), [HLS](#module-hls) or [MJPEG](#module-mjpeg)
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
- first project in the World with support H265 for WebRTC in browser (Safari only, [read more](https://github.com/AlexxIT/Blog/issues/5))
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
- play audio files and live streams on some cameras with [speaker](#stream-to-camera)
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
- mixing tracks from different sources to single stream
- auto match client supported codecs
- 2-way audio for `ONVIF Profile T` Cameras
- [2-way audio](#two-way-audio) for some cameras
- streaming from private networks via [Ngrok](#module-ngrok)
- can be [integrated to](#module-api) any smart home platform or be used as [standalone app](#go2rtc-binary)
@@ -31,39 +33,53 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
* [Fast start](#fast-start)
* [go2rtc: Binary](#go2rtc-binary)
* [go2rtc: Home Assistant Add-on](#go2rtc-home-assistant-add-on)
* [go2rtc: Docker](#go2rtc-docker)
* [go2rtc: Home Assistant Add-on](#go2rtc-home-assistant-add-on)
* [go2rtc: Home Assistant Integration](#go2rtc-home-assistant-integration)
* [Configuration](#configuration)
* [Module: Streams](#module-streams)
* [Two way audio](#two-way-audio)
* [Source: RTSP](#source-rtsp)
* [Source: RTMP](#source-rtmp)
* [Source: HTTP](#source-http)
* [Source: ONVIF](#source-onvif)
* [Source: FFmpeg](#source-ffmpeg)
* [Source: FFmpeg Device](#source-ffmpeg-device)
* [Source: Exec](#source-exec)
* [Source: Echo](#source-echo)
* [Source: HomeKit](#source-homekit)
* [Source: DVRIP](#source-dvrip)
* [Source: Tapo](#source-tapo)
* [Source: Ivideon](#source-ivideon)
* [Source: Hass](#source-hass)
* [Source: ISAPI](#source-isapi)
* [Source: Roborock](#source-roborock)
* [Source: WebRTC](#source-webrtc)
* [Source: WebTorrent](#source-webtorrent)
* [Incoming sources](#incoming-sources)
* [Stream to camera](#stream-to-camera)
* [Module: API](#module-api)
* [Module: RTSP](#module-rtsp)
* [Module: WebRTC](#module-webrtc)
* [Module: WebTorrent](#module-webtorrent)
* [Module: Ngrok](#module-ngrok)
* [Module: Hass](#module-hass)
* [From go2rtc to Hass](#from-go2rtc-to-hass)
* [From Hass to go2rtc](#from-hass-to-go2rtc)
* [Module: MP4](#module-mp4)
* [Module: HLS](#module-hls)
* [Module: MJPEG](#module-mjpeg)
* [Module: Log](#module-log)
* [Security](#security)
* [Codecs filters](#codecs-filters)
* [Codecs madness](#codecs-madness)
* [Codecs negotiation](#codecs-negotiation)
* [Projects using go2rtc](#projects-using-go2rtc)
* [Camera experience](#cameras-experience)
* [TIPS](#tips)
* [FAQ](#faq)
## Fast start
1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on)
1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or Home Assistant [Add-on](#go2rtc-home-assistant-add-on) or [Integration](#go2rtc-home-assistant-integration)
2. Open web interface: `http://localhost:1984/`
**Optionally:**
@@ -92,6 +108,10 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
### go2rtc: Docker
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [Ngrok](#module-ngrok) and [Python](#source-echo).
### go2rtc: Home Assistant Add-on
[![](https://my.home-assistant.io/badges/supervisor_addon.svg)](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons)
@@ -101,9 +121,9 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
- go2rtc > Install > Start
2. Setup [Integration](#module-hass)
### go2rtc: Docker
### go2rtc: Home Assistant Integration
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [Ngrok](#module-ngrok) and [Python](#source-echo).
[WebRTC Camera](https://github.com/AlexxIT/WebRTC) custom component can be used on any [Home Assistant installation](https://www.home-assistant.io/installation/), including [HassWP](https://github.com/AlexxIT/HassWP) on Windows. It can automatically download and use the latest version of go2rtc. Or it can connect to an existing version of go2rtc. Addon installation in this case is optional.
## Configuration
@@ -122,6 +142,7 @@ Available modules:
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
- [webrtc](#module-webrtc) - WebRTC Server
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot Server
- [hls](#module-hls) - HLS TS or fMP4 stream Server
- [mjpeg](#module-mjpeg) - MJPEG Server
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
@@ -134,41 +155,63 @@ Available modules:
Available source types:
- [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras
- [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras with [two way audio](#two-way-audio) support
- [rtmp](#source-rtmp) - `RTMP` streams
- [http](#source-http) - `HTTP-FLV`, `JPEG` (snapshots), `MJPEG` streams
- [http](#source-http) - `HTTP-FLV`, `MPEG-TS`, `JPEG` (snapshots), `MJPEG` streams
- [onvif](#source-onvif) - get camera `RTSP` link and snapshot link using `ONVIF` protocol
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`HLS`, `files` and many others)
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
- [exec](#source-exec) - get media from external app output
- [echo](#source-echo) - get stream link from bash or python
- [homekit](#source-homekit) - streaming from HomeKit Camera
- [dvrip](#source-dvrip) - streaming from DVR-IP NVR
- [tapo](#source-tapo) - TP-Link Tapo cameras with [two way audio](#two-way-audio) support
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
- [hass](#source-hass) - Home Assistant integration
- [isapi](#source-isapi) - two way audio for Hikvision (ISAPI) cameras
- [roborock](#source-roborock) - Roborock vacuums with cameras
- [webrtc](#source-webrtc) - WebRTC/WHEP sources
- [webtorrent](#source-webtorrent) - WebTorrent source from another go2rtc
Read more about [incoming sources](#incoming-sources)
#### Two way audio
Supported for sources:
- [RTSP cameras](#source-rtsp) with [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) (back channel connection)
- [TP-Link Tapo](#source-tapo) cameras
- [Hikvision ISAPI](#source-isapi) cameras
- [Roborock vacuums](#source-roborock) models with cameras
- [Any Browser](#incoming-browser) as IP-camera
Two way audio can be used in browser with [WebRTC](#module-webrtc) technology. The browser will give access to the microphone only for HTTPS sites ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https)).
go2rtc also support [play audio](#stream-to-camera) files and live streams on this cameras.
#### Source: RTSP
- Support **RTSP and RTSPS** links with multiple video and audio tracks
- Support **2-way audio** ONLY for [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) cameras (back channel connection)
**Attention:** other 2-way audio standards are not supported! ONVIF without Profile T is not supported!
```yaml
streams:
sonoff_camera: rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
```
If your camera has two RTSP links - you can add both of them as sources. This is useful when streams has different codecs, as example AAC audio with main stream and PCMU/PCMA audio with second stream.
**Attention:** Dahua cameras has different capabilities for different RTSP links. For example, it has support multiple codecs for 2-way audio with `&proto=Onvif` in link and only one codec without it.
```yaml
streams:
dahua_camera:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1
amcrest_doorbell:
- rtsp://username:password@192.168.1.123:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
unify_camera: rtspx://192.168.1.123:7441/fD6ouM72bWoFijxK
glichy_camera: ffmpeg:rstp://username:password@192.168.1.123/live/ch00_1
```
**PS.** For disable bachannel just add `#backchannel=0` to end of RTSP link.
**Recommendations**
- **Amcrest Doorbell** users may want to disable two way audio, because with an active stream you won't have a call button working. You need to add `#backchannel=0` to the end of your RTSP link in YAML config file
- **Dahua Doorbell** users may want to change backchannel [audio codec](https://github.com/AlexxIT/go2rtc/issues/52)
- **Unify** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81)
- **TP-Link Tapo** users may skip login and password, because go2rtc support login [without them](https://drmnsamoliu.github.io/video.html)
- If your camera has two RTSP links - you can add both of them as sources. This is useful when streams has different codecs, as example AAC audio with main stream and PCMU/PCMA audio with second stream
- If the stream from your camera is glitchy, try using [ffmpeg source](#source-ffmpeg). It will not add CPU load if you won't use transcoding
- If the stream from your camera is very glitchy, try to use transcoding with [ffmpeg source](#source-ffmpeg)
#### Source: RTMP
@@ -186,6 +229,9 @@ Support Content-Type:
- **HTTP-FLV** (`video/x-flv`) - same as RTMP, but over HTTP
- **HTTP-JPEG** (`image/jpeg`) - camera snapshot link, can be converted by go2rtc to MJPEG stream
- **HTTP-MJPEG** (`multipart/x`) - simple MJPEG stream over HTTP
- **MPEG-TS** (`video/mpeg`) - legacy [streaming format](https://en.wikipedia.org/wiki/MPEG_transport_stream)
Source also support HTTP and TCP streams with autodetection for different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**.
```yaml
streams:
@@ -197,10 +243,26 @@ streams:
# [MJPEG] stream will be proxied without modification
http_mjpeg: https://mjpeg.sanford.io/count.mjpeg
# [MJPEG or H.264/H.265 bitstream or MPEG-TS]
tcp_magic: tcp://192.168.1.123:12345
```
**PS.** Dahua camera has bug: if you select MJPEG codec for RTSP second stream - snapshot won't work.
#### Source: ONVIF
The source is not very useful if you already know RTSP and snapshot links for your camera. But it can be useful if you don't.
**WebUI > Add** webpage support ONVIF autodiscovery. Your server must be on the same subnet as the camera. If you use docker, you must use "network host".
```yaml
streams:
dahua1: onvif://admin:password@192.168.1.123
reolink1: onvif://admin:password@192.168.1.123:8000
tapo1: onvif://admin:password@192.168.1.123:2020
```
#### Source: FFmpeg
You can get any stream or file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream.
@@ -231,7 +293,7 @@ streams:
rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
```
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/cmd/ffmpeg/ffmpeg.go): `h264`, `h264/ultra`, `h264/high`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/internal/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
But you can override them via YAML config. You can also add your own formats to config and use them with source params.
@@ -240,13 +302,17 @@ ffmpeg:
bin: ffmpeg # path to ffmpeg binary
h264: "-codec:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1"
mycodec: "-any args that support ffmpeg..."
myinput: "-fflags nobuffer -flags low_delay -timeout 5000000 -i {input}"
```
- You can use `video` and `audio` params multiple times (ex. `#video=copy#audio=copy#audio=pcmu`)
- You can use go2rtc stream name as ffmpeg input (ex. `ffmpeg:camera1#video=h264`)
- You can use `rotate` params with `90`, `180`, `270` or `-90` values, important with transcoding (ex. `#video=h264#rotate=90`)
- You can use `width` and/or `height` params, important with transcoding (ex. `#video=h264#width=1280`)
- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`).
- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`)
- You can use `input` param to override default input template (ex. `#input=rtsp/udp` will change RTSP transport from TCP to UDP+TCP)
- You can use raw input value (ex. `#input=-timeout 5000000 -i {input}`)
- You can add your own input templates
Read more about encoding [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration).
@@ -255,25 +321,40 @@ Read more about encoding [hardware acceleration](https://github.com/AlexxIT/go2r
You can get video from any USB-camera or Webcam as RTSP or WebRTC stream. This is part of FFmpeg integration.
- check available devices in Web interface
- `resolution` and `framerate` must be supported by your camera!
- `video_size` and `framerate` must be supported by your camera!
- for Linux supported only video for now
- for macOS you can stream Facetime camera or whole Desktop!
- for macOS important to set right framerate
Format: `ffmpeg:device?{input-params}#{param1}#{param2}#{param3}`
```yaml
streams:
linux_usbcam: ffmpeg:device?video=0&resolution=1280x720#video=h264
linux_usbcam: ffmpeg:device?video=0&video_size=1280x720#video=h264
windows_webcam: ffmpeg:device?video=0#video=h264
macos_facetime: ffmpeg:device?video=0&audio=1&resolution=1280x720&framerate=30#video=h264#audio=pcma
macos_facetime: ffmpeg:device?video=0&audio=1&video_size=1280x720&framerate=30#video=h264#audio=pcma
```
#### Source: Exec
FFmpeg source just a shortcut to exec source. You can get any stream or file or device via FFmpeg or GStreamer and push it to go2rtc via RTSP protocol:
Exec source can run any external application and expect data from it. Two transports are supported - **pipe** and **RTSP**.
If you want to use **RTSP** transport - the command must contain the `{output}` argument in any place. On launch, it will be replaced by the local address of the RTSP server.
**pipe** reads data from app stdout in different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**.
The source can be used with:
- [FFmpeg](https://ffmpeg.org/) - go2rtc ffmpeg source just a shortcut to exec source
- [GStreamer](https://gstreamer.freedesktop.org/)
- [Raspberry Pi Cameras](https://www.raspberrypi.com/documentation/computers/camera_software.html)
- any your own software
```yaml
streams:
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
stream: exec:ffmpeg -re -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
picam_h264: exec:libcamera-vid -t 0 --inline -o -
picam_mjpeg: exec:libcamera-vid -t 0 --codec mjpeg -o -
```
#### Source: Echo
@@ -323,6 +404,36 @@ RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g
**This source is in active development!** Tested only with [Aqara Camera Hub G3](https://www.aqara.com/eu/product/camera-hub-g3) (both EU and CN versions).
#### Source: DVRIP
Other names: DVR-IP, NetSurveillance, Sofia protocol (NETsurveillance ActiveX plugin XMeye SDK).
- you can skip `username`, `password`, `port`, `channel` and `subtype` if they are default
- setup separate streams for different channels
- use `subtype=0` for Main stream, and `subtype=1` for Extra1 stream
- only the TCP protocol is supported
```yaml
streams:
camera1: dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
```
#### Source: Tapo
[TP-Link Tapo](https://www.tapo.com/) proprietary camera protocol with **two way audio** support.
- stream quality is the same as [RTSP protocol](https://www.tapo.com/en/faq/34/)
- use the **cloud password**, this is not the RTSP password! you do not need to add a login!
- you can also use UPPERCASE MD5 hash from your cloud password with `admin` username
```yaml
streams:
# cloud password without username
camera1: tapo://cloud-password@192.168.1.123
# admin username and UPPERCASE MD5 cloud-password hash
camera2: tapo://admin:MD5-PASSWORD-HASH@192.168.1.123
```
#### Source: Ivideon
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
@@ -350,6 +461,115 @@ streams:
More cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), [ONVIF](https://www.home-assistant.io/integrations/onvif/), and possibly others can also be imported by using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-home-assistant-cameras-to-go2rtc-andor-frigate).
#### Source: ISAPI
This source type support only backchannel audio for Hikvision ISAPI protocol. So it should be used as second source in addition to the RTSP protocol.
```yaml
streams:
hikvision1:
- rtsp://admin:password@192.168.1.123:554/Streaming/Channels/101
- isapi://admin:password@192.168.1.123:80/
```
#### Source: Roborock
This source type support Roborock vacuums with cameras. Known working models:
- Roborock S6 MaxV - only video (the vacuum has no microphone)
- Roborock S7 MaxV - video and two way audio
Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config.
If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 678) to the end of the roborock-link.
#### Source: WebRTC
This source type support two connection formats:
- [WebRTC/WHEP](https://www.ietf.org/id/draft-murillo-whep-01.html) - is an unapproved standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
- `go2rtc/WebSocket` - This format is only supported in go2rtc. Unlike WHEP it supports asynchronous WebRTC connection and two way audio.
```yaml
streams:
webrtc1: webrtc:http://192.168.1.123:1984/api/webrtc?src=dahua1
webrtc2: webrtc:ws://192.168.1.123:1984/api/ws?src=dahua1
```
#### Source: WebTorrent
This source can get a stream from another go2rtc via [WebTorrent](#module-webtorrent) protocol.
```yaml
streams:
webtorrent1: webtorrent:?share=huofssuxaty00izc&pwd=k3l2j9djeg8v8r7e
```
#### Incoming sources
By default, go2rtc establishes a connection to the source when any client requests it. Go2rtc drops the connection to the source when it has no clients left.
- Go2rtc also can accepts incoming sources in [RTSP](#source-rtsp), [HTTP](#source-http) and **WebRTC/WHIP** formats
- Go2rtc won't stop such a source if it has no clients
- You can push data only to existing stream (create stream with empty source in config)
- You can push multiple incoming sources to same stream
- You can push data to non empty stream, so it will have additional codecs inside
**Examples**
- RTSP with any codec
```yaml
ffmpeg -re -i BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp rtsp://localhost:8554/camera1
```
- HTTP-MJPEG with MJPEG codec
```yaml
ffmpeg -re -i BigBuckBunny.mp4 -c mjpeg -f mpjpeg http://localhost:1984/api/stream.mjpeg?dst=camera1
```
- HTTP-FLV with H264, AAC codecs
```yaml
ffmpeg -re -i BigBuckBunny.mp4 -c copy -f flv http://localhost:1984/api/stream.flv?dst=camera1
```
- MPEG-TS with H264 codec
```yaml
ffmpeg -re -i BigBuckBunny.mp4 -c copy -f mpegts http://localhost:1984/api/stream.ts?dst=camera1
```
#### Incoming: Browser
You can turn the browser of any PC or mobile into an IP-camera with support video and two way audio. Or even broadcast your PC screen:
1. Create empty stream in the `go2rtc.yaml`
2. Go to go2rtc WebUI
3. Open `links` page for you stream
4. Select `camera+microphone` or `display+speaker` option
5. Open `webrtc` local page (your go2rtc **should work over HTTPS!**) or `share link` via [WebTorrent](#module-webtorrent) technology (work over HTTPS by default)
#### Incoming: WebRTC/WHIP
You can use **OBS Studio** or any other broadcast software with [WHIP](https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html) protocol support. This standard has not yet been approved. But you can download OBS Studio [dev version](https://github.com/obsproject/obs-studio/actions/runs/3969201209):
- Settings > Stream > Service: WHIP > http://192.168.1.123:1984/api/webrtc?dst=camera1
#### Stream to camera
go2rtc support play audio files (ex. music or [TTS](https://www.home-assistant.io/integrations/#text-to-speech)) and live streams (ex. radio) on cameras with [two way audio](#two-way-audio) support (RTSP/ONVIF cameras, TP-Link Tapo, Hikvision ISAPI, Roborock vacuums, any Browser).
API example:
```
POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com/song.mp3#audio=pcma#input=file
```
- you can stream: local files, web files, live streams or any format, supported by FFmpeg
- you should use [ffmpeg source](#source-ffmpeg) for transcoding audio to codec, that your camera supports
- you can check camera codecs on the go2rtc WebUI info page when the stream is active
- some cameras support only low quality `PCMA/8000` codec (ex. [Tapo](#source-tapo))
- it is recommended to choose higher quality formats if your camera supports them (ex. `PCMA/48000` for some Dahua cameras)
- if you play files over http-link, you need to add `#input=file` params for transcoding, so file will be transcoded and played in real time
- if you play live streams, you should skip `#input` param, because it is already in real time
- you can stop active playback by calling the API with the empty `src` parameter
- you will see one active producer and one active consumer in go2rtc WebUI info page during streaming
### Module: API
The HTTP API is the main part for interacting with the application. Default address: `http://127.0.0.1:1984/`.
@@ -400,7 +620,6 @@ api:
**PS:**
- go2rtc doesn't provide HTTPS. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks
- you can access microphone (for 2-way audio) only with HTTPS ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https))
- MJPEG over WebSocket plays better than native MJPEG because Chrome [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=527446)
- MP4 over WebSocket was created only for Apple iOS because it doesn't support MSE and native MP4
@@ -408,23 +627,33 @@ api:
You can get any stream as RTSP-stream: `rtsp://192.168.1.123:8554/{stream_name}`
- you can omit the codec filters, so one first video and one first audio will be selected
- you can set `?video=copy` or just `?video`, so only one first video without audio will be selected
- you can set multiple video or audio, so all of them will be selected
- you can enable external password protection for your RTSP streams
Password protection always disabled for localhost calls (ex. FFmpeg or Hass on same server)
You can enable external password protection for your RTSP streams. Password protection always disabled for localhost calls (ex. FFmpeg or Hass on same server).
```yaml
rtsp:
listen: ":8554" # RTSP Server TCP port, default - 8554
username: "admin" # optional, default - disabled
password: "pass" # optional, default - disabled
default_query: "video&audio" # optional, default codecs filters
```
By default go2rtc provide RTSP-stream with only one first video and only one first audio. You can change it with the `default_query` setting:
- `default_query: "mp4"` - MP4 compatible codecs (H264, H265, AAC)
- `default_query: "video=all&audio=all"` - all tracks from all source (not all players can handle this)
- `default_query: "video=h264,h265"` - only one video track (H264 or H265)
- `default_query: "video&audio=all"` - only one first any video and all audio as separate tracks
Read more about [codecs filters](#codecs-filters).
### Module: WebRTC
WebRTC usually works without problems in the local network. But external access may require additional settings. It depends on what type of Internet do you have.
In most cases [WebRTC](https://en.wikipedia.org/wiki/WebRTC) uses direct peer-to-peer connection from your browser to go2rtc and sends media data via UDP.
It **can't pass** media data through your Nginx or Cloudflare or [Nabu Casa](https://www.nabucasa.com/) HTTP TCP connection!
It can automatically detects your external IP via public [STUN](https://en.wikipedia.org/wiki/STUN) server.
It can establish a external direct connection via [UDP hole punching](https://en.wikipedia.org/wiki/UDP_hole_punching) technology even if you not open your server to the World.
But about 10-20% of users may need to configure additional settings for external access if **mobile phone** or **go2rtc server** behing [Symmetric NAT](https://tomchen.github.io/symmetric-nat-test/).
- by default, WebRTC uses both TCP and UDP on port 8555 for connections
- you can use this port for external access
@@ -484,6 +713,32 @@ webrtc:
credential: your_pass
```
### Module: WebTorrent
This module support:
- Share any local stream via [WebTorrent](https://webtorrent.io/) technology
- Get any [incoming stream](#incoming-browser) from PC or mobile via [WebTorrent](https://webtorrent.io/) technology
- Get any remote [go2rtc source](#source-webtorrent) via [WebTorrent](https://webtorrent.io/) technology
Securely and free. You do not need to open a public access to the go2rtc server. But in some cases (Symmetric NAT) you may need to set up external access to [WebRTC module](#module-webrtc).
To generate sharing link or incoming link - goto go2rtc WebUI (stream links page). This link is **temporary** and will stop working after go2rtc is restarted!
You can create permanent external links in go2rtc config:
```yaml
webtorrent:
shares:
super-secret-share: # share name, should be unique among all go2rtc users!
pwd: super-secret-password
src: rtsp-dahua1 # stream name from streams section
```
Link example: https://alexxit.github.io/go2rtc/#share=02SNtgjKXY&pwd=wznEQqznxW&media=video+audio
TODO: article how it works...
### Module: Ngrok
With Ngrok integration you can get external access to your streams in situation when you have Internet with private IP-address.
@@ -540,24 +795,29 @@ tunnels:
### Module: Hass
If you install **go2rtc** as [Hass Add-on](#go2rtc-home-assistant-add-on) - you need to use localhost IP-address. In other cases you need to use IP-address of server with **go2rtc** application.
The best and easiest way to use go2rtc inside the Home Assistant is to install the custom integration [WebRTC Camera](#go2rtc-home-assistant-integration) and custom lovelace card.
#### From go2rtc to Hass
But go2rtc is also compatible and can be used with [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) built-in integration.
Add any supported [stream source](#module-streams) as [Generic Camera](https://www.home-assistant.io/integrations/generic/) and view stream with built-in [Stream](https://www.home-assistant.io/integrations/stream/) integration. Technology `HLS`, supported codecs: `H264`, poor latency.
You have several options on how to add a camera to Home Assistant:
1. Add your stream to [go2rtc config](#configuration)
2. Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://127.0.0.1:8554/camera1`
1. Camera RTSP source => [Generic Camera](https://www.home-assistant.io/integrations/generic/)
2. Camera [any source](#module-streams) => [go2rtc config](#configuration) => [Generic Camera](https://www.home-assistant.io/integrations/generic/)
- Install any [go2rtc](#fast-start)
- Add your stream to [go2rtc config](#configuration)
- Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://127.0.0.1:8554/camera1` (change to your stream name)
#### From Hass to go2rtc
You have several options on how to watch the stream from the cameras in Home Assistant:
View almost any Hass camera using `WebRTC` technology, supported codecs `H264`/`PCMU`/`PCMA`/`OPUS`, best latency.
When the stream starts - the camera `entity_id` will be added to go2rtc "on the fly". You don't need to add cameras manually to [go2rtc config](#configuration). Some cameras (like [Nest](https://www.home-assistant.io/integrations/nest/)) have a dynamic link to the stream, it will be updated each time a stream is started from the Hass interface.
1. Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/`
2. RTSPtoWebRTC > Configure > STUN server: `stun.l.google.com:19302`
3. Use Picture Entity or Picture Glance lovelace card
1. `Camera Entity` => `Picture Entity Card` => Technology `HLS`, codecs: `H264/H265/AAC`, poor latency.
2. `Camera Entity` => [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) => `Picture Entity Card` => Technology `WebRTC`, codecs: `H264/PCMU/PCMA/OPUS`, best latency.
- Install any [go2rtc](#fast-start)
- Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/`
- RTSPtoWebRTC > Configure > STUN server: `stun.l.google.com:19302`
- Use Picture Entity or Picture Glance lovelace card
3. `Camera Entity` or `Camera URL` => [WebRTC Camera](https://github.com/AlexxIT/WebRTC) => Technology: `WebRTC/MSE/MP4/MJPEG`, codecs: `H264/H265/AAC/PCMU/PCMA/OPUS`, best latency, best compatibility.
- Install and add [WebRTC Camera](https://github.com/AlexxIT/WebRTC) custom integration
- Use WebRTC Camera custom lovelace card
You can add camera `entity_id` to [go2rtc config](#configuration) if you need transcoding:
@@ -574,12 +834,27 @@ Provides several features:
1. MSE stream (fMP4 over WebSocket)
2. Camera snapshots in MP4 format (single frame), can be sent to [Telegram](https://github.com/AlexxIT/go2rtc/wiki/Snapshot-to-Telegram)
3. MP4 "file stream" - bad format for streaming because of high start delay, doesn't work in Safari
3. HTTP progressive streaming (MP4 file stream) - bad format for streaming because of high start delay. This format doesn't work in all Safari browsers, but go2rtc will automatically redirect it to HLS/fMP4 it this case.
API examples:
- MP4 stream: `http://192.168.1.123:1984/api/stream.mp4?src=camera1`
- MP4 snapshot: `http://192.168.1.123:1984/api/frame.mp4?src=camera1`
- MP4 snapshot: `http://192.168.1.123:1984/api/frame.mp4?src=camera1` (H264, H265)
- MP4 stream: `http://192.168.1.123:1984/api/stream.mp4?src=camera1` (H264, H265, AAC)
Read more about [codecs filters](#codecs-filters).
### Module: HLS
[HLS](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) is the worst technology for real-time streaming. It can only be useful on devices that do not support more modern technology, like [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4).
The go2rtc implementation differs from the standards and may not work with all players.
API examples:
- HLS/TS stream: `http://192.168.1.123:1984/api/stream.m3u8?src=camera1` (H264)
- HLS/fMP4 stream: `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4` (H264, H265, AAC)
Read more about [codecs filters](#codecs-filters).
### Module: MJPEG
@@ -647,21 +922,44 @@ If you need Web interface protection without Home Assistant Add-on - you need to
PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted media. It works without problems on the local network. And sometimes also works for external access, even if you haven't opened this port on your router ([read more](https://en.wikipedia.org/wiki/UDP_hole_punching)). But for stable external WebRTC access, you need to open the 8555 port on your router for both TCP and UDP.
## Codecs filters
go2rtc can automatically detect which codecs your device supports for [WebRTC](#module-webrtc) and [MSE](#module-mp4) technologies.
But it cannot be done for [RTSP](#module-rtsp), [HTTP progressive streaming](#module-mp4), [HLS](#module-hls) technologies. You can manually add a codec filter when you create a link to a stream. The filters work the same for all three technologies. Filters do not create a new codec. They only select the suitable codec from existing sources. You can add new codecs to the stream using the [FFmpeg transcoding](#source-ffmpeg).
Without filters:
- RTSP will provide only the first video and only the first audio (any codec)
- MP4 will include only compatible codecs (H264, H265, AAC)
- HLS will output in the legacy TS format (H264 without audio)
Some examples:
- `rtsp://192.168.1.123:8554/camera1?mp4` - useful for recording as MP4 files (e.g. Hass or Frigate)
- `rtsp://192.168.1.123:8554/camera1?video=h264,h265&audio=aac` - full version of the filter above
- `rtsp://192.168.1.123:8554/camera1?video=h264&audio=aac&audio=opus` - H264 video codec and two separate audio tracks
- `rtsp://192.168.1.123:8554/camera1?video&audio=all` - any video codec and all audio codecs as separate tracks
- `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4` - HLS stream with MP4 compatible codecs (HLS/fMP4)
- `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4=flac` - HLS stream with PCMA/PCMU/PCM audio support (HLS/fMP4), won't work on old devices
- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=flac` - MP4 file with PCMA/PCMU/PCM audio support, won't work on old devices (ex. iOS 12)
- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=all` - MP4 file with non standard audio codecs, won't work on some players
## Codecs madness
`AVC/H.264` codec can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
| Device | WebRTC | MSE | MP4 |
|---------------------|-------------|-------------|-------------|
| *latency* | best | medium | bad |
| Desktop Chrome 107+ | H264 | H264, H265* | H264, H265* |
| Desktop Edge | H264 | H264, H265* | H264, H265* |
| Desktop Safari | H264, H265* | H264, H265 | **no!** |
| Desktop Firefox | H264 | H264 | H264 |
| Android Chrome 107+ | H264 | H264, H265* | H264 |
| iPad Safari 13+ | H264, H265* | H264, H265 | **no!** |
| iPhone Safari 13+ | H264, H265* | **no!** | **no!** |
| masOS Hass App | no | no | no |
| Device | WebRTC | MSE | HTTP Progressive Streaming |
|---------------------|-------------------------------|-------------------------------|------------------------------------|
| *latency* | best | medium | bad |
| Desktop Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 |
| Desktop Edge | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 |
| Android Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 |
| Desktop Firefox | H264, OPUS, PCMU, PCMA | H264, AAC, FLAC*, OPUS | H264, AAC, FLAC*, OPUS |
| Desktop Safari | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** |
| iPad Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** |
| iPhone Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | **no!** | **no!** |
| masOS Hass App | no | no | no |
- Chrome H265: [read this](https://chromestatus.com/feature/5186511939567616) and [read this](https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding)
- Edge H265: [read this](https://www.reddit.com/r/MicrosoftEdge/comments/v9iw8k/enable_hevc_support_in_edge/)
@@ -670,15 +968,55 @@ PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted
**Audio**
- WebRTC audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2`
- MSE/MP4 audio codecs: `AAC`
- Go2rtc support [automatic repack](#built-in-transcoding) `PCMA/PCMU/PCM` codecs to `FLAC` for MSE/MP4/HLS so they will work almost anywhere
- **WebRTC** audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2`
- `OPUS` and `MP3` inside **MP4** is part of the standard, but some players do not support them anyway (especially Apple)
**Apple devices**
- all Apple devices don't support MP4 stream (they only support progressive loading of static files)
- iPhones don't support MSE technology because it competes with the HLS technology, invented by Apple
- all Apple devices don't support HTTP progressive streaming
- iPhones don't support MSE technology because it competes with the HTTP Live Streaming (HLS) technology, invented by Apple
- HLS is the worst technology for **live** streaming, it still exists only because of iPhones
**Codec names**
- H264 = H.264 = AVC (Advanced Video Coding)
- H265 = H.265 = HEVC (High Efficiency Video Coding)
- PCMU = G.711 PCM (A-law) = PCM A-law (`alaw`)
- PCMA = G.711 PCM (µ-law) = PCM mu-law (`mulaw`)
- PCM = L16 = PCM signed 16-bit big-endian (`s16be`)
- AAC = MPEG4-GENERIC
- MP3 = MPEG-1 Audio Layer III or MPEG-2 Audio Layer III
## Built-in transcoding
There are no plans to embed complex transcoding algorithms inside go2rtc. [FFmpeg source](#source-ffmpeg) does a great job with this. Including [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration) support.
But go2rtc has some simple algorithms. They are turned on automatically, you do not need to set them up additionally.
**PCM for MSE/MP4/HLS**
Go2rtc can pack `PCMA`, `PCMU` and `PCM` codecs into an MP4 container so that they work in all browsers and all built-in players on modern devices. Including Apple QuickTime:
```
PCMA/PCMU => PCM => FLAC => MSE/MP4/HLS
```
**Resample PCMA/PCMU for WebRTC**
By default WebRTC support only `PCMA/8000` and `PCMU/8000`. But go2rtc can automatically resample PCMA and PCMU codec with with a different sample rate. Also go2rtc can transcode `PCM` codec to `PCMA/8000`, so WebRTC can play it:
```
PCM/xxx => PCMA/8000 => WebRTC
PCMA/xxx => PCMA/8000 => WebRTC
PCMU/xxx => PCMU/8000 => WebRTC
```
**Important**
- FLAC codec not supported in a RTSP stream. If you using Frigate or Hass for recording MP4 files with PCMA/PCMU/PCM audio - you should setup transcoding to AAC codec.
- PCMA and PCMU are VERY low quality codecs. Them support only 256! different sounds. Use them only when you have no other options.
## Codecs negotiation
For example, you want to watch RTSP-stream from [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your Chrome browser.
@@ -708,6 +1046,23 @@ streams:
**PS.** You can select `PCMU` or `PCMA` codec in camera setting and don't use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.
## Projects using go2rtc
- [Frigate 12+](https://frigate.video/) - open source NVR built around real-time AI object detection
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge
- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - A small project that provides a Video/Audio Stream from Eufy cameras that don't directly support RTSP
- [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/)
- [Unraid](https://unraid.net/community/apps?q=go2rtc)
## Cameras experience
- [Dahua](https://www.dahuasecurity.com/) - reference implementation streaming protocols, a lot of settings, high stream quality, multiple streaming clients
- [Hikvision](https://www.hikvision.com/) - a lot of proprietary streaming technologies
- [Reolink](https://reolink.com/) - some models has awful unusable RTSP realisation and not best HTTP-FLV alternative (I recommend that you contact Reolink support for new firmware), few settings
- [Sonoff](https://sonoff.tech/) - very low stream quality, no settings, not best protocol implementation
- [TP-Link](https://www.tp-link.com/) - few streaming clients, packet loss?
- Chinese cheap noname cameras, Wyze Cams, Xiaomi cameras with hacks (usual has `/live/ch00_1` in RTSP URL) - awful but usable RTSP protocol realisation, low stream quality, few settings, packet loss?
## TIPS
**Using apps for low RTSP delay**
@@ -725,17 +1080,17 @@ streams:
**go2rtc** is a new version of the server-side [WebRTC Camera](https://github.com/AlexxIT/WebRTC) integration, completely rewritten from scratch, with a number of fixes and a huge number of new features. It is compatible with native Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration. So you [can use](#module-hass) default lovelace Picture Entity or Picture Glance.
**Q. Why go2rtc is an addon and not an integration?**
**Q. Should I use go2rtc addon or WebRTC Camera integration?**
Because **go2rtc** is more than just viewing your stream online with WebRTC. You can use it all the time for your various tasks. But every time the Hass is rebooted - all integrations are also rebooted. So your streams may be interrupted if you use them in additional tasks.
**go2rtc** is more than just viewing your stream online with WebRTC/MSE/HLS/etc. You can use it all the time for your various tasks. But every time the Hass is rebooted - all integrations are also rebooted. So your streams may be interrupted if you use them in additional tasks.
When **go2rtc** is released, the **WebRTC Camera** integration will be updated. And you can decide whether to use the integration or the addon.
Basic users can use **WebRTC Camera** integration. Advanced users can use go2rtc addon or Frigate 12+ addon.
**Q. Which RTSP link should I use inside Hass?**
You can use direct link to your cameras there (as you always do). **go2rtc** support zero-config feature. You may leave `streams` config section empty. And your streams will be created on the fly on first start from Hass. And your cameras will have multiple connections. Some from Hass directly and one from **go2rtc**.
Also you can specify your streams in **go2rtc** [config file](#configuration) and use RTSP links to this addon. With additional features: multi-source [codecs negotiation](#codecs-negotiation) or FFmpeg [transcoding](#source-ffmpeg) for unsupported codecs. Or use them as source for Frigate. And your cameras will have one connection from **go2rtc**. And **go2rtc** will have multiple connection - some from Hass via RTSP protocol, some from your browser via WebRTC protocol.
Also you can specify your streams in **go2rtc** [config file](#configuration) and use RTSP links to this addon. With additional features: multi-source [codecs negotiation](#codecs-negotiation) or FFmpeg [transcoding](#source-ffmpeg) for unsupported codecs. Or use them as source for Frigate. And your cameras will have one connection from **go2rtc**. And **go2rtc** will have multiple connection - some from Hass via RTSP protocol, some from your browser via WebRTC/MSE/HLS protocols.
Use any config what you like.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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,97 +0,0 @@
package hass
import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
"os"
"path"
)
func Init() {
var conf struct {
Mod struct {
Config string `yaml:"config"`
} `yaml:"hass"`
}
app.LoadConfig(&conf)
log = app.GetLogger("hass")
initAPI()
// 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
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,56 +0,0 @@
package http
import (
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"net/http"
"strings"
)
func Init() {
streams.HandleFunc("http", handle)
streams.HandleFunc("https", handle)
}
func handle(url string) (streamer.Producer, error) {
// first we get the Content-Type to define supported producer
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
res, err := tcp.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, errors.New(res.Status)
}
ct := res.Header.Get("Content-Type")
if i := strings.IndexByte(ct, ';'); i > 0 {
ct = ct[:i]
}
switch ct {
case "image/jpeg", "multipart/x-mixed-replace":
return mjpeg.NewClient(res), nil
case "video/x-flv":
var conn *rtmp.Client
if conn, err = rtmp.Accept(res); err != nil {
return nil, err
}
if err = conn.Describe(); err != nil {
return nil, err
}
return conn, nil
}
return nil, fmt.Errorf("unsupported Content-Type: %s", ct)
}

View File

@@ -1,22 +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
}
if err := conn.Describe(); err != nil {
return nil, err
}
return conn, nil
}

View File

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

View File

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

View File

@@ -1,256 +0,0 @@
package streams
import (
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
"sync"
"sync/atomic"
)
type Stream struct {
producers []*Producer
consumers []*Consumer
mu sync.Mutex
requests int32
}
func NewStream(source interface{}) *Stream {
switch source := source.(type) {
case string:
s := new(Stream)
prod := &Producer{url: source}
s.producers = append(s.producers, prod)
return s
case []interface{}:
s := new(Stream)
for _, source := range source {
prod := &Producer{url: source.(string)}
s.producers = append(s.producers, prod)
}
return s
case *Stream:
return source
case map[string]interface{}:
return NewStream(source["url"])
case nil:
return new(Stream)
default:
panic("wrong source type")
}
}
func (s *Stream) SetSource(source string) {
for _, prod := range s.producers {
prod.SetSource(source)
}
}
func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
// support for multiple simultaneous requests from different consumers
atomic.AddInt32(&s.requests, 1)
ic := len(s.consumers)
consumer := &Consumer{element: cons}
var producers []*Producer // matched producers for consumer
var codecs string
// Step 1. Get consumer medias
for icc, consMedia := range cons.GetMedias() {
log.Trace().Stringer("media", consMedia).
Msgf("[streams] consumer=%d candidate=%d", ic, icc)
producers:
for ip, prod := range s.producers {
// Step 2. Get producer medias (not tracks yet)
for ipc, prodMedia := range prod.GetMedias() {
log.Trace().Stringer("media", prodMedia).
Msgf("[streams] producer=%d candidate=%d", ip, ipc)
collectCodecs(prodMedia, &codecs)
// 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().Str("url", prod.url).Msg("[streams] 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)
producers = append(producers, prod)
break producers
}
}
}
}
if atomic.AddInt32(&s.requests, -1) == 0 {
s.stopProducers()
}
if len(producers) == 0 {
if len(codecs) > 0 {
return errors.New("codecs not match: " + codecs)
}
for i, producer := range s.producers {
if producer.lastErr != nil {
return fmt.Errorf("source %d error: %w", i, producer.lastErr)
}
}
return fmt.Errorf("sources unavailable: %d", len(s.producers))
}
s.mu.Lock()
s.consumers = append(s.consumers, consumer)
s.mu.Unlock()
// there may be duplicates, but that's not a problem
for _, prod := range producers {
prod.start()
}
return nil
}
func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
s.mu.Lock()
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
}
}
s.mu.Unlock()
s.stopProducers()
}
func (s *Stream) AddProducer(prod streamer.Producer) {
producer := &Producer{element: prod, state: stateExternal}
s.mu.Lock()
s.producers = append(s.producers, producer)
s.mu.Unlock()
}
func (s *Stream) RemoveProducer(prod streamer.Producer) {
s.mu.Lock()
for i, producer := range s.producers {
if producer.element == prod {
s.removeProducer(i)
break
}
}
s.mu.Unlock()
}
func (s *Stream) stopProducers() {
s.mu.Lock()
producers:
for _, producer := range s.producers {
for _, track := range producer.tracks {
if track.HasSink() {
continue producers
}
}
producer.stop()
}
s.mu.Unlock()
}
//func (s *Stream) Active() bool {
// if len(s.consumers) > 0 {
// return true
// }
//
// for _, prod := range s.producers {
// if prod.element != nil {
// return true
// }
// }
//
// return false
//}
func (s *Stream) MarshalJSON() ([]byte, error) {
if !s.mu.TryLock() {
log.Warn().Msgf("[streams] json locked")
return json.Marshal(nil)
}
var info struct {
Producers []*Producer `json:"producers"`
Consumers []*Consumer `json:"consumers"`
}
info.Producers = s.producers
info.Consumers = s.consumers
s.mu.Unlock()
return json.Marshal(info)
}
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:]...)
}
}
func collectCodecs(media *streamer.Media, codecs *string) {
if media.Direction == streamer.DirectionRecvonly {
return
}
for _, codec := range media.Codecs {
name := codec.Name
if name == streamer.CodecAAC {
name = "AAC"
}
if strings.Contains(*codecs, name) {
continue
}
if len(*codecs) > 0 {
*codecs += ","
}
*codecs += name
}
}

View File

@@ -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,82 +0,0 @@
package streams
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/app/store"
"github.com/rs/zerolog"
"net/http"
)
func Init() {
var cfg struct {
Mod map[string]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)
}
api.HandleFunc("api/streams", streamsHandler)
}
func Get(name string) *Stream {
return streams[name]
}
func New(name string, source interface{}) *Stream {
stream := NewStream(source)
streams[name] = stream
return stream
}
func GetOrNew(src string) *Stream {
if stream, ok := streams[src]; ok {
return stream
}
if !HasProducer(src) {
return nil
}
log.Info().Str("url", src).Msg("[streams] create new stream")
return New(src, src)
}
func streamsHandler(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
switch r.Method {
case "PUT":
name := r.URL.Query().Get("name")
if name == "" {
name = src
}
New(name, src)
return
case "DELETE":
delete(streams, src)
return
}
if src != "" {
e := json.NewEncoder(w)
e.SetIndent("", " ")
_ = e.Encode(streams[src])
} else {
_ = json.NewEncoder(w).Encode(streams)
}
}
var log zerolog.Logger
var streams = map[string]*Stream{}

View File

@@ -1,96 +0,0 @@
package webrtc
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/pion/sdp/v3"
)
var candidates []string
var networks = []string{"udp", "tcp"}
func AddCandidate(address string) {
candidates = append(candidates, address)
}
func asyncCandidates(tr *api.Transport) {
for _, address := range candidates {
address, err := webrtc.LookupIP(address)
if err != nil {
log.Warn().Err(err).Caller().Send()
continue
}
for _, network := range networks {
cand, err := webrtc.NewCandidate(network, address)
if err != nil {
log.Warn().Err(err).Caller().Send()
continue
}
log.Trace().Str("candidate", cand).Msg("[webrtc] config")
tr.Write(&api.Message{Type: "webrtc/candidate", Value: cand})
}
}
}
func syncCanditates(answer string) (string, error) {
if len(candidates) == 0 {
return answer, nil
}
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 {
var err error
address, err = webrtc.LookupIP(address)
if err != nil {
log.Warn().Err(err).Msg("[webrtc] candidate")
continue
}
for _, network := range networks {
cand, err := webrtc.NewCandidate(network, address)
if err != nil {
log.Warn().Err(err).Msg("[webrtc] candidate")
continue
}
md.WithPropertyAttribute(cand)
}
}
if end {
md.WithPropertyAttribute("end-of-candidates")
}
data, err := sd.Marshal()
if err != nil {
return "", err
}
return string(data), nil
}
func candidateHandler(tr *api.Transport, msg *api.Message) error {
if tr.Consumer == nil {
return nil
}
if conn := tr.Consumer.(*webrtc.Conn); conn != nil {
s := msg.Value.(string)
log.Trace().Str("candidate", s).Msg("[webrtc] remote")
conn.AddCandidate(s)
}
return nil
}

View File

@@ -1,218 +0,0 @@
package webrtc
import (
"errors"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
"github.com/rs/zerolog"
"io"
"net"
"net/http"
)
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"
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).Caller().Msg("webrtc.NewAPI")
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/offer", asyncHandler)
api.HandleWS("webrtc/candidate", candidateHandler)
api.HandleFunc("api/webrtc", syncHandler)
}
var Port string
var log zerolog.Logger
var NewPConn func() (*pion.PeerConnection, error)
func asyncHandler(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)
}
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).Caller().Send()
return err
}
conn.UserAgent = tr.Request.UserAgent()
conn.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateClosed {
stream.RemoveConsumer(conn)
}
case *pion.ICECandidate:
if msg != nil {
s := msg.ToJSON().Candidate
log.Trace().Str("candidate", s).Msg("[webrtc] local")
tr.Write(&api.Message{Type: "webrtc/candidate", Value: s})
}
}
})
// 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).Caller().Send()
return err
}
// 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil {
log.Debug().Err(err).Msg("[webrtc] add consumer")
_ = conn.Conn.Close()
return err
}
conn.Init()
// 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
}
tr.Consumer = conn
tr.Write(&api.Message{Type: "webrtc/answer", Value: answer})
asyncCandidates(tr)
return nil
}
func syncHandler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("src")
stream := streams.Get(url)
if stream == nil {
return
}
// get offer
offer, err := io.ReadAll(r.Body)
if err != nil {
log.Error().Err(err).Caller().Msg("ioutil.ReadAll")
return
}
answer, err := ExchangeSDP(stream, string(offer), r.UserAgent())
if err != nil {
log.Error().Err(err).Caller().Msg("ExchangeSDP")
return
}
// send SDP to client
if _, err = w.Write([]byte(answer)); err != nil {
log.Error().Err(err).Caller().Msg("w.Write")
}
}
func ExchangeSDP(
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).Caller().Msg("NewPConn")
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).Caller().Msg("conn.SetOffer")
return
}
// 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Caller().Msg("stream.AddConsumer")
_ = 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 = syncCanditates(answer)
}
log.Trace().Msgf("[webrtc] answer\n%s", answer)
if err != nil {
log.Error().Err(err).Caller().Msg("conn.GetCompleteAnswer")
}
return
}

56
go.mod
View File

@@ -1,52 +1,54 @@
module github.com/AlexxIT/go2rtc
go 1.19
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/kr/pretty v0.2.1 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/miekg/dns v1.1.50 // indirect
github.com/pion/datachannel v1.5.2 // indirect
github.com/pion/dtls/v2 v2.1.5 // indirect
github.com/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.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.11 // indirect
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 (

153
go.sum
View File

@@ -2,8 +2,9 @@ github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045 h1:xJf3FxQJReJSDyYX
github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045/go.mod h1:QNA3sm16zE5uUyC8+E/gNkMvQWjqQLuxQKkU5PMi8N4=
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92 h1:cIeYMGaAirSZnrKRDTb5VgZDDYqPLhYiczElMg4sQW0=
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
github.com/brutella/dnssd v1.2.3 h1:4fBLjZjPH7SbcHhEcIJhZcC9nOhIDZ0m3rn9bjl1/i0=
github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
github.com/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=
@@ -40,13 +41,17 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.12 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=
@@ -56,101 +61,110 @@ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042
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.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
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/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.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
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.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
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.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/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/srtp/v2 v2.0.10 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w=
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA=
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport v0.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.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
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/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/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=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.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/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -168,25 +182,36 @@ 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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
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/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
golang.org/x/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=

View File

@@ -1,7 +1,9 @@
# syntax=docker/dockerfile:labs
# 0. Prepare images
# only debian 12 (bookworm) has latest ffmpeg
ARG DEBIAN_VERSION="bookworm-slim"
ARG GO_VERSION="1.19-buster"
ARG GO_VERSION="1.20-buster"
ARG NGROK_VERSION="3"
FROM debian:${DEBIAN_VERSION} AS base
@@ -16,37 +18,43 @@ WORKDIR /build
# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
# 2. Collect all files
FROM scratch AS rootfs
COPY --from=build /build/go2rtc /usr/local/bin/
COPY --from=ngrok /bin/ngrok /usr/local/bin/
COPY ./build/docker/run.sh /
COPY --link --from=build /build/go2rtc /usr/local/bin/
COPY --link --from=ngrok /bin/ngrok /usr/local/bin/
# 3. Final image
FROM base
# Prepare apt for buildkit cache
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache
# Install ffmpeg, bash (for run.sh), tini (for signal handling),
# and other common tools for the echo source.
# non-free for Intel QSV support (not used by go2rtc, just for tests)
RUN echo 'deb http://deb.debian.org/debian bookworm non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
apt-get -y update && apt-get -y install tini ffmpeg python3 curl jq intel-media-va-driver-non-free
# libasound2-plugins for ALSA support
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \
echo 'deb http://deb.debian.org/debian bookworm non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
apt-get -y update && apt-get -y install tini ffmpeg \
python3 curl jq \
intel-media-va-driver-non-free \
libasound2-plugins
COPY --link --from=rootfs / /
COPY --from=rootfs / /
RUN chmod a+x /run.sh && mkdir -p /config
ENTRYPOINT ["/usr/bin/tini", "--"]
VOLUME /config
WORKDIR /config
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
ENV NVIDIA_VISIBLE_DEVICES all
ENV NVIDIA_DRIVER_CAPABILITIES compute,video,utility
CMD ["/run.sh"]
CMD ["go2rtc", "-config", "/config/go2rtc.yaml"]

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

View File

@@ -2,7 +2,7 @@ package api
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/rs/zerolog"
"net"
"net/http"
@@ -54,28 +54,31 @@ func Init() {
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
s := http.Server{}
s.Handler = http.DefaultServeMux // 4th
Handler = http.DefaultServeMux // 4th
if cfg.Mod.Origin == "*" {
s.Handler = middlewareCORS(s.Handler) // 3rd
Handler = middlewareCORS(Handler) // 3rd
}
if cfg.Mod.Username != "" {
s.Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, s.Handler) // 2nd
Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, Handler) // 2nd
}
if log.Trace().Enabled() {
s.Handler = middlewareLog(s.Handler) // 1st
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"
@@ -118,6 +121,7 @@ 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)
})
}
@@ -144,3 +148,23 @@ func exitHandler(w http.ResponseWriter, r *http.Request) {
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)
}
}

View File

@@ -1,7 +1,7 @@
package api
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/internal/app"
"gopkg.in/yaml.v3"
"io"
"net/http"
@@ -63,13 +63,13 @@ func mergeYAML(file1 string, yaml2 []byte) ([]byte, error) {
}
// Unmarshal the first YAML file into a map
var config1 map[string]interface{}
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]interface{}
var config2 map[string]any
if err = yaml.Unmarshal(yaml2, &config2); err != nil {
return nil, err
}
@@ -81,15 +81,15 @@ func mergeYAML(file1 string, yaml2 []byte) ([]byte, error) {
return yaml.Marshal(&config1)
}
func merge(dst, src map[string]interface{}) map[string]interface{} {
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]interface{}:
v := v.(map[string]interface{})
case map[string]any:
v := v.(map[string]any)
dst[k] = merge(vv, v)
case []interface{}:
v := v.([]interface{})
case []any:
v := v.([]any)
dst[k] = v
default:
dst[k] = v

View File

@@ -11,8 +11,24 @@ import (
// Message - struct for data exchange in Web API
type Message struct {
Type string `json:"type"`
Value interface{} `json:"value,omitempty"`
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
@@ -25,8 +41,8 @@ var wsHandlers = make(map[string]WSHandler)
func initWS(origin string) {
wsUp = &websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 2028,
ReadBufferSize: 4096, // for SDP
WriteBufferSize: 512 * 1024, // 512K
}
switch origin {
@@ -68,7 +84,7 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
}
tr := &Transport{Request: r}
tr.OnWrite(func(msg interface{}) {
tr.OnWrite(func(msg any) {
_ = ws.SetWriteDeadline(time.Now().Add(time.Second * 5))
if data, ok := msg.([]byte); ok {
@@ -88,6 +104,8 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
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 {
@@ -103,19 +121,20 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
var wsUp *websocket.Upgrader
type Transport struct {
Request *http.Request
Consumer interface{} // TODO: rewrite
Request *http.Request
ctx map[any]any
closed bool
mx sync.Mutex
wrmx sync.Mutex
onChange func()
onWrite func(msg interface{})
onWrite func(msg any)
onClose []func()
}
func (t *Transport) OnWrite(f func(msg interface{})) {
func (t *Transport) OnWrite(f func(msg any)) {
t.mx.Lock()
if t.onChange != nil {
t.onChange()
@@ -124,7 +143,7 @@ func (t *Transport) OnWrite(f func(msg interface{})) {
t.mx.Unlock()
}
func (t *Transport) Write(msg interface{}) {
func (t *Transport) Write(msg any) {
t.wrmx.Lock()
t.onWrite(msg)
t.wrmx.Unlock()
@@ -154,3 +173,13 @@ func (t *Transport) OnClose(f func()) {
}
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()
}

View File

@@ -2,32 +2,41 @@ package app
import (
"flag"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
"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.0.1"
var Version = "1.5.0"
var UserAgent = "go2rtc/" + Version
var ConfigPath string
var Info = map[string]interface{}{
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"}
}
@@ -94,7 +103,7 @@ func NewLogger(format string, level string) zerolog.Logger {
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
}
func LoadConfig(v interface{}) {
func LoadConfig(v any) {
for _, data := range configs {
if err := yaml.Unmarshal(data, v); err != nil {
log.Warn().Err(err).Msg("[app] read config")

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,15 +13,15 @@ var stackSkip = [][]byte{
[]byte("created by os/signal.Notify"),
// api/stack.go
[]byte("github.com/AlexxIT/go2rtc/cmd/api.stackHandler"),
[]byte("github.com/AlexxIT/go2rtc/internal/api.stackHandler"),
// api/api.go
[]byte("created by github.com/AlexxIT/go2rtc/cmd/api.Init"),
[]byte("created by github.com/AlexxIT/go2rtc/internal/api.Init"),
[]byte("created by net/http.(*connReader).startBackgroundRead"),
[]byte("created by net/http.(*Server).Serve"), // TODO: why two?
[]byte("created by github.com/AlexxIT/go2rtc/cmd/rtsp.Init"),
[]byte("created by github.com/AlexxIT/go2rtc/cmd/srtp.Init"),
[]byte("created by github.com/AlexxIT/go2rtc/internal/rtsp.Init"),
[]byte("created by github.com/AlexxIT/go2rtc/internal/srtp.Init"),
// webrtc/api.go
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),

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
}

View File

@@ -2,17 +2,17 @@ package echo
import (
"bytes"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"os/exec"
)
func Init() {
log := app.GetLogger("echo")
streams.HandleFunc("echo", func(url string) (streamer.Producer, error) {
streams.HandleFunc("echo", func(url string) (core.Producer, error) {
args := shell.QuoteSplit(url[5:])
b, err := exec.Command(args[0], args[1:]...).Output()

View File

@@ -5,26 +5,21 @@ import (
"encoding/hex"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"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/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
"os"
"os/exec"
"strings"
"sync"
"time"
)
func Init() {
// depends on RTSP server
if rtsp.Port == "" {
return
}
rtsp.HandleFunc(func(conn *pkg.Conn) bool {
waitersMu.Lock()
waiter := waiters[conn.URL.Path]
@@ -43,31 +38,67 @@ func Init() {
}
})
streams.HandleFunc("exec", Handle)
streams.HandleFunc("exec", execHandle)
log = app.GetLogger("exec")
}
func Handle(url string) (streamer.Producer, error) {
sum := md5.Sum([]byte(url))
path := "/" + hex.EncodeToString(sum[:])
func execHandle(url string) (core.Producer, error) {
var path string
url = strings.Replace(
url, "{output}", "rtsp://localhost:"+rtsp.Port+path, 1,
)
args := shell.QuoteSplit(url[5:]) // remove `exec:`
for i, arg := range args {
if arg == "{output}" {
if rtsp.Port == "" {
return nil, errors.New("rtsp module disabled")
}
// remove `exec:`
args := shell.QuoteSplit(url[5:])
cmd := exec.Command(args[0], args[1:]...)
if log.Trace().Enabled() {
cmd.Stdout = os.Stdout
sum := md5.Sum([]byte(url))
path = "/" + hex.EncodeToString(sum[:])
args[i] = "rtsp://127.0.0.1:" + rtsp.Port + path
break
}
}
cmd := exec.Command(args[0], args[1:]...)
if log.Debug().Enabled() {
cmd.Stderr = os.Stderr
}
ch := make(chan streamer.Producer)
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
@@ -116,5 +147,5 @@ func Handle(url string) (streamer.Producer, error) {
// internal
var log zerolog.Logger
var waiters = map[string]chan streamer.Producer{}
var waiters = map[string]chan 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

@@ -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)
}

View File

@@ -1,16 +1,15 @@
package ffmpeg
import (
"bytes"
"errors"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"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"
"strconv"
"strings"
)
@@ -27,16 +26,16 @@ func Init() {
defaults["global"] += " -v error"
}
streams.HandleFunc("ffmpeg", func(url string) (streamer.Producer, error) {
streams.HandleFunc("ffmpeg", func(url string) (core.Producer, error) {
args := parseArgs(url[7:]) // remove `ffmpeg:`
if args == nil {
return nil, errors.New("can't generate ffmpeg command")
}
return exec.Handle("exec:" + args.String())
return streams.GetProducer("exec:" + args.String())
})
device.Bin = defaults["bin"]
device.Init()
device.Init(defaults["bin"])
hardware.Init(defaults["bin"])
}
var defaults = map[string]string{
@@ -44,21 +43,26 @@ var defaults = map[string]string{
"global": "-hide_banner",
// inputs
"file": "-re -stream_loop -1 -i {input}",
"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": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
"output/mjpeg": "-f mjpeg -",
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
// `-tune zerolatency` - for minimal latency
// `-profile high -level 4.1` - most used streaming profile
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency",
"h265": "-c:v libx265 -g 50 -profile:v high -level:v 5.1 -preset:v superfast -tune:v zerolatency",
"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
"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",
"opus": "-c:a libopus -ar:a 48000 -ac:a 2",
// 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",
@@ -67,6 +71,10 @@ var defaults = map[string]string{
"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
@@ -94,19 +102,34 @@ var defaults = map[string]string{
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1",
}
func parseArgs(s string) *Args {
// 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 := &Args{
bin: defaults["bin"],
global: defaults["global"],
output: defaults["output"],
args := &ffmpeg.Args{
Bin: defaults["bin"],
Global: defaults["global"],
Output: defaults["output"],
}
var query url.Values
if i := strings.IndexByte(s, '#'); i > 0 {
query = parseQuery(s[i+1:])
args.video = len(query["video"])
args.audio = len(query["audio"])
args.Video = len(query["video"])
args.Audio = len(query["audio"])
s = s[:i]
}
@@ -117,44 +140,46 @@ func parseArgs(s string) *Args {
if i := strings.Index(s, "://"); i > 0 {
switch s[:i] {
case "http", "https", "rtmp":
args.input = strings.Replace(defaults["http"], "{input}", s, 1)
args.Input = inputTemplate("http", s, query)
case "rtsp", "rtsps":
// https://ffmpeg.org/ffmpeg-protocols.html#rtsp
// skip unnecessary input tracks
switch {
case (args.video > 0 && args.audio > 0) || (args.video == 0 && args.audio == 0):
args.input = "-allowed_media_types video+audio "
case args.video > 0:
args.input = "-allowed_media_types video "
case args.audio > 0:
args.input = "-allowed_media_types audio "
case (args.Video > 0 && args.Audio > 0) || (args.Video == 0 && args.Audio == 0):
args.Input = "-allowed_media_types video+audio "
case args.Video > 0:
args.Input = "-allowed_media_types video "
case args.Audio > 0:
args.Input = "-allowed_media_types audio "
}
args.input += strings.Replace(defaults["rtsp"], "{input}", s, 1)
args.Input += inputTemplate("rtsp", s, query)
default:
args.input = "-i " + s
args.Input = "-i " + s
}
} else if streams.Get(s) != nil {
s = "rtsp://localhost:" + rtsp.Port + "/" + s
s = "rtsp://127.0.0.1:" + rtsp.Port + "/" + s
switch {
case args.video > 0 && args.audio == 0:
case args.Video > 0 && args.Audio == 0:
s += "?video"
case args.audio > 0 && args.video == 0:
case args.Audio > 0 && args.Video == 0:
s += "?audio"
default:
s += "?video&audio"
}
args.input = strings.Replace(defaults["rtsp"], "{input}", s, 1)
args.Input = inputTemplate("rtsp", s, query)
} else if strings.HasPrefix(s, "device?") {
var err error
args.input, err = device.GetInput(s)
args.Input, err = device.GetInput(s)
if err != nil {
return nil
}
} else {
args.input = strings.Replace(defaults["file"], "{input}", s, 1)
args.Input = inputTemplate("file", s, query)
}
if query["async"] != nil {
args.input = "-use_wallclock_as_timestamps 1 -async 1 " + args.input
args.Input = "-use_wallclock_as_timestamps 1 -async 1 " + args.Input
}
// Parse query params:
@@ -202,10 +227,14 @@ func parseArgs(s string) *Args {
}
// 3. Process video codecs
if args.video > 0 {
if args.Video > 0 {
for _, video := range query["video"] {
if video != "copy" {
args.AddCodec(defaults[video])
if codec := defaults[video]; codec != "" {
args.AddCodec(codec)
} else {
args.AddCodec(video)
}
} else {
args.AddCodec("-c:v copy")
}
@@ -215,10 +244,14 @@ func parseArgs(s string) *Args {
}
// 4. Process audio codecs
if args.audio > 0 {
if args.Audio > 0 {
for _, audio := range query["audio"] {
if audio != "copy" {
args.AddCodec(defaults[audio])
if codec := defaults[audio]; codec != "" {
args.AddCodec(codec)
} else {
args.AddCodec(audio)
}
} else {
args.AddCodec("-c:a copy")
}
@@ -228,14 +261,21 @@ func parseArgs(s string) *Args {
}
if query["hardware"] != nil {
MakeHardware(args, query["hardware"][0])
hardware.MakeHardware(args, query["hardware"][0], defaults)
}
}
if args.codecs == nil {
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
}
@@ -251,76 +291,3 @@ func parseQuery(s string) map[string][]string {
}
return query
}
type Args struct {
bin string // ffmpeg
global string // -hide_banner -v error
input string // -re -stream_loop -1 -i /media/bunny.mp4
codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency
filters []string // scale=1920:1080
output string // -f rtsp {output}
video, audio int // count of video and audio params
}
func (a *Args) AddCodec(codec string) {
a.codecs = append(a.codecs, codec)
}
func (a *Args) AddFilter(filter string) {
a.filters = append(a.filters, filter)
}
func (a *Args) InsertFilter(filter string) {
a.filters = append([]string{filter}, a.filters...)
}
func (a *Args) String() string {
b := bytes.NewBuffer(make([]byte, 0, 512))
b.WriteString(a.bin)
if a.global != "" {
b.WriteByte(' ')
b.WriteString(a.global)
}
b.WriteByte(' ')
b.WriteString(a.input)
multimode := a.video > 1 || a.audio > 1
var iv, ia int
for _, codec := range a.codecs {
// support multiple video and/or audio codecs
if multimode && len(codec) >= 5 {
switch codec[:5] {
case "-c:v ":
codec = "-map 0:v:0? " + strings.ReplaceAll(codec, ":v ", ":v:"+strconv.Itoa(iv)+" ")
iv++
case "-c:a ":
codec = "-map 0:a:0? " + strings.ReplaceAll(codec, ":a ", ":a:"+strconv.Itoa(ia)+" ")
ia++
}
}
b.WriteByte(' ')
b.WriteString(codec)
}
if a.filters != nil {
for i, filter := range a.filters {
if i == 0 {
b.WriteString(" -vf ")
} else {
b.WriteByte(',')
}
b.WriteString(filter)
}
}
b.WriteByte(' ')
b.WriteString(a.output)
return b.String()
}

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()
}

View File

@@ -3,11 +3,11 @@ package hass
import (
"encoding/base64"
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"net"
"net/http"
"net/url"
"strings"
)
@@ -24,6 +24,7 @@ func initAPI() {
api.HandleFunc("/streams", ok)
// api from RTSPtoWeb
api.HandleFunc("/stream/", func(w http.ResponseWriter, r *http.Request) {
switch {
// /stream/{id}/add
@@ -39,13 +40,7 @@ func initAPI() {
// 3. dynamic link to Hass camera
stream := streams.Get(v.Name)
if stream == nil {
// check if it is rtsp link to go2rtc
stream = rtspStream(v.Channels.First.Url)
if stream != nil {
streams.New(v.Name, stream)
} else {
stream = streams.New(v.Name, "{input}")
}
stream = streams.NewTemplate(v.Name, v.Channels.First.Url)
}
stream.SetSource(v.Channels.First.Url)
@@ -79,7 +74,7 @@ func initAPI() {
return
}
s, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
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
@@ -89,57 +84,25 @@ func initAPI() {
_, _ = w.Write([]byte(s))
}
})
// api from RTSPtoWebRTC
api.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
return
}
str := r.FormValue("sdp64")
offer, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return
}
src := r.FormValue("url")
src, err = url.QueryUnescape(src)
if err != nil {
return
}
stream := streams.Get(src)
if stream == nil {
if stream = rtspStream(src); stream != nil {
streams.New(src, stream)
} else {
stream = streams.New(src, src)
}
}
str, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
if err != nil {
return
}
v := struct {
Answer string `json:"sdp64"`
}{
Answer: base64.StdEncoding.EncodeToString([]byte(str)),
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
})
}
func rtspStream(url string) *streams.Stream {
if strings.HasPrefix(url, "rtsp://") {
if i := strings.IndexByte(url[7:], '/'); i > 0 {
return streams.Get(url[8+i:])
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 nil
return ""
}
type addJSON struct {

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()
}
}

View File

@@ -3,8 +3,8 @@ package homekit
import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/app/store"
"github.com/AlexxIT/go2rtc/cmd/streams"
"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"
@@ -15,7 +15,7 @@ import (
func apiHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
items := make([]interface{}, 0)
items := make([]any, 0)
for name, src := range store.GetDict("streams") {
if src := src.(string); strings.HasPrefix(src, "homekit") {

View File

@@ -1,12 +1,12 @@
package homekit
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/srtp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/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/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
)
@@ -20,12 +20,12 @@ func Init() {
var log zerolog.Logger
func streamHandler(url string) (streamer.Producer, error) {
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{
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

@@ -1,14 +1,14 @@
package ivideon
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ivideon"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
func Init() {
streams.HandleFunc("ivideon", func(url string) (streamer.Producer, error) {
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 {

View File

@@ -2,12 +2,18 @@ package mjpeg
import (
"errors"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"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() {
@@ -27,14 +33,16 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
exit := make(chan []byte)
cons := &mjpeg.Consumer{
RemoteAddr: r.RemoteAddr,
cons := &magic.Keyframe{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case []byte:
exit <- msg
cons.Listen(func(msg any) {
if b, ok := msg.([]byte); ok {
select {
case exit <- b:
default:
}
}
})
@@ -47,6 +55,17 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
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)))
@@ -62,6 +81,14 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
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 {
@@ -72,10 +99,10 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
flusher := w.(http.Flusher)
cons := &mjpeg.Consumer{
RemoteAddr: r.RemoteAddr,
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg interface{}) {
cons.Listen(func(msg any) {
switch msg := msg.(type) {
case []byte:
data := []byte(header + strconv.Itoa(len(msg)))
@@ -108,6 +135,27 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
//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)
@@ -116,10 +164,10 @@ func handlerWS(tr *api.Transport, _ *api.Message) error {
}
cons := &mjpeg.Consumer{
RemoteAddr: tr.Request.RemoteAddr,
RemoteAddr: tcp.RemoteAddr(tr.Request),
UserAgent: tr.Request.UserAgent(),
}
cons.Listen(func(msg interface{}) {
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
tr.Write(data)
}

View File

@@ -1,15 +1,17 @@
package mp4
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/rs/zerolog"
"net/http"
"strconv"
"strings"
"time"
"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() {
@@ -25,8 +27,14 @@ func Init() {
var log zerolog.Logger
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
if isChromeFirst(w, r) {
return
// 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")
@@ -36,18 +44,22 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
return
}
exit := make(chan []byte)
exit := make(chan []byte, 1)
cons := &mp4.Segment{OnlyKeyframe: true}
cons.Listen(func(msg interface{}) {
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok && exit != nil {
exit <- data
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
}
@@ -67,27 +79,45 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
func handlerMP4(w http.ResponseWriter, r *http.Request) {
log.Trace().Msgf("[mp4] %s %+v", r.Method, r.Header)
if isChromeFirst(w, r) || isSafari(w, r) {
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 := r.URL.Query().Get("src")
src := query.Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
exit := make(chan error)
exit := make(chan error, 1) // Add buffer to prevent blocking
cons := &mp4.Consumer{
RemoteAddr: r.RemoteAddr,
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
Medias: mp4.ParseQuery(r.URL.Query()),
}
cons.Listen(func(msg interface{}) {
cons.Listen(func(msg any) {
if exit == nil {
return
}
if data, ok := msg.([]byte); ok {
if _, err := w.Write(data); err != nil && exit != nil {
exit <- err
if _, err := w.Write(data); err != nil {
select {
case exit <- err:
default:
}
exit = nil
}
}
@@ -95,6 +125,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -105,22 +136,27 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
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 := r.URL.Query().Get("duration"); s != "" {
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 {
exit <- nil
select {
case exit <- nil:
default:
}
exit = nil
}
})
@@ -128,6 +164,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
}
err = <-exit
exit = nil
log.Trace().Err(err).Caller().Send()
@@ -135,23 +172,3 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
duration.Stop()
}
}
func isChromeFirst(w http.ResponseWriter, r *http.Request) bool {
// Chrome 105 does two requests: without Range and with `Range: bytes=0-`
if strings.Contains(r.UserAgent(), " Chrome/") {
if r.Header.Values("Range") == nil {
w.Header().Set("Content-Type", "video/mp4")
w.WriteHeader(http.StatusOK)
return true
}
}
return false
}
func isSafari(w http.ResponseWriter, r *http.Request) bool {
if r.Header.Get("Range") == "bytes=0-1" {
handlerKeyframe(w, r)
return true
}
return false
}

View File

@@ -2,15 +2,14 @@ package mp4
import (
"errors"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"strings"
)
const packetSize = 1400
func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
@@ -19,23 +18,17 @@ func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
}
cons := &mp4.Consumer{
RemoteAddr: tr.Request.RemoteAddr,
RemoteAddr: tcp.RemoteAddr(tr.Request),
UserAgent: tr.Request.UserAgent(),
}
cons.UserAgent = tr.Request.UserAgent()
cons.RemoteAddr = tr.Request.RemoteAddr
if codecs, ok := msg.Value.(string); ok {
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 interface{}) {
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
for len(data) > packetSize {
tr.Write(data[:packetSize])
data = data[packetSize:]
}
tr.Write(data)
}
})
@@ -72,17 +65,17 @@ func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
}
cons := &mp4.Segment{
RemoteAddr: tr.Request.RemoteAddr,
RemoteAddr: tcp.RemoteAddr(tr.Request),
UserAgent: tr.Request.UserAgent(),
OnlyKeyframe: true,
}
if codecs, ok := msg.Value.(string); ok {
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 interface{}) {
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
tr.Write(data)
}
@@ -102,37 +95,46 @@ func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
return nil
}
func parseMedias(codecs string, parseAudio bool) (medias []*streamer.Media) {
var videos []*streamer.Codec
var audios []*streamer.Codec
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 "avc1.640029":
codec := &streamer.Codec{Name: streamer.CodecH264}
case mp4.MimeH264:
codec := &core.Codec{Name: core.CodecH264}
videos = append(videos, codec)
case "hvc1.1.6.L153.B0":
codec := &streamer.Codec{Name: streamer.CodecH265}
case mp4.MimeH265:
codec := &core.Codec{Name: core.CodecH265}
videos = append(videos, codec)
case "mp4a.40.2":
codec := &streamer.Codec{Name: streamer.CodecAAC}
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 := &streamer.Media{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
media := &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: videos,
}
medias = append(medias, media)
}
if audios != nil && parseAudio {
media := &streamer.Media{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
media := &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: audios,
}
medias = append(medias, media)

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)
}

View File

@@ -1,27 +1,33 @@
package rtsp
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"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/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
"net"
"strings"
)
func Init() {
var conf struct {
Mod struct {
Listen string `yaml:"listen" json:"listen"`
Username string `yaml:"username" json:"-"`
Password string `yaml:"password" json:"-"`
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
@@ -49,6 +55,10 @@ func Init() {
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()
@@ -57,6 +67,7 @@ func Init() {
}
c := rtsp.NewServer(conn)
c.PacketSize = conf.Mod.PacketSize
// skip check auth for localhost
if conf.Mod.Username != "" && !conn.RemoteAddr().(*net.TCPAddr).IP.IsLoopback() {
c.Auth(conf.Mod.Username, conf.Mod.Password)
@@ -78,8 +89,9 @@ var Port string
var log zerolog.Logger
var handlers []Handler
var defaultMedias []*core.Media
func rtspHandler(url string) (streamer.Producer, error) {
func rtspHandler(url string) (core.Producer, error) {
backchannel := true
if i := strings.IndexByte(url, '#'); i > 0 {
@@ -89,33 +101,32 @@ func rtspHandler(url string) (streamer.Producer, error) {
url = url[:i]
}
conn, err := rtsp.NewClient(url)
if err != nil {
return nil, err
}
conn := rtsp.NewClient(url)
conn.UserAgent = app.UserAgent
if log.Trace().Enabled() {
conn.Listen(func(msg interface{}) {
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 {
if err := conn.Dial(); err != nil {
return nil, err
}
conn.Backchannel = backchannel
if err = conn.Describe(); err != nil {
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
@@ -136,7 +147,7 @@ func tcpHandler(conn *rtsp.Conn) {
trace := log.Trace().Enabled()
conn.Listen(func(msg interface{}) {
conn.Listen(func(msg any) {
if trace {
switch msg := msg.(type) {
case *tcp.Request:
@@ -164,7 +175,17 @@ func tcpHandler(conn *rtsp.Conn) {
conn.SessionName = app.UserAgent
initMedias(conn)
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]")
@@ -195,14 +216,13 @@ func tcpHandler(conn *rtsp.Conn) {
closer = func() {
stream.RemoveProducer(conn)
}
case streamer.StatePlaying:
log.Debug().Str("stream", name).Msg("[rtsp] start")
}
})
if err := conn.Accept(); err != nil {
log.Warn().Err(err).Caller().Send()
if err != io.EOF {
log.Warn().Err(err).Caller().Send()
}
if closer != nil {
closer()
}
@@ -229,44 +249,26 @@ func tcpHandler(conn *rtsp.Conn) {
_ = conn.Close()
}
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 _, name := range value {
name = strings.ToUpper(name)
// check aliases
switch name {
case "COPY":
name = "" // pass empty codecs list
case "MJPEG":
name = streamer.CodecJPEG
case "AAC":
name = streamer.CodecAAC
}
media := &streamer.Media{
Kind: key, Direction: streamer.DirectionRecvonly,
}
// empty codecs match all codecs
if name != "" {
// empty clock rate and channels match any values
media.Codecs = []*streamer.Codec{{Name: name}}
}
conn.Medias = append(conn.Medias, media)
}
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},
},
},
}
}
// 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},
}
}
return core.ParseQuery(query)
}

View File

@@ -1,7 +1,7 @@
package srtp
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/pkg/srtp"
"net"
)

View File

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

67
main.go
View File

@@ -1,24 +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/echo"
"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/http"
"github.com/AlexxIT/go2rtc/cmd/ivideon"
"github.com/AlexxIT/go2rtc/cmd/mjpeg"
"github.com/AlexxIT/go2rtc/cmd/mp4"
"github.com/AlexxIT/go2rtc/cmd/ngrok"
"github.com/AlexxIT/go2rtc/cmd/rtmp"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/srtp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"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"
@@ -26,28 +34,33 @@ import (
func main() {
app.Init() // init config and logs
api.Init() // init HTTP API server
streams.Init() // load streams list
api.Init() // init HTTP API server
echo.Init()
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
webrtc.Init()
mp4.Init()
mjpeg.Init()
echo.Init()
ivideon.Init()
http.Init()
dvrip.Init()
tapo.Init()
isapi.Init()
mpegts.Init()
roborock.Init()
srtp.Init()
homekit.Init()
ivideon.Init()
webrtc.Init()
mp4.Init()
hls.Init()
mjpeg.Init()
webtorrent.Init()
ngrok.Init()
debug.Init()

View File

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

144
pkg/core/codec.go Normal file
View File

@@ -0,0 +1,144 @@
package core
import (
"encoding/base64"
"fmt"
"github.com/pion/sdp/v3"
"strconv"
"strings"
"unicode"
)
type Codec struct {
Name string // H264, PCMU, PCMA, opus...
ClockRate uint32 // 90000, 8000, 16000...
Channels uint16 // 0, 1, 2
FmtpLine string
PayloadType uint8
}
func (c *Codec) String() string {
s := fmt.Sprintf("%d %s", c.PayloadType, c.Name)
if c.ClockRate != 0 && c.ClockRate != 90000 {
s = fmt.Sprintf("%s/%d", s, c.ClockRate)
}
if c.Channels > 0 {
s = fmt.Sprintf("%s/%d", s, c.Channels)
}
return s
}
func (c *Codec) Text() string {
switch c.Name {
case CodecH264:
if profile := DecodeH264(c.FmtpLine); profile != "" {
return "H.264 " + profile
}
return c.Name
}
s := c.Name
if c.ClockRate != 0 && c.ClockRate != 90000 {
s += "/" + strconv.Itoa(int(c.ClockRate))
}
if c.Channels > 0 {
s += "/" + strconv.Itoa(int(c.Channels))
}
return s
}
func (c *Codec) IsRTP() bool {
return c.PayloadType != PayloadTypeRAW
}
func (c *Codec) Clone() *Codec {
clone := *c
return &clone
}
func (c *Codec) Match(remote *Codec) bool {
switch remote.Name {
case CodecAll, CodecAny:
return true
}
return c.Name == remote.Name &&
(c.ClockRate == remote.ClockRate || remote.ClockRate == 0) &&
(c.Channels == remote.Channels || remote.Channels == 0)
}
func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
c := &Codec{PayloadType: byte(Atoi(payloadType))}
for _, attr := range md.Attributes {
switch {
case c.Name == "" && attr.Key == "rtpmap" && strings.HasPrefix(attr.Value, payloadType):
i := strings.IndexByte(attr.Value, ' ')
ss := strings.Split(attr.Value[i+1:], "/")
c.Name = strings.ToUpper(ss[0])
// fix tailing space: `a=rtpmap:96 H264/90000 `
c.ClockRate = uint32(Atoi(strings.TrimRightFunc(ss[1], unicode.IsSpace)))
if len(ss) == 3 && ss[2] == "2" {
c.Channels = 2
}
case c.FmtpLine == "" && attr.Key == "fmtp" && strings.HasPrefix(attr.Value, payloadType):
if i := strings.IndexByte(attr.Value, ' '); i > 0 {
c.FmtpLine = attr.Value[i+1:]
}
}
}
if c.Name == "" {
// https://en.wikipedia.org/wiki/RTP_payload_formats
switch payloadType {
case "0":
c.Name = CodecPCMU
c.ClockRate = 8000
case "8":
c.Name = CodecPCMA
c.ClockRate = 8000
case "10":
c.Name = CodecPCM
c.ClockRate = 44100
c.Channels = 2
case "11":
c.Name = CodecPCM
c.ClockRate = 44100
case "14":
c.Name = CodecMP3
c.ClockRate = 90000 // it's not real sample rate
case "26":
c.Name = CodecJPEG
c.ClockRate = 90000
default:
c.Name = payloadType
}
}
return c
}
func DecodeH264(fmtp string) string {
if ps := Between(fmtp, "sprop-parameter-sets=", ","); ps != "" {
if sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 {
var profile string
switch sps[1] {
case 0x42:
profile = "Baseline"
case 0x4D:
profile = "Main"
case 0x58:
profile = "Extended"
case 0x64:
profile = "High"
default:
profile = fmt.Sprintf("0x%02X", sps[1])
}
return fmt.Sprintf("%s %d.%d", profile, sps[3]/10, sps[3]%10)
}
}
return ""
}

101
pkg/core/core.go Normal file
View File

@@ -0,0 +1,101 @@
package core
const (
DirectionRecvonly = "recvonly"
DirectionSendonly = "sendonly"
DirectionSendRecv = "sendrecv"
)
const (
KindVideo = "video"
KindAudio = "audio"
)
const (
CodecH264 = "H264" // payloadType: 96
CodecH265 = "H265"
CodecVP8 = "VP8"
CodecVP9 = "VP9"
CodecAV1 = "AV1"
CodecJPEG = "JPEG" // payloadType: 26
CodecPCMU = "PCMU" // payloadType: 0
CodecPCMA = "PCMA" // payloadType: 8
CodecAAC = "MPEG4-GENERIC"
CodecOpus = "OPUS" // payloadType: 111
CodecG722 = "G722"
CodecMP3 = "MPA" // payload: 14, aka MPEG-1 Layer III
CodecPCM = "L16" // Linear PCM
CodecELD = "ELD" // AAC-ELD
CodecFLAC = "FLAC"
CodecAll = "ALL"
CodecAny = "ANY"
)
const PayloadTypeRAW byte = 255
type Producer interface {
// GetMedias - return Media(s) with local Media.Direction:
// - recvonly for Producer Video/Audio
// - sendonly for Producer backchannel
GetMedias() []*Media
// GetTrack - return Receiver, that can only produce rtp.Packet(s)
GetTrack(media *Media, codec *Codec) (*Receiver, error)
Start() error
Stop() error
}
type Consumer interface {
// GetMedias - return Media(s) with local Media.Direction:
// - sendonly for Consumer Video/Audio
// - recvonly for Consumer backchannel
GetMedias() []*Media
AddTrack(media *Media, codec *Codec, track *Receiver) error
Stop() error
}
type Mode byte
const (
ModeActiveProducer Mode = iota + 1 // typical source (client)
ModePassiveConsumer
ModePassiveProducer
ModeActiveConsumer
)
func (m Mode) String() string {
switch m {
case ModeActiveProducer:
return "active producer"
case ModePassiveConsumer:
return "passive consumer"
case ModePassiveProducer:
return "passive producer"
case ModeActiveConsumer:
return "active consumer"
}
return "unknown"
}
type Info struct {
Type string `json:"type,omitempty"`
URL string `json:"url,omitempty"`
RemoteAddr string `json:"remote_addr,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
Medias []*Media `json:"medias,omitempty"`
Receivers []*Receiver `json:"receivers,omitempty"`
Senders []*Sender `json:"senders,omitempty"`
Recv int `json:"recv,omitempty"`
Send int `json:"send,omitempty"`
}
const (
UnsupportedCodec = "unsupported codec"
WrongMediaDirection = "wrong media direction"
)

76
pkg/core/helpers.go Normal file
View File

@@ -0,0 +1,76 @@
package core
import (
cryptorand "crypto/rand"
"github.com/rs/zerolog/log"
"runtime"
"strconv"
"strings"
"time"
)
// Now90000 - timestamp for Video (clock rate = 90000 samples per second)
// same as: uint32(time.Duration(time.Now().UnixNano()) * 90000 / time.Second)
func Now90000() uint32 {
return uint32(time.Duration(time.Now().UnixMilli()) * 90)
}
const symbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
// RandString base10 - numbers, base16 - hex, base36 - digits+letters, base64 - URL safe symbols
func RandString(size, base byte) string {
b := make([]byte, size)
if _, err := cryptorand.Read(b); err != nil {
panic(err)
}
for i := byte(0); i < size; i++ {
b[i] = symbols[b[i]%base]
}
return string(b)
}
func Any(errs ...error) error {
for _, err := range errs {
if err != nil {
return err
}
}
return nil
}
func Between(s, sub1, sub2 string) string {
i := strings.Index(s, sub1)
if i < 0 {
return ""
}
s = s[i+len(sub1):]
if len(sub2) == 1 {
i = strings.IndexByte(s, sub2[0])
} else {
i = strings.Index(s, sub2)
}
if i >= 0 {
return s[:i]
}
return s
}
func Atoi(s string) (i int) {
i, _ = strconv.Atoi(s)
return
}
func Assert(ok bool) {
if !ok {
_, file, line, _ := runtime.Caller(1)
panic(file + ":" + strconv.Itoa(line))
}
}
func Caller() string {
log.Error().Caller(0).Send()
_, file, line, _ := runtime.Caller(1)
return file + ":" + strconv.Itoa(line)
}

18
pkg/core/listener.go Normal file
View File

@@ -0,0 +1,18 @@
package core
type EventFunc func(msg any)
// Listener base struct for all classes with support feedback
type Listener struct {
events []EventFunc
}
func (l *Listener) Listen(f EventFunc) {
l.events = append(l.events, f)
}
func (l *Listener) Fire(msg any) {
for _, f := range l.events {
f(msg)
}
}

202
pkg/core/media.go Normal file
View File

@@ -0,0 +1,202 @@
package core
import (
"encoding/json"
"fmt"
"github.com/pion/sdp/v3"
"strings"
)
// Media take best from:
// - deepch/vdk/format/rtsp/sdp.Media
// - pion/sdp.MediaDescription
type Media struct {
Kind string `json:"kind,omitempty"` // video or audio
Direction string `json:"direction,omitempty"` // sendonly, recvonly
Codecs []*Codec `json:"codecs,omitempty"`
ID string `json:"id,omitempty"` // MID for WebRTC, Control for RTSP
}
func (m *Media) String() string {
s := fmt.Sprintf("%s, %s", m.Kind, m.Direction)
for _, codec := range m.Codecs {
name := codec.Text()
if strings.Contains(s, name) {
continue
}
s += ", " + name
}
return s
}
func (m *Media) MarshalJSON() ([]byte, error) {
return json.Marshal(m.String())
}
func (m *Media) Clone() *Media {
clone := *m
clone.Codecs = make([]*Codec, len(m.Codecs))
for i, codec := range m.Codecs {
clone.Codecs[i] = codec.Clone()
}
return &clone
}
func (m *Media) MatchMedia(remote *Media) (codec, remoteCodec *Codec) {
// check same kind and opposite dirrection
if m.Kind != remote.Kind ||
m.Direction == DirectionSendonly && remote.Direction != DirectionRecvonly ||
m.Direction == DirectionRecvonly && remote.Direction != DirectionSendonly {
return nil, nil
}
for _, codec = range m.Codecs {
for _, remoteCodec = range remote.Codecs {
if codec.Match(remoteCodec) {
return
}
}
}
return nil, nil
}
func (m *Media) MatchCodec(remote *Codec) *Codec {
for _, codec := range m.Codecs {
if codec.Match(remote) {
return codec
}
}
return nil
}
func (m *Media) MatchAll() bool {
for _, codec := range m.Codecs {
if codec.Name == CodecAll {
return true
}
}
return false
}
func (m *Media) Equal(media *Media) bool {
if media.ID != "" {
return m.ID == media.ID
}
return m.String() == media.String()
}
func GetKind(name string) string {
switch name {
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
return KindVideo
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecELD, CodecFLAC:
return KindAudio
}
return ""
}
func MarshalSDP(name string, medias []*Media) ([]byte, error) {
sd := &sdp.SessionDescription{
Origin: sdp.Origin{
Username: "-", SessionID: 1, SessionVersion: 1,
NetworkType: "IN", AddressType: "IP4", UnicastAddress: "0.0.0.0",
},
SessionName: sdp.SessionName(name),
ConnectionInformation: &sdp.ConnectionInformation{
NetworkType: "IN", AddressType: "IP4", Address: &sdp.Address{
Address: "0.0.0.0",
},
},
TimeDescriptions: []sdp.TimeDescription{
{Timing: sdp.Timing{}},
},
}
for _, media := range medias {
if media.Codecs == nil {
continue
}
codec := media.Codecs[0]
name := codec.Name
if name == CodecELD {
name = CodecAAC
}
md := &sdp.MediaDescription{
MediaName: sdp.MediaName{
Media: media.Kind,
Protos: []string{"RTP", "AVP"},
},
}
md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
if media.ID != "" {
md.WithValueAttribute("control", media.ID)
}
sd.MediaDescriptions = append(sd.MediaDescriptions, md)
}
return sd.Marshal()
}
func UnmarshalMedia(md *sdp.MediaDescription) *Media {
m := &Media{
Kind: md.MediaName.Media,
}
for _, attr := range md.Attributes {
switch attr.Key {
case DirectionSendonly, DirectionRecvonly, DirectionSendRecv:
m.Direction = attr.Key
case "control", "mid":
m.ID = attr.Value
}
}
for _, format := range md.MediaName.Formats {
m.Codecs = append(m.Codecs, UnmarshalCodec(md, format))
}
return m
}
func ParseQuery(query map[string][]string) (medias []*Media) {
// set media candidates from query list
for key, values := range query {
switch key {
case KindVideo, KindAudio:
for _, value := range values {
media := &Media{Kind: key, Direction: DirectionSendonly}
for _, name := range strings.Split(value, ",") {
name = strings.ToUpper(name)
// check aliases
switch name {
case "", "COPY":
name = CodecAny
case "MJPEG":
name = CodecJPEG
case "AAC":
name = CodecAAC
case "MP3":
name = CodecMP3
}
media.Codecs = append(media.Codecs, &Codec{Name: name})
}
medias = append(medias, media)
}
}
}
return
}

63
pkg/core/media_test.go Normal file
View File

@@ -0,0 +1,63 @@
package core
import (
"fmt"
"github.com/pion/sdp/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/url"
"testing"
)
func TestSDP(t *testing.T) {
medias := []*Media{{
Kind: KindAudio, Direction: DirectionSendonly,
Codecs: []*Codec{
{Name: CodecPCMU, ClockRate: 8000},
},
}}
data, err := MarshalSDP("go2rtc/1.0.0", medias)
assert.Empty(t, err)
sd := &sdp.SessionDescription{}
err = sd.Unmarshal(data)
assert.Empty(t, err)
}
func TestParseQuery(t *testing.T) {
u, _ := url.Parse("rtsp://localhost:8554/camera1")
medias := ParseQuery(u.Query())
assert.Nil(t, medias)
for _, rawULR := range []string{
"rtsp://localhost:8554/camera1?video",
"rtsp://localhost:8554/camera1?video=copy",
"rtsp://localhost:8554/camera1?video=any",
} {
u, _ = url.Parse(rawULR)
medias = ParseQuery(u.Query())
assert.Equal(t, []*Media{
{Kind: KindVideo, Direction: DirectionRecvonly, Codecs: []*Codec{{Name: CodecAny}}},
}, medias)
}
}
func TestClone(t *testing.T) {
media1 := &Media{
Kind: KindVideo,
Direction: DirectionRecvonly,
Codecs: []*Codec{
{Name: CodecPCMU, ClockRate: 8000},
},
}
media2 := media1.Clone()
p1 := fmt.Sprintf("%p", media1)
p2 := fmt.Sprintf("%p", media2)
require.NotEqualValues(t, p1, p2)
p3 := fmt.Sprintf("%p", media1.Codecs[0])
p4 := fmt.Sprintf("%p", media2.Codecs[0])
require.NotEqualValues(t, p3, p4)
}

31
pkg/core/probe.go Normal file
View File

@@ -0,0 +1,31 @@
package core
import "time"
type Probe struct {
deadline time.Time
items map[any]struct{}
}
func NewProbe(enable bool) *Probe {
if enable {
return &Probe{
deadline: time.Now().Add(time.Second * 3),
items: map[any]struct{}{},
}
} else {
return nil
}
}
// Active return true if probe enabled and not finish
func (p *Probe) Active() bool {
return len(p.items) < 2 && time.Now().Before(p.deadline)
}
// Append safe to run if Probe is nil
func (p *Probe) Append(v any) {
if p != nil {
p.items[v] = struct{}{}
}
}

183
pkg/core/track.go Normal file
View File

@@ -0,0 +1,183 @@
package core
import (
"encoding/json"
"errors"
"fmt"
"github.com/pion/rtp"
"strconv"
"sync"
)
var ErrCantGetTrack = errors.New("can't get track")
type Receiver struct {
Codec *Codec
Media *Media
ID byte // Channel for RTSP, PayloadType for MPEG-TS
senders map[*Sender]chan *rtp.Packet
mu sync.RWMutex
bytes int
}
func NewReceiver(media *Media, codec *Codec) *Receiver {
Assert(codec != nil)
return &Receiver{Codec: codec, Media: media}
}
// WriteRTP - fast and non blocking write to all readers buffers
func (t *Receiver) WriteRTP(packet *rtp.Packet) {
t.mu.Lock()
t.bytes += len(packet.Payload)
for sender, buffer := range t.senders {
select {
case buffer <- packet:
default:
sender.overflow++
}
}
t.mu.Unlock()
}
func (t *Receiver) Senders() (senders []*Sender) {
t.mu.RLock()
for sender := range t.senders {
senders = append(senders, sender)
}
t.mu.RUnlock()
return
}
func (t *Receiver) Close() {
t.mu.Lock()
// close all sender channel buffers and erase senders list
for _, buffer := range t.senders {
close(buffer)
}
t.senders = nil
t.mu.Unlock()
}
func (t *Receiver) Replace(target *Receiver) {
// move this receiver senders to new receiver
t.mu.Lock()
senders := t.senders
t.mu.Unlock()
target.mu.Lock()
target.senders = senders
target.mu.Unlock()
}
func (t *Receiver) String() string {
s := t.Codec.String() + ", bytes=" + strconv.Itoa(t.bytes)
t.mu.RLock()
s += fmt.Sprintf(", senders=%d", len(t.senders))
t.mu.RUnlock()
return s
}
func (t *Receiver) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
}
type Sender struct {
Codec *Codec
Media *Media
Handler HandlerFunc
receivers []*Receiver
mu sync.RWMutex
bytes int
overflow int
}
func NewSender(media *Media, codec *Codec) *Sender {
return &Sender{Codec: codec, Media: media}
}
// HandlerFunc like http.HandlerFunc
type HandlerFunc func(packet *rtp.Packet)
func (s *Sender) HandleRTP(track *Receiver) {
bufferSize := 100
if GetKind(track.Codec.Name) == KindVideo {
if track.Codec.IsRTP() {
// H.264 2560x1440 4096kbs can have 700+ packets between 25 frames
// H.265 5120x1440 can have 700+ packets between two keyframes
bufferSize = 1000
} else {
bufferSize = 50
}
}
buffer := make(chan *rtp.Packet, bufferSize)
track.mu.Lock()
if track.senders == nil {
track.senders = map[*Sender]chan *rtp.Packet{}
}
track.senders[s] = buffer
track.mu.Unlock()
s.mu.Lock()
s.receivers = append(s.receivers, track)
s.mu.Unlock()
go func() {
// read packets from buffer channel until it will be closed
for packet := range buffer {
s.mu.Lock()
s.bytes += len(packet.Payload)
s.mu.Unlock()
s.Handler(packet)
}
// remove current receiver from list
// it can only happen when receiver close buffer channel
s.mu.Lock()
for i, receiver := range s.receivers {
if receiver == track {
s.receivers = append(s.receivers[:i], s.receivers[i+1:]...)
break
}
}
s.mu.Unlock()
}()
}
func (s *Sender) Close() {
s.mu.Lock()
// remove this sender from all receivers list
for _, receiver := range s.receivers {
receiver.mu.Lock()
if buffer := receiver.senders[s]; buffer != nil {
// remove channel from list
delete(receiver.senders, s)
// close channel
close(buffer)
}
receiver.mu.Unlock()
}
s.receivers = nil
s.mu.Unlock()
}
func (s *Sender) String() string {
info := s.Codec.String() + ", bytes=" + strconv.Itoa(s.bytes)
s.mu.RLock()
info += ", receivers=" + strconv.Itoa(len(s.receivers))
s.mu.RUnlock()
if s.overflow > 0 {
info += ", overflow=" + strconv.Itoa(s.overflow)
}
return info
}
func (s *Sender) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}

71
pkg/core/waiter.go Normal file
View File

@@ -0,0 +1,71 @@
package core
import (
"sync"
)
// Waiter support:
// - autotart on first Wait
// - block new waiters after last Done
// - safe Done after finish
type Waiter struct {
sync.WaitGroup
mu sync.Mutex
state int // state < 0 means finish
}
func (w *Waiter) Add(delta int) {
w.mu.Lock()
if w.state >= 0 {
w.state += delta
w.WaitGroup.Add(delta)
}
w.mu.Unlock()
}
func (w *Waiter) Wait() {
w.mu.Lock()
// first wait auto start waiter
if w.state == 0 {
w.state++
w.WaitGroup.Add(1)
}
w.mu.Unlock()
w.WaitGroup.Wait()
}
func (w *Waiter) Done() {
w.mu.Lock()
// safe run Done only when have tasks
if w.state > 0 {
w.state--
w.WaitGroup.Done()
}
// block waiter for any operations after last done
if w.state == 0 {
w.state = -1
}
w.mu.Unlock()
}
func (w *Waiter) WaitChan() <-chan struct{} {
var ch chan struct{}
w.mu.Lock()
if w.state >= 0 {
ch = make(chan struct{})
go func() {
w.Wait()
ch <- struct{}{}
}()
}
w.mu.Unlock()
return ch
}

52
pkg/core/worker.go Normal file
View File

@@ -0,0 +1,52 @@
package core
import (
"time"
)
type Worker struct {
timer *time.Timer
done chan struct{}
}
// NewWorker run f after d
func NewWorker(d time.Duration, f func() time.Duration) *Worker {
timer := time.NewTimer(d)
done := make(chan struct{})
go func() {
for {
select {
case <-timer.C:
if d = f(); d > 0 {
timer.Reset(d)
continue
}
case <-done:
timer.Stop()
}
break
}
}()
return &Worker{timer: timer, done: done}
}
// Do - instant timer run
func (w *Worker) Do() {
if w == nil {
return
}
w.timer.Reset(0)
}
func (w *Worker) Stop() {
if w == nil {
return
}
select {
case w.done <- struct{}{}:
default:
}
}

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